Merge branch 'master' into 14965-arv-mount-py-three
authorEric Biagiotti <ebiagiotti@veritasgenetics.com>
Tue, 16 Jul 2019 16:29:04 +0000 (12:29 -0400)
committerEric Biagiotti <ebiagiotti@veritasgenetics.com>
Tue, 16 Jul 2019 16:29:04 +0000 (12:29 -0400)
refs #14965

Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti@veritasgenetics.com>

240 files changed:
apps/workbench/app/controllers/actions_controller.rb
apps/workbench/app/controllers/application_controller.rb
apps/workbench/app/controllers/collections_controller.rb
apps/workbench/app/controllers/container_requests_controller.rb
apps/workbench/app/controllers/containers_controller.rb
apps/workbench/app/controllers/healthcheck_controller.rb
apps/workbench/app/controllers/jobs_controller.rb
apps/workbench/app/controllers/pipeline_instances_controller.rb
apps/workbench/app/controllers/pipeline_templates_controller.rb
apps/workbench/app/controllers/projects_controller.rb
apps/workbench/app/controllers/users_controller.rb
apps/workbench/app/controllers/virtual_machines_controller.rb
apps/workbench/app/controllers/work_units_controller.rb
apps/workbench/app/controllers/workflows_controller.rb
apps/workbench/app/helpers/application_helper.rb
apps/workbench/app/helpers/collections_helper.rb
apps/workbench/app/mailers/issue_reporter.rb
apps/workbench/app/mailers/request_shell_access_reporter.rb
apps/workbench/app/models/arvados_api_client.rb
apps/workbench/app/models/repository.rb
apps/workbench/app/views/api_client_authorizations/_show_help.html.erb
apps/workbench/app/views/application/_breadcrumbs.html.erb
apps/workbench/app/views/application/_report_error.html.erb
apps/workbench/app/views/application/_report_issue_popup.html.erb
apps/workbench/app/views/application/_show_sharing.html.erb
apps/workbench/app/views/collections/show_file_links.html.erb
apps/workbench/app/views/getting_started/_getting_started_popup.html.erb
apps/workbench/app/views/jobs/_show_log.html.erb
apps/workbench/app/views/layouts/application.html.erb
apps/workbench/app/views/layouts/body.html.erb
apps/workbench/app/views/notifications/_collections_notification.html.erb
apps/workbench/app/views/notifications/_jobs_notification.html.erb
apps/workbench/app/views/notifications/_pipelines_notification.html.erb
apps/workbench/app/views/projects/_show_dashboard.html.erb
apps/workbench/app/views/repositories/_show_help.html.erb
apps/workbench/app/views/user_agreements/index.html.erb
apps/workbench/app/views/users/_add_ssh_key_popup.html.erb
apps/workbench/app/views/users/_current_token.html.erb
apps/workbench/app/views/users/_setup_popup.html.erb
apps/workbench/app/views/users/_ssh_keys.html.erb
apps/workbench/app/views/users/_tables.html.erb
apps/workbench/app/views/users/_virtual_machines.html.erb
apps/workbench/app/views/users/profile.html.erb
apps/workbench/app/views/users/welcome.html.erb
apps/workbench/app/views/virtual_machines/_show_help.html.erb
apps/workbench/app/views/virtual_machines/webshell.html.erb
apps/workbench/app/views/work_units/_show_log.html.erb
apps/workbench/config/application.default.yml
apps/workbench/config/application.rb
apps/workbench/config/arvados_config.rb [new file with mode: 0644]
apps/workbench/config/load_config.rb [deleted file]
apps/workbench/fpm-info.sh
apps/workbench/lib/config_loader.rb [new file with mode: 0644]
apps/workbench/lib/config_validators.rb
apps/workbench/lib/tasks/config.rake [new file with mode: 0644]
apps/workbench/lib/tasks/config_check.rake [deleted file]
apps/workbench/lib/tasks/config_dump.rake [deleted file]
apps/workbench/test/controllers/actions_controller_test.rb
apps/workbench/test/controllers/application_controller_test.rb
apps/workbench/test/controllers/collections_controller_test.rb
apps/workbench/test/controllers/container_requests_controller_test.rb
apps/workbench/test/controllers/disabled_api_test.rb
apps/workbench/test/controllers/projects_controller_test.rb
apps/workbench/test/controllers/users_controller_test.rb
apps/workbench/test/integration/anonymous_access_test.rb
apps/workbench/test/integration/application_layout_test.rb
apps/workbench/test/integration/download_test.rb
apps/workbench/test/integration/errors_test.rb
apps/workbench/test/integration/jobs_test.rb
apps/workbench/test/integration/link_account_test.rb
apps/workbench/test/integration/pipeline_instances_test.rb
apps/workbench/test/integration/projects_test.rb
apps/workbench/test/integration/report_issue_test.rb
apps/workbench/test/integration/user_profile_test.rb
apps/workbench/test/integration/user_settings_menu_test.rb
apps/workbench/test/integration/websockets_test.rb
apps/workbench/test/integration/work_units_test.rb
apps/workbench/test/integration_helper.rb
apps/workbench/test/integration_performance/collection_unit_test.rb
apps/workbench/test/test_helper.rb
apps/workbench/test/unit/user_test.rb
apps/workbench/test/unit/work_unit_test.rb
build/package-build-dockerfiles/Makefile
build/package-build-dockerfiles/centos7/Dockerfile
build/package-build-dockerfiles/debian8/Dockerfile
build/package-build-dockerfiles/debian9/Dockerfile
build/package-build-dockerfiles/ubuntu1404/Dockerfile
build/package-build-dockerfiles/ubuntu1604/Dockerfile
build/package-build-dockerfiles/ubuntu1804/Dockerfile
build/package-test-dockerfiles/Makefile
build/package-test-dockerfiles/debian8/Dockerfile [deleted file]
build/package-test-dockerfiles/ubuntu1404/Dockerfile [deleted file]
build/rails-package-scripts/postinst.sh
build/run-build-packages-one-target.sh
build/run-build-packages-python-and-ruby.sh
build/run-build-packages.sh
build/run-library.sh
build/run-tests.sh
cmd/arvados-client/cmd.go
cmd/arvados-server/cmd.go
doc/_config.yml
doc/_includes/_admin_list_collections_without_property_py.liquid
doc/_includes/_admin_set_property_to_collections_under_project_py.liquid
doc/_includes/_admin_update_collection_property_py.liquid
doc/admin/cloudtest.html.textile.liquid [new file with mode: 0644]
doc/admin/config-migration.html.textile.liquid
doc/admin/troubleshooting.html.textile.liquid [new file with mode: 0644]
doc/admin/upgrading.html.textile.liquid
doc/api/methods.html.textile.liquid
doc/api/methods/groups.html.textile.liquid
doc/install/index.html.textile.liquid
doc/install/install-api-server.html.textile.liquid
doc/install/install-dispatch-cloud.html.textile.liquid
doc/install/install-manual-prerequisites.html.textile.liquid
doc/install/install-nodemanager.html.textile.liquid
doc/install/install-workbench-app.html.textile.liquid
doc/sdk/cli/reference.html.textile.liquid
doc/user/composer/composer.html.textile.liquid
doc/user/cwl/cwl-extensions.html.textile.liquid
doc/user/topics/arv-docker.html.textile.liquid
lib/cloud/cloudtest/cmd.go [new file with mode: 0644]
lib/cloud/cloudtest/tester.go [new file with mode: 0644]
lib/cloud/cloudtest/tester_test.go [new file with mode: 0644]
lib/cmd/cmd.go
lib/cmdtest/leakcheck.go
lib/config/cmd.go
lib/config/cmd_test.go
lib/config/config.default.yml
lib/config/deprecated.go
lib/config/export.go
lib/config/export_test.go
lib/config/generated_config.go
lib/config/load.go
lib/config/load_test.go
lib/controller/federation/conn.go [new file with mode: 0644]
lib/controller/federation_test.go
lib/controller/handler.go
lib/controller/handler_test.go
lib/controller/proxy.go
lib/controller/railsproxy/railsproxy.go [new file with mode: 0644]
lib/controller/router/checker_test.go [new file with mode: 0644]
lib/controller/router/error.go [new file with mode: 0644]
lib/controller/router/request.go [new file with mode: 0644]
lib/controller/router/request_test.go [new file with mode: 0644]
lib/controller/router/response.go [new file with mode: 0644]
lib/controller/router/router.go [new file with mode: 0644]
lib/controller/router/router_test.go [new file with mode: 0644]
lib/controller/rpc/conn.go [new file with mode: 0644]
lib/controller/rpc/conn_test.go [new file with mode: 0644]
lib/controller/server_test.go
lib/dispatchcloud/container/queue_test.go
lib/dispatchcloud/dispatcher_test.go
lib/dispatchcloud/driver.go
lib/dispatchcloud/ssh_executor/executor.go
lib/dispatchcloud/worker/pool.go
lib/dispatchcloud/worker/verify.go
lib/service/cmd.go
sdk/cwl/arvados_cwl/fsaccess.py
sdk/cwl/setup.py
sdk/go/arvados/api.go [new file with mode: 0644]
sdk/go/arvados/api_client_authorization.go
sdk/go/arvados/client.go
sdk/go/arvados/collection.go
sdk/go/arvados/config.go
sdk/go/arvados/context.go [new file with mode: 0644]
sdk/go/arvados/error.go
sdk/go/arvados/fs_backend.go
sdk/go/arvados/fs_collection.go
sdk/go/arvados/fs_project_test.go
sdk/go/arvados/group.go
sdk/go/arvados/link.go
sdk/go/arvados/log.go
sdk/go/arvados/node.go
sdk/go/arvados/resource_list.go
sdk/go/arvados/resource_list_test.go
sdk/go/arvados/specimen.go [new file with mode: 0644]
sdk/go/arvados/user.go
sdk/go/arvados/workflow.go
sdk/go/arvadostest/api.go [new file with mode: 0644]
sdk/go/arvadostest/fixtures.go
sdk/go/auth/auth.go
sdk/go/auth/handlers.go
sdk/go/httpserver/error.go
sdk/go/httpserver/logger.go
sdk/go/httpserver/logger_test.go
sdk/go/httpserver/metrics.go
sdk/go/keepclient/keepclient.go
sdk/pam/setup.py
sdk/python/tests/nginx.conf
sdk/python/tests/run_test_server.py
services/api/app/controllers/application_controller.rb
services/api/app/controllers/arvados/v1/groups_controller.rb
services/api/app/models/collection.rb
services/api/config/arvados_config.rb
services/api/config/config.default.yml [deleted symlink]
services/api/config/initializers/lograge.rb
services/api/fpm-info.sh
services/api/test/fixtures/collections.yml
services/api/test/fixtures/container_requests.yml
services/api/test/functional/application_controller_test.rb
services/api/test/functional/arvados/v1/schema_controller_test.rb
services/api/test/integration/login_workflow_test.rb
services/api/test/unit/collection_test.rb
services/crunch-run/crunchrun.go
services/fuse/setup.py
services/health/main.go
services/keep-balance/collection_test.go
services/keep-balance/server.go
services/keep-web/cache.go
services/keep-web/cadaver_test.go
services/keep-web/handler_test.go
services/keep-web/server.go
services/keepproxy/keepproxy.go
services/keepstore/handlers.go
services/login-sync/Gemfile.lock
services/nodemanager/setup.py
services/ws/session_v0_test.go
tools/arvbox/bin/arvbox
tools/arvbox/lib/arvbox/docker/Dockerfile.base
tools/arvbox/lib/arvbox/docker/api-setup.sh
tools/arvbox/lib/arvbox/docker/cluster-config.sh [new file with mode: 0755]
tools/arvbox/lib/arvbox/docker/common.sh
tools/arvbox/lib/arvbox/docker/createusers.sh
tools/arvbox/lib/arvbox/docker/go-setup.sh
tools/arvbox/lib/arvbox/docker/service/certificate/run
tools/arvbox/lib/arvbox/docker/service/controller/run
tools/arvbox/lib/arvbox/docker/service/nginx/run
tools/arvbox/lib/arvbox/docker/service/sso/run-service
tools/arvbox/lib/arvbox/docker/service/workbench/run
tools/arvbox/lib/arvbox/docker/service/workbench/run-service
tools/crunchstat-summary/crunchstat_summary/dygraphs.py
tools/crunchstat-summary/crunchstat_summary/summarizer.py
tools/crunchstat-summary/crunchstat_summary/webchart.py
tools/crunchstat-summary/tests/container_9tee4-dz642-lymtndkpy39eibk-arv-mount.txt.gz.report
tools/crunchstat-summary/tests/container_9tee4-dz642-lymtndkpy39eibk-crunchstat.txt.gz.report
tools/crunchstat-summary/tests/container_9tee4-dz642-lymtndkpy39eibk.txt.gz.report
tools/crunchstat-summary/tests/logfile_20151204190335.txt.gz.report
tools/crunchstat-summary/tests/logfile_20151210063411.txt.gz.report
tools/crunchstat-summary/tests/logfile_20151210063439.txt.gz.report
tools/crunchstat-summary/tests/test_examples.py

index b1bbb122670dcb6b11aac21d915f26174851aed9..e6f20be37064ea7f396206ca0b7d68ae3482ef09 100644 (file)
@@ -10,7 +10,7 @@ class ActionsController < ApplicationController
   # Skip require_thread_api_token if this is a show action
   # for an object uuid that supports anonymous access.
   skip_around_action :require_thread_api_token, if: proc { |ctrl|
-    Rails.configuration.anonymous_user_token and
+    !Rails.configuration.Users.AnonymousUserToken.empty? and
     'show' == ctrl.action_name and
     params['uuid'] and
     model_class.in?([Collection, Group, Job, PipelineInstance, PipelineTemplate])
index f913a15ffe23c997c7e5554f9755a470599c2e4c..ccd0abdc33e5245c2c05014e5d7cd176f297961b 100644 (file)
@@ -756,14 +756,14 @@ class ApplicationController < ActionController::Base
   def missing_required_profile?
     missing_required = false
 
-    profile_config = Rails.configuration.user_profile_form_fields
-    if current_user && profile_config
+    profile_config = Rails.configuration.Workbench.UserProfileFormFields
+    if current_user && !profile_config.empty?
       current_user_profile = current_user.prefs[:profile]
-      profile_config.kind_of?(Array) && profile_config.andand.each do |entry|
-        if entry['required']
+      profile_config.each do |k, entry|
+        if entry['Required']
           if !current_user_profile ||
-             !current_user_profile[entry['key'].to_sym] ||
-             current_user_profile[entry['key'].to_sym].empty?
+             !current_user_profile[k] ||
+             current_user_profile[k].empty?
             missing_required = true
             break
           end
@@ -775,13 +775,13 @@ class ApplicationController < ActionController::Base
   end
 
   def select_theme
-    return Rails.configuration.arvados_theme
+    return Rails.configuration.Workbench.Theme
   end
 
   @@notification_tests = []
 
   @@notification_tests.push lambda { |controller, current_user|
-    return nil if Rails.configuration.shell_in_a_box_url
+    return nil if Rails.configuration.Services.WebShell.ExternalURL != URI("")
     AuthorizedKey.limit(1).where(authorized_user_uuid: current_user.uuid).each do
       return nil
     end
@@ -815,7 +815,7 @@ class ApplicationController < ActionController::Base
   helper_method :user_notifications
   def user_notifications
     @errors = nil if !defined?(@errors)
-    return [] if @errors or not current_user.andand.is_active or not Rails.configuration.show_user_notifications
+    return [] if @errors or not current_user.andand.is_active or not Rails.configuration.Workbench.ShowUserNotifications
     @notifications ||= @@notification_tests.map do |t|
       t.call(self, current_user)
     end.compact
index 8d7e6ee332af5e3cf53dac674e6185626f43d413..5141012443c33f9dc98492cf41535219d0f6a2a4 100644 (file)
@@ -10,7 +10,7 @@ class CollectionsController < ApplicationController
   include ActionController::Live
 
   skip_around_action :require_thread_api_token, if: proc { |ctrl|
-    Rails.configuration.anonymous_user_token and
+    !Rails.configuration.Users.AnonymousUserToken.empty? and
     'show' == ctrl.action_name
   }
   skip_around_action(:require_thread_api_token,
@@ -124,7 +124,8 @@ class CollectionsController < ApplicationController
     # Otherwise, it's impossible to know whether any other request succeeded
     # because of the reader token.
     coll = nil
-    tokens = [(Rails.configuration.anonymous_user_token || nil),
+    tokens = [(if !Rails.configuration.Users.AnonymousUserToken.empty? then
+                Rails.configuration.Users.AnonymousUserToken else nil end),
               params[:reader_token],
               Thread.current[:arvados_api_token]].compact
     usable_token = find_usable_token(tokens) do
@@ -138,7 +139,7 @@ class CollectionsController < ApplicationController
     opts = {}
     if usable_token == params[:reader_token]
       opts[:path_token] = usable_token
-    elsif usable_token == Rails.configuration.anonymous_user_token
+    elsif usable_token == Rails.configuration.Users.AnonymousUserToken
       # Don't pass a token at all
     else
       # We pass the current user's real token only if it's necessary
@@ -334,24 +335,25 @@ class CollectionsController < ApplicationController
 
   def keep_web_url(uuid_or_pdh, file, opts)
     munged_id = uuid_or_pdh.sub('+', '-')
-    fmt = {uuid_or_pdh: munged_id}
 
-    tmpl = Rails.configuration.keep_web_url
-    if Rails.configuration.keep_web_download_url and
-        (!tmpl or opts[:disposition] == 'attachment')
+    tmpl = Rails.configuration.Services.WebDAV.ExternalURL.to_s
+
+    if Rails.configuration.Services.WebDAVDownload.ExternalURL != URI("") and
+        (tmpl.empty? or opts[:disposition] == 'attachment')
       # Prefer the attachment-only-host when we want an attachment
       # (and when there is no preview link configured)
-      tmpl = Rails.configuration.keep_web_download_url
-    elsif not Rails.configuration.trust_all_content
-      check_uri = URI.parse(tmpl % fmt)
+      tmpl = Rails.configuration.Services.WebDAVDownload.ExternalURL.to_s
+    elsif not Rails.configuration.Workbench.TrustAllContent
+      check_uri = URI.parse(tmpl.sub("*", munged_id))
       if opts[:query_token] and
+        (check_uri.host.nil? or (
           not check_uri.host.start_with?(munged_id + "--") and
-          not check_uri.host.start_with?(munged_id + ".")
+          not check_uri.host.start_with?(munged_id + ".")))
         # We're about to pass a token in the query string, but
         # keep-web can't accept that safely at a single-origin URL
         # template (unless it's -attachment-only-host).
-        tmpl = Rails.configuration.keep_web_download_url
-        if not tmpl
+        tmpl = Rails.configuration.Services.WebDAVDownload.ExternalURL.to_s
+        if tmpl.empty?
           raise ArgumentError, "Download precluded by site configuration"
         end
         logger.warn("Using download link, even though inline content " \
@@ -359,13 +361,16 @@ class CollectionsController < ApplicationController
       end
     end
 
-    if tmpl == Rails.configuration.keep_web_download_url
+    if tmpl == Rails.configuration.Services.WebDAVDownload.ExternalURL.to_s
       # This takes us to keep-web's -attachment-only-host so there is
       # no need to add ?disposition=attachment.
       opts.delete :disposition
     end
 
-    uri = URI.parse(tmpl % fmt)
+    uri = URI.parse(tmpl.sub("*", munged_id))
+    if tmpl.index("*").nil?
+      uri.path = "/c=#{munged_id}"
+    end
     uri.path += '/' unless uri.path.end_with? '/'
     if opts[:path_token]
       uri.path += 't=' + opts[:path_token] + '/'
index d5627076f5e1d0b0dee23531fbe884dacb16aa64..385d9dc6d7292c8cc5d47cf18b94ecf5189b8354 100644 (file)
@@ -4,7 +4,7 @@
 
 class ContainerRequestsController < ApplicationController
   skip_around_action :require_thread_api_token, if: proc { |ctrl|
-    Rails.configuration.anonymous_user_token and
+    !Rails.configuration.Users.AnonymousUserToken.empty? and
     'show' == ctrl.action_name
   }
 
index a8549cd5b82e8b9e82420d7f7478b2c3a9660247..4b5606778f409f072fd44e4449200eb73ed788d0 100644 (file)
@@ -4,7 +4,7 @@
 
 class ContainersController < ApplicationController
   skip_around_action :require_thread_api_token, if: proc { |ctrl|
-    Rails.configuration.anonymous_user_token and
+    !Rails.configuration.Users.AnonymousUserToken.empty? and
     'show' == ctrl.action_name
   }
 
index 7afe4032a2c19f726465e8aae7b1f6cdf55dafc0..691bef8ee578bbd3beeb616906ac44a4df253631 100644 (file)
@@ -19,7 +19,7 @@ class HealthcheckController < ApplicationController
     mgmt_token = Rails.configuration.ManagementToken
     auth_header = request.headers['Authorization']
 
-    if !mgmt_token
+    if mgmt_token.empty?
       render :json => {:errors => "disabled"}, :status => 404
     elsif !auth_header
       render :json => {:errors => "authorization required"}, :status => 401
index e38d3ba87b3e40e6df08e4d6150a2a3c392220a7..4f7bfcee53e5f11acceefc5c2a0d1707287143c1 100644 (file)
@@ -4,7 +4,7 @@
 
 class JobsController < ApplicationController
   skip_around_action :require_thread_api_token, if: proc { |ctrl|
-    Rails.configuration.anonymous_user_token and
+    !Rails.configuration.Users.AnonymousUserToken.empty? and
     'show' == ctrl.action_name
   }
 
@@ -67,7 +67,7 @@ class JobsController < ApplicationController
 
   def logs
     @logs = @object.
-      stderr_log_query(Rails.configuration.running_job_log_records_to_fetch).
+      stderr_log_query(Rails.configuration.Workbench.RunningJobLogRecordsToFetch).
       map { |e| e.serializable_hash.merge({ 'prepend' => true }) }
     respond_to do |format|
       format.json { render json: @logs }
index 26a9f85d4e4e890b85ab722151737afe12d2dd12..c8863653a1b8f30433ce77d2570bd831cc7cfbd7 100644 (file)
@@ -6,7 +6,7 @@ class PipelineInstancesController < ApplicationController
   skip_before_action :find_object_by_uuid, only: :compare
   before_action :find_objects_by_uuid, only: :compare
   skip_around_action :require_thread_api_token, if: proc { |ctrl|
-    Rails.configuration.anonymous_user_token and
+    !Rails.configuration.Users.AnonymousUserToken.empty? and
     'show' == ctrl.action_name
   }
 
index c497c70d434c4b67edb1dab42c2558bdd8242b16..aa444c153b6163d3a65d43bbeb790d2c06688844 100644 (file)
@@ -4,7 +4,7 @@
 
 class PipelineTemplatesController < ApplicationController
   skip_around_action :require_thread_api_token, if: proc { |ctrl|
-    Rails.configuration.anonymous_user_token and
+    !Rails.configuration.Users.AnonymousUserToken.empty? and
     'show' == ctrl.action_name
   }
 
index 8237dc7152c9e72dddf2ace2515bf8b80f0b1294..b649b710b01598d73c3c53f19a54df2152dd75aa 100644 (file)
@@ -5,7 +5,7 @@
 class ProjectsController < ApplicationController
   before_action :set_share_links, if: -> { defined? @object and @object}
   skip_around_action :require_thread_api_token, if: proc { |ctrl|
-    Rails.configuration.anonymous_user_token and
+    !Rails.configuration.Users.AnonymousUserToken.empty? and
     %w(show tab_counts public).include? ctrl.action_name
   }
 
@@ -315,8 +315,8 @@ class ProjectsController < ApplicationController
   end
 
   def public  # Yes 'public' is the name of the action for public projects
-    return render_not_found if not Rails.configuration.anonymous_user_token or not Rails.configuration.enable_public_projects_page
-    @objects = using_specific_api_token Rails.configuration.anonymous_user_token do
+    return render_not_found if Rails.configuration.Users.AnonymousUserToken.empty? or not Rails.configuration.Workbench.EnablePublicProjectsPage
+    @objects = using_specific_api_token Rails.configuration.Users.AnonymousUserToken do
       Group.where(group_class: 'project').order("modified_at DESC")
     end
   end
index d934af796509e2c172dcda8d2941daf889acd716..b27482c4cc47351bd474244205c8f60ea634f8ea 100644 (file)
@@ -202,7 +202,7 @@ class UsersController < ApplicationController
     respond_to do |format|
       if current_user.andand.is_admin
         setup_params = {}
-        setup_params[:send_notification_email] = "#{Rails.configuration.send_user_setup_notification_email}"
+        setup_params[:send_notification_email] = "#{Rails.configuration.Mail.SendUserSetupNotificationEmail}"
         if params['user_uuid'] && params['user_uuid'].size>0
           setup_params[:uuid] = params['user_uuid']
         end
index 19763b926c78077b97de261d49714d37c0c6b7bb..764571c4c5b635352439fa7d0e09f2d2431e39e9 100644 (file)
@@ -25,11 +25,14 @@ class VirtualMachinesController < ApplicationController
   end
 
   def webshell
-    return render_not_found if not Rails.configuration.shell_in_a_box_url
-    @webshell_url = Rails.configuration.shell_in_a_box_url % {
-      uuid: @object.uuid,
-      hostname: @object.hostname,
-    }
+    return render_not_found if Rails.configuration.Workbench.ShellInABoxURL == URI("")
+    webshell_url = URI(Rails.configuration.Workbench.ShellInABoxURL)
+    if webshell_url.host.index("*") != nil
+      webshell_url.host = webshell_url.host.sub("*", @object.hostname)
+    else
+      webshell_url.path = "/#{@object.hostname}"
+    end
+    @webshell_url = webshell_url.to_s
     render layout: false
   end
 
index 0f0033ce4965663ef76a7f2e479d8e38d7642dfb..1ecea99babce40e6755839889d0f6dad6ef26b18 100644 (file)
@@ -4,7 +4,7 @@
 
 class WorkUnitsController < ApplicationController
   skip_around_action :require_thread_api_token, if: proc { |ctrl|
-    Rails.configuration.anonymous_user_token and
+    !Rails.configuration.Users.AnonymousUserToken.empty? and
     'show_child_component' == ctrl.action_name
   }
 
index b7f99e855e69dab591e633d9a07a54baa6f03282..4d78ca7ed92f834086b91c6f83a04d31ea2e94b1 100644 (file)
@@ -4,7 +4,7 @@
 
 class WorkflowsController < ApplicationController
   skip_around_action :require_thread_api_token, if: proc { |ctrl|
-    Rails.configuration.anonymous_user_token and
+    !Rails.configuration.Users.AnonymousUserToken.empty? and
     'show' == ctrl.action_name
   }
 
index 83123b26c35c469e3efad7ec2833679f55445a39..0a872446d594d42f8485316663b437cbfab0c3c8 100644 (file)
@@ -12,11 +12,11 @@ module ApplicationHelper
   end
 
   def current_api_host
-    Rails.configuration.arvados_v1_base.gsub(/https?:\/\/|\/arvados\/v1/, '')
+    "#{Rails.configuration.Services.Controller.ExternalURL.hostname}:#{Rails.configuration.Services.Controller.ExternalURL.port}"
   end
 
   def current_uuid_prefix
-    current_api_host[0..4]
+    Rails.configuration.ClusterID
   end
 
   def render_markup(markup)
@@ -359,8 +359,8 @@ module ApplicationHelper
           display_value = link.name
         elsif value_info[:link_name]
           display_value = value_info[:link_name]
-        elsif value_info[:selection_name]
-          display_value = value_info[:selection_name]
+        elsif (sn = value_info[:selection_name]) && sn != ""
+          display_value = sn
         end
       end
       if (attr == :components) and (subattr.size > 2)
index 5eb1e8c768d927dfdc14e814143d2a945ed0c011..0c89ca8783c0c45b523a33c4c8fd495ce17f0c58 100644 (file)
@@ -72,7 +72,7 @@ module CollectionsHelper
     elsif (file_type.raw_media_type == "text") || (file_type.raw_media_type == "image")
       true
     elsif (file_type.raw_media_type == "application") &&
-          (Rails.configuration.application_mimetypes_with_view_icon.include? (file_type.sub_type))
+          Rails.configuration.Workbench.ApplicationMimetypesWithViewIcon[file_type.sub_type]
       true
     else
       false
index de07122c2fba3a86a169ad32c6a9ed62dc5e81e5..8066b0b5bb83112c4309e0958eae97a8899e5e92 100644 (file)
@@ -3,8 +3,8 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 class IssueReporter < ActionMailer::Base
-  default from: Rails.configuration.issue_reporter_email_from
-  default to: Rails.configuration.issue_reporter_email_to
+  default from: Rails.configuration.Mail.IssueReporterEmailFrom
+  default to: Rails.configuration.Mail.IssueReporterEmailTo
 
   def send_report(user, params)
     @user = user
index 8615cea3a86648ec88a28d0421c039852f470ff6..32de8d7821e2a9ce4430cb1abb475aedff7b76fb 100644 (file)
@@ -3,8 +3,8 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 class RequestShellAccessReporter < ActionMailer::Base
-  default from: Rails.configuration.email_from
-  default to: Rails.configuration.support_email_address
+  default from: Rails.configuration.Mail.EmailFrom
+  default to: Rails.configuration.Mail.SupportEmailAddress
 
   def send_request(user, params)
     @user = user
index 5a8fd518d386ec89125552c9fe17730e0488d4c4..3c9bfa793daa5a8ba143cbbf8fc40122654b6fff 100644 (file)
@@ -61,7 +61,7 @@ class ArvadosApiClient
     404 => NotFoundException,
   }
 
-  @@profiling_enabled = Rails.configuration.profiling_enabled
+  @@profiling_enabled = Rails.configuration.Workbench.ProfilingEnabled
   @@discovery = nil
 
   # An API client object suitable for handling API requests on behalf
@@ -89,10 +89,10 @@ class ArvadosApiClient
     if not @api_client
       @client_mtx.synchronize do
         @api_client = HTTPClient.new
-        @api_client.ssl_config.timeout = Rails.configuration.api_client_connect_timeout
-        @api_client.connect_timeout = Rails.configuration.api_client_connect_timeout
-        @api_client.receive_timeout = Rails.configuration.api_client_receive_timeout
-        if Rails.configuration.arvados_insecure_https
+        @api_client.ssl_config.timeout = Rails.configuration.Workbench.APIClientConnectTimeout
+        @api_client.connect_timeout = Rails.configuration.Workbench.APIClientConnectTimeout
+        @api_client.receive_timeout = Rails.configuration.Workbench.APIClientReceiveTimeout
+        if Rails.configuration.TLS.Insecure
           @api_client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
         else
           # Use system CA certificates
@@ -101,7 +101,7 @@ class ArvadosApiClient
             .select { |ca_path| File.readable?(ca_path) }
             .each { |ca_path| @api_client.ssl_config.add_trust_ca(ca_path) }
         end
-        if Rails.configuration.api_response_compression
+        if Rails.configuration.Workbench.APIResponseCompression
           @api_client.transparent_gzip_decompression = true
         end
       end
@@ -113,11 +113,13 @@ class ArvadosApiClient
     # Clean up /arvados/v1/../../discovery/v1 to /discovery/v1
     url.sub! '/arvados/v1/../../', '/'
 
+    anon_tokens = [Rails.configuration.Users.AnonymousUserToken].select { |x| !x.empty? && include_anon_token }
+
     query = {
       'reader_tokens' => ((tokens[:reader_tokens] ||
                            Thread.current[:reader_tokens] ||
                            []) +
-                          (include_anon_token ? [Rails.configuration.anonymous_user_token] : [])).to_json,
+                          anon_tokens).to_json,
     }
     if !data.nil?
       data.each do |k,v|
@@ -233,17 +235,13 @@ class ArvadosApiClient
   end
 
   def arvados_login_url(params={})
-    if Rails.configuration.respond_to? :arvados_login_base
-      uri = Rails.configuration.arvados_login_base
-    else
-      uri = self.arvados_v1_base.sub(%r{/arvados/v\d+.*}, '/login')
-    end
-    if params.size > 0
-      uri += '?' << params.collect { |k,v|
-        CGI.escape(k.to_s) + '=' + CGI.escape(v.to_s)
-      }.join('&')
+    uri = URI.parse(Rails.configuration.Services.Controller.ExternalURL.to_s)
+    if Rails.configuration.testing_override_login_url
+      uri = URI(Rails.configuration.testing_override_login_url)
     end
-    uri
+    uri.path = "/login"
+    uri.query = URI.encode_www_form(params)
+    uri.to_s
   end
 
   def arvados_logout_url(params={})
@@ -251,7 +249,11 @@ class ArvadosApiClient
   end
 
   def arvados_v1_base
-    Rails.configuration.arvados_v1_base
+    # workaround Ruby 2.3 bug, can't duplicate URI objects
+    # https://github.com/httprb/http/issues/388
+    u = URI.parse(Rails.configuration.Services.Controller.ExternalURL.to_s)
+    u.path = "/arvados/v1"
+    u.to_s
   end
 
   def discovery
index 6e8b68bd6e0bac1eb06a8501e3520b68bc6817ba..8b89d57ee904d92bc481d8b2b54dedf6c2cdb140 100644 (file)
@@ -76,9 +76,9 @@ class Repository < ArvadosBase
   # non-zero.
   def run_git *gitcmd
     if not @workdir
-      workdir = File.expand_path uuid+'.git', Rails.configuration.repository_cache
+      workdir = File.expand_path uuid+'.git', Rails.configuration.Workbench.RepositoryCache
       if not File.exists? workdir
-        FileUtils.mkdir_p Rails.configuration.repository_cache
+        FileUtils.mkdir_p Rails.configuration.Workbench.RepositoryCache
         [['git', 'init', '--bare', workdir],
         ].each do |cmd|
           system *cmd
@@ -94,7 +94,7 @@ class Repository < ArvadosBase
       '!cred(){ cat >/dev/null; if [ "$1" = get ]; then echo password=$ARVADOS_API_TOKEN; fi; };cred'],
      ['git', '--git-dir', @workdir, 'config', '--local',
            'http.sslVerify',
-           Rails.configuration.arvados_insecure_https ? 'false' : 'true'],
+           Rails.configuration.TLS.Insecure ? 'false' : 'true'],
      ].each do |cmd|
       system *cmd
       raise GitCommandError.new($?.to_s) unless $?.exitstatus == 0
index 0118390e5b6ad923727bdafef31728e3955766fa..18907ed71b6fe433dc06eb35b8f68a2799866891 100644 (file)
@@ -10,7 +10,7 @@ read ARVADOS_API_TOKEN &lt;&lt;EOF
 <%= Thread.current[:arvados_api_token] %>
 EOF
 export ARVADOS_API_TOKEN ARVADOS_API_HOST=<%= current_api_host %>
-<% if Rails.configuration.arvados_insecure_https %>
+<% if Rails.configuration.TLS.Insecure %>
 export ARVADOS_API_HOST_INSECURE=true
 <% else %>
 unset ARVADOS_API_HOST_INSECURE
index 7a2d08e54a1afa4faacb3041f0268eaec47f0094..c3c2e07da7ddb776c2a489918b19540bebc9b036 100644 (file)
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
                     <i class="glyphicon fa-fw glyphicon-search"></i> Search all projects ...
                   <% end %>
                </li>
-              <% if Rails.configuration.anonymous_user_token and Rails.configuration.enable_public_projects_page %>
+              <% if !Rails.configuration.Users.AnonymousUserToken.empty? and Rails.configuration.Workbench.EnablePublicProjectsPage %>
                 <li role="menuitem"><a href="/projects/public" role="menuitem"><i class="fa fa-fw fa-list"></i> Browse public projects </a>
                 </li>
               <% end %>
index ab0fd67d3a11cada3052dba88e29414229da6799..6027208b28272f2d01fc3936258e3088d1fd4a4a 100644 (file)
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
 
 or
 
-<%= mail_to(Rails.configuration.support_email_address, "email us",
+<%= mail_to(Rails.configuration.Mail.SupportEmailAddress, "email us",
             subject: "Workbench problem report",
             body: "Problem while viewing page #{request.url}") %>
 
index 8823fdd5f78f8ebfe7c4a336c3a144bc479de26f..3dc332651fd40aaa17c2104833e0257bdb02f13d 100644 (file)
@@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0 %>
 
 <%
   generated_at = arvados_api_client.discovery[:generatedAt]
-  arvados_base = Rails.configuration.arvados_v1_base
-  support_email = Rails.configuration.support_email_address
+  arvados_base = Rails.configuration.Services.Controller.ExternalURL.to_s + "/arvados/v1"
+  support_email = Rails.configuration.Mail.SupportEmailAddress
 
   additional_info = {}
   additional_info['Current location'] = params[:current_location]
index 8403ee0b4fe20345d92cbf2e30fe843150b6d635..7877e60d3018096bdc3ad6b9ef4c4bd892631925 100644 (file)
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
    choose_filters = {
      "groups" => [["group_class", "=", "role"]],
    }
-   if not Rails.configuration.anonymous_user_token
+   if Rails.configuration.Users.AnonymousUserToken.empty?
      # It would be ideal to filter out the anonymous group by UUID,
      # but that's not readily doable.  Workbench can't generate the
      # UUID for a != filter, because it can't introspect the API
index 8a2ce6b8268195605ba370153c1292be0a2a0c03..a82d2556e7c94a2be04d376447016635ff92e5f7 100644 (file)
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
 <head>
   <meta charset="utf-8">
   <title>
-    <%= coll_name %> / <%= Rails.configuration.site_name %>
+    <%= coll_name %> / <%= Rails.configuration.Workbench.SiteName %>
   </title>
   <meta name="description" content="">
   <meta name="author" content="">
index fa75ec26e27cad11f8d896efe8867e63977e041d..99880f2416e4a8a88371e2c06c76cf904b38900f 100644 (file)
@@ -98,7 +98,7 @@ div.figure p {
                 <li>
                   Also known as a “workflow” in other systems
                 </li><li>
-                  A list of well-documented public pipelines can be found in the upper right corner by clicking the <span class="fa fa-lg fa-question-circle"></span> &gt; <a href="<%= Rails.configuration.arvados_public_data_doc_url %>">Public Pipelines and Datasets</a>
+                  A list of well-documented public pipelines can be found in the upper right corner by clicking the <span class="fa fa-lg fa-question-circle"></span> &gt; <a href="<%= Rails.configuration.Workbench.ArvadosPublicDataDocURL %>">Public Pipelines and Datasets</a>
                 </li><li>
                   Pro-tip: A Pipeline contains Jobs which contain Tasks
                 </li><li>
index e84641d431205cdf816822836071183a61009822..10b7fa1c4eec3090c47a7ce60bacaf87b22ec8b8 100644 (file)
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
 <pre id="event_log_div"
      class="arv-log-event-listener arv-log-event-handler-append-logs arv-job-log-window"
      data-object-uuid="<%= @object.uuid %>"
-  ><%= @object.stderr_log_lines(Rails.configuration.running_job_log_records_to_fetch).join("\n") %>
+  ><%= @object.stderr_log_lines(Rails.configuration.Workbench.RunningJobLogRecordsToFetch).join("\n") %>
 </pre>
 
 <%# Applying a long throttle suppresses the auto-refresh of this
@@ -68,7 +68,7 @@ var makeFilter = function() {
   <% logcollection = Collection.find @object.log %>
   <% if logcollection %>
     var log_size = <%= logcollection.files[0][2] %>
-    var log_maxbytes = <%= Rails.configuration.log_viewer_max_bytes %>;
+    var log_maxbytes = <%= Rails.configuration.Workbench.LogViewerMaxBytes %>;
     var logcollection_url = '<%=j url_for logcollection %>/<%=j logcollection.files[0][1] %>';
     $("#log-viewer-download-url").attr('href', logcollection_url);
     $("#log-viewer-download-pane").show();
index 638ee8970c35a5b5af83528d7b3e650e0e487cc3..bd3afbb681f098a1f3fe726e7d9bc49f671240ac 100644 (file)
@@ -8,9 +8,9 @@ SPDX-License-Identifier: AGPL-3.0 %>
   <meta charset="utf-8">
   <title>
     <% if content_for? :page_title %>
-    <%= yield :page_title %> / <%= Rails.configuration.site_name %>
+    <%= yield :page_title %> / <%= Rails.configuration.Workbench.SiteName %>
     <% else %>
-    <%= Rails.configuration.site_name %>
+    <%= Rails.configuration.Workbench.SiteName %>
     <% end %>
   </title>
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
   <meta name="author" content="">
   <% if current_user %>
     <% content_for :js do %>
-      window.defaultSession = <%=raw({baseURL: Rails.configuration.arvados_v1_base.sub(/\/arvados\/v1$/, '/'), token: Thread.current[:arvados_api_token], user: current_user}.to_json)%>
+      window.defaultSession = <%=raw({baseURL: Rails.configuration.Services.Controller.ExternalURL.to_s, token: Thread.current[:arvados_api_token], user: current_user}.to_json)%>
     <% end %>
   <% end %>
   <% if current_user and $arvados_api_client.discovery[:websocketUrl] %>
@@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
       social graph that search engines can use. http://ogp.me/ %>
   <meta property="og:type" content="article" />
   <meta property="og:url" content="<%= request.url %>" />
-  <meta property="og:site_name" content="<%= Rails.configuration.site_name %>" />
+  <meta property="og:site_name" content="<%= Rails.configuration.Workbench.SiteName %>" />
   <% if defined?(@object) && @object %>
     <% if @object.respond_to?(:name) and @object.name.present? %>
       <meta property="og:title" content="<%= @object.name%>" />
@@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
   <%= yield :head %>
   <%= javascript_tag do %>
     angular.module('Arvados').value('arvadosApiToken', '<%=Thread.current[:arvados_api_token]%>');
-    angular.module('Arvados').value('arvadosDiscoveryUri', '<%= Rails.configuration.arvados_v1_base.sub '/arvados/v1', '/discovery/v1/apis/arvados/v1/rest' %>');
+    angular.module('Arvados').value('arvadosDiscoveryUri', '<%= Rails.configuration.Services.Controller.ExternalURL.to_s + '/discovery/v1/apis/arvados/v1/rest' %>');
   <%= yield :js %>
   <% end %>
   <style>
index b2cd097f3174c03fa49c7d1f280d7c809ecf280c..9da55cbeb3fedd36a3add088ab8a9d0e29607ed1 100644 (file)
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
           <span class="icon-bar"></span>
           <span class="icon-bar"></span>
         </button>
-        <% site_name = Rails.configuration.site_name.downcase rescue Rails.application.class.parent_name %>
+        <% site_name = Rails.configuration.Workbench.SiteName.downcase rescue Rails.application.class.parent_name %>
         <% if current_user %>
           <a class="navbar-brand" href="/" data-push=true><%= site_name %></a>
         <% else %>
@@ -28,12 +28,12 @@ SPDX-License-Identifier: AGPL-3.0 %>
 
           <% if current_user %>
             <% if current_user.is_active %>
-              <% if Rails.configuration.multi_site_search %>
+              <% if !Rails.configuration.Workbench.MultiSiteSearch.empty? %>
                 <li>
                   <form class="navbar-form">
                     <%=
-                       target = Rails.configuration.multi_site_search
-                       if target == true
+                       target = Rails.configuration.Workbench.MultiSiteSearch
+                       if target == "true"
                          target = {controller: 'search', action: 'index'}
                        end
                        link_to("Multi-site search", target, {class: 'btn btn-default'}) %>
@@ -72,9 +72,9 @@ SPDX-License-Identifier: AGPL-3.0 %>
                 </li>
                 <% if current_user.is_active %>
                 <li role="menuitem"><a href="/projects/<%=current_user.uuid%>" role="menuitem"><i class="fa fa-lg fa-home fa-fw"></i> Home project </a></li>
-                  <% if Rails.configuration.composer_url %>
+                  <% if Rails.configuration.Services.Composer.ExternalURL != URI("") %>
                     <li role="menuitem">
-                     <form action="<%= Rails.configuration.composer_url %>" method="GET">
+                     <form action="<%= Rails.configuration.Services.Composer.ExternalURL.to_s %>" method="GET">
                        <input type="hidden" name="api_token" value="<%= Thread.current[:arvados_api_token] %>" />
                        <button role="menuitem" type="submit">
                          <i class="fa fa-lg fa-share-alt fa-fw"></i> Workflow Composer
@@ -82,10 +82,10 @@ SPDX-License-Identifier: AGPL-3.0 %>
                      </form>
                     </li>
                   <% end %>
-                <% if Rails.configuration.workbench2_url %>
+                <% if Rails.configuration.Services.Workbench2.ExternalURL != URI("") %>
                 <li role="menuitem">
                   <%
-                    wb2_url = Rails.configuration.workbench2_url
+                    wb2_url = Rails.configuration.Services.Workbench2.ExternalURL.to_s
                     wb2_url += '/' if wb2_url[-1] != '/'
                     wb2_url += 'token'
                   %>
@@ -102,7 +102,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
                     <i class="fa fa-lg fa-terminal fa-fw"></i> Virtual machines
                   <% end %>
                 </li>
-                <% if Rails.configuration.repositories %>
+                <% if Rails.configuration.Workbench.Repositories %>
                 <li role="menuitem"><a href="/repositories" role="menuitem"><i class="fa fa-lg fa-code-fork fa-fw"></i> Repositories </a></li>
                 <% end -%>
                 <li role="menuitem"><a href="/current_token" role="menuitem"><i class="fa fa-lg fa-ticket fa-fw"></i> Current token</a></li>
@@ -112,7 +112,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
                   <% end %>
 </li>
                 <li role="menuitem"><a href="/users/link_account" role="menuitem"><i class="fa fa-lg fa-link fa-fw"></i> Link account </a></li>
-                <% if Rails.configuration.user_profile_form_fields %>
+                <% if !Rails.configuration.Workbench.UserProfileFormFields.empty? %>
                   <li role="menuitem"><a href="/users/<%=current_user.uuid%>/profile" role="menuitem"><i class="fa fa-lg fa-user fa-fw"></i> Manage profile</a></li>
                 <% end %>
                 <% end %>
@@ -138,7 +138,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
                   <li role="presentation" class="dropdown-header">
                     Admin Settings
                   </li>
-                  <% if Rails.configuration.repositories %>
+                  <% if Rails.configuration.Workbench.Repositories %>
                   <li role="menuitem"><a href="/repositories">
                       <i class="fa fa-lg fa-code-fork fa-fw"></i> Repositories
                   </a></li>
@@ -174,7 +174,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
               </li>
             <% end %>
           <% else %>
-            <% if Rails.configuration.anonymous_user_token and Rails.configuration.enable_public_projects_page %>
+            <% if !Rails.configuration.Users.AnonymousUserToken.empty? and Rails.configuration.Workbench.EnablePublicProjectsPage %>
               <li><%= link_to 'Browse public projects', "/projects/public" %></li>
             <% end %>
             <li class="dropdown hover-dropdown login-menu">
@@ -199,18 +199,18 @@ SPDX-License-Identifier: AGPL-3.0 %>
               <li role="presentation" class="dropdown-header">
                 Help
               </li>
-              <% if Rails.configuration.enable_getting_started_popup %>
+              <% if Rails.configuration.Workbench.EnableGettingStartedPopup %>
                 <li>
                 <%= link_to raw('<i class="fa fa-fw fa-info"></i> Getting Started ...'), "#",
                      {'data-toggle' => "modal", 'data-target' => '#getting-started-modal-window'}  %>
                 </li>
               <% end %>
-              <% if Rails.configuration.arvados_public_data_doc_url %>
-                <li><%= link_to raw('<i class="fa fa-book fa-fw"></i> Public Pipelines and Data sets'), "#{Rails.configuration.arvados_public_data_doc_url}", target: "_blank" %></li>
+              <% if !Rails.configuration.Workbench.ArvadosPublicDataDocURL.empty? %>
+                <li><%= link_to raw('<i class="fa fa-book fa-fw"></i> Public Pipelines and Data sets'), "#{Rails.configuration.Workbench.ArvadosPublicDataDocURL}", target: "_blank" %></li>
               <% end %>
-              <li><%= link_to raw('<i class="fa fa-book fa-fw"></i> Tutorials and User guide'), "#{Rails.configuration.arvados_docsite}/user", target: "_blank" %></li>
-              <li><%= link_to raw('<i class="fa fa-book fa-fw"></i> API Reference'), "#{Rails.configuration.arvados_docsite}/api", target: "_blank" %></li>
-              <li><%= link_to raw('<i class="fa fa-book fa-fw"></i> SDK Reference'), "#{Rails.configuration.arvados_docsite}/sdk", target: "_blank" %></li>
+              <li><%= link_to raw('<i class="fa fa-book fa-fw"></i> Tutorials and User guide'), "#{Rails.configuration.Workbench.ArvadosDocsite}/user", target: "_blank" %></li>
+              <li><%= link_to raw('<i class="fa fa-book fa-fw"></i> API Reference'), "#{Rails.configuration.Workbench.ArvadosDocsite}/api", target: "_blank" %></li>
+              <li><%= link_to raw('<i class="fa fa-book fa-fw"></i> SDK Reference'), "#{Rails.configuration.Workbench.ArvadosDocsite}/sdk", target: "_blank" %></li>
               <li role="presentation" class="divider"></li>
               <li> <%= link_to report_issue_popup_path(popup_type: 'version', current_location: request.url, current_path: request.fullpath, action_method: 'post'),
                       {class: 'report-issue-modal-window', remote: true, return_to: request.url} do %>
@@ -264,7 +264,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
 <div id="report-issue-modal-window"></div>
 <script src="/browser_unsupported.js"></script>
 
-<%  if Rails.configuration.enable_getting_started_popup and current_user and !current_user.prefs[:getting_started_shown] and
+<%  if Rails.configuration.Workbench.EnableGettingStartedPopup and current_user and !current_user.prefs[:getting_started_shown] and
        !request.url.include?("/profile") and
        !request.url.include?("/user_agreements") and
        !request.url.include?("/inactive")%>
index 22643bf5a10dfe03bdc36db7724d733d5ac1944f..7769046cf950b7564fd6889a6209462a05080577 100644 (file)
@@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0 %>
 
   <%= image_tag "dax.png", class: "dax" %>
   <p>
-    Hi, I noticed you haven't uploaded a new collection yet. 
-    <%= link_to "Click here to learn how to upload data to Arvados Keep.", 
-       "#{Rails.configuration.arvados_docsite}/user/tutorials/tutorial-keep.html", 
+    Hi, I noticed you haven't uploaded a new collection yet.
+    <%= link_to "Click here to learn how to upload data to Arvados Keep.",
+       "#{Rails.configuration.Workbench.ArvadosDocsite}/user/tutorials/tutorial-keep.html",
        style: "font-weight: bold", target: "_blank" %>
   </p>
index d9cc7a667e3b9b260dbc3977dbd44feb767b9558..d793ea0b2b56ce005b211647b2b7d803975d841c 100644 (file)
@@ -3,10 +3,9 @@
 SPDX-License-Identifier: AGPL-3.0 %>
 
   <p><%= image_tag "dax.png", class: "dax" %>
-    Hi, I noticed you haven't run a job yet. 
-    <%= link_to "Click here to learn how to run an Arvados Crunch job.", 
-       "#{Rails.configuration.arvados_docsite}/user/tutorials/tutorial-job1.html", 
+    Hi, I noticed you haven't run a job yet.
+    <%= link_to "Click here to learn how to run an Arvados Crunch job.",
+       "#{Rails.configuration.Workbench.ArvadosDocsite}/user/tutorials/tutorial-job1.html",
        style: "font-weight: bold",
        target: "_blank" %>
   </p>
-
index e70fc59d40bf2402a48e56d823ceb6298e8c008b..b275ed8eaa217cad8925d59f193ef6a08d5b1c79 100644 (file)
@@ -3,9 +3,9 @@
 SPDX-License-Identifier: AGPL-3.0 %>
 
   <p><%= image_tag "dax.png", class: "dax" %>
-    Hi, I noticed you haven't run a pipeline yet.  
-    <%= link_to "Click here to learn how to run an Arvados Crunch pipeline.", 
-       "#{Rails.configuration.arvados_docsite}/user/tutorials/tutorial-pipeline-workbench.html",
+    Hi, I noticed you haven't run a pipeline yet.
+    <%= link_to "Click here to learn how to run an Arvados Crunch pipeline.",
+       "#{Rails.configuration.Workbench.ArvadosDocsite}/user/tutorials/tutorial-pipeline-workbench.html",
        style: "font-weight: bold",
        target: "_blank" %>
   </p>
index 22d89fff6d8e9133b310f9a6efcc2a4d22040c69..71ef2454190d7625cae12cf77d44f05a1ad7ed27 100644 (file)
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
   collection_pdhs = outputs.select {|x| !(m = CollectionsHelper.match(x)).nil?}.uniq.compact
   collection_uuids = outputs - collection_pdhs
 
-  if Rails.configuration.show_recent_collections_on_dashboard
+  if Rails.configuration.Workbench.ShowRecentCollectionsOnDashboard
     recent_cs = recent_collections(8)
     collection_uuids = collection_uuids + recent_cs[:collections].collect {|c| c.uuid}
     collection_uuids.flatten.uniq
@@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
     show_node_status = false
     # Recent processes panel should take the entire width when is the only one
     # being rendered.
-    if !Rails.configuration.show_recent_collections_on_dashboard
+    if !Rails.configuration.Workbench.ShowRecentCollectionsOnDashboard
       recent_procs_panel_width = 12
     end
   else
@@ -197,7 +197,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
        </div>
       </div>
       <% end %>
-      <% if Rails.configuration.show_recent_collections_on_dashboard %>
+      <% if Rails.configuration.Workbench.ShowRecentCollectionsOnDashboard %>
       <div class="panel panel-default">
         <div class="panel-heading"><span class="panel-title">Recent collections</span>
           <span class="pull-right">
index 5904fb29db8a513d05f0d9532cf666b650c1bb49..4a7b45491183a9bec056ded0f3220594f63b5df4 100644 (file)
@@ -28,10 +28,10 @@ git push
 <p>
   See also:
   <%= link_to raw('Arvados Docs &rarr; User Guide &rarr; SSH access'),
-  "#{Rails.configuration.arvados_docsite}/user/getting_started/ssh-access-unix.html",
-      target: "_blank"%> and 
+  "#{Rails.configuration.Workbench.ArvadosDocsite}/user/getting_started/ssh-access-unix.html",
+      target: "_blank"%> and
   <%= link_to raw('Arvados Docs &rarr; User Guide &rarr; Writing a Crunch
   Script'),
-  "#{Rails.configuration.arvados_docsite}/user/tutorials/tutorial-firstscript.html",
+  "#{Rails.configuration.Workbench.ArvadosDocsite}/user/tutorials/tutorial-firstscript.html",
   target: "_blank"%>.
 </p>
index 5f70c4735b69c4ffd3fdf28c2e048ef1c45e9869..d52ad649c94853865aea6d606d66fbc8e3131d6b 100644 (file)
@@ -19,7 +19,7 @@ User agreements
   <div class="alert alert-info">
     <strong>Please check <%= n_files > 1 ? 'each' : 'the' %> box below</strong> to indicate that you have read and accepted the user agreement<%= 's' if n_files > 1 %>.
   </div>
-  <% if n_files == 1 and (Rails.configuration.show_user_agreement_inline rescue false) %>
+  <% if n_files == 1 and (Rails.configuration.Workbench.ShowUserAgreementInline rescue false) %>
   <% ua = unsigned_user_agreements.first; file = ua.files.first %>
   <object data="<%= url_for(controller: 'collections', action: 'show_file', uuid: ua.uuid, file: "#{file[0]}/#{file[1]}") %>" type="<%= Rack::Mime::MIME_TYPES[file[1].match(/\.\w+$/)[0]] rescue '' %>" width="100%" height="400px">
   </object>
index 5abaf155df6a0f90be733ad8c5599352fe450342..1d0814c2a01549f0e2fc840a45c990d36d0de0f3 100644 (file)
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
 
       <div class="modal-body">
         <div> <%= link_to "Click here to learn about SSH keys in Arvados.",
-                  "#{Rails.configuration.arvados_docsite}/user/getting_started/ssh-access-unix.html",
+                  "#{Rails.configuration.Workbench.ArvadosDocsite}/user/getting_started/ssh-access-unix.html",
                   style: "font-weight: bold",
                   target: "_blank" %>
         </div>
index deab2d722448b2000776cf335609b4932e1e5882..6b1ec33a2666545e7b2b71a6e012788a6caee995 100644 (file)
@@ -12,14 +12,14 @@ SPDX-License-Identifier: AGPL-3.0 %>
   </div>
 
 <div id="#manage_current_token" class="panel-body">
-<p>The Arvados API token is a secret key that enables the Arvados SDKs to access Arvados with the proper permissions. For more information see <%= link_to raw('Getting an API token'), "#{Rails.configuration.arvados_docsite}/user/reference/api-tokens.html", target: "_blank"%>.</p>
+<p>The Arvados API token is a secret key that enables the Arvados SDKs to access Arvados with the proper permissions. For more information see <%= link_to raw('Getting an API token'), "#{Rails.configuration.Workbench.ArvadosDocsite}/user/reference/api-tokens.html", target: "_blank"%>.</p>
 <p>Paste the following lines at a shell prompt to set up the necessary environment for Arvados SDKs to authenticate to your <b><%= current_user.username %></b> account.</p>
 
 <pre>
 HISTIGNORE=$HISTIGNORE:'export ARVADOS_API_TOKEN=*'
 export ARVADOS_API_TOKEN=<%= Thread.current[:arvados_api_token] %>
 export ARVADOS_API_HOST=<%= current_api_host %>
-<% if Rails.configuration.arvados_insecure_https %>
+<% if Rails.configuration.TLS.Insecure %>
 export ARVADOS_API_HOST_INSECURE=true
 <% else %>
 unset ARVADOS_API_HOST_INSECURE
index 3b3794bf50a8b791e23ef006d623c5ff121ca66a..d6f25136c438a39c6eb9659c198effec62afbd69 100644 (file)
@@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
                value="<%=identity_url_prefix%>" disabled=true>
         <% else %>
         <input class="form-control" id="openid_prefix" maxlength="250" name="openid_prefix" type="text"
-               value="<%= Rails.configuration.default_openid_prefix %>">
+               value="<%= Rails.configuration.Workbench.DefaultOpenIdPrefix %>">
         <% end %>
       </div>
       <div class="form-group">
index 8d2f51364e4d0aab5b80a796aebc0289ce5d1098..fa26bc4aa1af8f379edb8d73c7026475a5d7c149 100644 (file)
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
 <div id="manage_ssh_keys" class="panel-body">
   <% if !@my_ssh_keys.any? %>
      <p> You have not yet set up an SSH public key for use with Arvados. <%= link_to "Learn more.",
-                  "#{Rails.configuration.arvados_docsite}/user/getting_started/ssh-access-unix.html",
+                  "#{Rails.configuration.Workbench.ArvadosDocsite}/user/getting_started/ssh-access-unix.html",
                   style: "font-weight: bold",
                   target: "_blank" %>
      </p>
index 5667951124378a8a34990138529935627e4b7946..01a77cdd6188fd35ea409e9c8e90c7f5babb29b4 100644 (file)
@@ -251,7 +251,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
           Your account must be activated by an Arvados administrator.  If this
           is your first time accessing Arvados and would like to request
           access, or you believe you are seeing the page in error, please
-          <%= link_to "contact us", Rails.configuration.activation_contact_link %>.
+          <%= link_to "contact us", Rails.configuration.Workbench.ActivationContactLink %>.
           You should receive an email at the address you used to log in when
           your account is activated.  In the mean time, you can
           <%= link_to "learn more about Arvados", "https://arvados.org/projects/arvados/wiki/Introduction_to_Arvados" %>,
@@ -259,7 +259,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
         </p>
         <p style="padding-bottom: 1em">
           <%= link_to raw('Contact us &#x2709;'),
-              Rails.configuration.activation_contact_link, class: "pull-right btn btn-primary" %></p>
+              Rails.configuration.Workbench.ActivationContactLink, class: "pull-right btn btn-primary" %></p>
       </div>
     </div>
   </div>
index c891b0c594af329b9f0a7790217596b11b0109fc..026f016f8cd7186ebcc6eb39e123ae0dcbb5e9dc 100644 (file)
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
 <div id="manage_virtual_machines" class="panel-body">
   <p>
     For more information see <%= link_to raw('Arvados Docs &rarr; User Guide &rarr; VM access'),
-  "#{Rails.configuration.arvados_docsite}/user/getting_started/vm-login-with-webshell.html",
+  "#{Rails.configuration.Workbench.ArvadosDocsite}/user/getting_started/vm-login-with-webshell.html",
   target: "_blank"%>.
   </p>
 
@@ -68,7 +68,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
           <th> Host name </th>
           <th> Login name </th>
           <th> Command line </th>
-          <% if Rails.configuration.shell_in_a_box_url %>
+          <% if Rails.configuration.Services.WebShell.ExternalURL != URI("") %>
             <th> Web shell <span class="label label-info">beta</span></th>
           <% end %>
         </tr>
@@ -89,7 +89,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
                 <% end %>
               <% end %>
             </td>
-            <% if Rails.configuration.shell_in_a_box_url %>
+            <% if Rails.configuration.Services.WebShell.ExternalURL != URI("") %>
               <td>
                 <% @my_vm_logins[vm[:uuid]].andand.each do |login| %>
                   <%= link_to webshell_virtual_machine_path(vm, login: login), title: "Open a terminal session in your browser", class: 'btn btn-xs btn-default', target: "_blank" do %>
index 26d1f57211b257a524461dd70f049a3e2afd9bf6..6692196dabf717e40defd77e9c6c0c2538d3c393 100644 (file)
@@ -3,11 +3,18 @@
 SPDX-License-Identifier: AGPL-3.0 %>
 
 <%
-    profile_config = Rails.configuration.user_profile_form_fields
+    profile_config = []
+    Rails.configuration.Workbench.UserProfileFormFields.each do |k, v|
+      r = v.dup
+      r["Key"] = k
+      profile_config << r
+    end
+    profile_config.sort_by! { |v| v["Position"] }
+
     current_user_profile = current_user.prefs[:profile]
     show_save_button = false
 
-    profile_message = Rails.configuration.user_profile_form_message
+    profile_message = Rails.configuration.Workbench.UserProfileFormMessage
 %>
 
 <div>
@@ -61,29 +68,29 @@ SPDX-License-Identifier: AGPL-3.0 %>
               </div>
 
               <% profile_config.kind_of?(Array) && profile_config.andand.each do |entry| %>
-                <% if entry['key'] %>
+                <% if entry['Key'] %>
                   <%
                       show_save_button = true
-                      label = entry['required'] ? '* ' : ''
-                      label += entry['form_field_title']
-                      value = current_user_profile[entry['key'].to_sym] if current_user_profile
+                      label = entry['Required'] ? '* ' : ''
+                      label += entry['FormFieldTitle']
+                      value = current_user_profile[entry['Key'].to_sym] if current_user_profile
                   %>
                   <div class="form-group">
-                    <label for="<%=entry['key']%>"
+                    <label for="<%=entry['Key']%>"
                            class="col-sm-3 control-label"
-                           style=<%="color:red" if entry['required']&&(!value||value.empty?)%>> <%=label%>
+                           style=<%="color:red" if entry['Required']&&(!value||value.empty?)%>> <%=label%>
                     </label>
-                    <% if entry['type'] == 'select' %>
+                    <% if entry['Type'] == 'select' %>
                       <div class="col-sm-8">
-                        <select class="form-control" name="user[prefs][profile][<%=entry['key']%>]">
-                          <% entry['options'].each do |option| %>
+                        <select class="form-control" name="user[prefs][profile][<%=entry['Key']%>]">
+                          <% entry['Options'].each do |option, _| %>
                             <option value="<%=option%>" <%='selected' if option==value%>><%=option%></option>
                           <% end %>
                         </select>
                       </div>
                     <% else %>
                       <div class="col-sm-8">
-                        <input type="text" class="form-control" name="user[prefs][profile][<%=entry['key']%>]" placeholder="<%=entry['form_field_description']%>" value="<%=value%>" ></input>
+                        <input type="text" class="form-control" name="user[prefs][profile][<%=entry['Key']%>]" placeholder="<%=entry['FormFieldDescription']%>" value="<%=value%>" ></input>
                       </div>
                     <% end %>
                   </div>
index b10ca8b15de8f8c6778dad98050bb978d0599de3..479e3e1d89dec50c5d6398ad3e7dc470be211d8c 100644 (file)
@@ -16,17 +16,17 @@ SPDX-License-Identifier: AGPL-3.0 %>
         The "Log in" button below will show you a Google sign-in page.
         After you assure Google that you want to log in here with your
         Google account, you will be redirected back here to
-        <%= Rails.configuration.site_name %>.
+        <%= Rails.configuration.Workbench.SiteName %>.
 
       </p><p>
 
-        If you have never used <%= Rails.configuration.site_name %>
+        If you have never used <%= Rails.configuration.Workbench.SiteName %>
         before, logging in for the first time will automatically
         create a new account.
 
       </p><p>
 
-        <i><%= Rails.configuration.site_name %> uses your name and
+        <i><%= Rails.configuration.Workbench.SiteName %> uses your name and
           email address only for identification, and does not retrieve
           any other personal information from Google.</i>
 
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
             now, don't provide 'auth_provider' to get the default one. %>
         <div class="pull-right">
           <%= link_to arvados_api_client.arvados_login_url(return_to: request.url), class: "btn btn-primary" do %>
-          Log in to <%= Rails.configuration.site_name %>
+          Log in to <%= Rails.configuration.Workbench.SiteName %>
           <i class="fa fa-fw fa-arrow-circle-right"></i>
           <% end %>
         </div>
index 13916572cef9d8bd381809e1f8928504a9310995..204e71a9147e6b9e5317e8882192800d6178af44 100644 (file)
@@ -25,6 +25,6 @@ ssh <%= @objects.first.andand.hostname.andand.sub('.'+current_api_host,'') or 'v
 <p>
   See also:
   <%= link_to raw('Arvados Docs &rarr; User Guide &rarr; SSH access'),
-  "#{Rails.configuration.arvados_docsite}/user/getting_started/ssh-access-unix.html",
+  "#{Rails.configuration.Workbench.ArvadosDocsite}/user/getting_started/ssh-access-unix.html",
   target: "_blank"%>.
 </p>
index 202ae70fb0083a8fe722267a063c96efdf0ec5ba..4c63115a1669cb389cf9c97d77fa6fef75a056b3 100644 (file)
@@ -3,7 +3,7 @@
 SPDX-License-Identifier: AGPL-3.0 %>
 
 <html>
-    <title><%= @object.hostname %> / <%= Rails.configuration.site_name %></title>
+    <title><%= @object.hostname %> / <%= Rails.configuration.Workbench.SiteName %></title>
     <link rel="stylesheet" href="<%= asset_path 'webshell/styles.css' %>" type="text/css">
     <style type="text/css">
       body {
index 1f643acdc9ff6a1dc042a3de86f6ab9a78357481..d2c565750c9f76e86b47c390bd63cf3b15df4d79 100644 (file)
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
   </div>
 <% end %>
 
-<% live_log_lines = wu.live_log_lines(Rails.configuration.running_job_log_records_to_fetch).join("\n") %>
+<% live_log_lines = wu.live_log_lines(Rails.configuration.Workbench.RunningJobLogRecordsToFetch).join("\n") %>
 <% if !render_log or (live_log_lines.size > 0) %>
 <%# Still running, or recently finished and logs are still available from logs table %>
 <%# Show recent logs in terminal window %>
index d38742248b52fac025d72bf95f89c86e91c29380..8d1dff995248d9f98feaea4b77ff5ba723b0a738 100644 (file)
@@ -53,12 +53,6 @@ development:
   action_dispatch.best_standards_support: :builtin
   assets.debug: true
   profiling_enabled: true
-  site_name: Arvados Workbench (dev)
-
-  # API server configuration
-  arvados_login_base: ~
-  arvados_v1_base: ~
-  arvados_insecure_https: ~
 
 production:
   force_ssl: true
@@ -73,18 +67,6 @@ production:
   profiling_enabled: false
   log_level: info
 
-  arvados_insecure_https: false
-
-  data_import_dir: /data/arvados-workbench-upload/data
-  data_export_dir: /data/arvados-workbench-download/data
-
-  # API server configuration
-  arvados_login_base: ~
-  arvados_v1_base: ~
-  arvados_insecure_https: ~
-
-  site_name: Arvados Workbench
-
 test:
   cache_classes: true
   eager_load: false
@@ -130,67 +112,11 @@ test:
         - IT
         - Other
 
+  repository_cache: <%= File.expand_path 'tmp/git', Rails.root %>
+
 common:
   assets.js_compressor: false
   assets.css_compressor: false
-  data_import_dir: /tmp/arvados-workbench-upload
-  data_export_dir: /tmp/arvados-workbench-download
-  arvados_login_base: https://arvados.local/login
-  arvados_v1_base: https://arvados.local/arvados/v1
-  arvados_insecure_https: true
-  activation_contact_link: mailto:info@arvados.org
-  arvados_docsite: http://doc.arvados.org
-  arvados_public_data_doc_url: http://arvados.org/projects/arvados/wiki/Public_Pipelines_and_Datasets
-  arvados_theme: default
-  show_user_agreement_inline: false
-  secret_token: ~
-  secret_key_base: false
-  default_openid_prefix: https://www.google.com/accounts/o8/id
-  send_user_setup_notification_email: true
-
-  # Scratch directory used by the remote repository browsing
-  # feature. If it doesn't exist, it (and any missing parents) will be
-  # created using mkdir_p.
-  repository_cache: <%= File.expand_path 'tmp/git', Rails.root %>
-
-  # Set user_profile_form_fields to enable and configure the user
-  # profile page. Default is set to false. A commented example with
-  # full description is provided below.
-  user_profile_form_fields: false
-
-  # Below is a sample setting of user_profile_form_fields config parameter.
-  # This configuration parameter should be set to either false (to disable) or
-  # to an array as shown below.
-  # Configure the list of input fields to be displayed in the profile page
-  # using the attribute "key" for each of the input fields.
-  # This sample shows configuration with one required and one optional form fields.
-  # For each of these input fields:
-  #   You can specify "type" as "text" or "select".
-  #   List the "options" to be displayed for each of the "select" menu.
-  #   Set "required" as "true" for any of these fields to make them required.
-  # If any of the required fields are missing in the user's profile, the user will be
-  # redirected to the profile page before they can access any Workbench features.
-  #user_profile_form_fields:
-  #  - key: organization
-  #    type: text
-  #    form_field_title: Institution/Company
-  #    form_field_description: Your organization
-  #    required: true
-  #  - key: role
-  #    type: select
-  #    form_field_title: Your role
-  #    form_field_description: Choose the category that best describes your role in your organization.
-  #    options:
-  #      - Bio-informatician
-  #      - Computational biologist
-  #      - Biologist or geneticist
-  #      - Software developer
-  #      - IT
-  #      - Other
-
-  # Use "user_profile_form_message" to configure the message you want to display on
-  # the profile page.
-  user_profile_form_message: Welcome to Arvados. All <span style="color:red">required fields</span> must be completed before you can proceed.
 
   # Override the automatic version string. With the default value of
   # false, the version string is read from git-commit.version in
@@ -203,131 +129,5 @@ common:
   # Rails.root (included in vendor packages).
   package_version: false
 
-  # report notification to and from addresses
-  issue_reporter_email_from: arvados@example.com
-  issue_reporter_email_to: arvados@example.com
-  support_email_address: arvados@example.com
-
-  # generic issue email from
-  email_from: arvados@example.com
-
-  # Mimetypes of applications for which the view icon
-  # would be enabled in a collection's show page.
-  # It is sufficient to list only applications here.
-  # No need to list text and image types.
-  application_mimetypes_with_view_icon: [cwl, fasta, go, javascript, json, pdf, python, x-python, r, rtf, sam, x-sh, vnd.realvnc.bed, xml, xsl]
-
-  # the maximum number of bytes to load in the log viewer
-  log_viewer_max_bytes: 1000000
-
-  # Set anonymous_user_token to enable anonymous user access. You can get
-  # the token by running "bundle exec ./script/get_anonymous_user_token.rb"
-  # in the directory where your API server is running.
-  anonymous_user_token: false
-
-  # when anonymous_user_token is configured, show public projects page
-  enable_public_projects_page: true
-
-  # by default, disable the "Getting Started" popup which is specific to the public beta install
-  enable_getting_started_popup: false
-
-  # Ask Arvados API server to compress its response payloads.
-  api_response_compression: true
-
-  # Timeouts for API requests.
-  api_client_connect_timeout: 120
-  api_client_receive_timeout: 300
-
-  # ShellInABox service endpoint URL for a given VM.  If false, do not
-  # offer web shell logins.
-  #
-  # E.g., using a path-based proxy server to forward connections to shell hosts:
-  # https://webshell.uuid_prefix.arvadosapi.com/%{hostname}
-  #
-  # E.g., using a name-based proxy server to forward connections to shell hosts:
-  # https://%{hostname}.webshell.uuid_prefix.arvadosapi.com/
-  shell_in_a_box_url: false
-
-  # Format of preview links. If false, use keep_web_download_url
-  # instead, and disable inline preview.
-  # If both are false, Workbench won't start, this is a mandatory configuration.
-  #
-  # Examples:
-  # keep_web_url: https://%{uuid_or_pdh}.collections.uuid_prefix.arvadosapi.com
-  # keep_web_url: https://%{uuid_or_pdh}--collections.uuid_prefix.arvadosapi.com
-  #
-  # Example supporting only public data and collection-sharing links
-  # (other data will be handled as downloads via keep_web_download_url):
-  # keep_web_url: https://collections.uuid_prefix.arvadosapi.com/c=%{uuid_or_pdh}
-  keep_web_url: false
-
-  # Format of download links. If false, use keep_web_url with
-  # disposition=attachment query param.
-  #
-  # The host part of the keep_web_download_url value here must match
-  # the -attachment-only-host argument given to keep-web: if
-  # keep_web_download_url is "https://FOO.EXAMPLE/c=..." then keep-web
-  # must run with "-attachment-only-host=FOO.EXAMPLE".
-  #
-  # If keep_web_download_url is false, and keep_web_url uses a
-  # single-origin form, then Workbench will show an error page
-  # when asked to download or preview private data.
-  #
-  # Example:
-  # keep_web_download_url: https://download.uuid_prefix.arvadosapi.com/c=%{uuid_or_pdh}
-  keep_web_download_url: false
-
-  # In "trust all content" mode, Workbench will redirect download
-  # requests to keep-web, even in the cases when keep-web would have
-  # to expose XSS vulnerabilities in order to handle the redirect.
-  #
-  # When enabling this setting, the -trust-all-content flag on the
-  # keep-web server must also be enabled.  For more detail, see
-  # https://godoc.org/github.com/curoverse/arvados/services/keep-web
-  #
-  # This setting has no effect in the recommended configuration, where
-  # the host part of keep_web_url begins with %{uuid_or_pdh}: in this
-  # case XSS protection is provided by browsers' same-origin policy.
-  #
-  # The default setting (false) is appropriate for a multi-user site.
-  trust_all_content: false
-
-  # Maximum number of historic log records of a running job to fetch
-  # and display in the Log tab, while subscribing to web sockets.
-  running_job_log_records_to_fetch: 2000
-
-  # In systems with many shared projects, loading of dashboard and topnav
-  # cab be slow due to collections indexing; use the following parameters
-  # to suppress these properties
-  show_recent_collections_on_dashboard: true
-  show_user_notifications: true
-
-  # Token to be included in all healthcheck requests. Disabled by default.
-  # Workbench expects request header of the format "Authorization: Bearer xxx"
-  ManagementToken: false
-
-  # Enable/disable "multi-site search" in top nav (true/false), or
-  # link it to the multi-site search on a remote Workbench site.
-  #
-  # Example:
-  # multi_site_search: https://workbench.qr1hi.arvadosapi.com/collections/multisite
-  multi_site_search: false
-
-  #
-  # Link to use for Arvados Workflow Composer app, or false if not available.
-  #
-  composer_url: false
-
-  #
-  # Should workbench allow management of local git repositories? Set to false if
-  # the jobs api is disabled and there are no local git repositories.
-  #
-  repositories: true
-
-  #
-  # Add an item to the user menu pointing to workbench2_url, if not false.
-  #
-  # Example:
-  # workbench2_url: https://workbench2.qr1hi.arvadosapi.com
-  #
-  workbench2_url: false
+  # only used by tests
+  testing_override_login_url: false
index 1c7a9d0dac8866511f795e1fea048d7f4989a300..514d57196d3fcc802dbc83a640907bbf166ceb76 100644 (file)
@@ -21,6 +21,9 @@ Bundler.require(:default, Rails.env)
 
 module ArvadosWorkbench
   class Application < Rails::Application
+
+    require_relative "arvados_config.rb"
+
     # Settings in config/environments/* take precedence over those specified here.
     # Application configuration should go into files in config/initializers
     # -- all .rb files in that directory are automatically loaded.
@@ -71,5 +74,3 @@ module ArvadosWorkbench
     config.assets.paths << Rails.root.join('node_modules')
   end
 end
-
-require File.expand_path('../load_config', __FILE__)
diff --git a/apps/workbench/config/arvados_config.rb b/apps/workbench/config/arvados_config.rb
new file mode 100644 (file)
index 0000000..c0ecfde
--- /dev/null
@@ -0,0 +1,194 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+#
+# Load Arvados configuration from /etc/arvados/config.yml, using defaults
+# from config.default.yml
+#
+# Existing application.yml is migrated into the new config structure.
+# Keys in the legacy application.yml take precedence.
+#
+# Use "bundle exec config:dump" to get the complete active configuration
+#
+# Use "bundle exec config:migrate" to migrate application.yml to
+# config.yml.  After adding the output of config:migrate to
+# /etc/arvados/config.yml, you will be able to delete application.yml.
+
+require 'config_loader'
+require 'config_validators'
+require 'open3'
+
+# Load the defaults, used by config:migrate and fallback loading
+# legacy application.yml
+Open3.popen2("arvados-server", "config-dump", "-config=-") do |stdin, stdout, status_thread|
+  stdin.write("Clusters: {xxxxx: {}}")
+  stdin.close
+  confs = YAML.load(stdout, deserialize_symbols: false)
+  clusterID, clusterConfig = confs["Clusters"].first
+  $arvados_config_defaults = clusterConfig
+  $arvados_config_defaults["ClusterID"] = clusterID
+end
+
+# Load the global config file
+Open3.popen2("arvados-server", "config-dump") do |stdin, stdout, status_thread|
+  confs = YAML.load(stdout, deserialize_symbols: false)
+  if confs && !confs.empty?
+    # config-dump merges defaults with user configuration, so every
+    # key should be set.
+    clusterID, clusterConfig = confs["Clusters"].first
+    $arvados_config_global = clusterConfig
+    $arvados_config_global["ClusterID"] = clusterID
+  else
+    # config-dump failed, assume we will be loading from legacy
+    # application.yml, initialize with defaults.
+    $arvados_config_global = $arvados_config_defaults.deep_dup
+  end
+end
+
+# Now make a copy
+$arvados_config = $arvados_config_global.deep_dup
+
+# Declare all our configuration items.
+arvcfg = ConfigLoader.new
+
+arvcfg.declare_config "ManagementToken", String, :ManagementToken
+arvcfg.declare_config "TLS.Insecure", Boolean, :arvados_insecure_https
+arvcfg.declare_config "Collections.TrustAllContent", Boolean, :trust_all_content
+
+arvcfg.declare_config "Services.Controller.ExternalURL", URI, :arvados_v1_base, ->(cfg, k, v) {
+  u = URI(v)
+  u.path = ""
+  ConfigLoader.set_cfg cfg, "Services.Controller.ExternalURL", u
+}
+
+arvcfg.declare_config "Services.WebShell.ExternalURL", URI, :shell_in_a_box_url, ->(cfg, k, v) {
+  v ||= ""
+  u = URI(v.sub("%{hostname}", "*"))
+  u.path = ""
+  ConfigLoader.set_cfg cfg, "Services.WebShell.ExternalURL", u
+}
+
+arvcfg.declare_config "Services.WebDAV.ExternalURL", URI, :keep_web_url, ->(cfg, k, v) {
+  v ||= ""
+  u = URI(v.sub("%{uuid_or_pdh}", "*"))
+  u.path = ""
+  ConfigLoader.set_cfg cfg, "Services.WebDAV.ExternalURL", u
+}
+
+arvcfg.declare_config "Services.WebDAVDownload.ExternalURL", URI, :keep_web_download_url, ->(cfg, k, v) {
+  v ||= ""
+  u = URI(v.sub("%{uuid_or_pdh}", "*"))
+  u.path = ""
+  ConfigLoader.set_cfg cfg, "Services.WebDAVDownload.ExternalURL", u
+}
+
+arvcfg.declare_config "Services.Composer.ExternalURL", URI, :composer_url
+arvcfg.declare_config "Services.Workbench2.ExternalURL", URI, :workbench2_url
+
+arvcfg.declare_config "Users.AnonymousUserToken", String, :anonymous_user_token
+
+arvcfg.declare_config "Workbench.SecretKeyBase", String, :secret_key_base
+
+arvcfg.declare_config "Workbench.ApplicationMimetypesWithViewIcon", Hash, :application_mimetypes_with_view_icon, ->(cfg, k, v) {
+  mimetypes = {}
+  v.each do |m|
+    mimetypes[m] = {}
+  end
+  ConfigLoader.set_cfg cfg, "Workbench.ApplicationMimetypesWithViewIcon", mimetypes
+}
+
+arvcfg.declare_config "Workbench.RunningJobLogRecordsToFetch", Integer, :running_job_log_records_to_fetch
+arvcfg.declare_config "Workbench.LogViewerMaxBytes", Integer, :log_viewer_max_bytes
+arvcfg.declare_config "Workbench.ProfilingEnabled", Boolean, :profiling_enabled
+arvcfg.declare_config "Workbench.APIResponseCompression", Boolean, :api_response_compression
+arvcfg.declare_config "Workbench.UserProfileFormFields", Hash, :user_profile_form_fields, ->(cfg, k, v) {
+  if !v
+    v = []
+  end
+  entries = {}
+  v.each_with_index do |s,i|
+    entries[s["key"]] = {
+      "Type" => s["type"],
+      "FormFieldTitle" => s["form_field_title"],
+      "FormFieldDescription" => s["form_field_description"],
+      "Required" => s["required"],
+      "Position": i
+    }
+    if s["options"]
+      entries[s["key"]]["Options"] = {}
+      s["options"].each do |o|
+        entries[s["key"]]["Options"][o] = {}
+      end
+    end
+  end
+  ConfigLoader.set_cfg cfg, "Workbench.UserProfileFormFields", entries
+}
+arvcfg.declare_config "Workbench.UserProfileFormMessage", String, :user_profile_form_message
+arvcfg.declare_config "Workbench.Theme", String, :arvados_theme
+arvcfg.declare_config "Workbench.ShowUserNotifications", Boolean, :show_user_notifications
+arvcfg.declare_config "Workbench.ShowUserAgreementInline", Boolean, :show_user_agreement_inline
+arvcfg.declare_config "Workbench.RepositoryCache", String, :repository_cache
+arvcfg.declare_config "Workbench.Repositories", Boolean, :repositories
+arvcfg.declare_config "Workbench.APIClientConnectTimeout", ActiveSupport::Duration, :api_client_connect_timeout
+arvcfg.declare_config "Workbench.APIClientReceiveTimeout", ActiveSupport::Duration, :api_client_receive_timeout
+arvcfg.declare_config "Workbench.APIResponseCompression", Boolean, :api_response_compression
+arvcfg.declare_config "Workbench.SiteName", String, :site_name
+arvcfg.declare_config "Workbench.MultiSiteSearch", String, :multi_site_search, ->(cfg, k, v) {
+  if !v
+    v = ""
+  end
+  ConfigLoader.set_cfg cfg, "Workbench.MultiSiteSearch", v.to_s
+}
+arvcfg.declare_config "Workbench.EnablePublicProjectsPage", Boolean, :enable_public_projects_page
+arvcfg.declare_config "Workbench.EnableGettingStartedPopup", Boolean, :enable_getting_started_popup
+arvcfg.declare_config "Workbench.ArvadosPublicDataDocURL", String, :arvados_public_data_doc_url
+arvcfg.declare_config "Workbench.ArvadosDocsite", String, :arvados_docsite
+arvcfg.declare_config "Workbench.ShowRecentCollectionsOnDashboard", Boolean, :show_recent_collections_on_dashboard
+arvcfg.declare_config "Workbench.ActivationContactLink", String, :activation_contact_link
+arvcfg.declare_config "Workbench.DefaultOpenIdPrefix", String, :default_openid_prefix
+
+arvcfg.declare_config "Mail.SendUserSetupNotificationEmail", Boolean, :send_user_setup_notification_email
+arvcfg.declare_config "Mail.IssueReporterEmailFrom", String, :issue_reporter_email_from
+arvcfg.declare_config "Mail.IssueReporterEmailTo", String, :issue_reporter_email_to
+arvcfg.declare_config "Mail.SupportEmailAddress", String, :support_email_address
+arvcfg.declare_config "Mail.EmailFrom", String, :email_from
+
+application_config = {}
+%w(application.default application).each do |cfgfile|
+  path = "#{::Rails.root.to_s}/config/#{cfgfile}.yml"
+  confs = ConfigLoader.load(path, erb: true)
+  # Ignore empty YAML file:
+  next if confs == false
+  application_config.deep_merge!(confs['common'] || {})
+  application_config.deep_merge!(confs[::Rails.env.to_s] || {})
+end
+
+$remaining_config = arvcfg.migrate_config(application_config, $arvados_config)
+
+# Checks for wrongly typed configuration items, coerces properties
+# into correct types (such as Duration), and optionally raise error
+# for essential configuration that can't be empty.
+arvcfg.coercion_and_check $arvados_config_defaults, check_nonempty: false
+arvcfg.coercion_and_check $arvados_config_global, check_nonempty: false
+arvcfg.coercion_and_check $arvados_config, check_nonempty: true
+
+# * $arvados_config_defaults is the defaults
+# * $arvados_config_global is $arvados_config_defaults merged with the contents of /etc/arvados/config.yml
+# These are used by the rake config: tasks
+#
+# * $arvados_config is $arvados_config_global merged with the migrated contents of application.yml
+# This is what actually gets copied into the Rails configuration object.
+
+ArvadosWorkbench::Application.configure do
+  # Copy into the Rails config object.  This also turns Hash into
+  # OrderedOptions so that application code can use
+  # Rails.configuration.API.Blah instead of
+  # Rails.configuration.API["Blah"]
+  ConfigLoader.copy_into_config $arvados_config, config
+  ConfigLoader.copy_into_config $remaining_config, config
+  secrets.secret_key_base = $arvados_config["Workbench"]["SecretKeyBase"]
+  ConfigValidators.validate_wb2_url_config()
+  ConfigValidators.validate_download_config()
+
+end
diff --git a/apps/workbench/config/load_config.rb b/apps/workbench/config/load_config.rb
deleted file mode 100644 (file)
index 5f0d9ca..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
-# This file must be loaded _after_ secret_token.rb if secret_token is
-# defined there instead of in config/application.yml.
-
-$application_config = {}
-
-%w(application.default application).each do |cfgfile|
-  path = "#{::Rails.root.to_s}/config/#{cfgfile}.yml"
-  if File.exist? path
-    yaml = ERB.new(IO.read path).result(binding)
-    confs = YAML.load(yaml, deserialize_symbols: true)
-    $application_config.merge!(confs['common'] || {})
-    $application_config.merge!(confs[::Rails.env.to_s] || {})
-  end
-end
-
-ArvadosWorkbench::Application.configure do
-  nils = []
-  $application_config.each do |k, v|
-    # "foo.bar: baz" --> { config.foo.bar = baz }
-    cfg = config
-    ks = k.split '.'
-    k = ks.pop
-    ks.each do |kk|
-      cfg = cfg.send(kk)
-    end
-    if v.nil? and cfg.respond_to?(k) and !cfg.send(k).nil?
-      # Config is nil in *.yml, but has been set already in
-      # environments/*.rb (or has a Rails default). Don't overwrite
-      # the default/upstream config with nil.
-      #
-      # After config files have been migrated, this mechanism should
-      # be removed.
-      Rails.logger.warn <<EOS
-DEPRECATED: Inheriting config.#{ks.join '.'} from Rails config.
-            Please move this config into config/application.yml.
-EOS
-    elsif v.nil?
-      # Config variables are not allowed to be nil. Make a "naughty"
-      # list, and present it below.
-      nils << k
-    else
-      cfg.send "#{k}=", v
-    end
-  end
-  if !nils.empty? and not ::Rails.groups.include?('assets')
-    raise <<EOS
-#{::Rails.groups.include?('assets')}
-Refusing to start in #{::Rails.env.to_s} mode with missing configuration.
-
-The following configuration settings must be specified in
-config/application.yml:
-* #{nils.join "\n* "}
-
-EOS
-  end
-  # Refuse to start if keep-web isn't configured
-  if not (config.keep_web_url or config.keep_web_download_url) and not ::Rails.groups.include?('assets')
-    raise <<EOS
-Refusing to start in #{::Rails.env.to_s} mode with missing configuration.
-
-Keep-web service must be configured in config/application.yml:
-* keep_web_url
-* keep_web_download_url
-
-EOS
-  end
-end
index 22ec1ba14c6dad9a83cc3594aa0353f835331e91..48913a14d7fd1929b1ada72fb94cba1ad78be7bf 100644 (file)
@@ -4,9 +4,9 @@
 
 case "$TARGET" in
     centos*)
-        fpm_depends+=(git)
+        fpm_depends+=(git arvados-server)
         ;;
     debian* | ubuntu*)
-        fpm_depends+=(git g++)
+        fpm_depends+=(git g++ arvados-server)
         ;;
 esac
diff --git a/apps/workbench/lib/config_loader.rb b/apps/workbench/lib/config_loader.rb
new file mode 100644 (file)
index 0000000..730e468
--- /dev/null
@@ -0,0 +1,243 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+module Psych
+  module Visitors
+    class YAMLTree < Psych::Visitors::Visitor
+      def visit_ActiveSupport_Duration o
+        seconds = o.to_i
+        outstr = ""
+        if seconds / 3600 > 0
+          outstr += "#{seconds / 3600}h"
+          seconds = seconds % 3600
+        end
+        if seconds / 60 > 0
+          outstr += "#{seconds / 60}m"
+          seconds = seconds % 60
+        end
+        if seconds > 0
+          outstr += "#{seconds}s"
+        end
+        if outstr == ""
+          outstr = "0s"
+        end
+        @emitter.scalar outstr, nil, nil, true, false, Nodes::Scalar::ANY
+      end
+
+      def visit_URI_Generic o
+        @emitter.scalar o.to_s, nil, nil, true, false, Nodes::Scalar::ANY
+      end
+
+      def visit_URI_HTTP o
+        @emitter.scalar o.to_s, nil, nil, true, false, Nodes::Scalar::ANY
+      end
+
+      def visit_Pathname o
+        @emitter.scalar o.to_s, nil, nil, true, false, Nodes::Scalar::ANY
+      end
+    end
+  end
+end
+
+
+module Boolean; end
+class TrueClass; include Boolean; end
+class FalseClass; include Boolean; end
+
+class NonemptyString < String
+end
+
+class ConfigLoader
+  def initialize
+    @config_migrate_map = {}
+    @config_types = {}
+  end
+
+  def declare_config(assign_to, configtype, migrate_from=nil, migrate_fn=nil)
+    if migrate_from
+      @config_migrate_map[migrate_from] = migrate_fn || ->(cfg, k, v) {
+        ConfigLoader.set_cfg cfg, assign_to, v
+      }
+    end
+    @config_types[assign_to] = configtype
+  end
+
+
+  def migrate_config from_config, to_config
+    remainders = {}
+    from_config.each do |k, v|
+      if @config_migrate_map[k.to_sym]
+        begin
+          @config_migrate_map[k.to_sym].call to_config, k, v
+        rescue => e
+          raise "Error migrating '#{k}: #{v}' got error #{e}"
+        end
+      else
+        remainders[k] = v
+      end
+    end
+    remainders
+  end
+
+  def coercion_and_check check_cfg, check_nonempty: true
+    @config_types.each do |cfgkey, cfgtype|
+      begin
+        cfg = check_cfg
+        k = cfgkey
+        ks = k.split '.'
+        k = ks.pop
+        ks.each do |kk|
+          cfg = cfg[kk]
+          if cfg.nil?
+            break
+          end
+        end
+
+        if cfg.nil?
+          raise "missing #{cfgkey}"
+        end
+
+        if cfgtype == String and !cfg[k]
+          cfg[k] = ""
+        end
+
+        if cfgtype == String and cfg[k].is_a? Symbol
+          cfg[k] = cfg[k].to_s
+        end
+
+        if cfgtype == Pathname and cfg[k].is_a? String
+
+          if cfg[k] == ""
+            cfg[k] = Pathname.new("")
+          else
+            cfg[k] = Pathname.new(cfg[k])
+            if !cfg[k].exist?
+              raise "#{cfgkey} path #{cfg[k]} does not exist"
+            end
+          end
+        end
+
+        if cfgtype == NonemptyString
+          if (!cfg[k] || cfg[k] == "") && check_nonempty
+            raise "#{cfgkey} cannot be empty"
+          end
+          if cfg[k].is_a? String
+            next
+          end
+        end
+
+        if cfgtype == ActiveSupport::Duration
+          if cfg[k].is_a? Integer
+            cfg[k] = cfg[k].seconds
+          elsif cfg[k].is_a? String
+            cfg[k] = ConfigLoader.parse_duration(cfg[k], cfgkey: cfgkey)
+          end
+        end
+
+        if cfgtype == URI
+          if cfg[k]
+            cfg[k] = URI(cfg[k])
+          else
+            cfg[k] = URI("")
+          end
+        end
+
+        if cfgtype == Integer && cfg[k].is_a?(String)
+          v = cfg[k].sub(/B\s*$/, '')
+          if mt = /(-?\d*\.?\d+)\s*([KMGTPE]i?)$/.match(v)
+            if mt[1].index('.')
+              v = mt[1].to_f
+            else
+              v = mt[1].to_i
+            end
+            cfg[k] = v * {
+              'K' => 1000,
+              'Ki' => 1 << 10,
+              'M' => 1000000,
+              'Mi' => 1 << 20,
+             "G" =>  1000000000,
+             "Gi" => 1 << 30,
+             "T" =>  1000000000000,
+             "Ti" => 1 << 40,
+             "P" =>  1000000000000000,
+             "Pi" => 1 << 50,
+             "E" =>  1000000000000000000,
+             "Ei" => 1 << 60,
+            }[mt[2]]
+          end
+        end
+
+      rescue => e
+        raise "#{cfgkey} expected #{cfgtype} but '#{cfg[k]}' got error #{e}"
+      end
+
+      if !cfg[k].is_a? cfgtype
+        raise "#{cfgkey} expected #{cfgtype} but was #{cfg[k].class}"
+      end
+    end
+  end
+
+  def self.set_cfg cfg, k, v
+    # "foo.bar = baz" --> { cfg["foo"]["bar"] = baz }
+    ks = k.split '.'
+    k = ks.pop
+    ks.each do |kk|
+      cfg = cfg[kk]
+      if cfg.nil?
+        break
+      end
+    end
+    if !cfg.nil?
+      cfg[k] = v
+    end
+  end
+
+  def self.parse_duration durstr, cfgkey:
+    duration_re = /-?(\d+(\.\d+)?)(s|m|h)/
+    dursec = 0
+    while durstr != ""
+      mt = duration_re.match durstr
+      if !mt
+        raise "#{cfgkey} not a valid duration: '#{durstr}', accepted suffixes are s, m, h"
+      end
+      multiplier = {s: 1, m: 60, h: 3600}
+      dursec += (Float(mt[1]) * multiplier[mt[3].to_sym])
+      durstr = durstr[mt[0].length..-1]
+    end
+    return dursec.seconds
+  end
+
+  def self.copy_into_config src, dst
+    src.each do |k, v|
+      dst.send "#{k}=", self.to_OrderedOptions(v)
+    end
+  end
+
+  def self.to_OrderedOptions confs
+    if confs.is_a? Hash
+      opts = ActiveSupport::OrderedOptions.new
+      confs.each do |k,v|
+        opts[k] = self.to_OrderedOptions(v)
+      end
+      opts
+    elsif confs.is_a? Array
+      confs.map { |v| self.to_OrderedOptions v }
+    else
+      confs
+    end
+  end
+
+  def self.load path, erb: false
+    if File.exist? path
+      yaml = IO.read path
+      if erb
+        yaml = ERB.new(yaml).result(binding)
+      end
+      YAML.load(yaml, deserialize_symbols: false)
+    else
+      {}
+    end
+  end
+
+end
index ec769168c28ac2d6cdfb6ff77c5221c1bf557038..804e3e397015bd9706d978c3067f805645a88aeb 100644 (file)
@@ -5,24 +5,22 @@
 require 'uri'
 
 module ConfigValidators
-    def validate_wb2_url_config
-        if Rails.configuration.workbench2_url
-            begin
-                if !URI.parse(Rails.configuration.workbench2_url).is_a?(URI::HTTP)
-                    Rails.logger.warn("workbench2_url config is not an HTTP URL: #{Rails.configuration.workbench2_url}")
-                    Rails.configuration.workbench2_url = false
-                elsif /.*[\/]{2,}$/.match(Rails.configuration.workbench2_url)
-                    Rails.logger.warn("workbench2_url config shouldn't have multiple trailing slashes: #{Rails.configuration.workbench2_url}")
-                    Rails.configuration.workbench2_url = false
-                else
-                    return true
-                end
-            rescue URI::InvalidURIError
-                Rails.logger.warn("workbench2_url config invalid URL: #{Rails.configuration.workbench2_url}")
-                Rails.configuration.workbench2_url = false
-            end
-        end
-        return false
+  def self.validate_wb2_url_config
+    if Rails.configuration.Services.Workbench2.ExternalURL != URI("")
+      if !Rails.configuration.Services.Workbench2.ExternalURL.is_a?(URI::HTTP)
+        raise "workbench2_url config is not an HTTP URL: #{Rails.configuration.Services.Workbench2.ExternalURL}"
+      elsif /.*[\/]{2,}$/.match(Rails.configuration.Services.Workbench2.ExternalURL.to_s)
+        raise "workbench2_url config shouldn't have multiple trailing slashes: #{Rails.configuration.Services.Workbench2.ExternalURL}"
+      else
+        return true
+      end
     end
-end
+    return false
+  end
 
+  def self.validate_download_config
+    if Rails.configuration.Services.WebDAV.ExternalURL == URI("") and Rails.configuration.Services.WebDAVDownload.ExternalURL == URI("")
+      raise "Keep-web service must be configured in Services.WebDAV and/or Services.WebDAVDownload"
+    end
+  end
+end
diff --git a/apps/workbench/lib/tasks/config.rake b/apps/workbench/lib/tasks/config.rake
new file mode 100644 (file)
index 0000000..6067208
--- /dev/null
@@ -0,0 +1,56 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+def diff_hash base, final
+  diffed = {}
+  base.each do |k,v|
+    bk = base[k]
+    fk = final[k]
+    if bk.is_a? Hash
+      d = diff_hash bk, fk
+      if d.length > 0
+        diffed[k] = d
+      end
+    else
+      if bk.to_yaml != fk.to_yaml
+        diffed[k] = fk
+      end
+    end
+  end
+  diffed
+end
+
+namespace :config do
+  desc 'Print items that differ between legacy application.yml and system config.yml'
+  task diff: :environment do
+    diffed = diff_hash $arvados_config_global, $arvados_config
+    cfg = { "Clusters" => {}}
+    cfg["Clusters"][$arvados_config["ClusterID"]] = diffed.select {|k,v| k != "ClusterID"}
+    if cfg["Clusters"][$arvados_config["ClusterID"]].empty?
+      puts "No migrations required for /etc/arvados/config.yml"
+    else
+      puts cfg.to_yaml
+    end
+  end
+
+  desc 'Print config.yml after merging with legacy application.yml'
+  task migrate: :environment do
+    diffed = diff_hash $arvados_config_defaults, $arvados_config
+    cfg = { "Clusters" => {}}
+    cfg["Clusters"][$arvados_config["ClusterID"]] = diffed.select {|k,v| k != "ClusterID"}
+    puts cfg.to_yaml
+  end
+
+  desc 'Print configuration as accessed through Rails.configuration'
+  task dump: :environment do
+    combined = $arvados_config.deep_dup
+    combined.update $remaining_config
+    puts combined.to_yaml
+  end
+
+  desc 'Legacy config check task -- it is a noop now'
+  task check: :environment do
+    # This exists so that build/rails-package-scripts/postinst.sh doesn't fail.
+  end
+end
diff --git a/apps/workbench/lib/tasks/config_check.rake b/apps/workbench/lib/tasks/config_check.rake
deleted file mode 100644 (file)
index 9fd5435..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
-namespace :config do
-  desc 'Ensure site configuration has all required settings'
-  task check: :environment do
-    $application_config.sort.each do |k, v|
-      if ENV.has_key?('QUIET') then
-        # Make sure we still check for the variable to exist
-        eval("Rails.configuration.#{k}")
-      else
-        if /(password|secret)/.match(k) then
-          # Make sure we still check for the variable to exist, but don't print the value
-          eval("Rails.configuration.#{k}")
-          $stderr.puts "%-32s %s" % [k, '*********']
-        else
-          $stderr.puts "%-32s %s" % [k, eval("Rails.configuration.#{k}")]
-        end
-      end
-    end
-  end
-end
diff --git a/apps/workbench/lib/tasks/config_dump.rake b/apps/workbench/lib/tasks/config_dump.rake
deleted file mode 100644 (file)
index ed34960..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
-namespace :config do
-  desc 'Show site configuration'
-  task dump: :environment do
-    puts $application_config.to_yaml
-  end
-end
index 86aa304e472324f992b7e72b5b1e7d27580f38e3..fbbffe87cb875e93528f84c3f29f3908ca996946 100644 (file)
@@ -173,7 +173,7 @@ class ActionsControllerTest < ActionController::TestCase
     ['pipeline_templates', 'pipeline_template_in_publicly_accessible_project'],
   ].each do |dm, fixture|
     test "access show method for public #{dm} and expect to see page" do
-      Rails.configuration.anonymous_user_token = api_fixture('api_client_authorizations')['anonymous']['api_token']
+      Rails.configuration.Users.AnonymousUserToken = api_fixture('api_client_authorizations')['anonymous']['api_token']
       get(:show, params: {uuid: api_fixture(dm)[fixture]['uuid']})
       assert_response :redirect
       if dm == 'groups'
@@ -193,7 +193,7 @@ class ActionsControllerTest < ActionController::TestCase
     ['traits', 'owned_by_aproject_with_no_name', :redirect],
   ].each do |dm, fixture, expected|
     test "access show method for non-public #{dm} and expect #{expected}" do
-      Rails.configuration.anonymous_user_token = api_fixture('api_client_authorizations')['anonymous']['api_token']
+      Rails.configuration.Users.AnonymousUserToken = api_fixture('api_client_authorizations')['anonymous']['api_token']
       get(:show, params: {uuid: api_fixture(dm)[fixture]['uuid']})
       assert_response expected
       if expected == 404
index 1b13d8f328def28af9064afff27191a6052195fe..72c3e0ab0cfe4f30f43d29f856ab5ef43c375e78 100644 (file)
@@ -325,9 +325,9 @@ class ApplicationControllerTest < ActionController::TestCase
     # Each pdh has more than one collection; however, we should get only one for each
     assert collections.size == 2, 'Expected two objects in the preloaded collection hash'
     assert collections[pdh1], 'Expected collections for the passed in pdh #{pdh1}'
-    assert_equal collections[pdh1].size, 1, 'Expected one collection for the passed in pdh #{pdh1}'
+    assert_equal collections[pdh1].size, 1, "Expected one collection for the passed in pdh #{pdh1}"
     assert collections[pdh2], 'Expected collections for the passed in pdh #{pdh2}'
-    assert_equal collections[pdh2].size, 1, 'Expected one collection for the passed in pdh #{pdh2}'
+    assert_equal collections[pdh2].size, 1, "Expected one collection for the passed in pdh #{pdh2}"
   end
 
   test "requesting a nonexistent object returns 404" do
@@ -346,7 +346,7 @@ class ApplicationControllerTest < ActionController::TestCase
       true
     end.returns fake_api_response('{}', 200, {})
 
-    Rails.configuration.anonymous_user_token =
+    Rails.configuration.Users.AnonymousUserToken =
       api_fixture("api_client_authorizations", "anonymous", "api_token")
     @controller = ProjectsController.new
     test_uuid = "zzzzz-j7d0g-zzzzzzzzzzzzzzz"
@@ -374,7 +374,7 @@ class ApplicationControllerTest < ActionController::TestCase
   ].each do |css_selector|
     test "login link at #{css_selector.inspect} includes return_to param" do
       # Without an anonymous token, we're immediately redirected to login.
-      Rails.configuration.anonymous_user_token =
+      Rails.configuration.Users.AnonymousUserToken =
         api_fixture("api_client_authorizations", "anonymous", "api_token")
       @controller = ProjectsController.new
       test_uuid = "zzzzz-j7d0g-zzzzzzzzzzzzzzz"
@@ -393,17 +393,17 @@ class ApplicationControllerTest < ActionController::TestCase
     # We're really testing ApplicationController's render_exception.
     # Our primary concern is that it doesn't raise an error and
     # return 500.
-    orig_api_server = Rails.configuration.arvados_v1_base
+    orig_api_server = Rails.configuration.Services.Controller.ExternalURL
     begin
       # The URL should look valid in all respects, and avoid talking over a
       # network.  100::/64 is the IPv6 discard prefix, so it's perfect.
-      Rails.configuration.arvados_v1_base = "https://[100::f]:1/"
+      Rails.configuration.Services.Controller.ExternalURL = "https://[100::f]:1/"
       @controller = NodesController.new
       get(:index, params: {}, session: session_for(:active))
       assert_includes(405..422, @response.code.to_i,
                       "bad response code when API server is unreachable")
     ensure
-      Rails.configuration.arvados_v1_base = orig_api_server
+      Rails.configuration.Services.Controller.ExternalURL = orig_api_server
     end
   end
 
@@ -421,9 +421,9 @@ class ApplicationControllerTest < ActionController::TestCase
   ].each do |controller, fixture, anon_config=true|
     test "#{controller} show method with anonymous config #{anon_config ? '' : 'not '}enabled" do
       if anon_config
-        Rails.configuration.anonymous_user_token = api_fixture('api_client_authorizations')['anonymous']['api_token']
+        Rails.configuration.Users.AnonymousUserToken = api_fixture('api_client_authorizations')['anonymous']['api_token']
       else
-        Rails.configuration.anonymous_user_token = false
+        Rails.configuration.Users.AnonymousUserToken = ""
       end
 
       @controller = controller
@@ -449,7 +449,7 @@ class ApplicationControllerTest < ActionController::TestCase
     false,
   ].each do |config|
     test "invoke show with include_accept_encoding_header config #{config}" do
-      Rails.configuration.include_accept_encoding_header_in_api_requests = config
+      Rails.configuration.APIResponseCompression = config
 
       @controller = CollectionsController.new
       get(:show, params: {id: api_fixture('collections')['foo_file']['uuid']}, session: session_for(:admin))
index 88287cd3f3d1cd0b5e65ed980b4c58b6ce4b13dd..4fce54a8ab4e2b042be8c5b25e75cc80bf97aff0 100644 (file)
@@ -15,11 +15,11 @@ class CollectionsControllerTest < ActionController::TestCase
   NONEXISTENT_COLLECTION = "ffffffffffffffffffffffffffffffff+0"
 
   def config_anonymous enable
-    Rails.configuration.anonymous_user_token =
+    Rails.configuration.Users.AnonymousUserToken =
       if enable
         api_token('anonymous')
       else
-        false
+        ""
       end
   end
 
@@ -471,9 +471,9 @@ class CollectionsControllerTest < ActionController::TestCase
     assert_not_includes @response.body, '<a href="#Upload"'
   end
 
-  def setup_for_keep_web cfg='https://%{uuid_or_pdh}.example', dl_cfg=false
-    Rails.configuration.keep_web_url = cfg
-    Rails.configuration.keep_web_download_url = dl_cfg
+  def setup_for_keep_web cfg='https://*.example', dl_cfg=""
+    Rails.configuration.Services.WebDAV.ExternalURL = URI(cfg)
+    Rails.configuration.Services.WebDAVDownload.ExternalURL = URI(dl_cfg)
   end
 
   %w(uuid portable_data_hash).each do |id_type|
@@ -522,8 +522,8 @@ class CollectionsControllerTest < ActionController::TestCase
     end
 
     test "Redirect to keep_web_download_url via #{id_type}" do
-      setup_for_keep_web('https://collections.example/c=%{uuid_or_pdh}',
-                         'https://download.example/c=%{uuid_or_pdh}')
+      setup_for_keep_web('https://collections.example',
+                         'https://download.example')
       tok = api_token('active')
       id = api_fixture('collections')['w_a_z_file'][id_type]
       get :show_file, params: {uuid: id, file: "w a z"}, session: session_for(:active)
@@ -532,9 +532,9 @@ class CollectionsControllerTest < ActionController::TestCase
     end
 
     test "Redirect to keep_web_url via #{id_type} when trust_all_content enabled" do
-      Rails.configuration.trust_all_content = true
-      setup_for_keep_web('https://collections.example/c=%{uuid_or_pdh}',
-                         'https://download.example/c=%{uuid_or_pdh}')
+      Rails.configuration.Workbench.TrustAllContent = true
+      setup_for_keep_web('https://collections.example',
+                         'https://download.example')
       tok = api_token('active')
       id = api_fixture('collections')['w_a_z_file'][id_type]
       get :show_file, params: {uuid: id, file: "w a z"}, session: session_for(:active)
@@ -554,8 +554,8 @@ class CollectionsControllerTest < ActionController::TestCase
 
     test "Redirect download to keep_web_download_url, anon #{anon}" do
       config_anonymous anon
-      setup_for_keep_web('https://collections.example/c=%{uuid_or_pdh}',
-                         'https://download.example/c=%{uuid_or_pdh}')
+      setup_for_keep_web('https://collections.example/',
+                         'https://download.example/')
       tok = api_token('active')
       id = api_fixture('collections')['public_text_file']['uuid']
       get :show_file, params: {
@@ -575,7 +575,7 @@ class CollectionsControllerTest < ActionController::TestCase
   test "Error if file is impossible to retrieve from keep_web_url" do
     # Cannot pass a session token using a single-origin keep-web URL,
     # cannot read this collection without a session token.
-    setup_for_keep_web 'https://collections.example/c=%{uuid_or_pdh}', false
+    setup_for_keep_web 'https://collections.example/', ""
     id = api_fixture('collections')['w_a_z_file']['uuid']
     get :show_file, params: {uuid: id, file: "w a z"}, session: session_for(:active)
     assert_response 422
@@ -583,8 +583,8 @@ class CollectionsControllerTest < ActionController::TestCase
 
   [false, true].each do |trust_all_content|
     test "Redirect preview to keep_web_download_url when preview is disabled and trust_all_content is #{trust_all_content}" do
-      Rails.configuration.trust_all_content = trust_all_content
-      setup_for_keep_web false, 'https://download.example/c=%{uuid_or_pdh}'
+      Rails.configuration.Workbench.TrustAllContent = trust_all_content
+      setup_for_keep_web "", 'https://download.example/'
       tok = api_token('active')
       id = api_fixture('collections')['w_a_z_file']['uuid']
       get :show_file, params: {uuid: id, file: "w a z"}, session: session_for(:active)
index 93686aa6b14668d762185a5f19a5431d8398a60f..140b59fa5e7d0d2c923d974a3537ff501e0647af 100644 (file)
@@ -137,7 +137,7 @@ class ContainerRequestsControllerTest < ActionController::TestCase
     assert_includes @response.body, "href=\"\/collections/fa7aeb5140e2848d39b416daeef4ffc5+45/foobar\?" # locator on command
     assert_includes @response.body, "href=\"\/collections/fa7aeb5140e2848d39b416daeef4ffc5+45/foo" # mount input1
     assert_includes @response.body, "href=\"\/collections/fa7aeb5140e2848d39b416daeef4ffc5+45/bar" # mount input2
-    assert_includes @response.body, "href=\"\/collections/1fd08fc162a5c6413070a8bd0bffc818+150" # mount workflow
+    assert_includes @response.body, "href=\"\/collections/f9ddda46bb293b6847da984e3aa735db+290" # mount workflow
     assert_includes @response.body, "href=\"#Log\""
     assert_includes @response.body, "href=\"#Provenance\""
   end
index 556b958d00a8f0fe50c1a9af9d81b62c2eeb1771..9144564c912bb96735b4ce967e06b2e1795ec30e 100644 (file)
@@ -59,7 +59,7 @@ class DisabledApiTest < ActionController::TestCase
     test "project tabs as user #{user} when pipeline related index APIs are disabled" do
       @controller = ProjectsController.new
 
-      Rails.configuration.anonymous_user_token = api_fixture('api_client_authorizations')['anonymous']['api_token']
+      Rails.configuration.Users.AnonymousUserToken = api_fixture('api_client_authorizations')['anonymous']['api_token']
 
       dd = ArvadosApiClient.new_or_current.discovery.deep_dup
       dd[:resources][:pipeline_templates][:methods].delete(:index)
index 09a6950cead6fad12ce20262c3eda4a6d978d91c..750c779ae7ca2d537911feabfa25484b85338443 100644 (file)
@@ -240,14 +240,14 @@ class ProjectsControllerTest < ActionController::TestCase
   end
 
   test "visit non-public project as anonymous when anonymous browsing is enabled and expect page not found" do
-    Rails.configuration.anonymous_user_token = api_fixture('api_client_authorizations')['anonymous']['api_token']
+    Rails.configuration.Users.AnonymousUserToken = api_fixture('api_client_authorizations')['anonymous']['api_token']
     get(:show, params: {id: api_fixture('groups')['aproject']['uuid']})
     assert_response 404
     assert_match(/log ?in/i, @response.body)
   end
 
   test "visit home page as anonymous when anonymous browsing is enabled and expect login" do
-    Rails.configuration.anonymous_user_token = api_fixture('api_client_authorizations')['anonymous']['api_token']
+    Rails.configuration.Users.AnonymousUserToken = api_fixture('api_client_authorizations')['anonymous']['api_token']
     get(:index)
     assert_response :redirect
     assert_match /\/users\/welcome/, @response.redirect_url
@@ -258,7 +258,7 @@ class ProjectsControllerTest < ActionController::TestCase
     :active,
   ].each do |user|
     test "visit public projects page when anon config is enabled, as user #{user}, and expect page" do
-      Rails.configuration.anonymous_user_token = api_fixture('api_client_authorizations')['anonymous']['api_token']
+      Rails.configuration.Users.AnonymousUserToken = api_fixture('api_client_authorizations')['anonymous']['api_token']
 
       if user
         get :public, params: {}, session: session_for(user)
@@ -276,22 +276,22 @@ class ProjectsControllerTest < ActionController::TestCase
   end
 
   test "visit public projects page when anon config is not enabled as active user and expect 404" do
-    Rails.configuration.anonymous_user_token = nil
-    Rails.configuration.enable_public_projects_page = false
+    Rails.configuration.Users.AnonymousUserToken = ""
+    Rails.configuration.Workbench.EnablePublicProjectsPage = false
     get :public, params: {}, session: session_for(:active)
     assert_response 404
   end
 
   test "visit public projects page when anon config is enabled but public projects page is disabled as active user and expect 404" do
-    Rails.configuration.anonymous_user_token = api_fixture('api_client_authorizations')['anonymous']['api_token']
-    Rails.configuration.enable_public_projects_page = false
+    Rails.configuration.Users.AnonymousUserToken = api_fixture('api_client_authorizations')['anonymous']['api_token']
+    Rails.configuration.Workbench.EnablePublicProjectsPage = false
     get :public, params: {}, session: session_for(:active)
     assert_response 404
   end
 
   test "visit public projects page when anon config is not enabled as anonymous and expect login page" do
-    Rails.configuration.anonymous_user_token = nil
-    Rails.configuration.enable_public_projects_page = false
+    Rails.configuration.Users.AnonymousUserToken = ""
+    Rails.configuration.Workbench.EnablePublicProjectsPage = false
     get :public
     assert_response :redirect
     assert_match /\/users\/welcome/, @response.redirect_url
@@ -299,8 +299,8 @@ class ProjectsControllerTest < ActionController::TestCase
   end
 
   test "visit public projects page when anon config is enabled and public projects page is disabled and expect login page" do
-    Rails.configuration.anonymous_user_token = api_fixture('api_client_authorizations')['anonymous']['api_token']
-    Rails.configuration.enable_public_projects_page = false
+    Rails.configuration.Users.AnonymousUserToken = api_fixture('api_client_authorizations')['anonymous']['api_token']
+    Rails.configuration.Workbench.EnablePublicProjectsPage = false
     get :index
     assert_response :redirect
     assert_match /\/users\/welcome/, @response.redirect_url
@@ -308,7 +308,7 @@ class ProjectsControllerTest < ActionController::TestCase
   end
 
   test "visit public projects page when anon config is not enabled and public projects page is enabled and expect login page" do
-    Rails.configuration.enable_public_projects_page = true
+    Rails.configuration.Workbench.EnablePublicProjectsPage = true
     get :index
     assert_response :redirect
     assert_match /\/users\/welcome/, @response.redirect_url
@@ -501,7 +501,7 @@ EOT
   end
 
   test "visit a public project and verify the public projects page link exists" do
-    Rails.configuration.anonymous_user_token = api_fixture('api_client_authorizations')['anonymous']['api_token']
+    Rails.configuration.Users.AnonymousUserToken = api_fixture('api_client_authorizations')['anonymous']['api_token']
     uuid = api_fixture('groups')['anonymously_accessible_project']['uuid']
     get :show, params: {id: uuid}
     project = assigns(:object)
index 57b8705963d752e41123fcb094f1f4505a8a3862..742fe6b13e1c3ed5b0a4a7ff7e720bc8f6874565 100644 (file)
@@ -14,7 +14,7 @@ class UsersControllerTest < ActionController::TestCase
   test "ignore previously valid token (for deleted user), don't crash" do
     get :activity, params: {}, session: session_for(:valid_token_deleted_user)
     assert_response :redirect
-    assert_match /^#{Rails.configuration.arvados_login_base}/, @response.redirect_url
+    assert_match /^#{Rails.configuration.Services.Controller.ExternalURL}\/login/, @response.redirect_url
     assert_nil assigns(:my_jobs)
     assert_nil assigns(:my_ssh_keys)
   end
@@ -24,7 +24,7 @@ class UsersControllerTest < ActionController::TestCase
       id: api_fixture('users')['active']['uuid']
     }, session: session_for(:expired_trustedclient)
     assert_response :redirect
-    assert_match /^#{Rails.configuration.arvados_login_base}/, @response.redirect_url
+    assert_match /^#{Rails.configuration.Services.Controller.ExternalURL}\/login/, @response.redirect_url
     assert_nil assigns(:my_jobs)
     assert_nil assigns(:my_ssh_keys)
   end
index 8d772b087f54f02d0f2172d6820bec3823d36baf..c414f8a7ad76e4a5c3844c0ce61a088fe5fcd403 100644 (file)
@@ -14,7 +14,7 @@ class AnonymousAccessTest < ActionDispatch::IntegrationTest
 
   setup do
     need_javascript
-    Rails.configuration.anonymous_user_token = api_fixture('api_client_authorizations')['anonymous']['api_token']
+    Rails.configuration.Users.AnonymousUserToken = api_fixture('api_client_authorizations')['anonymous']['api_token']
   end
 
   PUBLIC_PROJECT = "/projects/#{api_fixture('groups')['anonymously_accessible_project']['uuid']}"
@@ -35,7 +35,7 @@ class AnonymousAccessTest < ActionDispatch::IntegrationTest
         assert_text 'indicate that you have read and accepted the user agreement'
       end
       within('.navbar-fixed-top') do
-        assert_selector 'a', text: Rails.configuration.site_name.downcase
+        assert_selector 'a', text: Rails.configuration.Workbench.SiteName.downcase
         assert(page.has_link?("notifications-menu"), 'no user menu')
         page.find("#notifications-menu").click
         within('.dropdown-menu') do
@@ -45,8 +45,8 @@ class AnonymousAccessTest < ActionDispatch::IntegrationTest
     else  # anonymous
       assert_text 'Unrestricted public data'
       within('.navbar-fixed-top') do
-        assert_text Rails.configuration.site_name.downcase
-        assert_no_selector 'a', text: Rails.configuration.site_name.downcase
+        assert_text Rails.configuration.Workbench.SiteName.downcase
+        assert_no_selector 'a', text: Rails.configuration.Workbench.SiteName.downcase
         assert_selector 'a', text: 'Log in'
         assert_selector 'a', text: 'Browse public projects'
       end
index 51c3720985a85e0fbfd3ebd60ff1473134af78ab..505767814a3c4612e95578886121bdf5d2c7a457 100644 (file)
@@ -16,13 +16,13 @@ class ApplicationLayoutTest < ActionDispatch::IntegrationTest
   end
 
   def verify_homepage user, invited, has_profile
-    profile_config = Rails.configuration.user_profile_form_fields
+    profile_config = Rails.configuration.Workbench.UserProfileFormFields
 
     if !user
       assert page.has_text?('Please log in'), 'Not found text - Please log in'
       assert page.has_text?('The "Log in" button below will show you a Google sign-in page'), 'Not found text - google sign in page'
       assert page.has_no_text?('My projects'), 'Found text - My projects'
-      assert page.has_link?("Log in to #{Rails.configuration.site_name}"), 'Not found text - log in to'
+      assert page.has_link?("Log in to #{Rails.configuration.Workbench.SiteName}"), 'Not found text - log in to'
     elsif user['is_active']
       if profile_config && !has_profile
         assert page.has_text?('Save profile'), 'No text - Save profile'
@@ -42,12 +42,12 @@ class ApplicationLayoutTest < ActionDispatch::IntegrationTest
 
     within('.navbar-fixed-top') do
       if !user
-        assert_text Rails.configuration.site_name.downcase
-        assert_no_selector 'a', text: Rails.configuration.site_name.downcase
+        assert_text Rails.configuration.Workbench.SiteName.downcase
+        assert_no_selector 'a', text: Rails.configuration.Workbench.SiteName.downcase
         assert page.has_link?('Log in'), 'Not found link - Log in'
       else
         # my account menu
-        assert_selector 'a', text: Rails.configuration.site_name.downcase
+        assert_selector 'a', text: Rails.configuration.Workbench.SiteName.downcase
         assert(page.has_link?("notifications-menu"), 'no user menu')
         page.find("#notifications-menu").click
         within('.dropdown-menu') do
@@ -142,7 +142,7 @@ class ApplicationLayoutTest < ActionDispatch::IntegrationTest
   end
 
   [
-    [false, false],
+    ["", false],
     ['http://wb2.example.org//', false],
     ['ftp://wb2.example.org', false],
     ['wb2.example.org', false],
@@ -152,8 +152,13 @@ class ApplicationLayoutTest < ActionDispatch::IntegrationTest
     ['https://wb2.example.org/', true],
   ].each do |wb2_url_config, wb2_menu_appear|
     test "workbench2_url=#{wb2_url_config} should#{wb2_menu_appear ? '' : ' not'} show WB2 menu" do
-      Rails.configuration.workbench2_url = wb2_url_config
-      assert_equal wb2_menu_appear, ConfigValidators::validate_wb2_url_config()
+      Rails.configuration.Services.Workbench2.ExternalURL = URI(wb2_url_config)
+      if !wb2_menu_appear and !wb2_url_config.empty?
+        assert_raises RuntimeError do
+          ConfigValidators.validate_wb2_url_config()
+        end
+        Rails.configuration.Services.Workbench2.ExternalURL = URI("")
+      end
 
       visit page_with_token('active')
       within('.navbar-fixed-top') do
@@ -170,7 +175,7 @@ class ApplicationLayoutTest < ActionDispatch::IntegrationTest
     ['active_with_prefs_profile_no_getting_started_shown', false],
   ].each do |token, getting_started_shown|
     test "getting started help menu item #{getting_started_shown}" do
-      Rails.configuration.enable_getting_started_popup = true
+      Rails.configuration.Workbench.EnableGettingStartedPopup = true
 
       visit page_with_token(token)
 
@@ -213,7 +218,7 @@ class ApplicationLayoutTest < ActionDispatch::IntegrationTest
   end
 
   test "test arvados_public_data_doc_url config unset" do
-    Rails.configuration.arvados_public_data_doc_url = false
+    Rails.configuration.Workbench.ArvadosPublicDataDocURL = ""
 
     visit page_with_token('active')
     within '.navbar-fixed-top' do
@@ -231,7 +236,7 @@ class ApplicationLayoutTest < ActionDispatch::IntegrationTest
   end
 
   test "no SSH public key notification when shell_in_a_box_url is configured" do
-    Rails.configuration.shell_in_a_box_url = 'example.com'
+    Rails.configuration.Services.WebShell.ExternalURL = URI('http://example.com')
     visit page_with_token('job_reader')
     click_link 'notifications-menu'
     assert_no_selector 'a', text:'Click here to set up an SSH public key for use with Arvados.'
index 407458b62bd4c3557e8c21e20b4bde201cfda1e3..b19c00dae99c9b8839ff904dec648e45833149e2 100644 (file)
@@ -42,7 +42,7 @@ class DownloadTest < ActionDispatch::IntegrationTest
     end
 
     test "preview anonymous content from keep-web by #{id_type}" do
-      Rails.configuration.anonymous_user_token =
+      Rails.configuration.Users.AnonymousUserToken =
         api_fixture('api_client_authorizations')['anonymous']['api_token']
       uuid_or_pdh =
         api_fixture('collections')['public_text_file'][id_type]
@@ -55,7 +55,7 @@ class DownloadTest < ActionDispatch::IntegrationTest
     end
 
     test "download anonymous content from keep-web by #{id_type}" do
-      Rails.configuration.anonymous_user_token =
+      Rails.configuration.Users.AnonymousUserToken =
         api_fixture('api_client_authorizations')['anonymous']['api_token']
       uuid_or_pdh =
         api_fixture('collections')['public_text_file'][id_type]
index 81d4bbbaa1735dd98ce2ccbb7a54b25b8675a43b..86d5902ff5b1ef11ec62d6a0c8219225d5b71c45 100644 (file)
@@ -72,15 +72,15 @@ class ErrorsTest < ActionDispatch::IntegrationTest
 
   test "API error page has Report problem button" do
     # point to a bad api server url to generate fiddlesticks error
-    original_arvados_v1_base = Rails.configuration.arvados_v1_base
-    Rails.configuration.arvados_v1_base = "https://[::1]:1/"
+    original_arvados_v1_base = Rails.configuration.Services.Controller.ExternalURL
+    Rails.configuration.Services.Controller.ExternalURL = URI("https://[::1]:1/")
 
     visit page_with_token("active")
 
     assert_text 'fiddlesticks'
 
     # reset api server base config to let the popup rendering to work
-    Rails.configuration.arvados_v1_base = original_arvados_v1_base
+    Rails.configuration.Services.Controller.ExternalURL = original_arvados_v1_base
 
     click_link 'Report problem'
 
index bf48d88cf3f754455d9902727c3b91f4806672d1..b54a31380ce93f9cbb0ae726a47a636e4ed57981 100644 (file)
@@ -44,7 +44,7 @@ class JobsTest < ActionDispatch::IntegrationTest
     use_keep_web_config
 
     # This config will be restored during teardown by ../test_helper.rb:
-    Rails.configuration.log_viewer_max_bytes = 100
+    Rails.configuration.Workbench.LogViewerMaxBytes = 100
 
     logdata = fakepipe_with_log_data.read
     job_uuid = api_fixture('jobs')['running']['uuid']
index 9c22f5a7721a7b388111ecdac8133a7a0e9302e7..53c7ec85532be6022d2dc89b35e09def57653f2b 100644 (file)
@@ -10,6 +10,10 @@ class LinkAccountTest < ActionDispatch::IntegrationTest
     need_javascript
   end
 
+  teardown do
+    Rails.configuration.testing_override_login_url = false
+  end
+
   def start_sso_stub token
     port = available_port('sso_stub')
 
@@ -38,8 +42,7 @@ class LinkAccountTest < ActionDispatch::IntegrationTest
 
   test "Add another login to this account" do
     visit page_with_token('active_trustedclient')
-    stub = start_sso_stub(api_fixture('api_client_authorizations')['project_viewer_trustedclient']['api_token'])
-    Rails.configuration.arvados_login_base = stub + "login"
+    Rails.configuration.testing_override_login_url = start_sso_stub(api_fixture('api_client_authorizations')['project_viewer_trustedclient']['api_token'])
 
     find("#notifications-menu").click
     assert_text "active-user@arvados.local"
@@ -58,8 +61,7 @@ class LinkAccountTest < ActionDispatch::IntegrationTest
 
   test "Use this login to access another account" do
     visit page_with_token('project_viewer_trustedclient')
-    stub = start_sso_stub(api_fixture('api_client_authorizations')['active_trustedclient']['api_token'])
-    Rails.configuration.arvados_login_base = stub + "login"
+    Rails.configuration.testing_override_login_url = start_sso_stub(api_fixture('api_client_authorizations')['active_trustedclient']['api_token'])
 
     find("#notifications-menu").click
     assert_text "project-viewer@arvados.local"
@@ -78,8 +80,7 @@ class LinkAccountTest < ActionDispatch::IntegrationTest
 
   test "Link login of inactive user to this account" do
     visit page_with_token('active_trustedclient')
-    stub = start_sso_stub(api_fixture('api_client_authorizations')['inactive_uninvited_trustedclient']['api_token'])
-    Rails.configuration.arvados_login_base = stub + "login"
+    Rails.configuration.testing_override_login_url = start_sso_stub(api_fixture('api_client_authorizations')['inactive_uninvited_trustedclient']['api_token'])
 
     find("#notifications-menu").click
     assert_text "active-user@arvados.local"
@@ -98,8 +99,7 @@ class LinkAccountTest < ActionDispatch::IntegrationTest
 
   test "Cannot link to inactive user" do
     visit page_with_token('active_trustedclient')
-    stub = start_sso_stub(api_fixture('api_client_authorizations')['inactive_uninvited_trustedclient']['api_token'])
-    Rails.configuration.arvados_login_base = stub + "login"
+    Rails.configuration.testing_override_login_url = start_sso_stub(api_fixture('api_client_authorizations')['inactive_uninvited_trustedclient']['api_token'])
 
     find("#notifications-menu").click
     assert_text "active-user@arvados.local"
@@ -122,8 +122,7 @@ class LinkAccountTest < ActionDispatch::IntegrationTest
 
   test "Inactive user can link to active account" do
     visit page_with_token('inactive_uninvited_trustedclient')
-    stub = start_sso_stub(api_fixture('api_client_authorizations')['active_trustedclient']['api_token'])
-    Rails.configuration.arvados_login_base = stub + "login"
+    Rails.configuration.testing_override_login_url = start_sso_stub(api_fixture('api_client_authorizations')['active_trustedclient']['api_token'])
 
     find("#notifications-menu").click
     assert_text "inactive-uninvited-user@arvados.local"
@@ -147,8 +146,7 @@ class LinkAccountTest < ActionDispatch::IntegrationTest
 
   test "Admin cannot link to non-admin" do
     visit page_with_token('admin_trustedclient')
-    stub = start_sso_stub(api_fixture('api_client_authorizations')['active_trustedclient']['api_token'])
-    Rails.configuration.arvados_login_base = stub + "login"
+    Rails.configuration.testing_override_login_url = start_sso_stub(api_fixture('api_client_authorizations')['active_trustedclient']['api_token'])
 
     find("#notifications-menu").click
     assert_text "admin@arvados.local"
index 801609fbb6747f74ee52d0e0bc38b9d7a57ae12b..adfd62bd8e04c73767ed2756a442ee15442691ec 100644 (file)
@@ -393,6 +393,7 @@ class PipelineInstancesTest < ActionDispatch::IntegrationTest
   def create_and_run_pipeline_in_aproject in_aproject, template_name, collection_fixture, choose_file=false
     # collection in aproject to be used as input
     collection = api_fixture('collections', collection_fixture)
+    collection['name'] ||= '' # API response is "" even if fixture attr is null
 
     # create a pipeline instance
     find('.btn', text: 'Run a process').click
@@ -421,7 +422,7 @@ class PipelineInstancesTest < ActionDispatch::IntegrationTest
 
       if collection_fixture == 'foo_collection_in_aproject'
         first('span', text: 'foo_tag').click
-      elsif collection['name']
+      elsif collection['name'] != ''
         first('span', text: "#{collection['name']}").click
       else
         collection_uuid = collection['uuid']
index ac78344ea5b398566289c531f21400e3f5abc76b..279d851017f0a9270d970a39e5ce030659b30075 100644 (file)
@@ -93,7 +93,7 @@ class ProjectsTest < ActionDispatch::IntegrationTest
   end
 
   test "projects not publicly sharable when anonymous browsing disabled" do
-    Rails.configuration.anonymous_user_token = false
+    Rails.configuration.Users.AnonymousUserToken = ""
     open_groups_sharing
     # Check for a group we do expect first, to make sure the modal's loaded.
     assert_selector(".modal-container .selectable",
@@ -103,7 +103,7 @@ class ProjectsTest < ActionDispatch::IntegrationTest
   end
 
   test "projects publicly sharable when anonymous browsing enabled" do
-    Rails.configuration.anonymous_user_token = "testonlytoken"
+    Rails.configuration.Users.AnonymousUserToken = "testonlytoken"
     open_groups_sharing
     assert_selector(".modal-container .selectable",
                     text: group_name("anonymous_group"))
@@ -539,19 +539,19 @@ class ProjectsTest < ActionDispatch::IntegrationTest
   end
 
   test "error while loading tab" do
-    original_arvados_v1_base = Rails.configuration.arvados_v1_base
+    original_arvados_v1_base = Rails.configuration.Services.Controller.ExternalURL
 
     visit page_with_token 'active', '/projects/' + api_fixture('groups')['aproject']['uuid']
 
     # Point to a bad api server url to generate error
-    Rails.configuration.arvados_v1_base = "https://[::1]:1/"
+    Rails.configuration.Services.Controller.ExternalURL = "https://[::1]:1/"
     click_link 'Other objects'
     within '#Other_objects' do
       # Error
       assert_selector('a', text: 'Reload tab')
 
       # Now point back to the orig api server and reload tab
-      Rails.configuration.arvados_v1_base = original_arvados_v1_base
+      Rails.configuration.Services.Controller.ExternalURL = original_arvados_v1_base
       click_link 'Reload tab'
       assert_no_selector('a', text: 'Reload tab')
       assert_selector('button', text: 'Selection')
index dc898689e593b6992707ee6bcbb141f9e3329d5c..d2c4372bce0de489954afa620fabd861f1b42948 100644 (file)
@@ -7,11 +7,11 @@ require 'integration_helper'
 class ReportIssueTest < ActionDispatch::IntegrationTest
   setup do
     need_javascript
-    @user_profile_form_fields = Rails.configuration.user_profile_form_fields
+    @user_profile_form_fields = Rails.configuration.Workbench.UserProfileFormFields
   end
 
   teardown do
-    Rails.configuration.user_profile_form_fields = @user_profile_form_fields
+    Rails.configuration.Workbench.UserProfileFormFields = @user_profile_form_fields
   end
 
   # test version info and report issue from help menu
index e4d9894e5fb4e902f90dffc4019e2a771722cd37..547ef06a6827f013b0e958226e10d87ef57464fe 100644 (file)
@@ -7,20 +7,20 @@ require 'integration_helper'
 class UserProfileTest < ActionDispatch::IntegrationTest
   setup do
     need_javascript
-    @user_profile_form_fields = Rails.configuration.user_profile_form_fields
+    @user_profile_form_fields = Rails.configuration.Workbench.UserProfileFormFields
   end
 
   teardown do
-    Rails.configuration.user_profile_form_fields = @user_profile_form_fields
+    Rails.configuration.Workbench.UserProfileFormFields = @user_profile_form_fields
   end
 
   def verify_homepage_with_profile user, invited, has_profile
-    profile_config = Rails.configuration.user_profile_form_fields
+    profile_config = Rails.configuration.Workbench.UserProfileFormFields
 
     if !user
       assert_text('Please log in')
     elsif user['is_active']
-      if profile_config && !has_profile
+      if !profile_config.empty? && !has_profile
         assert_text('Save profile')
         add_profile user
       else
@@ -62,7 +62,7 @@ class UserProfileTest < ActionDispatch::IntegrationTest
             assert_selector('a', text: 'Current token')
             assert_selector('a', text: 'SSH keys')
 
-            if profile_config
+            if !profile_config.empty?
               assert_selector('a', text: 'Manage profile')
             else
               assert_no_selector('a', text: 'Manage profile')
@@ -96,14 +96,14 @@ class UserProfileTest < ActionDispatch::IntegrationTest
     assert_text('Save profile')
 
     # This time fill in required field and then save. Expect to go to requested page after that.
-    profile_message = Rails.configuration.user_profile_form_message
+    profile_message = Rails.configuration.Workbench.UserProfileFormMessage
     required_field_title = ''
     required_field_key = ''
-    profile_config = Rails.configuration.user_profile_form_fields
-    profile_config.each do |entry|
-      if entry['required']
-        required_field_key = entry['key']
-        required_field_title = entry['form_field_title']
+    profile_config = Rails.configuration.Workbench.UserProfileFormFields
+    profile_config.each do |k, entry|
+      if entry['Required']
+        required_field_key = k.to_s
+        required_field_title = entry['FormFieldTitle']
         break
       end
     end
@@ -142,11 +142,11 @@ class UserProfileTest < ActionDispatch::IntegrationTest
     [true, false].each do |profile_required|
       test "visit #{token} home page when profile is #{'not ' if !profile_required}configured" do
         if !profile_required
-          Rails.configuration.user_profile_form_fields = false
+          Rails.configuration.Workbench.UserProfileFormFields = []
         else
           # Our test config enabled profile by default. So, no need to update config
         end
-        Rails.configuration.enable_getting_started_popup = true
+        Rails.configuration.Workbench.EnableGettingStartedPopup = true
 
         if !token
           visit ('/')
index 6a0e46e26aea5b6455fd400ac73d55596ab5895b..562dc7d1f41cb885aee778aace35ae81d9e2d11f 100644 (file)
@@ -78,7 +78,7 @@ class UserSettingsMenuTest < ActionDispatch::IntegrationTest
 
   test "pipeline notification shown even though public pipelines exist" do
     skip "created_by doesn't work that way"
-    Rails.configuration.anonymous_user_token = api_fixture('api_client_authorizations')['anonymous']['api_token']
+    Rails.configuration.Users.AnonymousUserToken = api_fixture('api_client_authorizations')['anonymous']['api_token']
     visit page_with_token 'job_reader'
     click_link 'notifications-menu'
     assert_selector 'a', text: 'Click here to learn how to run an Arvados Crunch pipeline'
@@ -89,7 +89,7 @@ class UserSettingsMenuTest < ActionDispatch::IntegrationTest
     ['active'],
   ].each do |user, *expect|
     test "user settings menu for #{user} with notifications #{expect.inspect}" do
-      Rails.configuration.anonymous_user_token = false
+      Rails.configuration.Users.AnonymousUserToken = ""
       visit page_with_token(user)
       click_link 'notifications-menu'
       if expect.include? :ssh
index e377da3237ea69b820ece1125ee58d11cda4d4c4..a79220a8870095cb70315f088dab5ca7bb5c4743 100644 (file)
@@ -215,7 +215,7 @@ class WebsocketTest < ActionDispatch::IntegrationTest
     visit page_with_token("active", "/jobs/#{job['uuid']}\#Log")
 
     # Expect "all" historic log records because we have less than
-    # default Rails.configuration.running_job_log_records_to_fetch count
+    # default Rails.configuration.Workbench.RunningJobLogRecordsToFetch
     assert_text 'Historic log message'
 
     # Create new log record and expect it to show up in log tab
@@ -228,7 +228,7 @@ class WebsocketTest < ActionDispatch::IntegrationTest
 
   test "test running job with too many previous log records" do
     max = 5
-    Rails.configuration.running_job_log_records_to_fetch = max
+    Rails.configuration.Workbench.RunningJobLogRecordsToFetch = max
     job = api_fixture("jobs")['running']
 
     # Create max+1 log records
index e5cc6e4dc050d47bfa2d6220a4ec02528ef7aa05..b58d59a82ae414012935543d21bf9ec211274f01 100644 (file)
@@ -258,7 +258,7 @@ class WorkUnitsTest < ActionDispatch::IntegrationTest
       if token
         visit page_with_token token, "/#{type}/#{obj['uuid']}"
       else
-        Rails.configuration.anonymous_user_token =
+        Rails.configuration.Users.AnonymousUserToken =
           api_fixture("api_client_authorizations", "anonymous", "api_token")
         visit "/#{type}/#{obj['uuid']}"
       end
index 9337daf4eed8c1d90d31cb9537af8f5f50a4ff0c..34ee1f479a6a2a9a9eacd8402d79dd343bbaf08a 100644 (file)
@@ -164,8 +164,8 @@ module KeepWebConfig
   def use_keep_web_config
     @kwport = getport 'keep-web-ssl'
     @kwdport = getport 'keep-web-dl-ssl'
-    Rails.configuration.keep_web_url = "https://localhost:#{@kwport}/c=%{uuid_or_pdh}"
-    Rails.configuration.keep_web_download_url = "https://localhost:#{@kwdport}/c=%{uuid_or_pdh}"
+    Rails.configuration.Services.WebDAV.ExternalURL = URI("https://localhost:#{@kwport}")
+    Rails.configuration.Services.WebDAVDownload.ExternalURL = URI("https://localhost:#{@kwdport}")
   end
 end
 
index 44b9ad9e3e93d483c27cfb97aff5ad10b8b585b4..3feef945d1d6105301cf7fe4da32ba2ef2371115 100644 (file)
@@ -24,7 +24,7 @@ class BigCollectionTest < ActiveSupport::TestCase
   # didn't make a significant difference.
   [true].each do |compress|
     test "crud cycle for collection with big manifest (compress=#{compress})" do
-      Rails.configuration.api_response_compression = compress
+      Rails.configuration.Workbench.APIResponseCompression = compress
       Thread.current[:arvados_api_client] = nil
       crudtest
     end
index a71d0b46141fd74f2497e01447506831830d83d6..1d26deefa38bbe7c593d07d47dfe0d7d409cf49d 100644 (file)
@@ -61,11 +61,11 @@ class ActiveSupport::TestCase
   end
 
   def self.reset_application_config
-    $application_config.each do |k,v|
-      if k.match /^[^.]*$/
-        Rails.configuration.send (k + '='), v
-      end
-    end
+    # Restore configuration settings changed during tests
+    ConfigLoader.copy_into_config $arvados_config, Rails.configuration
+    ConfigLoader.copy_into_config $remaining_config, Rails.configuration
+    Rails.configuration.Services.Controller.ExternalURL = URI("https://#{ENV['ARVADOS_API_HOST']}")
+    Rails.configuration.TLS.Insecure = true
   end
 end
 
@@ -207,9 +207,6 @@ class ApiServerForTests
     end
 
     run_test_server
-    $application_config['arvados_login_base'] = "https://#{ENV['ARVADOS_API_HOST']}/login"
-    $application_config['arvados_v1_base'] = "https://#{ENV['ARVADOS_API_HOST']}/arvados/v1"
-    $application_config['arvados_insecure_host'] = true
     ActiveSupport::TestCase.reset_application_config
 
     @@server_is_running = true
index fa9a69d38097041b05c988eef8691a79d2182df3..a73e506d14ae6f7a4983da03be5e4105920cca5e 100644 (file)
@@ -16,7 +16,7 @@ class UserTest < ActiveSupport::TestCase
 
   test "User.current doesn't return anonymous user when using invalid token" do
     # Set up anonymous user token
-    Rails.configuration.anonymous_user_token = api_fixture('api_client_authorizations')['anonymous']['api_token']
+    Rails.configuration.Users.AnonymousUserToken = api_fixture('api_client_authorizations')['anonymous']['api_token']
     # First, try with a valid user
     use_token :active
     u = User.current
index eaf65c578dbfb3afbfbdbe3d39081ee25cdee778..4e5ad396789eb57c582101ba868a88e1a854d4d8 100644 (file)
@@ -9,7 +9,7 @@ class WorkUnitTest < ActiveSupport::TestCase
   reset_api_fixtures :after_each_test, false
 
   setup do
-    Rails.configuration.anonymous_user_token = api_fixture('api_client_authorizations')['anonymous']['api_token']
+    Rails.configuration.Users.AnonymousUserToken = api_fixture('api_client_authorizations')['anonymous']['api_token']
   end
 
   [
index 523205021304cb1462c4da66d966072293972e4a..6972415152a8f9456906b9c90bf7ddceb70691ae 100644 (file)
@@ -2,24 +2,16 @@
 #
 # SPDX-License-Identifier: AGPL-3.0
 
-all: centos7/generated debian8/generated debian9/generated ubuntu1404/generated ubuntu1604/generated ubuntu1804/generated
+all: centos7/generated debian9/generated ubuntu1604/generated ubuntu1804/generated
 
 centos7/generated: common-generated-all
        test -d centos7/generated || mkdir centos7/generated
        cp -rlt centos7/generated common-generated/*
 
-debian8/generated: common-generated-all
-       test -d debian8/generated || mkdir debian8/generated
-       cp -rlt debian8/generated common-generated/*
-
 debian9/generated: common-generated-all
        test -d debian9/generated || mkdir debian9/generated
        cp -rlt debian9/generated common-generated/*
 
-ubuntu1404/generated: common-generated-all
-       test -d ubuntu1404/generated || mkdir ubuntu1404/generated
-       cp -rlt ubuntu1404/generated common-generated/*
-
 ubuntu1604/generated: common-generated-all
        test -d ubuntu1604/generated || mkdir ubuntu1604/generated
        cp -rlt ubuntu1604/generated common-generated/*
@@ -28,7 +20,7 @@ ubuntu1804/generated: common-generated-all
        test -d ubuntu1804/generated || mkdir ubuntu1804/generated
        cp -rlt ubuntu1804/generated common-generated/*
 
-GOTARBALL=go1.10.1.linux-amd64.tar.gz
+GOTARBALL=go1.12.7.linux-amd64.tar.gz
 NODETARBALL=node-v6.11.2-linux-x64.tar.xz
 RVMKEY1=mpapis.asc
 RVMKEY2=pkuczynski.asc
@@ -36,7 +28,7 @@ RVMKEY2=pkuczynski.asc
 common-generated-all: common-generated/$(GOTARBALL) common-generated/$(NODETARBALL) common-generated/$(RVMKEY1) common-generated/$(RVMKEY2)
 
 common-generated/$(GOTARBALL): common-generated
-       wget -cqO common-generated/$(GOTARBALL) http://storage.googleapis.com/golang/$(GOTARBALL)
+       wget -cqO common-generated/$(GOTARBALL) https://dl.google.com/go/$(GOTARBALL)
 
 common-generated/$(NODETARBALL): common-generated
        wget -cqO common-generated/$(NODETARBALL) https://nodejs.org/dist/v6.11.2/$(NODETARBALL)
index cc8265c3d4b3098aeff6ee46bfdeeb249cd823bc..610dac90fe89443ffe9a18f53d59b686429c7e3b 100644 (file)
@@ -19,7 +19,7 @@ RUN gpg --import --no-tty /tmp/mpapis.asc && \
     /usr/local/rvm/bin/rvm-exec default gem install fpm --version 1.10.2
 
 # Install golang binary
-ADD generated/go1.10.1.linux-amd64.tar.gz /usr/local/
+ADD generated/go1.12.7.linux-amd64.tar.gz /usr/local/
 RUN ln -s /usr/local/go/bin/go /usr/local/bin/
 
 # Install nodejs and npm
@@ -35,7 +35,11 @@ RUN scl enable rh-python35 "easy_install-3.5 pip" && easy_install-2.7 pip
 RUN wget http://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
 RUN rpm -ivh epel-release-latest-7.noarch.rpm
 
-RUN git clone --depth 1 git://git.curoverse.com/arvados.git /tmp/arvados && cd /tmp/arvados/services/api && /usr/local/rvm/bin/rvm-exec default bundle && cd /tmp/arvados/apps/workbench && /usr/local/rvm/bin/rvm-exec default bundle && rm -rf /tmp/arvados
+RUN git clone --depth 1 git://git.curoverse.com/arvados.git /tmp/arvados && cd /tmp/arvados/services/api && /usr/local/rvm/bin/rvm-exec default bundle && cd /tmp/arvados/apps/workbench && /usr/local/rvm/bin/rvm-exec default bundle
+
+# Workbench depends on arvados-server for config manipulation
+ENV GOPATH /tmp
+RUN mkdir -p $GOPATH/src/git.curoverse.com && ln -sT /tmp/arvados $GOPATH/src/git.curoverse.com/arvados.git && cd $GOPATH/src/git.curoverse.com/arvados.git/cmd/arvados-server && go get -v github.com/kardianos/govendor && $GOPATH/bin/govendor sync && go get && go build && cp arvados-server /usr/local/bin/ && rm -rf /tmp/arvados
 
 # The version of setuptools that comes with CentOS is way too old
 RUN pip install --upgrade setuptools
index a1a1ed6f798676be5e862551df47952d80eb0e1e..4dc78de00a1998b4689961f50d7887c96cf58449 100644 (file)
@@ -26,14 +26,18 @@ RUN gpg --import --no-tty /tmp/mpapis.asc && \
     /usr/local/rvm/bin/rvm-exec default gem install fpm --version 1.10.2
 
 # Install golang binary
-ADD generated/go1.10.1.linux-amd64.tar.gz /usr/local/
+ADD generated/go1.12.7.linux-amd64.tar.gz /usr/local/
 RUN ln -s /usr/local/go/bin/go /usr/local/bin/
 
 # Install nodejs and npm
 ADD generated/node-v6.11.2-linux-x64.tar.xz /usr/local/
 RUN ln -s /usr/local/node-v6.11.2-linux-x64/bin/* /usr/local/bin/
 
-RUN git clone --depth 1 git://git.curoverse.com/arvados.git /tmp/arvados && cd /tmp/arvados/services/api && /usr/local/rvm/bin/rvm-exec default bundle && cd /tmp/arvados/apps/workbench && /usr/local/rvm/bin/rvm-exec default bundle && rm -rf /tmp/arvados
+RUN git clone --depth 1 git://git.curoverse.com/arvados.git /tmp/arvados && cd /tmp/arvados/services/api && /usr/local/rvm/bin/rvm-exec default bundle && cd /tmp/arvados/apps/workbench && /usr/local/rvm/bin/rvm-exec default bundle
+
+# Workbench depends on arvados-server for config manipulation
+ENV GOPATH /tmp
+RUN mkdir -p $GOPATH/src/git.curoverse.com && ln -sT /tmp/arvados $GOPATH/src/git.curoverse.com/arvados.git && cd $GOPATH/src/git.curoverse.com/arvados.git/cmd/arvados-server && go get -v github.com/kardianos/govendor && $GOPATH/bin/govendor sync && go get && go build && cp arvados-server /usr/local/bin/ && rm -rf /tmp/arvados
 
 ENV WORKSPACE /arvados
 CMD ["/usr/local/rvm/bin/rvm-exec", "default", "bash", "/jenkins/run-build-packages.sh", "--target", "debian8"]
index 770db51e7c5180bd0ba83d7e5c57d2899b269c61..f8104472b6dd20e81988c3505646af5ae0f629df 100644 (file)
@@ -25,14 +25,18 @@ RUN gpg --import --no-tty /tmp/mpapis.asc && \
     /usr/local/rvm/bin/rvm-exec default gem install fpm --version 1.10.2
 
 # Install golang binary
-ADD generated/go1.10.1.linux-amd64.tar.gz /usr/local/
+ADD generated/go1.12.7.linux-amd64.tar.gz /usr/local/
 RUN ln -s /usr/local/go/bin/go /usr/local/bin/
 
 # Install nodejs and npm
 ADD generated/node-v6.11.2-linux-x64.tar.xz /usr/local/
 RUN ln -s /usr/local/node-v6.11.2-linux-x64/bin/* /usr/local/bin/
 
-RUN git clone --depth 1 git://git.curoverse.com/arvados.git /tmp/arvados && cd /tmp/arvados/services/api && /usr/local/rvm/bin/rvm-exec default bundle && cd /tmp/arvados/apps/workbench && /usr/local/rvm/bin/rvm-exec default bundle && rm -rf /tmp/arvados
+RUN git clone --depth 1 git://git.curoverse.com/arvados.git /tmp/arvados && cd /tmp/arvados/services/api && /usr/local/rvm/bin/rvm-exec default bundle && cd /tmp/arvados/apps/workbench && /usr/local/rvm/bin/rvm-exec default bundle
+
+# Workbench depends on arvados-server for config manipulation
+ENV GOPATH /tmp
+RUN mkdir -p $GOPATH/src/git.curoverse.com && ln -sT /tmp/arvados $GOPATH/src/git.curoverse.com/arvados.git && cd $GOPATH/src/git.curoverse.com/arvados.git/cmd/arvados-server && go get -v github.com/kardianos/govendor && $GOPATH/bin/govendor sync && go get && go build && cp arvados-server /usr/local/bin/ && rm -rf /tmp/arvados
 
 ENV WORKSPACE /arvados
 CMD ["/usr/local/rvm/bin/rvm-exec", "default", "bash", "/jenkins/run-build-packages.sh", "--target", "debian9"]
index 4c01c9e8180fefb268ff80e9d930f339144eb879..a356b1c798dec7ec7c2c496b20d7dc51da4e8042 100644 (file)
@@ -25,14 +25,18 @@ RUN gpg --import --no-tty /tmp/mpapis.asc && \
     /usr/local/rvm/bin/rvm-exec default gem install fpm --version 1.10.2
 
 # Install golang binary
-ADD generated/go1.10.1.linux-amd64.tar.gz /usr/local/
+ADD generated/go1.12.7.linux-amd64.tar.gz /usr/local/
 RUN ln -s /usr/local/go/bin/go /usr/local/bin/
 
 # Install nodejs and npm
 ADD generated/node-v6.11.2-linux-x64.tar.xz /usr/local/
 RUN ln -s /usr/local/node-v6.11.2-linux-x64/bin/* /usr/local/bin/
 
-RUN git clone --depth 1 git://git.curoverse.com/arvados.git /tmp/arvados && cd /tmp/arvados/services/api && /usr/local/rvm/bin/rvm-exec default bundle && cd /tmp/arvados/apps/workbench && /usr/local/rvm/bin/rvm-exec default bundle && rm -rf /tmp/arvados
+RUN git clone --depth 1 git://git.curoverse.com/arvados.git /tmp/arvados && cd /tmp/arvados/services/api && /usr/local/rvm/bin/rvm-exec default bundle && cd /tmp/arvados/apps/workbench && /usr/local/rvm/bin/rvm-exec default bundle
+
+# Workbench depends on arvados-server for config manipulation
+ENV GOPATH /tmp
+RUN mkdir -p $GOPATH/src/git.curoverse.com && ln -sT /tmp/arvados $GOPATH/src/git.curoverse.com/arvados.git && cd $GOPATH/src/git.curoverse.com/arvados.git/cmd/arvados-server && go get -v github.com/kardianos/govendor && $GOPATH/bin/govendor sync && go get && go build && cp arvados-server /usr/local/bin/ && rm -rf /tmp/arvados
 
 ENV WORKSPACE /arvados
 CMD ["/usr/local/rvm/bin/rvm-exec", "default", "bash", "/jenkins/run-build-packages.sh", "--target", "ubuntu1404"]
index dac82097bd6c7b3ab4b9dd02a5e41dbb88f6fc61..63c119c8405f1dc38020db111656af3d12576310 100644 (file)
@@ -24,14 +24,18 @@ RUN gpg --import --no-tty /tmp/mpapis.asc && \
     /usr/local/rvm/bin/rvm-exec default gem install fpm --version 1.10.2
 
 # Install golang binary
-ADD generated/go1.10.1.linux-amd64.tar.gz /usr/local/
+ADD generated/go1.12.7.linux-amd64.tar.gz /usr/local/
 RUN ln -s /usr/local/go/bin/go /usr/local/bin/
 
 # Install nodejs and npm
 ADD generated/node-v6.11.2-linux-x64.tar.xz /usr/local/
 RUN ln -s /usr/local/node-v6.11.2-linux-x64/bin/* /usr/local/bin/
 
-RUN git clone --depth 1 git://git.curoverse.com/arvados.git /tmp/arvados && cd /tmp/arvados/services/api && /usr/local/rvm/bin/rvm-exec default bundle && cd /tmp/arvados/apps/workbench && /usr/local/rvm/bin/rvm-exec default bundle && rm -rf /tmp/arvados
+RUN git clone --depth 1 git://git.curoverse.com/arvados.git /tmp/arvados && cd /tmp/arvados/services/api && /usr/local/rvm/bin/rvm-exec default bundle && cd /tmp/arvados/apps/workbench && /usr/local/rvm/bin/rvm-exec default bundle
+
+# Workbench depends on arvados-server for config manipulation
+ENV GOPATH /tmp
+RUN mkdir -p $GOPATH/src/git.curoverse.com && ln -sT /tmp/arvados $GOPATH/src/git.curoverse.com/arvados.git && cd $GOPATH/src/git.curoverse.com/arvados.git/cmd/arvados-server && go get -v github.com/kardianos/govendor && $GOPATH/bin/govendor sync && go get && go build && cp arvados-server /usr/local/bin/ && rm -rf /tmp/arvados
 
 ENV WORKSPACE /arvados
 CMD ["/usr/local/rvm/bin/rvm-exec", "default", "bash", "/jenkins/run-build-packages.sh", "--target", "ubuntu1604"]
index fdfbd04206f4a90e923034dcaee1474504b3e149..5e4d067671777843373536fe7b2ad42b98cf00e0 100644 (file)
@@ -24,14 +24,18 @@ RUN gpg --import --no-tty /tmp/mpapis.asc && \
     /usr/local/rvm/bin/rvm-exec default gem install fpm --version 1.10.2
 
 # Install golang binary
-ADD generated/go1.10.1.linux-amd64.tar.gz /usr/local/
+ADD generated/go1.12.7.linux-amd64.tar.gz /usr/local/
 RUN ln -s /usr/local/go/bin/go /usr/local/bin/
 
 # Install nodejs and npm
 ADD generated/node-v6.11.2-linux-x64.tar.xz /usr/local/
 RUN ln -s /usr/local/node-v6.11.2-linux-x64/bin/* /usr/local/bin/
 
-RUN git clone --depth 1 git://git.curoverse.com/arvados.git /tmp/arvados && cd /tmp/arvados/services/api && /usr/local/rvm/bin/rvm-exec default bundle && cd /tmp/arvados/apps/workbench && /usr/local/rvm/bin/rvm-exec default bundle && rm -rf /tmp/arvados
+RUN git clone --depth 1 git://git.curoverse.com/arvados.git /tmp/arvados && cd /tmp/arvados/services/api && /usr/local/rvm/bin/rvm-exec default bundle && cd /tmp/arvados/apps/workbench && /usr/local/rvm/bin/rvm-exec default bundle
+
+# Workbench depends on arvados-server for config manipulation
+ENV GOPATH /tmp
+RUN mkdir -p $GOPATH/src/git.curoverse.com && ln -sT /tmp/arvados $GOPATH/src/git.curoverse.com/arvados.git && cd $GOPATH/src/git.curoverse.com/arvados.git/cmd/arvados-server && go get -v github.com/kardianos/govendor && $GOPATH/bin/govendor sync && go get && go build && cp arvados-server /usr/local/bin/ && rm -rf /tmp/arvados
 
 ENV WORKSPACE /arvados
 CMD ["/usr/local/rvm/bin/rvm-exec", "default", "bash", "/jenkins/run-build-packages.sh", "--target", "ubuntu1804"]
index c6d5a15fbab5915f0854b300a72d522aa90b5b29..c7b32968ff044f52b2616b74815d53c8b2d17fa2 100644 (file)
@@ -2,24 +2,16 @@
 #
 # SPDX-License-Identifier: AGPL-3.0
 
-all: centos7/generated debian8/generated debian9/generated ubuntu1404/generated ubuntu1604/generated ubuntu1804/generated
+all: centos7/generated debian9/generated ubuntu1604/generated ubuntu1804/generated
 
 centos7/generated: common-generated-all
        test -d centos7/generated || mkdir centos7/generated
        cp -rlt centos7/generated common-generated/*
 
-debian8/generated: common-generated-all
-       test -d debian8/generated || mkdir debian8/generated
-       cp -rlt debian8/generated common-generated/*
-
 debian9/generated: common-generated-all
        test -d debian9/generated || mkdir debian9/generated
        cp -rlt debian9/generated common-generated/*
 
-ubuntu1404/generated: common-generated-all
-       test -d ubuntu1404/generated || mkdir ubuntu1404/generated
-       cp -rlt ubuntu1404/generated common-generated/*
-
 ubuntu1604/generated: common-generated-all
        test -d ubuntu1604/generated || mkdir ubuntu1604/generated
        cp -rlt ubuntu1604/generated common-generated/*
diff --git a/build/package-test-dockerfiles/debian8/Dockerfile b/build/package-test-dockerfiles/debian8/Dockerfile
deleted file mode 100644 (file)
index 1d3bb87..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
-FROM debian:8
-MAINTAINER Ward Vandewege <wvandewege@veritasgenetics.com>
-
-RUN perl -ni~ -e 'print unless /jessie-updates/' /etc/apt/sources.list
-
-ENV DEBIAN_FRONTEND noninteractive
-
-# Install dependencies
-RUN apt-get update && \
-    apt-get -y install --no-install-recommends curl ca-certificates
-
-# Install RVM
-ADD generated/mpapis.asc /tmp/
-ADD generated/pkuczynski.asc /tmp/
-RUN gpg --import --no-tty /tmp/mpapis.asc && \
-    gpg --import --no-tty /tmp/pkuczynski.asc && \
-    curl -L https://get.rvm.io | bash -s stable && \
-    /usr/local/rvm/bin/rvm install 2.5 && \
-    /usr/local/rvm/bin/rvm alias create default ruby-2.5
-
-# udev daemon can't start in a container, so don't try.
-RUN mkdir -p /etc/udev/disabled
-
-RUN echo "deb file:///arvados/packages/debian8/ /" >>/etc/apt/sources.list
diff --git a/build/package-test-dockerfiles/ubuntu1404/Dockerfile b/build/package-test-dockerfiles/ubuntu1404/Dockerfile
deleted file mode 100644 (file)
index c05dbee..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
-FROM ubuntu:trusty
-MAINTAINER Ward Vandewege <wvandewege@veritasgenetics.com>
-
-ENV DEBIAN_FRONTEND noninteractive
-
-# Install dependencies
-RUN apt-get update && \
-    apt-get -y install --no-install-recommends curl ca-certificates python2.7-dev python3 python-setuptools python3-setuptools libcurl4-gnutls-dev curl git libattr1-dev libfuse-dev libpq-dev python-pip unzip binutils build-essential ca-certificates
-
-# Install RVM
-ADD generated/mpapis.asc /tmp/
-ADD generated/pkuczynski.asc /tmp/
-RUN gpg --import --no-tty /tmp/mpapis.asc && \
-    gpg --import --no-tty /tmp/pkuczynski.asc && \
-    curl -L https://get.rvm.io | bash -s stable && \
-    /usr/local/rvm/bin/rvm install 2.3 && \
-    /usr/local/rvm/bin/rvm alias create default ruby-2.3
-
-# udev daemon can't start in a container, so don't try.
-RUN mkdir -p /etc/udev/disabled
-
-RUN echo "deb file:///arvados/packages/ubuntu1404/ /" >>/etc/apt/sources.list
index 789a7ee17e6cd6dd514e10ac1e7ea40840d76102..56d55d3276472c43d0fcbd268fefe76b7fb7e6de 100644 (file)
@@ -259,4 +259,8 @@ elif [ "$1" = "0" ] || [ "$1" = "1" ] || [ "$1" = "2" ]; then
 fi
 
 report_not_ready "$DATABASE_READY" "$CONFIG_PATH/database.yml"
-report_not_ready "$APPLICATION_READY" "$CONFIG_PATH/application.yml"
+if printf '%s\n' "$CONFIG_PATH" | grep -Fqe "sso"; then
+       report_not_ready "$APPLICATION_READY" "$CONFIG_PATH/application.yml"
+else
+       report_not_ready "$APPLICATION_READY" "/etc/arvados/config.yml"
+fi
index 69defb003be91b7bbfc0dfd45533eb1ae654e231..5c7dc342ee9c40b546ad38e377555193d6192975 100755 (executable)
@@ -232,8 +232,7 @@ if [[ -n "$test_packages" ]]; then
         fi
         echo
         echo "START: $p test on $IMAGE" >&2
-        # ulimit option can be removed when debian8 and ubuntu1404 are retired
-        if docker run --ulimit nofile=4096:4096 \
+        if docker run \
             --rm \
             "${docker_volume_args[@]}" \
             --env ARVADOS_DEBUG=$ARVADOS_DEBUG \
@@ -263,8 +262,8 @@ else
     mv -f ${WORKSPACE}/packages/${TARGET}/* ${WORKSPACE}/packages/${TARGET}/processed/ 2>/dev/null
     set -e
 set -x
-    # Build packages. ulimit option can be removed when debian8 and ubuntu1404 are retired
-    if docker run --ulimit nofile=4096:4096 \
+    # Build packages.
+    if docker run \
         --rm \
         "${docker_volume_args[@]}" \
         --env ARVADOS_BUILDING_VERSION="$ARVADOS_BUILDING_VERSION" \
index 4c5f39a373e66cdf160ac71aaa7edf7fc47cd2e3..f9b61179cae7d21f9e0129bb6d4b00c3d9f64a32 100755 (executable)
@@ -159,8 +159,8 @@ if [ ${#failures[@]} -ne 0 ]; then
   GEM_BUILD_FAILURES=${#failures[@]}
 fi
 
-python_wrapper arvados-pam "$WORKSPACE/sdk/pam"
 python_wrapper arvados-python-client "$WORKSPACE/sdk/python"
+python_wrapper arvados-pam "$WORKSPACE/sdk/pam"
 python_wrapper arvados-cwl-runner "$WORKSPACE/sdk/cwl"
 python_wrapper arvados_fuse "$WORKSPACE/services/fuse"
 python_wrapper arvados-node-manager "$WORKSPACE/services/nodemanager"
index b800d43e08a060e1231cf1bfdfee5facdca02ed9..ae677a12c16059fc4ca7796a69dbf6337856f09e 100755 (executable)
@@ -19,7 +19,7 @@ Options:
 --debug
     Output debug information (default: false)
 --target <target>
-    Distribution to build packages for (default: debian8)
+    Distribution to build packages for (default: debian9)
 --only-build <package>
     Build only a specific package (or $ONLY_BUILD from environment)
 --command
@@ -42,7 +42,7 @@ VENDOR="Veritas Genetics, Inc."
 
 DEBUG=${ARVADOS_DEBUG:-0}
 EXITCODE=0
-TARGET=debian8
+TARGET=debian9
 COMMAND=
 
 PARSEDOPTS=$(getopt --name "$0" --longoptions \
@@ -323,9 +323,8 @@ package_go_binary tools/keep-rsync keep-rsync \
 package_go_binary tools/keep-exercise keep-exercise \
     "Performance testing tool for Arvados Keep"
 
-# The Python SDK
+# The Python SDK - Should be built first because it's needed by others
 fpm_build_virtualenv "arvados-python-client" "sdk/python"
-fpm_build_virtualenv "arvados-python-client" "sdk/python" "python3"
 
 # Arvados cwl runner
 fpm_build_virtualenv "arvados-cwl-runner" "sdk/cwl"
@@ -339,12 +338,15 @@ fpm_build_virtualenv "arvados-fuse" "services/fuse"
 # The node manager
 fpm_build_virtualenv "arvados-node-manager" "services/nodemanager"
 
-# The Docker image cleaner
-fpm_build_virtualenv "arvados-docker-cleaner" "services/dockercleaner" "python3"
-
 # The Arvados crunchstat-summary tool
 fpm_build_virtualenv "crunchstat-summary" "tools/crunchstat-summary"
 
+# The Python SDK - Python3 package
+fpm_build_virtualenv "arvados-python-client" "sdk/python" "python3"
+
+# The Docker image cleaner
+fpm_build_virtualenv "arvados-docker-cleaner" "services/dockercleaner" "python3"
+
 # The cwltest package, which lives out of tree
 cd "$WORKSPACE"
 if [[ -e "$WORKSPACE/cwltest" ]]; then
@@ -382,17 +384,17 @@ if [[ "$?" == "0" ]] ; then
       rm -rf tmp
       mkdir tmp
 
-      # Set up application.yml and production.rb so that asset precompilation works
-      \cp config/application.yml.example config/application.yml -f
-      \cp config/environments/production.rb.example config/environments/production.rb -f
-      sed -i 's/secret_token: ~/secret_token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/' config/application.yml
-      sed -i 's/keep_web_url: false/keep_web_url: exampledotcom/' config/application.yml
+      # Set up an appropriate config.yml
+      arvados-server config-dump -config <(cat /etc/arvados/config.yml 2>/dev/null || echo  "Clusters: {zzzzz: {}}") > /tmp/x
+      mkdir -p /etc/arvados/
+      mv /tmp/x /etc/arvados/config.yml
+      perl -p -i -e 'BEGIN{undef $/;} s/WebDAV(.*?):\n( *)ExternalURL: ""/WebDAV$1:\n$2ExternalURL: "example.com"/g' /etc/arvados/config.yml
 
       RAILS_ENV=production RAILS_GROUPS=assets bundle exec rake npm:install >/dev/null
       RAILS_ENV=production RAILS_GROUPS=assets bundle exec rake assets:precompile >/dev/null
 
       # Remove generated configuration files so they don't go in the package.
-      rm config/application.yml config/environments/production.rb
+      rm -rf /etc/arvados/
   )
 
   if [[ "$?" != "0" ]]; then
index 1aa3e3cfd1147ebe15b0c041637a055b015f3c93..b4c2d1ddc8e840c3602b0ebc62c81c6f99dd83f8 100755 (executable)
@@ -262,10 +262,8 @@ test_package_presence() {
 
     if [[ "$FORMAT" == "deb" ]]; then
       declare -A dd
-      dd[debian8]=jessie
       dd[debian9]=stretch
       dd[debian10]=buster
-      dd[ubuntu1404]=trusty
       dd[ubuntu1604]=xenial
       dd[ubuntu1804]=bionic
       D=${dd[$TARGET]}
@@ -352,15 +350,6 @@ handle_rails_package() {
     if  [[ "$pkgname" != "arvados-workbench" ]]; then
       exclude_list+=('config/database.yml')
     fi
-    # for arvados-api-server, we need to dereference the
-    # config/config.default.yml file. There is no fpm way to do that, sadly
-    # (excluding the existing symlink and then adding the file from its source
-    # path doesn't work, sadly.
-    if [[ "$pkgname" == "arvados-api-server" ]]; then
-      mv /arvados/services/api/config/config.default.yml /arvados/services/api/config/config.default.yml.bu
-      cp -p /arvados/lib/config/config.default.yml /arvados/services/api/config/
-      exclude_list+=('config/config.default.yml.bu')
-    fi
     for exclude in ${exclude_list[@]}; do
         switches+=(-x "$exclude_root/$exclude")
     done
@@ -368,11 +357,6 @@ handle_rails_package() {
               -x "$exclude_root/vendor/cache-*" \
               -x "$exclude_root/vendor/bundle" "$@" "$license_arg"
     rm -rf "$scripts_dir"
-    # Undo the deferencing we did above
-    if [[ "$pkgname" == "arvados-api-server" ]]; then
-      rm -f /arvados/services/api/config/config.default.yml
-      mv /arvados/services/api/config/config.default.yml.bu /arvados/services/api/config/config.default.yml
-    fi
 }
 
 # Build python packages with a virtualenv built-in
@@ -427,7 +411,9 @@ fpm_build_virtualenv () {
     PYTHON_PKG=$PKG
   fi
 
-  if [[ -n "$ONLY_BUILD" ]] && [[ "$PYTHON_PKG" != "$ONLY_BUILD" ]] && [[ "$PKG" != "$ONLY_BUILD" ]]; then
+  # arvados-python-client sdist should always be built, to be available
+  # for other dependant packages.
+  if [[ -n "$ONLY_BUILD" ]] && [[ "arvados-python-client" != "$PKG" ]] && [[ "$PYTHON_PKG" != "$ONLY_BUILD" ]] && [[ "$PKG" != "$ONLY_BUILD" ]]; then
     return 0
   fi
 
@@ -449,6 +435,14 @@ fpm_build_virtualenv () {
 
   PACKAGE_PATH=`(cd dist; ls *tar.gz)`
 
+  if [[ "arvados-python-client" == "$PKG" ]]; then
+    PYSDK_PATH=`pwd`/dist/
+  fi
+
+  if [[ -n "$ONLY_BUILD" ]] && [[ "$PYTHON_PKG" != "$ONLY_BUILD" ]] && [[ "$PKG" != "$ONLY_BUILD" ]]; then
+    return 0
+  fi
+
   # Determine the package version from the generated sdist archive
   PYTHON_VERSION=${ARVADOS_BUILDING_VERSION:-$(awk '($1 == "Version:"){print $2}' *.egg-info/PKG-INFO)}
 
@@ -500,16 +494,16 @@ fpm_build_virtualenv () {
   echo "wheel version:      `build/usr/share/$python/dist/$PYTHON_PKG/bin/wheel version`"
 
   if [[ "$TARGET" != "centos7" ]] || [[ "$PYTHON_PKG" != "python-arvados-fuse" ]]; then
-    build/usr/share/$python/dist/$PYTHON_PKG/bin/$pip install $DASHQ_UNLESS_DEBUG $CACHE_FLAG $PACKAGE_PATH
+    build/usr/share/$python/dist/$PYTHON_PKG/bin/$pip install $DASHQ_UNLESS_DEBUG $CACHE_FLAG -f $PYSDK_PATH $PACKAGE_PATH
   else
     # centos7 needs these special tweaks to install python-arvados-fuse
     build/usr/share/$python/dist/$PYTHON_PKG/bin/$pip install $DASHQ_UNLESS_DEBUG $CACHE_FLAG docutils
-    PYCURL_SSL_LIBRARY=nss build/usr/share/$python/dist/$PYTHON_PKG/bin/$pip install $DASHQ_UNLESS_DEBUG $CACHE_FLAG $PACKAGE_PATH
+    PYCURL_SSL_LIBRARY=nss build/usr/share/$python/dist/$PYTHON_PKG/bin/$pip install $DASHQ_UNLESS_DEBUG $CACHE_FLAG -f $PYSDK_PATH $PACKAGE_PATH
   fi
 
   if [[ "$?" != "0" ]]; then
     echo "Error, unable to run"
-    echo "  build/usr/share/$python/dist/$PYTHON_PKG/bin/$pip install $DASHQ_UNLESS_DEBUG $CACHE_FLAG $PACKAGE_PATH"
+    echo "  build/usr/share/$python/dist/$PYTHON_PKG/bin/$pip install $DASHQ_UNLESS_DEBUG $CACHE_FLAG -f $PYSDK_PATH $PACKAGE_PATH"
     exit 1
   fi
 
index ebcedf8cda5f31971ca9b197811c3edff029592c..3cf96f3b4ae01eb85873d5f8cfca9ce1dbfd8995 100755 (executable)
@@ -77,9 +77,14 @@ doc
 lib/cli
 lib/cmd
 lib/controller
+lib/controller/federation
+lib/controller/railsproxy
+lib/controller/router
+lib/controller/rpc
 lib/crunchstat
 lib/cloud
 lib/cloud/azure
+lib/cloud/cloudtest
 lib/dispatchcloud
 lib/dispatchcloud/container
 lib/dispatchcloud/scheduler
@@ -205,8 +210,8 @@ sanity_checks() {
     echo -n 'go: '
     go version \
         || fatal "No go binary. See http://golang.org/doc/install"
-    [[ $(go version) =~ go1.([0-9]+) ]] && [[ ${BASH_REMATCH[1]} -ge 10 ]] \
-        || fatal "Go >= 1.10 required. See http://golang.org/doc/install"
+    [[ $(go version) =~ go1.([0-9]+) ]] && [[ ${BASH_REMATCH[1]} -ge 12 ]] \
+        || fatal "Go >= 1.12 required. See http://golang.org/doc/install"
     echo -n 'gcc: '
     gcc --version | egrep ^gcc \
         || fatal "No gcc. Try: apt-get install build-essential"
@@ -253,12 +258,6 @@ sanity_checks() {
     echo -n 'libpq libpq-fe.h: '
     find /usr/include -path '*/postgresql/libpq-fe.h' | egrep --max-count=1 . \
         || fatal "No libpq libpq-fe.h. Try: apt-get install libpq-dev"
-    echo -n 'services/api/config/database.yml: '
-    if [[ ! -f "$WORKSPACE/services/api/config/database.yml" ]]; then
-           fatal "Please provide a database.yml file for the test suite"
-    else
-           echo "OK"
-    fi
     echo -n 'postgresql: '
     psql --version || fatal "No postgresql. Try: apt-get install postgresql postgresql-client-common"
     echo -n 'phantomjs: '
@@ -573,8 +572,6 @@ setup_virtualenv() {
     else
         "$venvdest/bin/pip" install --no-cache-dir 'setuptools>=18.5' 'pip>=7'
     fi
-    # ubuntu1404 can't seem to install mock via tests_require, but it can do this.
-    "$venvdest/bin/pip" install --no-cache-dir 'mock>=1.0' 'pbr<1.7.0'
 }
 
 initialize() {
@@ -614,6 +611,9 @@ initialize() {
     export R_LIBS
 
     export GOPATH
+    # Make sure our compiled binaries under test override anything
+    # else that might be in the environment.
+    export PATH=$GOPATH/bin:$PATH
 
     # Jenkins config requires that glob tmp/*.log match something. Ensure
     # that happens even if we don't end up running services that set up
@@ -623,11 +623,30 @@ initialize() {
 
     unset http_proxy https_proxy no_proxy
 
-
     # Note: this must be the last time we change PATH, otherwise rvm will
     # whine a lot.
     setup_ruby_environment
 
+    if [[ -s "$CONFIGSRC/config.yml" ]] ; then
+       cp "$CONFIGSRC/config.yml" "$temp/test-config.yml"
+       export ARVADOS_CONFIG="$temp/test-config.yml"
+    else
+       if [[ -s /etc/arvados/config.yml ]] ; then
+           python > "$temp/test-config.yml" <<EOF
+import yaml
+import json
+v = list(yaml.safe_load(open('/etc/arvados/config.yml'))['Clusters'].values())[0]['PostgreSQL']
+v['Connection']['dbname'] = 'arvados_test'
+print(json.dumps({"Clusters": { "zzzzz": {'PostgreSQL': v}}}))
+EOF
+           export ARVADOS_CONFIG="$temp/test-config.yml"
+       else
+           if [[ ! -f "$WORKSPACE/services/api/config/database.yml" ]]; then
+               fatal "Please provide a database.yml file for the test suite"
+           fi
+       fi
+    fi
+
     echo "PATH is $PATH"
 }
 
@@ -737,7 +756,7 @@ do_test() {
         services/api)
             stop_services
             ;;
-        gofmt | govendor | doc | lib/cli | lib/cloud/azure | lib/cloud/ec2 | lib/cmd | lib/dispatchcloud/ssh_executor | lib/dispatchcloud/worker)
+        gofmt | govendor | doc | lib/cli | lib/cloud/azure | lib/cloud/ec2 | lib/cloud/cloudtest | lib/cmd | lib/dispatchcloud/ssh_executor | lib/dispatchcloud/worker)
             # don't care whether services are running
             ;;
         *)
@@ -769,7 +788,7 @@ do_test_once() {
         # before trying "go test". Otherwise, coverage-reporting
         # mode makes Go show the wrong line numbers when reporting
         # compilation errors.
-        go get -ldflags "-X main.version=${ARVADOS_VERSION:-$(git log -n1 --format=%H)-dev}" -t "git.curoverse.com/arvados.git/$1" && \
+        go get -ldflags "-X git.curoverse.com/arvados.git/lib/cmd.version=${ARVADOS_VERSION:-$(git log -n1 --format=%H)-dev}" -t "git.curoverse.com/arvados.git/$1" && \
             cd "$GOPATH/src/git.curoverse.com/arvados.git/$1" && \
             if [[ -n "${testargs[$1]}" ]]
         then
@@ -838,7 +857,7 @@ do_install_once() {
         result=1
     elif [[ "$2" == "go" ]]
     then
-        go get -ldflags "-X main.version=${ARVADOS_VERSION:-$(git log -n1 --format=%H)-dev}" -t "git.curoverse.com/arvados.git/$1"
+        go get -ldflags "-X git.curoverse.com/arvados.git/lib/cmd.version=${ARVADOS_VERSION:-$(git log -n1 --format=%H)-dev}" -t "git.curoverse.com/arvados.git/$1"
     elif [[ "$2" == "pip" ]]
     then
         # $3 can name a path directory for us to use, including trailing
@@ -989,51 +1008,7 @@ pythonstuff=(
 )
 
 declare -a gostuff
-gostuff=(
-    cmd/arvados-client
-    cmd/arvados-server
-    lib/cli
-    lib/cmd
-    lib/controller
-    lib/crunchstat
-    lib/cloud
-    lib/cloud/azure
-    lib/cloud/ec2
-    lib/config
-    lib/dispatchcloud
-    lib/dispatchcloud/container
-    lib/dispatchcloud/scheduler
-    lib/dispatchcloud/ssh_executor
-    lib/dispatchcloud/worker
-    lib/service
-    sdk/go/arvados
-    sdk/go/arvadosclient
-    sdk/go/auth
-    sdk/go/blockdigest
-    sdk/go/dispatch
-    sdk/go/health
-    sdk/go/httpserver
-    sdk/go/manifest
-    sdk/go/asyncbuf
-    sdk/go/crunchrunner
-    sdk/go/stats
-    services/arv-git-httpd
-    services/crunchstat
-    services/health
-    services/keep-web
-    services/keepstore
-    sdk/go/keepclient
-    services/keep-balance
-    services/keepproxy
-    services/crunch-dispatch-local
-    services/crunch-dispatch-slurm
-    services/crunch-run
-    services/ws
-    tools/keep-block-check
-    tools/keep-exercise
-    tools/keep-rsync
-    tools/sync-groups
-)
+gostuff=($(cd "$WORKSPACE" && git grep -lw func | grep \\.go | sed -e 's/\/[^\/]*$//' | sort -u))
 
 install_apps/workbench() {
     cd "$WORKSPACE/apps/workbench" \
@@ -1119,28 +1094,48 @@ test_services/nodemanager_integration() {
 }
 
 test_apps/workbench_units() {
+    local TASK="test:units"
+    if [[ -n "${testargs[apps/workbench]}" ]] || [[ -n "${testargs[apps/workbench_units]}" ]]; then
+        TASK="test"
+    fi
     cd "$WORKSPACE/apps/workbench" \
-        && env RAILS_ENV=test ${short:+RAILS_TEST_SHORT=1} bundle exec rake test:units TESTOPTS='-v -d' ${testargs[apps/workbench]} ${testargs[apps/workbench_units]}
+        && env RAILS_ENV=test ${short:+RAILS_TEST_SHORT=1} bundle exec rake ${TASK} TESTOPTS='-v -d' ${testargs[apps/workbench]} ${testargs[apps/workbench_units]}
 }
 
 test_apps/workbench_functionals() {
+    local TASK="test:functionals"
+    if [[ -n "${testargs[apps/workbench]}" ]] || [[ -n "${testargs[apps/workbench_functionals]}" ]]; then
+        TASK="test"
+    fi
     cd "$WORKSPACE/apps/workbench" \
-        && env RAILS_ENV=test ${short:+RAILS_TEST_SHORT=1} bundle exec rake test:functionals TESTOPTS='-v -d' ${testargs[apps/workbench]} ${testargs[apps/workbench_functionals]}
+        && env RAILS_ENV=test ${short:+RAILS_TEST_SHORT=1} bundle exec rake ${TASK} TESTOPTS='-v -d' ${testargs[apps/workbench]} ${testargs[apps/workbench_functionals]}
 }
 
 test_apps/workbench_integration() {
+    local TASK="test:integration"
+    if [[ -n "${testargs[apps/workbench]}" ]] || [[ -n "${testargs[apps/workbench_integration]}" ]]; then
+        TASK="test"
+    fi
     cd "$WORKSPACE/apps/workbench" \
-        && env RAILS_ENV=test ${short:+RAILS_TEST_SHORT=1} bundle exec rake test:integration TESTOPTS='-v -d' ${testargs[apps/workbench]} ${testargs[apps/workbench_integration]}
+        && env RAILS_ENV=test ${short:+RAILS_TEST_SHORT=1} bundle exec rake ${TASK} TESTOPTS='-v -d' ${testargs[apps/workbench]} ${testargs[apps/workbench_integration]}
 }
 
 test_apps/workbench_benchmark() {
+    local TASK="test:benchmark"
+    if [[ -n "${testargs[apps/workbench]}" ]] || [[ -n "${testargs[apps/workbench_benchmark]}" ]]; then
+        TASK="test"
+    fi
     cd "$WORKSPACE/apps/workbench" \
-        && env RAILS_ENV=test ${short:+RAILS_TEST_SHORT=1} bundle exec rake test:benchmark ${testargs[apps/workbench_benchmark]}
+        && env RAILS_ENV=test ${short:+RAILS_TEST_SHORT=1} bundle exec rake ${TASK} ${testargs[apps/workbench_benchmark]}
 }
 
 test_apps/workbench_profile() {
+    local TASK="test:profile"
+    if [[ -n "${testargs[apps/workbench]}" ]] || [[ -n "${testargs[apps/workbench_profile]}" ]]; then
+        TASK="test"
+    fi
     cd "$WORKSPACE/apps/workbench" \
-        && env RAILS_ENV=test ${short:+RAILS_TEST_SHORT=1} bundle exec rake test:profile ${testargs[apps/workbench_profile]}
+        && env RAILS_ENV=test ${short:+RAILS_TEST_SHORT=1} bundle exec rake ${TASK} ${testargs[apps/workbench_profile]}
 }
 
 install_deps() {
@@ -1180,11 +1175,11 @@ install_all() {
             fi
         fi
     done
-    do_install services/api
     for g in "${gostuff[@]}"
     do
         do_install "$g" go
     done
+    do_install services/api
     do_install apps/workbench
 }
 
@@ -1261,6 +1256,13 @@ for p in "${pythonstuff[@]}"; do
 done
 
 testfuncargs["sdk/cli"]="sdk/cli"
+testfuncargs["sdk/R"]="sdk/R"
+testfuncargs["sdk/java-v2"]="sdk/java-v2"
+testfuncargs["apps/workbench_units"]="apps/workbench_units"
+testfuncargs["apps/workbench_functionals"]="apps/workbench_functionals"
+testfuncargs["apps/workbench_integration"]="apps/workbench_integration"
+testfuncargs["apps/workbench_benchmark"]="apps/workbench_benchmark"
+testfuncargs["apps/workbench_profile"]="apps/workbench_profile"
 
 if [[ -z ${interactive} ]]; then
     install_all
@@ -1275,19 +1277,21 @@ else
             # assume emacs, or something, is offering a history buffer
             # and pre-populating the command will only cause trouble
             nextcmd=
-        elif [[ "$nextcmd" != "install deps" ]]; then
-            :
-        elif [[ -e "$VENVDIR/bin/activate" ]]; then
-            nextcmd="test lib/cmd"
-        else
+        elif [[ ! -e "$VENVDIR/bin/activate" ]]; then
             nextcmd="install deps"
+        else
+            nextcmd=""
         fi
     }
     echo
     help_interactive
     nextcmd="install deps"
     setnextcmd
-    while read -p 'What next? ' -e -i "${nextcmd}" nextcmd; do
+    HISTFILE="$WORKSPACE/tmp/.history"
+    history -r
+    while read -p 'What next? ' -e -i "$nextcmd" nextcmd; do
+        history -s "$nextcmd"
+        history -w
         read verb target opts <<<"${nextcmd}"
         target="${target%/}"
         target="${target/\/:/:}"
index 4550ae53aced128d0698891c76d95a1730cae316..589e05c8a14c92a84de4ba2ae2fe586d72bc3a3d 100644 (file)
@@ -12,12 +12,11 @@ import (
 )
 
 var (
-       version = "dev"
        handler = cmd.Multi(map[string]cmd.Handler{
-               "-e":        cmd.Version(version),
-               "version":   cmd.Version(version),
-               "-version":  cmd.Version(version),
-               "--version": cmd.Version(version),
+               "-e":        cmd.Version,
+               "version":   cmd.Version,
+               "-version":  cmd.Version,
+               "--version": cmd.Version,
 
                "copy":     cli.Copy,
                "create":   cli.Create,
index 983159382297dab0a5d95fbf1f35f440fc015720..14cd63e46de5fcf6c7e85d6566b79a10cccf26ca 100644 (file)
@@ -7,6 +7,7 @@ package main
 import (
        "os"
 
+       "git.curoverse.com/arvados.git/lib/cloud/cloudtest"
        "git.curoverse.com/arvados.git/lib/cmd"
        "git.curoverse.com/arvados.git/lib/config"
        "git.curoverse.com/arvados.git/lib/controller"
@@ -14,16 +15,17 @@ import (
 )
 
 var (
-       version = "dev"
        handler = cmd.Multi(map[string]cmd.Handler{
-               "version":   cmd.Version(version),
-               "-version":  cmd.Version(version),
-               "--version": cmd.Version(version),
+               "version":   cmd.Version,
+               "-version":  cmd.Version,
+               "--version": cmd.Version,
 
-               "config-check":   config.CheckCommand,
-               "config-dump":    config.DumpCommand,
-               "controller":     controller.Command,
-               "dispatch-cloud": dispatchcloud.Command,
+               "cloudtest":       cloudtest.Command,
+               "config-check":    config.CheckCommand,
+               "config-dump":     config.DumpCommand,
+               "config-defaults": config.DumpDefaultsCommand,
+               "controller":      controller.Command,
+               "dispatch-cloud":  dispatchcloud.Command,
        })
 )
 
index 20a2085c11b3403071c83d75ed12a4fc8068d119..023658d3ab4e369ff7c22d020cf1bd5f7afc76d7 100644 (file)
@@ -180,6 +180,7 @@ navbar:
     - Cloud:
       - admin/storage-classes.html.textile.liquid
       - admin/spot-instances.html.textile.liquid
+      - admin/cloudtest.html.textile.liquid
     - Data Management:
       - admin/collection-versioning.html.textile.liquid
       - admin/collection-managed-properties.html.textile.liquid
@@ -187,6 +188,7 @@ navbar:
       - admin/federation.html.textile.liquid
       - admin/controlling-container-reuse.html.textile.liquid
       - admin/logs-table-management.html.textile.liquid
+      - admin/troubleshooting.html.textile.liquid
   installguide:
     - Overview:
       - install/index.html.textile.liquid
index a65aec9b945b3cbe58cb4fea5985c02eb959bbb9..7e0cb0d518e1e7ad6506500fb577f3d146c24fc1 100644 (file)
@@ -10,6 +10,6 @@ import arvados.util as util
 filters = [['properties.responsible_person_uuid', 'exists', False]]
 cols = util.list_all(arvados.api().collections().list, filters=filters, select=['uuid', 'name'])
 
-print("Found {} collections:".format(len(cols)))
+print('Found {} collections:'.format(len(cols)))
 for c in cols:
     print('{}, "{}"'.format(c['uuid'], c['name']))
\ No newline at end of file
index 06ef6f097ad051359e1084039cf62cf973bfe03b..073db7ac690dff82c1438da3a8ceb3b9089a66d6 100644 (file)
@@ -28,9 +28,9 @@ for p_uuid in [root_uuid] + get_subproject_uuids(api, root_uuid):
     f = [['properties.responsible_person_uuid', 'exists', False],
          ['owner_uuid', '=', p_uuid]]
     cols = get_cols(api, f)
-    print("Found {} collections owned by {}".format(len(cols), p_uuid))
+    print('Found {} collections owned by {}'.format(len(cols), p_uuid))
     for c in cols:
-        print(" - Updating collection {}".format(c["uuid"]))
+        print(' - Updating collection {}'.format(c['uuid']))
         props = c['properties']
         props['responsible_person_uuid'] = responsible_uuid
         api.collections().update(uuid=c['uuid'], body={'properties': props}).execute()
\ No newline at end of file
index a2a8f9d82ae70d0bfbe0e40c06e777f0d79f16b8..d84e34699bf3f3f3ceaaca8667e31c8952cee6cb 100644 (file)
@@ -7,16 +7,16 @@ SPDX-License-Identifier: CC-BY-SA-3.0
 import arvados
 import arvados.util as util
 
-old_uuid = "zzzzz-tpzed-xxxxxxxxxxxxxxx"
-new_uuid = "zzzzz-tpzed-yyyyyyyyyyyyyyy"
+old_uuid = 'zzzzz-tpzed-xxxxxxxxxxxxxxx'
+new_uuid = 'zzzzz-tpzed-yyyyyyyyyyyyyyy'
 
 api = arvados.api()
 filters = [['properties.responsible_person_uuid', '=', '{}'.format(old_uuid)]]
 cols = util.list_all(api.collections().list, filters=filters, select=['uuid', 'properties'])
 
-print("Found {} collections".format(len(cols)))
+print('Found {} collections'.format(len(cols)))
 for c in cols:
-    print("Updating collection {}".format(c["uuid"]))
+    print('Updating collection {}'.format(c['uuid']))
     props = c['properties']
     props['responsible_person_uuid'] = new_uuid
     api.collections().update(uuid=c['uuid'], body={'properties': props}).execute()
\ No newline at end of file
diff --git a/doc/admin/cloudtest.html.textile.liquid b/doc/admin/cloudtest.html.textile.liquid
new file mode 100644 (file)
index 0000000..2adce90
--- /dev/null
@@ -0,0 +1,71 @@
+---
+layout: default
+navsection: admin
+title: Testing cloud configuration
+...
+
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+
+The @arvados-server@ package includes a @cloudtest@ tool that checks compatibility between your Arvados configuration, your cloud driver, your cloud provider's API, your cloud provider's VM instances, and the worker image you use with the *experimental* "cloud dispatcher":../install/install-dispatch-cloud.html.
+
+@arvados-server cloudtest@ performs the following steps:
+# Create a new instance
+# Wait for it to finish booting
+# Run a shell command on the new instance (optional)
+# Pause while you log in to the new instance and do other tests yourself (optional)
+# Shut down the instance
+
+This is an easy way to expose problems like these:
+* Configured cloud credentials don't work
+* Configured image types don't work
+* Configured driver is not compatible with your cloud API/region
+* Newly created instances are not usable due to a network problem or misconfiguration
+* Newly created instances do not accept the configured SSH private key
+* Selected machine image does not boot properly
+* Selected machine image is incompatible with some instance types
+* Driver has bugs
+
+h2. Typical uses
+
+Before bringing up the @arvados-dispatch-cloud@ service for the first time, we recommend running @cloudtest@ to check your configuration:
+
+<notextile><pre>
+$ <span class="userinput">arvados-server cloudtest -command "crunch-run --list"</span>
+</pre></notextile>
+
+Before updating your configuration to use a new VM image, we recommend running @cloudtest@ with the new image:
+
+<notextile><pre>
+$ <span class="userinput">arvados-server cloudtest -image-id <b>new_image_id</b> -command "crunch-run --list"</span>
+</pre></notextile>
+
+After adding an instance type to your configuration, we recommend running @cloudtest@ with the new instance type:
+
+<notextile><pre>
+$ <span class="userinput">arvados-server cloudtest -instance-type <b>new_instance_type_name</b></span>
+</pre></notextile>
+
+For a full list of options, use the @-help@ flag:
+
+<notextile><pre>
+$ <span class="userinput">arvados-server cloudtest -help</span>
+Usage:
+  -command string
+        Run an interactive shell command on the test instance when it boots
+  -config file
+        Site configuration file (default "/etc/arvados/config.yml")
+  -destroy-existing
+        Destroy any existing instances tagged with our InstanceSetID, instead of erroring out
+  -image-id string
+        Image ID to use when creating the test instance (if empty, use cluster config)
+  -instance-set-id value
+        InstanceSetID tag value to use on the test instance (default "cloudtest-user@hostname.example")
+  -instance-type string
+        Instance type to create (if empty, use cheapest type in config)
+  -pause-before-destroy
+        Prompt and wait before destroying the test instance
+</pre></notextile>
index 11546c032327385b680fe617885bbaac6055bbf8..3f10a87feb29927ed40a694e3d5ceffd85a9c145 100644 (file)
@@ -19,16 +19,37 @@ The legacy API server configuration is stored in @config/application.yml@ and @c
 Change to the API server directory and use the following commands:
 
 <pre>
-$ bundle exec rake config:migrate > config.yml
+$ RAILS_ENV=production bundle exec rake config:migrate > config.yml
 $ cp config.yml /etc/arvados/config.yml
 </pre>
 
-This will print the contents of @config.yml@ after merging with legacy @application.yml@.  It may then be redirected to a file and copied to @/etc/arvados/config.yml@.
+This will print the contents of @config.yml@ after merging the legacy @application.yml@ and @database.yml@ into the existing systemwide @config.yml@.  It may be redirected to a file and copied to @/etc/arvados/config.yml@ (it is safe to copy over, all configuration items from the existing @/etc/arvados/config.yml@ will be included in the migrated output).
 
 If you wish to update @config.yml@ configuration by hand, or check that everything has been migrated, use @config:diff@ to print configuration items that differ between @application.yml@ and the system @config.yml@.
 
 <pre>
-$ bundle exec rake config:diff
+$ RAILS_ENV=production bundle exec rake config:diff
+</pre>
+
+This command will also report if no migrations are required.
+
+h2. Workbench
+
+The legacy workbench configuration is stored in @config/application.yml@.  After migration to @/etc/arvados/config.yml@, this file should be moved out of the way and/or deleted.
+
+Change to the workbench server directory and use the following commands:
+
+<pre>
+$ RAILS_ENV=production bundle exec rake config:migrate > config.yml
+$ cp config.yml /etc/arvados/config.yml
+</pre>
+
+This will print the contents of @config.yml@ after merging the legacy @application.yml@ into the existing systemwide @config.yml@.  It may be redirected to a file and copied to @/etc/arvados/config.yml@ (it is safe to copy over, all configuration items from the existing @/etc/arvados/config.yml@ will be included in the migrated output).
+
+If you wish to update @config.yml@ configuration by hand, or check that everything has been migrated, use @config:diff@ to print configuration items that differ between @application.yml@ and the system @config.yml@.
+
+<pre>
+$ RAILS_ENV=production bundle exec rake config:diff
 </pre>
 
 This command will also report if no migrations are required.
@@ -43,8 +64,8 @@ Currently only reads @RemoteClusters@ from centralized configuration.  Still req
 
 h2. arvados-controller
 
-Only supports centralized config file.  No migration needed.
+Already uses centralized config exclusively.  No migration needed.
 
 h2. arvados-dispatch-cloud
 
-Only supports centralized config file.  No migration needed.
+Already uses centralized config exclusively.  No migration needed.
diff --git a/doc/admin/troubleshooting.html.textile.liquid b/doc/admin/troubleshooting.html.textile.liquid
new file mode 100644 (file)
index 0000000..66c75f3
--- /dev/null
@@ -0,0 +1,74 @@
+---
+layout: default
+navsection: admin
+title: Troubleshooting
+...
+
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+
+Using a distributed system with several services working together sometimes makes it difficult to find the root cause of errors, as one single client request usually means several different requests to more than one service.
+
+To deal with this difficulty, Arvados creates a request ID that gets carried over different services as the requests take place. This ID has a specific format and it's comprised of the prefix "@req-@" followed by 20 random alphanumeric characters:
+
+<pre>req-frdyrcgdh4rau1ajiq5q</pre>
+
+This ID gets propagated via an HTTP @X-Request-Id@ header, and gets logged on every service.
+
+h3. API Server error reporting and logging
+
+In addition to providing the request ID on every HTTP response, the API Server adds it to every error message so that all clients show enough information to the user to be able to track a particular issue. As an example, let's suppose that we get the following error when trying to create a collection using the CLI tools:
+
+<pre>
+$ arv collection create --collection '{}'
+Error: #<RuntimeError: Whoops, something bad happened> (req-ku5ct9ehw0y71f1c5p79)
+</pre>
+
+The API Server logs every request in JSON format on the @production.log@ (usually under @/var/www/arvados-api/current/log/@ when installing from packages) file, so we can retrieve more information about this by using @grep@ and @jq@ tools:
+
+<pre>
+# grep req-ku5ct9ehw0y71f1c5p79 /var/www/arvados-api/current/log/production.log | jq .
+{
+  "method": "POST",
+  "path": "/arvados/v1/collections",
+  "format": "json",
+  "controller": "Arvados::V1::CollectionsController",
+  "action": "create",
+  "status": 422,
+  "duration": 1.52,
+  "view": 0.25,
+  "db": 0,
+  "request_id": "req-ku5ct9ehw0y71f1c5p79",
+  "client_ipaddr": "127.0.0.1",
+  "client_auth": "zzzzz-gj3su-jllemyj9v3s5emu",
+  "exception": "#<RuntimeError: Whoops, something bad happened>",
+  "exception_backtrace": "/var/www/arvados-api/current/app/controllers/arvados/v1/collections_controller.rb:43:in `create'\n/var/lib/gems/ruby/2.3.0/gems/actionpack-5.0.7.2/lib/action_controller/metal/basic_implicit_render.rb:4:in `send_action'\n ...[snipped]",
+  "params": {
+    "collection": "{}",
+    "_profile": "true",
+    "cluster_id": "",
+    "collection_given": "true",
+    "ensure_unique_name": "false",
+    "help": "false"
+  },
+  "@timestamp": "2019-07-15T16:40:41.726634182Z",
+  "@version": "1",
+  "message": "[422] POST /arvados/v1/collections (Arvados::V1::CollectionsController#create)"
+}
+</pre>
+
+When logging a request that produced an error, the API Server adds @exception@ and @exception_backtrace@ keys to the JSON log. The latter includes the complete error stack trace as a string, and can be displayed in a more readable form like so:
+
+<pre>
+# grep req-ku5ct9ehw0y71f1c5p79 /var/www/arvados-api/current/log/production.log | jq -r .exception_backtrace
+/var/www/arvados-api/current/app/controllers/arvados/v1/collections_controller.rb:43:in `create'
+/var/lib/gems/ruby/2.3.0/gems/actionpack-5.0.7.2/lib/action_controller/metal/basic_implicit_render.rb:4:in `send_action'
+/var/lib/gems/ruby/2.3.0/gems/actionpack-5.0.7.2/lib/abstract_controller/base.rb:188:in `process_action'
+/var/lib/gems/ruby/2.3.0/gems/actionpack-5.0.7.2/lib/action_controller/metal/rendering.rb:30:in `process_action'
+/var/lib/gems/ruby/2.3.0/gems/actionpack-5.0.7.2/lib/abstract_controller/callbacks.rb:20:in `block in process_action'
+/var/lib/gems/ruby/2.3.0/gems/activesupport-5.0.7.2/lib/active_support/callbacks.rb:126:in `call'
+...
+</pre>
\ No newline at end of file
index 18e1abae3a9f1b81660c964b2be9a678decb667f..da37d743de4aa43d821ff449f7a21d772af73bb2 100644 (file)
@@ -76,6 +76,10 @@ The "postgres-contrib package":https://www.postgresql.org/docs/10/contrib.html h
 
 Subsequently, the <code class="userinput">psql -d 'arvados_production' -c '\dx'</code> command will display the installed extensions for the arvados_production database. This list should now contain @pg_trgm@.
 
+h4. Migrating to centralized config.yml
+
+See "Migrating Configuration":config-migration.html for notes on migrating legacy per-component configuration files to the new centralized @/etc/arvados/config.yml@.  To ensure a smooth transition, the per-component config files continue to be read, and take precedence over the centralized configuration.
+
 h3(#v1_4_0). v1.4.0 (2019-06-05)
 
 h4. Populating the new file_count and file_size_total columns on the collections table
index 4f97ba4cef5b5f6dd023aa97317840700ba82100..0e01b3c6dd740abe04210381bd0877931e1366c3 100644 (file)
@@ -107,9 +107,20 @@ table(table table-bordered table-condensed).
 |@is_a@|string|Arvados object type|@["head_uuid","is_a","arvados#collection"]@|
 |@exists@|string|Test if a subproperty is present.|@["properties","exists","my_subproperty"]@|
 
+
+h4(#substringsearchfilter). Filtering using substring search
+
+Resources can also be filtered by searching for a substring in attributes of type @string@, @array of strings@, @text@, and @hash@, which are indexed in the database specifically for search. To use substring search, the filter must:
+
+* Specify @any@ as the attribute
+* Use either the @like@ or @ilike@ operator
+* Have an operand of type @string@ that is wrapped in the SQL pattern match wildcard character @%@
+
+For example, the @["any", "like", "%foo%"]@ filter will return all resources that contain @foo@ in the content of at least one attribute of the previously defined types. This is the recommended way to do keyword and file name search across the entire database. Note that only exact substring matches are returned and results are unranked and returned in the order specified by the @list@ @order@ argument.
+
 h4(#subpropertyfilters). Filtering on subproperties
 
-Some record type have an additional @properties@ attribute that allows recording and filtering on additional key-value pairs.  To filter on a subproperty, the value in the @attribute@ position has the form @properties.user_property@.  You may also use JSON-LD / RDF style URIs for property keys by enclosing them in @<...>@ for example @properties.<http://example.com/user_property>@.  Alternately you may also provide a JSON-LD "@context" field, however at this time JSON-LD contexts are not interpreted by Arvados.
+Some record types have an additional @properties@ attribute that allows recording and filtering on additional key-value pairs.  To filter on a subproperty, the value in the @attribute@ position has the form @properties.user_property@.  You may also use JSON-LD / RDF style URIs for property keys by enclosing them in @<...>@ for example @properties.<http://example.com/user_property>@.  Alternately you may also provide a JSON-LD "@context" field, however at this time JSON-LD contexts are not interpreted by Arvados.
 
 table(table table-bordered table-condensed).
 |_. Operator|_. Operand type|_. Description|_. Example|
index 9c75fa8ec7aec59d9ffea144cbf67d9a6caae7b0..2653cccd5d257d74d2319d3c1da317b32389b84c 100644 (file)
@@ -42,7 +42,7 @@ Required arguments are displayed in %{background:#ccffcc}green%.
 
 h3. contents
 
-Retrieve a list of items owned by the group.
+Retrieve a list of items owned by the group.  Use "recursive" to list objects within subprojects as well.
 
 Arguments:
 
@@ -53,11 +53,17 @@ table(table table-bordered table-condensed).
 |order|array|Attributes to use as sort keys to determine the order resources are returned, each optionally followed by @asc@ or @desc@ to indicate ascending or descending order. Sort within a resource type by prefixing the attribute with the resource name and a period.|query|@["collections.modified_at desc"]@|
 |filters|array|Conditions for filtering items.|query|@[["uuid", "is_a", "arvados#job"]]@|
 |recursive|boolean (default false)|Include items owned by subprojects.|query|@true@|
-|exclude_home_project|boolean (default false)|Only return items which are visible to the user but not accessible within the user's home project.  Use this to get a list of items that are shared with the user.|query|@true@|
+|exclude_home_project|boolean (default false)|Only return items which are visible to the user but not accessible within the user's home project.  Use this to get a list of items that are shared with the user.  Uses the logic described under the "shared" endpoint.|query|@true@|
+|include|string|If provided with the value "owner_uuid", this will return owner objects in the "included" field of the response.|query||
 
-Note: Because adding access tokens to manifests can be computationally expensive, the @manifest_text@ field is not included in listed collections.  If you need it, request a "list of collections":{{site.baseurl}}/api/methods/collections.html with the filter @["owner_uuid", "=", GROUP_UUID]@, and @"manifest_text"@ listed in the select parameter.
+Notes:
+
+Because adding access tokens to manifests can be computationally expensive, the @manifest_text@ field is not included in listed collections.  If you need it, request a "list of collections":{{site.baseurl}}/api/methods/collections.html with the filter @["owner_uuid", "=", GROUP_UUID]@, and @"manifest_text"@ listed in the select parameter.
+
+Use filters with the attribute format @<item type>.<field name>@ to filter items of a specific type. For example: @["container_requests.state", "=", "Final"]@ to filter @container_requests@ where @state@ is @Final@. All other types of items owned by this group will be unimpacted by this filter and will still be included.
+
+When called with “include=owner_uuid”, the @included@ field of the response is populated with users, projects, or other groups that own the objects returned in @items@.  This can be used to fetch an object and its parent with a single API call.
 
-Note: Use filters with the attribute format @<item type>.<field name>@ to filter items of a specific type. For example: @["pipeline_instances.state", "=", "Complete"]@ to filter @pipeline_instances@ where @state@ is @Complete@. All other types of items owned by this group will be unimpacted by this filter and will still be included.
 
 h3. create
 
@@ -131,9 +137,7 @@ table(table table-bordered table-condensed).
 
 h3. shared
 
-This endpoint returns the toplevel set of groups to which access is granted through a chain of one or more permission links rather than through direct ownership by the current user account.  This is useful for clients which wish to browse the list of projects the user has permission to read which are not part of the "home" project tree.
-
-When called with "include=owner_uuid" this also returns (in the "included" field) the objects that own those projects (users or non-project groups).
+This endpoint returns the toplevel set of groups to which access is granted through a chain of one or more permission links rather than through direct ownership by the current user account.  This is useful for clients which wish to browse the list of projects the user has permission to read which are not part of the "home" project tree.  Similar behavior is also available with the @exclude_home_project@ option of the "contents" endpoint.
 
 Specifically, the logic is:
 
@@ -144,8 +148,12 @@ select groups that are readable by current user AND
      the owner_uuid is a group but group_class is not a project)
 </pre>
 
-In addition to the "include" parameter this endpoint also supports the same parameters as the "list method.":{{site.baseurl}}/api/methods.html#index
-
 table(table table-bordered table-condensed).
 |_. Argument |_. Type |_. Description |_. Location |_. Example |
-|include|string|If provided with the value "owner_uuid", this will return owner objects in the "included" field of the response.|query|?include=owner_uuid|
+|include|string|If provided with the value "owner_uuid", this will return owner objects in the @included@ field of the response.|query||
+
+Notes:
+
+When called with “include=owner_uuid”, the @included@ field of the response is populated with users and non-project groups that own the objects returned in @items@.
+
+In addition to the "include" parameter this endpoint also supports the same parameters as the "list method.":{{site.baseurl}}/api/methods.html#index
index c31b2ed43c89b92b5ef8c5c15c8abdd4dd185cbe..7181fed5cffac0a9491c5fd5870ff983be1cf09a 100644 (file)
@@ -9,7 +9,7 @@ Copyright (C) The Arvados Authors. All rights reserved.
 SPDX-License-Identifier: CC-BY-SA-3.0
 {% endcomment %}
 
-Arvados components run on GNU/Linux systems, and supports multiple cloud operating stacks.  Arvados supports Debian and derivatives such as Ubuntu, as well as Red Hat and derivatives such as CentOS.
+Arvados components run on GNU/Linux systems, and supports multiple cloud operating stacks.  Arvados supports Debian and derivatives such as Ubuntu, as well as Red Hat and derivatives such as CentOS.  Although Arvados development is sponsored by Veritas Genetics which offers commercial support, "Arvados is Free Software":{{site.baseurl}}/copying/copying.html and we encourage self supported/community supported installations.
 
 Arvados components can be installed and configured in a number of different ways.
 
index a6b843b160ba39161850005b0de858daab867ac4..c234bca9270a25268f4acab6d355fe36770ffbc0 100644 (file)
@@ -35,102 +35,124 @@ On a Red Hat-based system, install the following packages:
 
 {% include 'install_git' %}
 
-h2(#configure). Set up the database
-
-Configure the API server to connect to your database by updating @/etc/arvados/api/database.yml@. Replace the @xxxxxxxx@ database password placeholder with the "password you generated during database setup":install-postgresql.html#api. Be sure to update the @production@ section.
-
-<notextile>
-<pre><code>~$ <span class="userinput">editor /etc/arvados/api/database.yml</span>
-</code></pre></notextile>
-
 h2(#configure_application). Configure the API server
 
-Edit @/etc/arvados/api/application.yml@ to configure the settings described in the following sections.  The API server reads both @application.yml@ and its own @config/application.default.yml@ file.  The settings in @application.yml@ take precedence over the defaults that are defined in @config/application.default.yml@.  The @config/application.yml.example@ file is not read by the API server and is provided as a starting template only.
+Edit @/etc/arvados/config.yml@ to set the keys below.  Only the most important configuration options are listed here.  The example configuration fragments given below should be merged into a single configuration structure.  Correct indentation is important.  The full set of configuration options are listed in "config.yml":{{site.baseurl}}/admin/config.html
 
-@config/application.default.yml@ documents additional configuration settings not listed here.  You can "view the current source version":https://dev.arvados.org/projects/arvados/repository/revisions/master/entry/services/api/config/application.default.yml for reference.
+h3(#uuid_prefix). ClusterID
 
-Only put local configuration in @application.yml@.  Do not edit @application.default.yml@.
+The @ClusterID@ is used for all database identifiers to identify the record as originating from this site.  It is the first key under @Clusters@ in @config.yml@.  It must be exactly 5 lowercase ASCII letters and digits.  All configuration items go under the cluster id key (replace @zzzzz@ with your cluster id in the examples below).
 
-h3(#uuid_prefix). uuid_prefix
+<notextile>
+<pre><code>Clusters:
+  <span class="userinput">zzzzz</span>:
+    ...</code></pre>
+</notextile>
 
-Define your @uuid_prefix@ in @application.yml@ by setting the @uuid_prefix@ field in the section for your environment.  This prefix is used for all database identifiers to identify the record as originating from this site.  It must be exactly 5 lowercase ASCII letters and digits.
+h3(#configure). PostgreSQL.Connection
 
-Example @application.yml@:
+Replace the @xxxxxxxx@ database password placeholder with the "password you generated during database setup":install-postgresql.html#api.
 
 <notextile>
-<pre><code>  uuid_prefix: <span class="userinput">zzzzz</span></code></pre>
+<pre><code>Clusters:
+  zzzzz:
+    PostgreSQL:
+      Connection:
+        host: <span class="userinput">localhost</span>
+        user: <span class="userinput">arvados</span>
+        password: <span class="userinput">xxxxxxxx</span>
+        dbname: <span class="userinput">arvados_production</span>
+      </code></pre>
 </notextile>
 
-h3. secret_token
+h3. API.RailsSessionSecretToken
 
-The @secret_token@ is used for for signing cookies.  IMPORTANT: This is a site secret. It should be at least 50 characters.  Generate a random value and set it in @application.yml@:
+The @API.RailsSessionSecretToken@ is used for for signing cookies.  IMPORTANT: This is a site secret. It should be at least 50 characters.  Generate a random value and set it in @config.yml@:
 
 <notextile>
 <pre><code>~$ <span class="userinput">ruby -e 'puts rand(2**400).to_s(36)'</span>
 yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
 </code></pre></notextile>
 
-Example @application.yml@:
+Example @config.yml@:
 
 <notextile>
-<pre><code>  secret_token: <span class="userinput">yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy</span></code></pre>
+<pre><code>Clusters:
+  zzzzz:
+    API:
+      RailsSessionSecretToken: <span class="userinput">yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy</span></code></pre>
 </notextile>
 
-h3(#blob_signing_key). blob_signing_key
+h3(#blob_signing_key). Collections.BlobSigningKey
 
-The @blob_signing_key@ is used to enforce access control to Keep blocks.  This same key must be provided to the Keepstore daemons when "installing Keepstore servers.":install-keepstore.html  IMPORTANT: This is a site secret. It should be at least 50 characters.  Generate a random value and set it in @application.yml@:
+The @Collections.BlobSigningKey@ is used to enforce access control to Keep blocks.  This same key must be provided to the Keepstore daemons when "installing Keepstore servers.":install-keepstore.html  IMPORTANT: This is a site secret. It should be at least 50 characters.  Generate a random value and set it in @config.yml@:
 
 <notextile>
 <pre><code>~$ <span class="userinput">ruby -e 'puts rand(2**400).to_s(36)'</span>
 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
 </code></pre></notextile>
 
-Example @application.yml@:
+Example @config.yml@:
 
 <notextile>
-<pre><code>  blob_signing_key: <span class="userinput">xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</span></code></pre>
+<pre><code>Clusters:
+  zzzzz:
+    Collections:
+      BlobSigningKey: <span class="userinput">xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</span></code></pre>
 </notextile>
 
-h3(#omniauth). sso_app_secret, sso_app_id, sso_provider_url
+h3(#omniauth). Login.ProviderAppID, Login.ProviderAppSecret, Services.SSO.ExternalURL
 
 The following settings enable the API server to communicate with the "Single Sign On (SSO) server":install-sso.html to authenticate user log in.
 
-Set @sso_provider_url@ to the base URL where your SSO server is installed.  This should be a URL consisting of the scheme and host (and optionally, port), without a trailing slash.
+Set @Services.SSO.ExternalURL@ to the base URL where your SSO server is installed.  This should be a URL consisting of the scheme and host (and optionally, port), without a trailing slash.
 
-Set @sso_app_secret@ and @sso_app_id@ to the corresponding values for @app_secret@ and @app_id@ used in the "Create arvados-server client for Single Sign On (SSO)":install-sso.html#client step.
+Set @Login.ProviderAppID@ and @Login.ProviderAppSecret@ to the corresponding values for @app_id@ and @app_secret@ used in the "Create arvados-server client for Single Sign On (SSO)":install-sso.html#client step.
 
-Example @application.yml@:
+Example @config.yml@:
 
 <notextile>
-<pre><code>  sso_app_id: <span class="userinput">arvados-server</span>
-  sso_app_secret: <span class="userinput">wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww</span>
-  sso_provider_url: <span class="userinput">https://sso.example.com</span>
-</code></pre>
+<pre><code>Clusters:
+  zzzzz:
+    Services:
+      SSO:
+        ExternalURL: <span class="userinput">https://sso.example.com</span>
+    Login:
+      ProviderAppID: <span class="userinput">arvados-server</span>
+      ProviderAppSecret: <span class="userinput">wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww</span></code></pre>
 </notextile>
 
-h3. workbench_address
+h3. Services.Workbench1.ExternalURL
 
-Set @workbench_address@ to the URL of your workbench application after following "Install Workbench.":install-workbench-app.html
+Set @Services.Workbench1.ExternalURL@ to the URL of your workbench application after following "Install Workbench.":install-workbench-app.html
 
-Example @application.yml@:
+Example @config.yml@:
 
 <notextile>
-<pre><code>  workbench_address: <span class="userinput">https://workbench.zzzzz.example.com</span></code></pre>
+<pre><code>Clusters:
+  zzzzz:
+    Services:
+      Workbench1:
+        ExternalURL: <span class="userinput">https://workbench.zzzzz.example.com</span></code></pre>
 </notextile>
 
-h3. websocket_address
+h3. Services.Websocket.ExternalURL
 
-Set @websocket_address@ to the @wss://@ URL of the API server websocket endpoint after following "Set up Web servers":#set_up.  The path of the default endpoint is @/websocket@.
+Set @Services.Websocket.ExternalURL@ to the @wss://@ URL of the API server websocket endpoint after following "Install the websocket server":install-ws.html .
 
-Example @application.yml@:
+Example @config.yml@:
 
 <notextile>
-<pre><code>  websocket_address: <span class="userinput">wss://ws.zzzzz.example.com</span>/websocket</code></pre>
+<pre><code>Clusters:
+  zzzzz:
+    Services:
+      Websocket:
+        ExternalURL: <span class="userinput">wss://ws.zzzzz.example.com</span></code></pre>
 </notextile>
 
-h3(#git_repositories_dir). git_repositories_dir
+h3(#git_repositories_dir). Git.Repositories
 
-The @git_repositories_dir@ setting specifies the directory where user git repositories will be stored.
+The @Git.Repositories@ setting specifies the directory where user git repositories will be stored.
 
 The git server setup process is covered on "its own page":install-arv-git-httpd.html. For now, create an empty directory in the default location:
 
@@ -138,39 +160,47 @@ The git server setup process is covered on "its own page":install-arv-git-httpd.
 <pre><code>~$ <span class="userinput">sudo mkdir -p /var/lib/arvados/git/repositories</span>
 </code></pre></notextile>
 
-If you intend to store your git repositories in a different location, specify that location in @application.yml@.
-
-Default setting in @application.default.yml@:
+If you intend to store your git repositories in a different location, specify that location in @config.yml@.  Example:
 
 <notextile>
-<pre><code>  git_repositories_dir: <span class="userinput">/var/lib/arvados/git/repositories</span>
-</code></pre>
+<pre><code>Clusters:
+  zzzzz:
+    Git:
+      Repositories: <span class="userinput">/var/lib/arvados/git/repositories</span></code></pre>
 </notextile>
 
-h3(#git_internal_dir). git_internal_dir
+h3(#enable_legacy_jobs_api). Containers.JobsAPI.Enable
+
+Enable the legacy "Jobs API":install-crunch-dispatch.html .  Note: new installations should use the "Containers API":crunch2-slurm/install-prerequisites.html
 
-The @git_internal_dir@ setting specifies the location of Arvados' internal git repository.  By default this is @/var/lib/arvados/internal.git@.  This repository stores git commits that have been used to run Crunch jobs.  It should _not_ be a subdirectory of @git_repositories_dir@.
+Disabling the jobs API means methods involving @jobs@, @job_tasks@, @pipeline_templates@ and @pipeline_instances@ are disabled.  This functionality is superceded by the containers API which consists of @container_requests@, @containers@ and @workflows@.  Arvados clients (such as @arvados-cwl-runner@) detect which APIs are available and adjust behavior accordingly.  Note the configuration value must be a quoted string.
 
-Example @application.yml@:
+* 'auto' -- (default) enable the Jobs API only if it has been used before (i.e., there are job records in the database), otherwise disable jobs API .
+* 'true' -- enable the Jobs API even if there are no existing job records.
+* 'false' -- disable the Jobs API even in the presence of existing job records.
 
 <notextile>
-<pre><code>  git_internal_dir: <span class="userinput">/var/lib/arvados/internal.git</span>
-</code></pre>
+<pre><code>Clusters:
+  zzzzz:
+    Containers:
+      JobsAPI:
+        Enable: <span class="userinput">'auto'</span></code></pre>
 </notextile>
 
-h3(#enable_legacy_jobs_api). enable_legacy_jobs_api
+h4(#git_internal_dir). Containers.JobsAPI.GitInternalDir
 
-Enable the legacy "Jobs API":install-crunch-dispatch.html .  Note: new installations should use the "Containers API":crunch2-slurm/install-prerequisites.html
+Only required if the legacy "Jobs API" is enabled, otherwise you should skip this.
 
-Disabling the jobs API means methods involving @jobs@, @job_tasks@, @pipeline_templates@ and @pipeline_instances@ are disabled.  This functionality is superceded by the containers API which consists of @container_requests@, @containers@ and @workflows@.  Arvados clients (such as @arvados-cwl-runner@) detect which APIs are available and adjust behavior accordingly.
+The @Containers.JobsAPI.GitInternalDir@ setting specifies the location of Arvados' internal git repository.  By default this is @/var/lib/arvados/internal.git@.  This repository stores git commits that have been used to run Crunch jobs.  It should _not_ be a subdirectory of the directory in @Git.Repositories@.
 
-* auto -- (default) enable the Jobs API only if it has been used before (i.e., there are job records in the database), otherwise disable jobs API .
-* true -- enable the Jobs API even if there are no existing job records.
-* false -- disable the Jobs API even in the presence of existing job records.
+Example @config.yml@:
 
 <notextile>
-<pre><code>  enable_legacy_jobs_api: <span class="userinput">auto</span>
-</code></pre>
+<pre><code>Clusters:
+  zzzzz:
+    Containers:
+      JobsAPI:
+        GitInternalDir: <span class="userinput">/var/lib/arvados/internal.git</span></code></pre>
 </notextile>
 
 h2(#set_up). Set up Nginx and Passenger
@@ -199,7 +229,7 @@ server {
   # also ensure the following settings match it:
   # * `client_max_body_size` in the server section below
   # * `client_max_body_size` in the Workbench Nginx configuration (twice)
-  # * `max_request_size` in the API server's application.yml file
+  # * `API.MaxRequestSize` in config.yml
   client_max_body_size 128m;
 }
 
@@ -241,3 +271,7 @@ break this application for all non-root users on this machine.</pre></notextile>
 
 <notextile><pre>fatal: Not a git repository (or any of the parent directories): .git</pre></notextile>
 {% include 'notebox_end' %}
+
+h2. Troubleshooting
+
+Once you have the API Server up and running you may need to check it back if dealing with client related issues. Please read our "admin troubleshooting notes":{{site.baseurl}}/admin/troubleshooting.html on how requests can be tracked down between services.
\ No newline at end of file
index bc3be8f1d7e88f463d1e954245bec978a3ab967b..5d497cc114cd52ff1caa161181ee68ab1d7badf3 100644 (file)
@@ -59,7 +59,7 @@ You can delete the key files after you have copied the private key to your confi
 
 h2. Configure the dispatcher
 
-Add or update the following portions of your cluster configuration file, @/etc/arvados/config.yml@. Refer to "config.defaults.yml":https://dev.arvados.org/projects/arvados/repository/revisions/13996-new-api-config/entry/lib/config/config.defaults.yml for information about additional configuration options.
+Add or update the following portions of your cluster configuration file, @/etc/arvados/config.yml@. Refer to "config.defaults.yml":{{site.baseurl}}/admin/config.html for information about additional configuration options.
 
 <notextile>
 <pre><code>Clusters:
@@ -153,10 +153,37 @@ Minimal configuration example for Azure:
 </code></pre>
 </notextile>
 
-h2. Install the dispatcher
+h2. Test your configuration
 
 First, "add the appropriate package repository for your distribution":{{ site.baseurl }}/install/install-manual-prerequisites.html#repos.
 
+Next, install the arvados-server package.
+
+On Red Hat-based systems:
+
+<notextile>
+<pre><code>~$ <span class="userinput">sudo yum install arvados-server</span>
+</code></pre>
+</notextile>
+
+On Debian-based systems:
+
+<notextile>
+<pre><code>~$ <span class="userinput">sudo apt-get install arvados-server</span>
+</code></pre>
+</notextile>
+
+Run the @cloudtest@ tool to verify that your configuration works. This creates a new cloud VM, confirms that it boots correctly and accepts your configured SSH private key, and shuts it down.
+
+<notextile>
+<pre><code>~$ <span class="userinput">arvados-server cloudtest && echo "OK!"</span>
+</code></pre>
+</notextile>
+
+Refer to the "cloudtest tool documentation":../admin/cloudtest.html for more information.
+
+h2. Install the dispatcher
+
 On Red Hat-based systems:
 
 <notextile>
index 62017163d2b48540290ff3df29733eebfa759735..9aeb6e88888e7bed6119ff482664473a650faf74 100644 (file)
@@ -35,15 +35,17 @@ h2. Supported GNU/Linux distributions
 table(table table-bordered table-condensed).
 |_. Distribution|_. State|_. Last supported version|
 |CentOS 7|Supported|Latest|
-|Debian 8 ("jessie")|Supported|Latest|
 |Debian 9 ("stretch")|Supported|Latest|
-|Ubuntu 14.04 ("trusty")|Supported|Latest|
 |Ubuntu 16.04 ("xenial")|Supported|Latest|
 |Ubuntu 18.04 ("bionic")|Supported|Latest|
+|Ubuntu 14.04 ("trusty")|EOL|5f943cd451acfbdcddd84e791738c3aa5926bfed (2019-07-10)|
+|Debian 8 ("jessie")|EOL|5f943cd451acfbdcddd84e791738c3aa5926bfed (2019-07-10)|
 |Ubuntu 12.04 ("precise")|EOL|8ed7b6dd5d4df93a3f37096afe6d6f81c2a7ef6e (2017-05-03)|
 |Debian 7 ("wheezy")|EOL|997479d1408139e96ecdb42a60b4f727f814f6c9 (2016-12-28)|
 |CentOS 6 |EOL|997479d1408139e96ecdb42a60b4f727f814f6c9 (2016-12-28)|
 
+Arvados packages are published for current Debian releases (until the EOL date), current Ubuntu LTS releases (until the end of standard support), and the latest version of CentOS.
+
 h2(#repos). Arvados package repositories
 
 On any host where you install Arvados software, you'll need to set up an Arvados package repository.  They're available for several popular distributions.
@@ -65,7 +67,7 @@ gpgkey=http://rpm.arvados.org/CentOS/RPM-GPG-KEY-curoverse
 
 h3. Debian and Ubuntu
 
-Packages are available for Debian 8 ("jessie"), Debian 9 ("stretch"), Ubuntu 14.04 ("trusty"), Ubuntu 16.04 ("xenial") and Ubuntu 18.04 ("bionic").
+Packages are available for Debian 9 ("stretch"), Ubuntu 16.04 ("xenial") and Ubuntu 18.04 ("bionic").
 
 First, register the Curoverse signing key in apt's database:
 
@@ -75,9 +77,7 @@ Configure apt to retrieve packages from the Arvados package repository. This com
 
 table(table table-bordered table-condensed).
 |_. OS version|_. Command|
-|Debian 8 ("jessie")|<notextile><code><span class="userinput">echo "deb http://apt.arvados.org/ jessie main" &#x7c; sudo tee /etc/apt/sources.list.d/arvados.list</span></code></notextile>|
 |Debian 9 ("stretch")|<notextile><code><span class="userinput">echo "deb http://apt.arvados.org/ stretch main" &#x7c; sudo tee /etc/apt/sources.list.d/arvados.list</span></code></notextile>|
-|Ubuntu 14.04 ("trusty")[1]|<notextile><code><span class="userinput">echo "deb http://apt.arvados.org/ trusty main" &#x7c; sudo tee /etc/apt/sources.list.d/arvados.list</span></code></notextile>|
 |Ubuntu 16.04 ("xenial")[1]|<notextile><code><span class="userinput">echo "deb http://apt.arvados.org/ xenial main" &#x7c; sudo tee /etc/apt/sources.list.d/arvados.list</span></code></notextile>|
 |Ubuntu 18.04 ("bionic")[1]|<notextile><code><span class="userinput">echo "deb http://apt.arvados.org/ bionic main" &#x7c; sudo tee /etc/apt/sources.list.d/arvados.list</span></code></notextile>|
 
index 770527da1fe4096af877a67363ee4d6e369a0a7d..431fc10b8e6b59daed45dae173add3bbe08cba58 100644 (file)
@@ -385,7 +385,7 @@ ping_host = hostname:port
 # ssh_key = path
 
 # The GCE image name and network zone name to use when creating new nodes.
-image = debian-7
+image = debian
 # network = your_network_name
 
 # JSON string of service account authorizations for this cluster.
index 593e8018926ad42c0f8a22d6f67fa73c3c1ba7c1..72a80fd834e2c272544de6bae238113f6355032a 100644 (file)
@@ -40,11 +40,9 @@ On a Red Hat-based system, install the following packages:
 
 h2(#configure). Configure Workbench
 
-Edit @/etc/arvados/workbench/application.yml@ following the instructions below.  Workbench reads both @application.yml@ and its own @config/application.defaults.yml@ file.  Values in @application.yml@ take precedence over the defaults that are defined in @config/application.defaults.yml@.  The @config/application.yml.example@ file is not read by Workbench and is provided for installation convenience only.
+Edit @/etc/arvados/config.yml@ to set the keys below.  Only the most important configuration options are listed here.  The full set of configuration options are in the "Workbench section of config.yml":{{site.baseurl}}/admin/config.html
 
-Consult @config/application.default.yml@ for a full list of configuration options.  Always put your local configuration in @/etc/arvados/workbench/application.yml@&mdash;never edit @config/application.default.yml@.
-
-h3. secret_token
+h3. Workbench.SecretKeyBase
 
 This application needs a secret token. Generate a new secret:
 
@@ -54,33 +52,57 @@ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
 </code></pre>
 </notextile>
 
-Then put that value in the @secret_token@ field.
+Then put that value in the @Workbench.SecretKeyBase@ field.
+
+<notextile>
+<pre><code>Cluster:
+  zzzzz:
+    Workbench:
+      SecretKeyBase: <span class="userinput">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</span>
+</code></pre>
+</notextile>
 
-h3. arvados_login_base and arvados_v1_base
+h3. Services.Controller.ExternalURL
 
-Point @arvados_login_base@ and @arvados_v1_base@ at your "API server":install-api-server.html. For example like this:
+Ensure that @Services.Controller.ExternalURL@ is configured for "Arvados Controller":install-controller.html . For example like this:
 
 <notextile>
-<pre><code>arvados_login_base: https://prefix_uuid.your.domain/login
-arvados_v1_base: https://prefix_uuid.your.domain/arvados/v1
+<pre><code>Cluster:
+  zzzzz:
+    Services:
+      Controller:
+        ExternalURL: <span class="userinput">https://prefix_uuid.your.domain</span>
 </code></pre>
 </notextile>
 
-h3. site_name
+h3. Workbench.SiteName
+
+@Workbench.SiteName@ can be set to any arbitrary string. It is used to identify this Workbench to people visiting it.
 
-@site_name@ can be set to any arbitrary string. It is used to identify this Workbench to people visiting it.
 
-h3. arvados_insecure_https
+<notextile>
+<pre><code>Cluster:
+  zzzzz:
+    Workbench:
+      SiteName: <span class="userinput">My Arvados</span>
+</code></pre>
+</notextile>
 
-If the SSL certificate you use for your API server isn't an official certificate signed by a CA, make sure @arvados_insecure_https@ is @true@.
+h3. TLS.Insecure
 
-h3. Other options
+For testing only.  Allows use of self-signed certificates.  If true, workbench will not verify the TLS certificate of Arvados Controller.
 
-Consult @application.default.yml@ for a full list of configuration options. Always put your local configuration in @application.yml@ instead of editing @application.default.yml@.
+<notextile>
+<pre><code>Cluster:
+  zzzzz:
+    TLS:
+      Insecure: <span class="userinput">false</span>
+</code></pre>
+</notextile>
 
-h2. Configure Piwik
+h2. Configure Piwik (optional)
 
-In @/var/www/arvados-workbench/current/config@, copy @piwik.yml.example@ to @piwik.yml@ and edit to suit.
+Piwik can be used to gather usage analytics.  In @/var/www/arvados-workbench/current/config@, copy @piwik.yml.example@ to @piwik.yml@ and edit to suit.
 
 h2. Set up Web server
 
@@ -104,7 +126,7 @@ For best performance, we recommend you use Nginx as your Web server front-end, w
   #passenger_ruby /usr/local/rvm/wrappers/default/ruby;
 
   # `client_max_body_size` should match the corresponding setting in
-  # the API server's Nginx configuration.
+  # the API.MaxRequestSize and Controller's server's Nginx configuration.
   client_max_body_size 128m;
 }
 
@@ -124,7 +146,7 @@ server {
 
   index  index.html index.htm index.php;
   # `client_max_body_size` should match the corresponding setting in
-  # the API server's Nginx configuration.
+  # the API.MaxRequestSize and Controller's server's Nginx configuration.
   client_max_body_size 128m;
 
   location / {
index cd70dfd4ae8e44a077572c8a9cc0044905d7b737..e1d25aaa23019020da809943b8309c1b10dc0d07 100644 (file)
@@ -74,4 +74,4 @@ Example:
 
 @arv collection list --filters '[["name", "=", "PGP VAR inputs"], ["created_at", ">=", "2014-10-01"]]'@
 
-will return a list of all collections visible to the current user which are named "PGP VAR inputs" and were created on or after October 1, 2014.
+will return a list of all collections visible to the current user which are named "PGP VAR inputs" and were created on or after October 1, 2014. See the "Common resource methods":{{site.baseurl}}/api/methods.html#index page for more details on using @list@ and @--filters@.
index e8ef0b68858fa723265978638fce920ddfc803d8..400c55b976c566427987309edaad6a628369e9f7 100644 (file)
@@ -48,7 +48,7 @@ h3. 6. Create a new Command Line Tool
 
 h3. 7. Set Docker image, base command, and input port for "sort" tool
 
-The "Docker Repository" is the name:tag of a "Docker image uploaded Arvados.":{{site.baseurl}}/user/topics/arv-docker.html (Use @arv-keepdocker --pull debian:8@)  You can also find prepackaged bioinformatics tools on various sites, such as http://dockstore.org and http://biocontainers.pro/ .
+The "Docker Repository" is the name:tag of a "Docker image uploaded Arvados.":{{site.baseurl}}/user/topics/arv-docker.html (Use @arv-keepdocker --pull debian:9@)  You can also find prepackaged bioinformatics tools on various sites, such as http://dockstore.org and http://biocontainers.pro/ .
 
 !(screenshot)c6.png!
 
index d62002237a7e7b1d43aa7c59f4ef1afa7bc38b84..052d4509ac7d76bd5b9128b78e3036a379c9a9ec 100644 (file)
@@ -156,6 +156,6 @@ This is an optional extension field appearing on the standard @DockerRequirement
 <pre>
 requirements:
   DockerRequirement:
-    dockerPull: "debian:8"
+    dockerPull: "debian:9"
     arv:dockerCollectionPDH: "feaf1fc916103d7cdab6489e1f8c3a2b+174"
 </pre>
index f34c21a9d741042610e81891ea37848d12380506..e9e84502680cd8641150d5dee064d5ba4561e9fc 100644 (file)
@@ -64,19 +64,18 @@ Next, update the package list using @apt-get update@.
 
 <notextile>
 <pre><code>root@fbf1d0f529d5:/# apt-get update
-Hit http://security.debian.org jessie/updates InRelease
-Ign http://httpredir.debian.org jessie InRelease
-Ign http://apt.arvados.org jessie InRelease
-Hit http://apt.arvados.org jessie Release.gpg
-Get:1 http://security.debian.org jessie/updates/main amd64 Packages [431 kB]
-Hit http://apt.arvados.org jessie Release
-Hit http://httpredir.debian.org jessie-updates InRelease
-Get:2 http://apt.arvados.org jessie/main amd64 Packages [257 kB]
-Get:3 http://httpredir.debian.org jessie-updates/main amd64 Packages [17.6 kB]
-Hit http://httpredir.debian.org jessie Release.gpg
-Hit http://httpredir.debian.org jessie Release
-Get:4 http://httpredir.debian.org jessie/main amd64 Packages [9049 kB]
-Fetched 9755 kB in 2s (3894 kB/s)
+Get:2 http://apt.arvados.org stretch-dev InRelease [3260 B]
+Get:1 http://security-cdn.debian.org/debian-security stretch/updates InRelease [94.3 kB]
+Ign:3 http://cdn-fastly.deb.debian.org/debian stretch InRelease
+Get:4 http://cdn-fastly.deb.debian.org/debian stretch-updates InRelease [91.0 kB]
+Get:5 http://apt.arvados.org stretch-dev/main amd64 Packages [208 kB]
+Get:6 http://cdn-fastly.deb.debian.org/debian stretch Release [118 kB]
+Get:7 http://security-cdn.debian.org/debian-security stretch/updates/main amd64 Packages [499 kB]
+Get:8 http://cdn-fastly.deb.debian.org/debian stretch Release.gpg [2434 B]
+Get:9 http://cdn-fastly.deb.debian.org/debian stretch-updates/main amd64 Packages.diff/Index [10.6 kB]
+Get:10 http://cdn-fastly.deb.debian.org/debian stretch-updates/main amd64 Packages 2019-07-08-0821.07.pdiff [445 B]
+Get:10 http://cdn-fastly.deb.debian.org/debian stretch-updates/main amd64 Packages 2019-07-08-0821.07.pdiff [445 B]
+Fetched 1026 kB in 0s (1384 kB/s)
 Reading package lists... Done
 </code></pre>
 </notextile>
@@ -88,54 +87,9 @@ In this example, we will install the "R" statistical language Debian package "r-
 Reading package lists... Done
 Building dependency tree
 Reading state information... Done
-The following extra packages will be installed:
-  [...]
-  libxxf86vm1 make patch r-base-core r-base-dev r-cran-boot r-cran-class
-  r-cran-cluster r-cran-codetools r-cran-foreign r-cran-kernsmooth
-  r-cran-lattice r-cran-mass r-cran-matrix r-cran-mgcv r-cran-nlme r-cran-nnet
-  r-cran-rpart r-cran-spatial r-cran-survival r-doc-html r-recommended
-  [...]
-Suggested packages:
-  [...]
-The following NEW packages will be installed:
-  [...]
-  libxxf86vm1 make patch r-base-core r-base-dev r-cran-boot r-cran-class
-  r-cran-cluster r-cran-codetools r-cran-foreign r-cran-kernsmooth
-  r-cran-lattice r-cran-mass r-cran-matrix r-cran-mgcv r-cran-nlme r-cran-nnet
-  r-cran-rpart r-cran-spatial r-cran-survival r-doc-html r-recommended
-  [...]
-0 upgraded, 203 newly installed, 0 to remove and 39 not upgraded.
-Need to get 124 MB of archives.
-After this operation, 334 MB of additional disk space will be used.
-Do you want to continue [Y/n]? y
-[...]
-Get:130 http://httpredir.debian.org/debian/ jessie/main r-cran-cluster amd64 1.15.3-1 [475 kB]
-Get:131 http://httpredir.debian.org/debian/ jessie/main r-base-dev all 3.1.1-1 [4018 B]
-Get:132 http://httpredir.debian.org/debian/ jessie/main r-cran-boot all 1.3-13-1 [571 kB]
-Get:133 http://httpredir.debian.org/debian/ jessie/main r-cran-codetools all 0.2-9-1 [45.7 kB]
-Get:134 http://httpredir.debian.org/debian/ jessie/main r-cran-rpart amd64 4.1-8-1 [862 kB]
-Get:135 http://httpredir.debian.org/debian/ jessie/main r-cran-foreign amd64 0.8.61-1 [213 kB]
-[...]
-Fetched 124 MB in 52s (2380 kB/s)
-debconf: delaying package configuration, since apt-utils is not installed
-[...]
-Unpacking r-base-core (3.1.1-1+b2) ...
-Selecting previously unselected package r-base-dev.
-Preparing to unpack .../r-base-dev_3.1.1-1_all.deb ...
-Unpacking r-base-dev (3.1.1-1) ...
-Selecting previously unselected package r-cran-boot.
-Preparing to unpack .../r-cran-boot_1.3-13-1_all.deb ...
-Unpacking r-cran-boot (1.3-13-1) ...
-Selecting previously unselected package r-cran-mass.
-[...]
-Setting up r-base-core (3.1.1-1+b2) ...
-
-Creating config file /etc/R/Renviron with new version
-Setting up r-base-dev (3.1.1-1) ...
-Setting up r-cran-boot (1.3-13-1) ...
-Setting up r-cran-mass (7.3-34-1) ...
-Setting up r-cran-class (7.3-11-1) ...
+The following additional packages will be installed:
 [...]
+done.
 </code></pre>
 </notextile>
 
@@ -144,8 +98,8 @@ Now we can verify that "R" is installed:
 <notextile>
 <pre><code>root@fbf1d0f529d5:/# <span class="userinput">R</span>
 
-R version 3.1.1 (2014-07-10) -- "Sock it to Me"
-Copyright (C) 2014 The R Foundation for Statistical Computing
+R version 3.3.3 (2017-03-06) -- "Another Canoe"
+Copyright (C) 2017 The R Foundation for Statistical Computing
 Platform: x86_64-pc-linux-gnu (64-bit)
 
 R is free software and comes with ABSOLUTELY NO WARRANTY.
diff --git a/lib/cloud/cloudtest/cmd.go b/lib/cloud/cloudtest/cmd.go
new file mode 100644 (file)
index 0000000..9c3fc46
--- /dev/null
@@ -0,0 +1,145 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package cloudtest
+
+import (
+       "bufio"
+       "errors"
+       "flag"
+       "fmt"
+       "io"
+       "os"
+
+       "git.curoverse.com/arvados.git/lib/cloud"
+       "git.curoverse.com/arvados.git/lib/config"
+       "git.curoverse.com/arvados.git/lib/dispatchcloud"
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "git.curoverse.com/arvados.git/sdk/go/ctxlog"
+       "golang.org/x/crypto/ssh"
+)
+
+var Command command
+
+type command struct{}
+
+func (command) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+       var err error
+       defer func() {
+               if err != nil {
+                       fmt.Fprintf(stderr, "%s\n", err)
+               }
+       }()
+
+       flags := flag.NewFlagSet("", flag.ContinueOnError)
+       flags.SetOutput(stderr)
+       configFile := flags.String("config", arvados.DefaultConfigFile, "Site configuration `file`")
+       instanceSetID := flags.String("instance-set-id", "zzzzz-zzzzz-zzzzzzcloudtest", "InstanceSetID tag `value` to use on the test instance")
+       imageID := flags.String("image-id", "", "Image ID to use when creating the test instance (if empty, use cluster config)")
+       instanceType := flags.String("instance-type", "", "Instance type to create (if empty, use cheapest type in config)")
+       destroyExisting := flags.Bool("destroy-existing", false, "Destroy any existing instances tagged with our InstanceSetID, instead of erroring out")
+       shellCommand := flags.String("command", "", "Run an interactive shell command on the test instance when it boots")
+       pauseBeforeDestroy := flags.Bool("pause-before-destroy", false, "Prompt and wait before destroying the test instance")
+       err = flags.Parse(args)
+       if err == flag.ErrHelp {
+               err = nil
+               return 0
+       } else if err != nil {
+               return 2
+       }
+
+       if len(flags.Args()) != 0 {
+               flags.Usage()
+               return 2
+       }
+       logger := ctxlog.New(stderr, "text", "info")
+       defer func() {
+               if err != nil {
+                       logger.WithError(err).Error("fatal")
+                       // suppress output from the other error-printing func
+                       err = nil
+               }
+               logger.Info("exiting")
+       }()
+
+       loader := config.NewLoader(stdin, logger)
+       loader.Path = *configFile
+       cfg, err := loader.Load()
+       if err != nil {
+               return 1
+       }
+       cluster, err := cfg.GetCluster("")
+       if err != nil {
+               return 1
+       }
+       key, err := ssh.ParsePrivateKey([]byte(cluster.Containers.DispatchPrivateKey))
+       if err != nil {
+               err = fmt.Errorf("error parsing configured Containers.DispatchPrivateKey: %s", err)
+               return 1
+       }
+       driver, ok := dispatchcloud.Drivers[cluster.Containers.CloudVMs.Driver]
+       if !ok {
+               err = fmt.Errorf("unsupported cloud driver %q", cluster.Containers.CloudVMs.Driver)
+               return 1
+       }
+       if *imageID == "" {
+               *imageID = cluster.Containers.CloudVMs.ImageID
+       }
+       it, err := chooseInstanceType(cluster, *instanceType)
+       if err != nil {
+               return 1
+       }
+       tags := cloud.SharedResourceTags(cluster.Containers.CloudVMs.ResourceTags)
+       tagKeyPrefix := cluster.Containers.CloudVMs.TagKeyPrefix
+       tags[tagKeyPrefix+"CloudTestPID"] = fmt.Sprintf("%d", os.Getpid())
+       if !(&tester{
+               Logger:           logger,
+               Tags:             tags,
+               TagKeyPrefix:     tagKeyPrefix,
+               SetID:            cloud.InstanceSetID(*instanceSetID),
+               DestroyExisting:  *destroyExisting,
+               ProbeInterval:    cluster.Containers.CloudVMs.ProbeInterval.Duration(),
+               SyncInterval:     cluster.Containers.CloudVMs.SyncInterval.Duration(),
+               TimeoutBooting:   cluster.Containers.CloudVMs.TimeoutBooting.Duration(),
+               Driver:           driver,
+               DriverParameters: cluster.Containers.CloudVMs.DriverParameters,
+               ImageID:          cloud.ImageID(*imageID),
+               InstanceType:     it,
+               SSHKey:           key,
+               SSHPort:          cluster.Containers.CloudVMs.SSHPort,
+               BootProbeCommand: cluster.Containers.CloudVMs.BootProbeCommand,
+               ShellCommand:     *shellCommand,
+               PauseBeforeDestroy: func() {
+                       if *pauseBeforeDestroy {
+                               logger.Info("waiting for operator to press Enter")
+                               fmt.Fprint(stderr, "Press Enter to continue: ")
+                               bufio.NewReader(stdin).ReadString('\n')
+                       }
+               },
+       }).Run() {
+               return 1
+       }
+       return 0
+}
+
+// Return the named instance type, or the cheapest type if name=="".
+func chooseInstanceType(cluster *arvados.Cluster, name string) (arvados.InstanceType, error) {
+       if len(cluster.InstanceTypes) == 0 {
+               return arvados.InstanceType{}, errors.New("no instance types are configured")
+       } else if name == "" {
+               first := true
+               var best arvados.InstanceType
+               for _, it := range cluster.InstanceTypes {
+                       if first || best.Price > it.Price {
+                               best = it
+                               first = false
+                       }
+               }
+               return best, nil
+       } else if it, ok := cluster.InstanceTypes[name]; !ok {
+               return it, fmt.Errorf("requested instance type %q is not configured", name)
+       } else {
+               return it, nil
+       }
+}
diff --git a/lib/cloud/cloudtest/tester.go b/lib/cloud/cloudtest/tester.go
new file mode 100644 (file)
index 0000000..ad3f70f
--- /dev/null
@@ -0,0 +1,393 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package cloudtest
+
+import (
+       "crypto/rand"
+       "encoding/json"
+       "errors"
+       "fmt"
+       "time"
+
+       "git.curoverse.com/arvados.git/lib/cloud"
+       "git.curoverse.com/arvados.git/lib/dispatchcloud/ssh_executor"
+       "git.curoverse.com/arvados.git/lib/dispatchcloud/worker"
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "github.com/sirupsen/logrus"
+       "golang.org/x/crypto/ssh"
+)
+
+var (
+       errTestInstanceNotFound = errors.New("test instance missing from cloud provider's list")
+)
+
+// A tester does a sequence of operations to test a cloud driver and
+// configuration. Run() should be called only once, after assigning
+// suitable values to public fields.
+type tester struct {
+       Logger             logrus.FieldLogger
+       Tags               cloud.SharedResourceTags
+       TagKeyPrefix       string
+       SetID              cloud.InstanceSetID
+       DestroyExisting    bool
+       ProbeInterval      time.Duration
+       SyncInterval       time.Duration
+       TimeoutBooting     time.Duration
+       Driver             cloud.Driver
+       DriverParameters   json.RawMessage
+       InstanceType       arvados.InstanceType
+       ImageID            cloud.ImageID
+       SSHKey             ssh.Signer
+       SSHPort            string
+       BootProbeCommand   string
+       ShellCommand       string
+       PauseBeforeDestroy func()
+
+       is              cloud.InstanceSet
+       testInstance    *worker.TagVerifier
+       secret          string
+       executor        *ssh_executor.Executor
+       showedLoginInfo bool
+
+       failed bool
+}
+
+// Run the test suite as specified, clean up as needed, and return
+// true (everything is OK) or false (something went wrong).
+func (t *tester) Run() bool {
+       // This flag gets set when we encounter a non-fatal error, so
+       // we can continue doing more tests but remember to return
+       // false (failure) at the end.
+       deferredError := false
+
+       var err error
+       t.is, err = t.Driver.InstanceSet(t.DriverParameters, t.SetID, t.Tags, t.Logger)
+       if err != nil {
+               t.Logger.WithError(err).Info("error initializing driver")
+               return false
+       }
+
+       for {
+               // Don't send the driver any filters when getting the
+               // initial instance list. This way we can log an
+               // instance count (N=...)  that includes all instances
+               // in this service account, even if they don't have
+               // the same InstanceSetID.
+               insts, err := t.getInstances(nil)
+               if err != nil {
+                       t.Logger.WithError(err).Info("error getting list of instances")
+                       return false
+               }
+
+               foundExisting := false
+               for _, i := range insts {
+                       if i.Tags()[t.TagKeyPrefix+"InstanceSetID"] != string(t.SetID) {
+                               continue
+                       }
+                       lgr := t.Logger.WithFields(logrus.Fields{
+                               "Instance":      i.ID(),
+                               "InstanceSetID": t.SetID,
+                       })
+                       foundExisting = true
+                       if t.DestroyExisting {
+                               lgr.Info("destroying existing instance with our InstanceSetID")
+                               t0 := time.Now()
+                               err := i.Destroy()
+                               lgr := lgr.WithField("Duration", time.Since(t0))
+                               if err != nil {
+                                       lgr.WithError(err).Error("error destroying existing instance")
+                               } else {
+                                       lgr.Info("Destroy() call succeeded")
+                               }
+                       } else {
+                               lgr.Error("found existing instance with our InstanceSetID")
+                       }
+               }
+               if !foundExisting {
+                       break
+               } else if t.DestroyExisting {
+                       t.sleepSyncInterval()
+               } else {
+                       t.Logger.Error("cannot continue with existing instances -- clean up manually, use -destroy-existing=true, or choose a different -instance-set-id")
+                       return false
+               }
+       }
+
+       t.secret = randomHex(40)
+
+       tags := cloud.InstanceTags{}
+       for k, v := range t.Tags {
+               tags[k] = v
+       }
+       tags[t.TagKeyPrefix+"InstanceSetID"] = string(t.SetID)
+       tags[t.TagKeyPrefix+"InstanceSecret"] = t.secret
+
+       defer t.destroyTestInstance()
+
+       bootDeadline := time.Now().Add(t.TimeoutBooting)
+       initCommand := worker.TagVerifier{nil, t.secret}.InitCommand()
+
+       t.Logger.WithFields(logrus.Fields{
+               "InstanceType":         t.InstanceType.Name,
+               "ProviderInstanceType": t.InstanceType.ProviderType,
+               "ImageID":              t.ImageID,
+               "Tags":                 tags,
+               "InitCommand":          initCommand,
+       }).Info("creating instance")
+       t0 := time.Now()
+       inst, err := t.is.Create(t.InstanceType, t.ImageID, tags, initCommand, t.SSHKey.PublicKey())
+       lgrC := t.Logger.WithField("Duration", time.Since(t0))
+       if err != nil {
+               // Create() might have failed due to a bug or network
+               // error even though the creation was successful, so
+               // it's safer to wait a bit for an instance to appear.
+               deferredError = true
+               lgrC.WithError(err).Error("error creating test instance")
+               t.Logger.WithField("Deadline", bootDeadline).Info("waiting for instance to appear anyway, in case the Create response was incorrect")
+               for err = t.refreshTestInstance(); err != nil; err = t.refreshTestInstance() {
+                       if time.Now().After(bootDeadline) {
+                               t.Logger.Error("timed out")
+                               return false
+                       } else {
+                               t.sleepSyncInterval()
+                       }
+               }
+               t.Logger.WithField("Instance", t.testInstance.ID()).Info("new instance appeared")
+               t.showLoginInfo()
+       } else {
+               // Create() succeeded. Make sure the new instance
+               // appears right away in the Instances() list.
+               lgrC.WithField("Instance", inst.ID()).Info("created instance")
+               t.testInstance = &worker.TagVerifier{inst, t.secret}
+               t.showLoginInfo()
+               err = t.refreshTestInstance()
+               if err == errTestInstanceNotFound {
+                       t.Logger.WithError(err).Error("cloud/driver Create succeeded, but instance is not in list")
+                       deferredError = true
+               } else if err != nil {
+                       t.Logger.WithError(err).Error("error getting list of instances")
+                       return false
+               }
+       }
+
+       if !t.checkTags() {
+               // checkTags() already logged the errors
+               deferredError = true
+       }
+
+       if !t.waitForBoot(bootDeadline) {
+               deferredError = true
+       }
+
+       if t.ShellCommand != "" {
+               err = t.runShellCommand(t.ShellCommand)
+               if err != nil {
+                       t.Logger.WithError(err).Error("shell command failed")
+                       deferredError = true
+               }
+       }
+
+       if fn := t.PauseBeforeDestroy; fn != nil {
+               t.showLoginInfo()
+               fn()
+       }
+
+       return !deferredError
+}
+
+// If the test instance has an address, log an "ssh user@host" command
+// line that the operator can paste into another terminal, and set
+// t.showedLoginInfo.
+//
+// If the test instance doesn't have an address yet, do nothing.
+func (t *tester) showLoginInfo() {
+       t.updateExecutor()
+       host, port := t.executor.TargetHostPort()
+       if host == "" {
+               return
+       }
+       user := t.testInstance.RemoteUser()
+       t.Logger.WithField("Command", fmt.Sprintf("ssh -p%s %s@%s", port, user, host)).Info("showing login information")
+       t.showedLoginInfo = true
+}
+
+// Get the latest instance list from the driver. If our test instance
+// is found, assign it to t.testIntance.
+func (t *tester) refreshTestInstance() error {
+       insts, err := t.getInstances(cloud.InstanceTags{t.TagKeyPrefix + "InstanceSetID": string(t.SetID)})
+       if err != nil {
+               return err
+       }
+       for _, i := range insts {
+               if t.testInstance == nil {
+                       // Filter by InstanceSetID tag value
+                       if i.Tags()[t.TagKeyPrefix+"InstanceSetID"] != string(t.SetID) {
+                               continue
+                       }
+               } else {
+                       // Filter by instance ID
+                       if i.ID() != t.testInstance.ID() {
+                               continue
+                       }
+               }
+               t.Logger.WithFields(logrus.Fields{
+                       "Instance": i.ID(),
+                       "Address":  i.Address(),
+               }).Info("found our instance in returned list")
+               t.testInstance = &worker.TagVerifier{i, t.secret}
+               if !t.showedLoginInfo {
+                       t.showLoginInfo()
+               }
+               return nil
+       }
+       return errTestInstanceNotFound
+}
+
+// Get the list of instances, passing the given tags to the cloud
+// driver to filter results.
+//
+// Return only the instances that have our InstanceSetID tag.
+func (t *tester) getInstances(tags cloud.InstanceTags) ([]cloud.Instance, error) {
+       var ret []cloud.Instance
+       t.Logger.WithField("FilterTags", tags).Info("getting instance list")
+       t0 := time.Now()
+       insts, err := t.is.Instances(tags)
+       if err != nil {
+               return nil, err
+       }
+       t.Logger.WithFields(logrus.Fields{
+               "Duration": time.Since(t0),
+               "N":        len(insts),
+       }).Info("got instance list")
+       for _, i := range insts {
+               if i.Tags()[t.TagKeyPrefix+"InstanceSetID"] == string(t.SetID) {
+                       ret = append(ret, i)
+               }
+       }
+       return ret, nil
+}
+
+// Check that t.testInstance has every tag in t.Tags. If not, log an
+// error and return false.
+func (t *tester) checkTags() bool {
+       ok := true
+       for k, v := range t.Tags {
+               if got := t.testInstance.Tags()[k]; got != v {
+                       ok = false
+                       t.Logger.WithFields(logrus.Fields{
+                               "Key":           k,
+                               "ExpectedValue": v,
+                               "GotValue":      got,
+                       }).Error("tag is missing from test instance")
+               }
+       }
+       if ok {
+               t.Logger.Info("all expected tags are present")
+       }
+       return ok
+}
+
+// Run t.BootProbeCommand on t.testInstance until it succeeds or the
+// deadline arrives.
+func (t *tester) waitForBoot(deadline time.Time) bool {
+       for time.Now().Before(deadline) {
+               err := t.runShellCommand(t.BootProbeCommand)
+               if err == nil {
+                       return true
+               }
+               t.sleepProbeInterval()
+               t.refreshTestInstance()
+       }
+       t.Logger.Error("timed out")
+       return false
+}
+
+// Create t.executor and/or update its target to t.testInstance's
+// current address.
+func (t *tester) updateExecutor() {
+       if t.executor == nil {
+               t.executor = ssh_executor.New(t.testInstance)
+               t.executor.SetTargetPort(t.SSHPort)
+               t.executor.SetSigners(t.SSHKey)
+       } else {
+               t.executor.SetTarget(t.testInstance)
+       }
+}
+
+func (t *tester) runShellCommand(cmd string) error {
+       t.updateExecutor()
+       t.Logger.WithFields(logrus.Fields{
+               "Command": cmd,
+       }).Info("executing remote command")
+       t0 := time.Now()
+       stdout, stderr, err := t.executor.Execute(nil, cmd, nil)
+       lgr := t.Logger.WithFields(logrus.Fields{
+               "Duration": time.Since(t0),
+               "Command":  cmd,
+               "stdout":   string(stdout),
+               "stderr":   string(stderr),
+       })
+       if err != nil {
+               lgr.WithError(err).Info("remote command failed")
+       } else {
+               lgr.Info("remote command succeeded")
+       }
+       return err
+}
+
+// currently, this tries forever until it can return true (success).
+func (t *tester) destroyTestInstance() bool {
+       if t.testInstance == nil {
+               return true
+       }
+       for {
+               lgr := t.Logger.WithField("Instance", t.testInstance.ID())
+               lgr.Info("destroying instance")
+               t0 := time.Now()
+
+               err := t.testInstance.Destroy()
+               lgrDur := lgr.WithField("Duration", time.Since(t0))
+               if err != nil {
+                       lgrDur.WithError(err).Error("error destroying instance")
+               } else {
+                       lgrDur.Info("destroyed instance")
+               }
+
+               err = t.refreshTestInstance()
+               if err == errTestInstanceNotFound {
+                       lgr.Info("instance no longer appears in list")
+                       t.testInstance = nil
+                       return true
+               } else if err == nil {
+                       lgr.Info("instance still exists after calling Destroy")
+                       t.sleepSyncInterval()
+                       continue
+               } else {
+                       t.Logger.WithError(err).Error("error getting list of instances")
+                       continue
+               }
+       }
+}
+
+func (t *tester) sleepSyncInterval() {
+       t.Logger.WithField("Duration", t.SyncInterval).Info("waiting SyncInterval")
+       time.Sleep(t.SyncInterval)
+}
+
+func (t *tester) sleepProbeInterval() {
+       t.Logger.WithField("Duration", t.ProbeInterval).Info("waiting ProbeInterval")
+       time.Sleep(t.ProbeInterval)
+}
+
+// Return a random string of n hexadecimal digits (n*4 random bits). n
+// must be even.
+func randomHex(n int) string {
+       buf := make([]byte, n/2)
+       _, err := rand.Read(buf)
+       if err != nil {
+               panic(err)
+       }
+       return fmt.Sprintf("%x", buf)
+}
diff --git a/lib/cloud/cloudtest/tester_test.go b/lib/cloud/cloudtest/tester_test.go
new file mode 100644 (file)
index 0000000..358530b
--- /dev/null
@@ -0,0 +1,97 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package cloudtest
+
+import (
+       "bytes"
+       "testing"
+       "time"
+
+       "git.curoverse.com/arvados.git/lib/cloud"
+       "git.curoverse.com/arvados.git/lib/dispatchcloud/test"
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "git.curoverse.com/arvados.git/sdk/go/ctxlog"
+       "golang.org/x/crypto/ssh"
+       check "gopkg.in/check.v1"
+)
+
+// Gocheck boilerplate
+func Test(t *testing.T) {
+       check.TestingT(t)
+}
+
+var _ = check.Suite(&TesterSuite{})
+
+type TesterSuite struct {
+       stubDriver *test.StubDriver
+       cluster    *arvados.Cluster
+       tester     *tester
+       log        bytes.Buffer
+}
+
+func (s *TesterSuite) SetUpTest(c *check.C) {
+       pubkey, privkey := test.LoadTestKey(c, "../../dispatchcloud/test/sshkey_dispatch")
+       _, privhostkey := test.LoadTestKey(c, "../../dispatchcloud/test/sshkey_vm")
+       s.stubDriver = &test.StubDriver{
+               HostKey:                   privhostkey,
+               AuthorizedKeys:            []ssh.PublicKey{pubkey},
+               ErrorRateDestroy:          0.1,
+               MinTimeBetweenCreateCalls: time.Millisecond,
+       }
+       tagKeyPrefix := "tagprefix:"
+       s.cluster = &arvados.Cluster{
+               ManagementToken: "test-management-token",
+               Containers: arvados.ContainersConfig{
+                       CloudVMs: arvados.CloudVMsConfig{
+                               SyncInterval:   arvados.Duration(10 * time.Millisecond),
+                               TimeoutBooting: arvados.Duration(150 * time.Millisecond),
+                               TimeoutProbe:   arvados.Duration(15 * time.Millisecond),
+                               ProbeInterval:  arvados.Duration(5 * time.Millisecond),
+                               ResourceTags:   map[string]string{"testtag": "test value"},
+                       },
+               },
+               InstanceTypes: arvados.InstanceTypeMap{
+                       test.InstanceType(1).Name: test.InstanceType(1),
+                       test.InstanceType(2).Name: test.InstanceType(2),
+                       test.InstanceType(3).Name: test.InstanceType(3),
+               },
+       }
+       s.tester = &tester{
+               Logger:           ctxlog.New(&s.log, "text", "info"),
+               Tags:             cloud.SharedResourceTags{"testtagkey": "testtagvalue"},
+               TagKeyPrefix:     tagKeyPrefix,
+               SetID:            cloud.InstanceSetID("test-instance-set-id"),
+               ProbeInterval:    5 * time.Millisecond,
+               SyncInterval:     10 * time.Millisecond,
+               TimeoutBooting:   150 * time.Millisecond,
+               Driver:           s.stubDriver,
+               DriverParameters: nil,
+               InstanceType:     test.InstanceType(2),
+               ImageID:          "test-image-id",
+               SSHKey:           privkey,
+               BootProbeCommand: "crunch-run --list",
+               ShellCommand:     "true",
+       }
+}
+
+func (s *TesterSuite) TestSuccess(c *check.C) {
+       s.tester.Logger = ctxlog.TestLogger(c)
+       ok := s.tester.Run()
+       c.Check(ok, check.Equals, true)
+}
+
+func (s *TesterSuite) TestBootFail(c *check.C) {
+       s.tester.BootProbeCommand = "falsey"
+       ok := s.tester.Run()
+       c.Check(ok, check.Equals, false)
+       c.Check(s.log.String(), check.Matches, `(?ms).*\\"falsey\\": command not found.*`)
+}
+
+func (s *TesterSuite) TestShellCommandFail(c *check.C) {
+       s.tester.ShellCommand = "falsey"
+       ok := s.tester.Run()
+       c.Check(ok, check.Equals, false)
+       c.Check(s.log.String(), check.Matches, `(?ms).*\\"falsey\\": command not found.*`)
+}
index 9292ef7e5ff5b3afb6012833299d9f89a7ea346c..24b69f0cc5c529fe45b5be6f4ae6698cff607ff9 100644 (file)
@@ -28,11 +28,18 @@ func (f HandlerFunc) RunCommand(prog string, args []string, stdin io.Reader, std
        return f(prog, args, stdin, stdout, stderr)
 }
 
-type Version string
+// Version is a Handler that prints the package version (set at build
+// time using -ldflags) and Go runtime version to stdout, and returns
+// 0.
+var Version versionCommand
 
-func (v Version) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+var version = "dev"
+
+type versionCommand struct{}
+
+func (versionCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
        prog = regexp.MustCompile(` -*version$`).ReplaceAllLiteralString(prog, "")
-       fmt.Fprintf(stdout, "%s %s (%s)\n", prog, v, runtime.Version())
+       fmt.Fprintf(stdout, "%s %s (%s)\n", prog, version, runtime.Version())
        return 0
 }
 
index c132f1b3600664f3c959d7c86463aebcb19fed3c..ba4c3c123cabb7a6ad018c836a51b36fac6fada7 100644 (file)
@@ -43,7 +43,7 @@ func LeakCheck(c *check.C) func() {
                os.Stdout, os.Stderr = stdout, stderr
 
                for i, tmpfile := range tmpfiles {
-                       c.Log("checking %s", i)
+                       c.Logf("checking %s", i)
                        _, err := tmpfile.Seek(0, io.SeekStart)
                        c.Assert(err, check.IsNil)
                        leaked, err := ioutil.ReadAll(tmpfile)
index a41e4b0331548f977d69b3ce993795c51e28ea1d..5cb76fc35d63bd331f92e7ec02fccc1084a0c0e6 100644 (file)
@@ -9,13 +9,12 @@ import (
        "flag"
        "fmt"
        "io"
-       "io/ioutil"
        "os"
        "os/exec"
 
-       "git.curoverse.com/arvados.git/sdk/go/arvados"
        "git.curoverse.com/arvados.git/sdk/go/ctxlog"
        "github.com/ghodss/yaml"
+       "github.com/sirupsen/logrus"
 )
 
 var DumpCommand dumpCommand
@@ -30,9 +29,15 @@ func (dumpCommand) RunCommand(prog string, args []string, stdin io.Reader, stdou
                }
        }()
 
+       loader := &Loader{
+               Stdin:  stdin,
+               Logger: ctxlog.New(stderr, "text", "info"),
+       }
+
        flags := flag.NewFlagSet("", flag.ContinueOnError)
        flags.SetOutput(stderr)
-       configFile := flags.String("config", arvados.DefaultConfigFile, "Site configuration `file`")
+       loader.SetupFlags(flags)
+
        err = flags.Parse(args)
        if err == flag.ErrHelp {
                err = nil
@@ -45,8 +50,8 @@ func (dumpCommand) RunCommand(prog string, args []string, stdin io.Reader, stdou
                flags.Usage()
                return 2
        }
-       log := ctxlog.New(stderr, "text", "info")
-       cfg, err := loadFileOrStdin(*configFile, stdin, log)
+
+       cfg, err := loader.Load()
        if err != nil {
                return 1
        }
@@ -67,15 +72,25 @@ type checkCommand struct{}
 
 func (checkCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
        var err error
+       var logbuf = &bytes.Buffer{}
        defer func() {
+               io.Copy(stderr, logbuf)
                if err != nil {
                        fmt.Fprintf(stderr, "%s\n", err)
                }
        }()
 
+       logger := logrus.New()
+       logger.Out = logbuf
+       loader := &Loader{
+               Stdin:  stdin,
+               Logger: logger,
+       }
+
        flags := flag.NewFlagSet("", flag.ContinueOnError)
        flags.SetOutput(stderr)
-       configFile := flags.String("config", arvados.DefaultConfigFile, "Site configuration `file`")
+       loader.SetupFlags(flags)
+
        err = flags.Parse(args)
        if err == flag.ErrHelp {
                err = nil
@@ -88,21 +103,22 @@ func (checkCommand) RunCommand(prog string, args []string, stdin io.Reader, stdo
                flags.Usage()
                return 2
        }
-       log := &plainLogger{w: stderr}
-       var buf []byte
-       if *configFile == "-" {
-               buf, err = ioutil.ReadAll(stdin)
-       } else {
-               buf, err = ioutil.ReadFile(*configFile)
-       }
-       if err != nil {
-               return 1
-       }
-       withoutDepr, err := load(bytes.NewBuffer(buf), log, false)
+
+       // Load the config twice -- once without loading deprecated
+       // keys/files, once with -- and then compare the two resulting
+       // configs. This reveals whether the deprecated keys/files
+       // have any effect on the final configuration.
+       //
+       // If they do, show the operator how to update their config
+       // such that the deprecated keys/files are superfluous and can
+       // be deleted.
+       loader.SkipDeprecated = true
+       withoutDepr, err := loader.Load()
        if err != nil {
                return 1
        }
-       withDepr, err := load(bytes.NewBuffer(buf), nil, true)
+       loader.SkipDeprecated = false
+       withDepr, err := loader.Load()
        if err != nil {
                return 1
        }
@@ -124,6 +140,7 @@ func (checkCommand) RunCommand(prog string, args []string, stdin io.Reader, stdo
        if bytes.HasPrefix(diff, []byte("--- ")) {
                fmt.Fprintln(stdout, "Your configuration is relying on deprecated entries. Suggest making the following changes.")
                stdout.Write(diff)
+               err = nil
                return 1
        } else if len(diff) > 0 {
                fmt.Fprintf(stderr, "Unexpected diff output:\n%s", diff)
@@ -131,18 +148,21 @@ func (checkCommand) RunCommand(prog string, args []string, stdin io.Reader, stdo
        } else if err != nil {
                return 1
        }
-       if log.used {
+       if logbuf.Len() > 0 {
                return 1
        }
        return 0
 }
 
-type plainLogger struct {
-       w    io.Writer
-       used bool
-}
+var DumpDefaultsCommand defaultsCommand
+
+type defaultsCommand struct{}
 
-func (pl *plainLogger) Warnf(format string, args ...interface{}) {
-       pl.used = true
-       fmt.Fprintf(pl.w, format+"\n", args...)
+func (defaultsCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+       _, err := stdout.Write(DefaultYAML)
+       if err != nil {
+               fmt.Fprintln(stderr, err)
+               return 1
+       }
+       return 0
 }
index f2915a03917260aa07fd5c656b5a1b9c7833757f..af7c571203b3dcf48f8a2198ded9e6d90648e8be 100644 (file)
@@ -6,6 +6,9 @@ package config
 
 import (
        "bytes"
+       "io"
+       "io/ioutil"
+       "os"
 
        "git.curoverse.com/arvados.git/lib/cmd"
        check "gopkg.in/check.v1"
@@ -42,6 +45,21 @@ Clusters:
  z1234:
   API:
     MaxItemsPerResponse: 1234
+  PostgreSQL:
+    Connection:
+      sslmode: require
+  Services:
+    RailsAPI:
+      InternalURLs:
+        "http://0.0.0.0:8000": {}
+  Workbench:
+    UserProfileFormFields:
+      color:
+        Type: select
+        Options:
+          fuchsia: {}
+    ApplicationMimetypesWithViewIcon:
+      whitespace: {}
 `
        code := CheckCommand.RunCommand("arvados config-check", []string{"-config", "-"}, bytes.NewBufferString(in), &stdout, &stderr)
        c.Check(code, check.Equals, 0)
@@ -62,6 +80,26 @@ Clusters:
        c.Check(stdout.String(), check.Matches, `(?ms).*\n\- +.*MaxItemsPerResponse: 1000\n\+ +MaxItemsPerResponse: 1234\n.*`)
 }
 
+func (s *CommandSuite) TestCheckOldKeepstoreConfigFile(c *check.C) {
+       f, err := ioutil.TempFile("", "")
+       c.Assert(err, check.IsNil)
+       defer os.Remove(f.Name())
+
+       io.WriteString(f, "Debug: true\n")
+
+       var stdout, stderr bytes.Buffer
+       in := `
+Clusters:
+ z1234:
+  SystemLogs:
+    LogLevel: info
+`
+       code := CheckCommand.RunCommand("arvados config-check", []string{"-config", "-", "-legacy-keepstore-config", f.Name()}, bytes.NewBufferString(in), &stdout, &stderr)
+       c.Check(code, check.Equals, 1)
+       c.Check(stdout.String(), check.Matches, `(?ms).*\n\- +.*LogLevel: info\n\+ +LogLevel: debug\n.*`)
+       c.Check(stderr.String(), check.Matches, `.*you should remove the legacy keepstore config file.*\n`)
+}
+
 func (s *CommandSuite) TestCheckUnknownKey(c *check.C) {
        var stdout, stderr bytes.Buffer
        in := `
@@ -80,10 +118,10 @@ Clusters:
        code := CheckCommand.RunCommand("arvados config-check", []string{"-config", "-"}, bytes.NewBufferString(in), &stdout, &stderr)
        c.Log(stderr.String())
        c.Check(code, check.Equals, 1)
-       c.Check(stderr.String(), check.Matches, `(?ms).*deprecated or unknown config entry: Clusters.z1234.Bogus1\n.*`)
-       c.Check(stderr.String(), check.Matches, `(?ms).*deprecated or unknown config entry: Clusters.z1234.BogusSection\n.*`)
-       c.Check(stderr.String(), check.Matches, `(?ms).*deprecated or unknown config entry: Clusters.z1234.API.Bogus3\n.*`)
-       c.Check(stderr.String(), check.Matches, `(?ms).*unexpected object in config entry: Clusters.z1234.PostgreSQL.ConnectionPool\n.*`)
+       c.Check(stderr.String(), check.Matches, `(?ms).*deprecated or unknown config entry: Clusters.z1234.Bogus1"\n.*`)
+       c.Check(stderr.String(), check.Matches, `(?ms).*deprecated or unknown config entry: Clusters.z1234.BogusSection"\n.*`)
+       c.Check(stderr.String(), check.Matches, `(?ms).*deprecated or unknown config entry: Clusters.z1234.API.Bogus3"\n.*`)
+       c.Check(stderr.String(), check.Matches, `(?ms).*unexpected object in config entry: Clusters.z1234.PostgreSQL.ConnectionPool"\n.*`)
 }
 
 func (s *CommandSuite) TestDumpFormatting(c *check.C) {
index 0078da37eba25f61d6f807c6152d0db707d1cd94..7e5b47191e95d93d848df9fe56a6ca0762f6bae2 100644 (file)
@@ -19,6 +19,16 @@ Clusters:
     ManagementToken: ""
 
     Services:
+
+      # In each of the service sections below, the keys under
+      # InternalURLs are the endpoints where the service should be
+      # listening, and reachable from other hosts in the cluster.
+      SAMPLE:
+        InternalURLs:
+          "http://example.host:12345": {}
+          SAMPLE: {}
+        ExternalURL: "-"
+
       RailsAPI:
         InternalURLs: {}
         ExternalURL: "-"
@@ -48,10 +58,57 @@ Clusters:
         ExternalURL: ""
       WebDAV:
         InternalURLs: {}
+        # Base URL for Workbench inline preview.  If blank, use
+        # WebDAVDownload instead, and disable inline preview.
+        # If both are empty, downloading collections from workbench
+        # will be impossible.
+        #
+        # It is important to properly configure the download service
+        # to migitate cross-site-scripting (XSS) attacks.  A HTML page
+        # can be stored in collection.  If an attacker causes a victim
+        # to visit that page through Workbench, it will be rendered by
+        # the browser.  If all collections are served at the same
+        # domain, the browser will consider collections as coming from
+        # the same origin and having access to the same browsing data,
+        # enabling malicious Javascript on that page to access Arvados
+        # on behalf of the victim.
+        #
+        # This is mitigating by having separate domains for each
+        # collection, or limiting preview to circumstances where the
+        # collection is not accessed with the user's regular
+        # full-access token.
+        #
+        # Serve preview links using uuid or pdh in subdomain
+        # (requires wildcard DNS and TLS certificate)
+        #   https://*.collections.uuid_prefix.arvadosapi.com
+        #
+        # Serve preview links using uuid or pdh in main domain
+        # (requires wildcard DNS and TLS certificate)
+        #   https://*--collections.uuid_prefix.arvadosapi.com
+        #
+        # Serve preview links by setting uuid or pdh in the path.
+        # This configuration only allows previews of public data or
+        # collection-sharing links, because these use the anonymous
+        # user token or the token is already embedded in the URL.
+        # Other data must be handled as downloads via WebDAVDownload:
+        #   https://collections.uuid_prefix.arvadosapi.com
+        #
         ExternalURL: ""
+
       WebDAVDownload:
         InternalURLs: {}
+        # Base URL for download links. If blank, serve links to WebDAV
+        # with disposition=attachment query param.  Unlike preview links,
+        # browsers do not render attachments, so there is no risk of XSS.
+        #
+        # If WebDAVDownload is blank, and WebDAV uses a
+        # single-origin form, then Workbench will show an error page
+        #
+        # Serve download links by setting uuid or pdh in the path:
+        #   https://download.uuid_prefix.arvadosapi.com
+        #
         ExternalURL: ""
+
       Keepstore:
         InternalURLs: {}
         ExternalURL: "-"
@@ -60,6 +117,14 @@ Clusters:
         ExternalURL: ""
       WebShell:
         InternalURLs: {}
+        # ShellInABox service endpoint URL for a given VM.  If empty, do not
+        # offer web shell logins.
+        #
+        # E.g., using a path-based proxy server to forward connections to shell hosts:
+        # https://webshell.uuid_prefix.arvadosapi.com
+        #
+        # E.g., using a name-based proxy server to forward connections to shell hosts:
+        # https://*.webshell.uuid_prefix.arvadosapi.com
         ExternalURL: ""
       Workbench1:
         InternalURLs: {}
@@ -85,6 +150,7 @@ Clusters:
         user: ""
         password: ""
         dbname: ""
+        SAMPLE: ""
     API:
       # Maximum size (in bytes) allowed for a single API request.  This
       # limit is published in the discovery document for use by clients.
@@ -170,6 +236,11 @@ Clusters:
       NewUserNotificationRecipients: []
       NewInactiveUserNotificationRecipients: []
 
+      # Set anonymous_user_token to enable anonymous user access. You can get
+      # the token by running "bundle exec ./script/get_anonymous_user_token.rb"
+      # in the directory where your API server is running.
+      AnonymousUserToken: ""
+
     AuditLogs:
       # Time to keep audit logs, in seconds. (An audit log is a row added
       # to the "logs" table in the PostgreSQL database each time an
@@ -225,7 +296,7 @@ Clusters:
       # one another!
       BlobSigning: true
 
-      # blob_signing_key is a string of alphanumeric characters used to
+      # BlobSigningKey is a string of alphanumeric characters used to
       # generate permission signatures for Keep locators. It must be
       # identical to the permission key given to Keep. IMPORTANT: This is
       # a site secret. It should be at least 50 characters.
@@ -293,6 +364,19 @@ Clusters:
       ManagedProperties:
         SAMPLE: {Function: original_owner, Protected: true}
 
+      # In "trust all content" mode, Workbench will redirect download
+      # requests to WebDAV preview link, even in the cases when
+      # WebDAV would have to expose XSS vulnerabilities in order to
+      # handle the redirect (see discussion on Services.WebDAV).
+      #
+      # This setting has no effect in the recommended configuration,
+      # where the WebDAV is configured to have a separate domain for
+      # every collection; in this case XSS protection is provided by
+      # browsers' same-origin policy.
+      #
+      # The default setting (false) is appropriate for a multi-user site.
+      TrustAllContent: false
+
     Login:
       # These settings are provided by your OAuth2 provider (e.g.,
       # sso-provider).
@@ -500,7 +584,7 @@ Clusters:
         # Shell command to execute on each worker to determine whether
         # the worker is booted and ready to run containers. It should
         # exit zero if the worker is ready.
-        BootProbeCommand: "docker ps"
+        BootProbeCommand: "docker ps -q"
 
         # Minimum interval between consecutive probes to a single
         # worker.
@@ -613,11 +697,15 @@ Clusters:
     Mail:
       MailchimpAPIKey: ""
       MailchimpListID: ""
-      SendUserSetupNotificationEmail: ""
-      IssueReporterEmailFrom: ""
-      IssueReporterEmailTo: ""
-      SupportEmailAddress: ""
-      EmailFrom: ""
+      SendUserSetupNotificationEmail: true
+
+      # Bug/issue report notification to and from addresses
+      IssueReporterEmailFrom: "arvados@example.com"
+      IssueReporterEmailTo: "arvados@example.com"
+      SupportEmailAddress: "arvados@example.com"
+
+      # Generic issue email from
+      EmailFrom: "arvados@example.com"
     RemoteClusters:
       "*":
         Host: ""
@@ -651,16 +739,63 @@ Clusters:
       ArvadosDocsite: https://doc.arvados.org
       ArvadosPublicDataDocURL: https://playground.arvados.org/projects/public
       ShowUserAgreementInline: false
-      SecretToken: ""
       SecretKeyBase: ""
+
+      # Scratch directory used by the remote repository browsing
+      # feature. If it doesn't exist, it (and any missing parents) will be
+      # created using mkdir_p.
       RepositoryCache: /var/www/arvados-workbench/current/tmp/git
+
+      # Below is a sample setting of user_profile_form_fields config parameter.
+      # This configuration parameter should be set to either false (to disable) or
+      # to a map as shown below.
+      # Configure the map of input fields to be displayed in the profile page
+      # using the attribute "key" for each of the input fields.
+      # This sample shows configuration with one required and one optional form fields.
+      # For each of these input fields:
+      #   You can specify "Type" as "text" or "select".
+      #   List the "Options" to be displayed for each of the "select" menu.
+      #   Set "Required" as "true" for any of these fields to make them required.
+      # If any of the required fields are missing in the user's profile, the user will be
+      # redirected to the profile page before they can access any Workbench features.
       UserProfileFormFields:
         SAMPLE:
-          Type: text
-          FormFieldTitle: ""
-          FormFieldDescription: ""
-          Required: true
+          Type: select
+          FormFieldTitle: Best color
+          FormFieldDescription: your favorite color
+          Required: false
+          Position: 1
+          Options:
+            red: {}
+            blue: {}
+            green: {}
+            SAMPLE: {}
+
+        # exampleTextValue:  # key that will be set in properties
+        #   Type: text  #
+        #   FormFieldTitle: ""
+        #   FormFieldDescription: ""
+        #   Required: true
+        #   Position: 1
+        # exampleOptionsValue:
+        #   Type: select
+        #   FormFieldTitle: ""
+        #   FormFieldDescription: ""
+        #   Required: true
+        #   Position: 1
+        #   Options:
+        #     red: {}
+        #     blue: {}
+        #     yellow: {}
+
+      # Use "UserProfileFormMessage to configure the message you want
+      # to display on the profile page.
       UserProfileFormMessage: 'Welcome to Arvados. All <span style="color:red">required fields</span> must be completed before you can proceed.'
+
+      # Mimetypes of applications for which the view icon
+      # would be enabled in a collection's show page.
+      # It is sufficient to list only applications here.
+      # No need to list text and image types.
       ApplicationMimetypesWithViewIcon:
         cwl: {}
         fasta: {}
@@ -677,19 +812,55 @@ Clusters:
         vnd.realvnc.bed: {}
         xml: {}
         xsl: {}
+        SAMPLE: {}
+
+      # The maximum number of bytes to load in the log viewer
       LogViewerMaxBytes: 1M
+
+      # When anonymous_user_token is configured, show public projects page
       EnablePublicProjectsPage: true
+
+      # By default, disable the "Getting Started" popup which is specific to Arvados playground
       EnableGettingStartedPopup: false
+
+      # Ask Arvados API server to compress its response payloads.
       APIResponseCompression: true
+
+      # Timeouts for API requests.
       APIClientConnectTimeout: 2m
       APIClientReceiveTimeout: 5m
+
+      # Maximum number of historic log records of a running job to fetch
+      # and display in the Log tab, while subscribing to web sockets.
       RunningJobLogRecordsToFetch: 2000
+
+      # In systems with many shared projects, loading of dashboard and topnav
+      # cab be slow due to collections indexing; use the following parameters
+      # to suppress these properties
       ShowRecentCollectionsOnDashboard: true
       ShowUserNotifications: true
-      MultiSiteSearch: false
+
+      # Enable/disable "multi-site search" in top nav ("true"/"false"), or
+      # a link to the multi-site search page on a "home" Workbench site.
+      #
+      # Example:
+      #   https://workbench.qr1hi.arvadosapi.com/collections/multisite
+      MultiSiteSearch: ""
+
+      # Should workbench allow management of local git repositories? Set to false if
+      # the jobs api is disabled and there are no local git repositories.
       Repositories: true
+
       SiteName: Arvados Workbench
+      ProfilingEnabled: false
+
+      # This is related to obsolete Google OpenID 1.0 login
+      # but some workbench stuff still expects it to be set.
+      DefaultOpenIdPrefix: "https://www.google.com/accounts/o8/id"
 
       # Workbench2 configs
       VocabularyURL: ""
       FileViewersConfigURL: ""
+
+    # Use experimental controller code (see https://dev.arvados.org/issues/14287)
+    EnableBetaController14287: false
index 8ffa2a58341e952b67eca0220e59bd423273ab9b..0b0bb26689902af4fb8c2b668e75cc4cc7e00981 100644 (file)
@@ -6,6 +6,7 @@ package config
 
 import (
        "fmt"
+       "io/ioutil"
        "os"
        "strings"
 
@@ -47,9 +48,9 @@ type systemServiceInstance struct {
        Insecure bool
 }
 
-func applyDeprecatedConfig(cfg *arvados.Config, configdata []byte, log logger) error {
+func (ldr *Loader) applyDeprecatedConfig(cfg *arvados.Config) error {
        var dc deprecatedConfig
-       err := yaml.Unmarshal(configdata, &dc)
+       err := yaml.Unmarshal(ldr.configdata, &dc)
        if err != nil {
                return err
        }
@@ -65,8 +66,8 @@ func applyDeprecatedConfig(cfg *arvados.Config, configdata []byte, log logger) e
                for name, np := range dcluster.NodeProfiles {
                        if name == "*" || name == os.Getenv("ARVADOS_NODE_PROFILE") || name == hostname {
                                name = "localhost"
-                       } else if log != nil {
-                               log.Warnf("overriding Clusters.%s.Services using Clusters.%s.NodeProfiles.%s (guessing %q is a hostname)", id, id, name, name)
+                       } else if ldr.Logger != nil {
+                               ldr.Logger.Warnf("overriding Clusters.%s.Services using Clusters.%s.NodeProfiles.%s (guessing %q is a hostname)", id, id, name, name)
                        }
                        applyDeprecatedNodeProfile(name, np.RailsAPI, &cluster.Services.RailsAPI)
                        applyDeprecatedNodeProfile(name, np.Controller, &cluster.Services.Controller)
@@ -100,3 +101,45 @@ func applyDeprecatedNodeProfile(hostname string, ssi systemServiceInstance, svc
        }
        svc.InternalURLs[arvados.URL{Scheme: scheme, Host: host}] = arvados.ServiceInstance{}
 }
+
+const defaultKeepstoreConfigPath = "/etc/arvados/keepstore/keepstore.yml"
+
+type oldKeepstoreConfig struct {
+       Debug *bool
+}
+
+// update config using values from an old-style keepstore config file.
+func (ldr *Loader) loadOldKeepstoreConfig(cfg *arvados.Config) error {
+       path := ldr.KeepstorePath
+       if path == "" {
+               return nil
+       }
+       buf, err := ioutil.ReadFile(path)
+       if os.IsNotExist(err) && path == defaultKeepstoreConfigPath {
+               return nil
+       } else if err != nil {
+               return err
+       } else {
+               ldr.Logger.Warnf("you should remove the legacy keepstore config file (%s) after migrating all config keys to the cluster configuration file (%s)", path, ldr.Path)
+       }
+       cluster, err := cfg.GetCluster("")
+       if err != nil {
+               return err
+       }
+
+       var oc oldKeepstoreConfig
+       err = yaml.Unmarshal(buf, &oc)
+       if err != nil {
+               return fmt.Errorf("%s: %s", path, err)
+       }
+
+       if v := oc.Debug; v == nil {
+       } else if *v && cluster.SystemLogs.LogLevel != "debug" {
+               cluster.SystemLogs.LogLevel = "debug"
+       } else if !*v && cluster.SystemLogs.LogLevel != "info" {
+               cluster.SystemLogs.LogLevel = "info"
+       }
+
+       cfg.Clusters[cluster.ClusterID] = *cluster
+       return nil
+}
index 2f79c2b2969152469d61167306031ce496514537..b79dec4d9d1532b1f348965e5c657e71df21704e 100644 (file)
@@ -55,76 +55,122 @@ func ExportJSON(w io.Writer, cluster *arvados.Cluster) error {
 // exists.
 var whitelist = map[string]bool{
        // | sort -t'"' -k2,2
-       "API":                                        true,
-       "API.AsyncPermissionsUpdateInterval":         false,
-       "API.DisabledAPIs":                           false,
-       "API.MaxIndexDatabaseRead":                   false,
-       "API.MaxItemsPerResponse":                    true,
-       "API.MaxRequestAmplification":                false,
-       "API.MaxRequestSize":                         true,
-       "API.RailsSessionSecretToken":                false,
-       "API.RequestTimeout":                         true,
-       "AuditLogs":                                  false,
-       "AuditLogs.MaxAge":                           false,
-       "AuditLogs.MaxDeleteBatch":                   false,
-       "AuditLogs.UnloggedAttributes":               false,
-       "Collections":                                true,
-       "Collections.BlobSigning":                    true,
-       "Collections.BlobSigningKey":                 false,
-       "Collections.BlobSigningTTL":                 true,
-       "Collections.CollectionVersioning":           false,
-       "Collections.DefaultReplication":             true,
-       "Collections.DefaultTrashLifetime":           true,
-       "Collections.ManagedProperties":              true,
-       "Collections.ManagedProperties.*":            true,
-       "Collections.ManagedProperties.*.*":          true,
-       "Collections.PreserveVersionIfIdle":          true,
-       "Collections.TrashSweepInterval":             false,
-       "Containers":                                 true,
-       "Containers.CloudVMs":                        false,
-       "Containers.DefaultKeepCacheRAM":             true,
-       "Containers.DispatchPrivateKey":              false,
-       "Containers.JobsAPI":                         true,
-       "Containers.JobsAPI.CrunchJobUser":           false,
-       "Containers.JobsAPI.CrunchJobWrapper":        false,
-       "Containers.JobsAPI.CrunchRefreshTrigger":    false,
-       "Containers.JobsAPI.DefaultDockerImage":      false,
-       "Containers.JobsAPI.Enable":                  true,
-       "Containers.JobsAPI.GitInternalDir":          false,
-       "Containers.JobsAPI.ReuseJobIfOutputsDiffer": false,
-       "Containers.Logging":                         false,
-       "Containers.LogReuseDecisions":               false,
-       "Containers.MaxComputeVMs":                   false,
-       "Containers.MaxDispatchAttempts":             false,
-       "Containers.MaxRetryAttempts":                true,
-       "Containers.SLURM":                           false,
-       "Containers.StaleLockTimeout":                false,
-       "Containers.SupportedDockerImageFormats":     true,
-       "Containers.UsePreemptibleInstances":         true,
-       "Git":                                        false,
-       "InstanceTypes":                              true,
-       "InstanceTypes.*":                            true,
-       "InstanceTypes.*.*":                          true,
-       "Login":                                      false,
-       "Mail":                                       false,
-       "ManagementToken":                            false,
-       "PostgreSQL":                                 false,
-       "RemoteClusters":                             true,
-       "RemoteClusters.*":                           true,
-       "RemoteClusters.*.ActivateUsers":             true,
-       "RemoteClusters.*.Host":                      true,
-       "RemoteClusters.*.Insecure":                  true,
-       "RemoteClusters.*.Proxy":                     true,
-       "RemoteClusters.*.Scheme":                    true,
-       "Services":                                   true,
-       "Services.*":                                 true,
-       "Services.*.ExternalURL":                     true,
-       "Services.*.InternalURLs":                    false,
-       "SystemLogs":                                 false,
-       "SystemRootToken":                            false,
-       "TLS":                                        false,
-       "Users":                                      false,
-       "Workbench":                                  false,
+       "API":                                          true,
+       "API.AsyncPermissionsUpdateInterval":           false,
+       "API.DisabledAPIs":                             false,
+       "API.MaxIndexDatabaseRead":                     false,
+       "API.MaxItemsPerResponse":                      true,
+       "API.MaxRequestAmplification":                  false,
+       "API.MaxRequestSize":                           true,
+       "API.RailsSessionSecretToken":                  false,
+       "API.RequestTimeout":                           true,
+       "AuditLogs":                                    false,
+       "AuditLogs.MaxAge":                             false,
+       "AuditLogs.MaxDeleteBatch":                     false,
+       "AuditLogs.UnloggedAttributes":                 false,
+       "Collections":                                  true,
+       "Collections.BlobSigning":                      true,
+       "Collections.BlobSigningKey":                   false,
+       "Collections.BlobSigningTTL":                   true,
+       "Collections.CollectionVersioning":             false,
+       "Collections.DefaultReplication":               true,
+       "Collections.DefaultTrashLifetime":             true,
+       "Collections.ManagedProperties":                true,
+       "Collections.ManagedProperties.*":              true,
+       "Collections.ManagedProperties.*.*":            true,
+       "Collections.PreserveVersionIfIdle":            true,
+       "Collections.TrashSweepInterval":               false,
+       "Collections.TrustAllContent":                  false,
+       "Containers":                                   true,
+       "Containers.CloudVMs":                          false,
+       "Containers.DefaultKeepCacheRAM":               true,
+       "Containers.DispatchPrivateKey":                false,
+       "Containers.JobsAPI":                           true,
+       "Containers.JobsAPI.CrunchJobUser":             false,
+       "Containers.JobsAPI.CrunchJobWrapper":          false,
+       "Containers.JobsAPI.CrunchRefreshTrigger":      false,
+       "Containers.JobsAPI.DefaultDockerImage":        false,
+       "Containers.JobsAPI.Enable":                    true,
+       "Containers.JobsAPI.GitInternalDir":            false,
+       "Containers.JobsAPI.ReuseJobIfOutputsDiffer":   false,
+       "Containers.Logging":                           false,
+       "Containers.LogReuseDecisions":                 false,
+       "Containers.MaxComputeVMs":                     false,
+       "Containers.MaxDispatchAttempts":               false,
+       "Containers.MaxRetryAttempts":                  true,
+       "Containers.SLURM":                             false,
+       "Containers.StaleLockTimeout":                  false,
+       "Containers.SupportedDockerImageFormats":       true,
+       "Containers.UsePreemptibleInstances":           true,
+       "EnableBetaController14287":                    false,
+       "Git":                                          false,
+       "InstanceTypes":                                true,
+       "InstanceTypes.*":                              true,
+       "InstanceTypes.*.*":                            true,
+       "Login":                                        false,
+       "Mail":                                         false,
+       "ManagementToken":                              false,
+       "PostgreSQL":                                   false,
+       "RemoteClusters":                               true,
+       "RemoteClusters.*":                             true,
+       "RemoteClusters.*.ActivateUsers":               true,
+       "RemoteClusters.*.Host":                        true,
+       "RemoteClusters.*.Insecure":                    true,
+       "RemoteClusters.*.Proxy":                       true,
+       "RemoteClusters.*.Scheme":                      true,
+       "Services":                                     true,
+       "Services.*":                                   true,
+       "Services.*.ExternalURL":                       true,
+       "Services.*.InternalURLs":                      false,
+       "SystemLogs":                                   false,
+       "SystemRootToken":                              false,
+       "TLS":                                          false,
+       "Users":                                        true,
+       "Users.AnonymousUserToken":                     true,
+       "Users.AdminNotifierEmailFrom":                 false,
+       "Users.AutoAdminFirstUser":                     false,
+       "Users.AutoAdminUserWithEmail":                 false,
+       "Users.AutoSetupNewUsers":                      false,
+       "Users.AutoSetupNewUsersWithRepository":        false,
+       "Users.AutoSetupNewUsersWithVmUUID":            false,
+       "Users.AutoSetupUsernameBlacklist":             false,
+       "Users.EmailSubjectPrefix":                     false,
+       "Users.NewInactiveUserNotificationRecipients":  false,
+       "Users.NewUserNotificationRecipients":          false,
+       "Users.NewUsersAreActive":                      false,
+       "Users.UserNotifierEmailFrom":                  false,
+       "Users.UserProfileNotificationAddress":         false,
+       "Workbench":                                    true,
+       "Workbench.ActivationContactLink":              false,
+       "Workbench.APIClientConnectTimeout":            true,
+       "Workbench.APIClientReceiveTimeout":            true,
+       "Workbench.APIResponseCompression":             true,
+       "Workbench.ApplicationMimetypesWithViewIcon":   true,
+       "Workbench.ApplicationMimetypesWithViewIcon.*": true,
+       "Workbench.ArvadosDocsite":                     true,
+       "Workbench.ArvadosPublicDataDocURL":            true,
+       "Workbench.DefaultOpenIdPrefix":                false,
+       "Workbench.EnableGettingStartedPopup":          true,
+       "Workbench.EnablePublicProjectsPage":           true,
+       "Workbench.FileViewersConfigURL":               true,
+       "Workbench.LogViewerMaxBytes":                  true,
+       "Workbench.MultiSiteSearch":                    true,
+       "Workbench.ProfilingEnabled":                   true,
+       "Workbench.Repositories":                       false,
+       "Workbench.RepositoryCache":                    false,
+       "Workbench.RunningJobLogRecordsToFetch":        true,
+       "Workbench.SecretKeyBase":                      false,
+       "Workbench.ShowRecentCollectionsOnDashboard":   true,
+       "Workbench.ShowUserAgreementInline":            true,
+       "Workbench.ShowUserNotifications":              true,
+       "Workbench.SiteName":                           true,
+       "Workbench.Theme":                              true,
+       "Workbench.UserProfileFormFields":              true,
+       "Workbench.UserProfileFormFields.*":            true,
+       "Workbench.UserProfileFormFields.*.*":          true,
+       "Workbench.UserProfileFormFields.*.*.*":        true,
+       "Workbench.UserProfileFormMessage":             true,
+       "Workbench.VocabularyURL":                      true,
 }
 
 func redactUnsafe(m map[string]interface{}, mPrefix, lookupPrefix string) error {
index 581e54cdc6c80f463b3f922014b31b9a986e4638..e12fbdc7f6aa65ed3f181374aca15f932f2ff0da 100644 (file)
@@ -9,7 +9,6 @@ import (
        "regexp"
        "strings"
 
-       "git.curoverse.com/arvados.git/sdk/go/ctxlog"
        check "gopkg.in/check.v1"
 )
 
@@ -18,8 +17,8 @@ var _ = check.Suite(&ExportSuite{})
 type ExportSuite struct{}
 
 func (s *ExportSuite) TestExport(c *check.C) {
-       confdata := bytes.Replace(DefaultYAML, []byte("SAMPLE"), []byte("testkey"), -1)
-       cfg, err := Load(bytes.NewBuffer(confdata), ctxlog.TestLogger(c))
+       confdata := strings.Replace(string(DefaultYAML), "SAMPLE", "testkey", -1)
+       cfg, err := testLoader(c, confdata, nil).Load()
        c.Assert(err, check.IsNil)
        cluster := cfg.Clusters["xxxxx"]
        cluster.ManagementToken = "abcdefg"
index 6b2bb61112f7d371b58e88dfadad1a07d9476277..0a9d7a5b6df6ce8ec00d775081c732d3d7b91a50 100644 (file)
@@ -25,6 +25,16 @@ Clusters:
     ManagementToken: ""
 
     Services:
+
+      # In each of the service sections below, the keys under
+      # InternalURLs are the endpoints where the service should be
+      # listening, and reachable from other hosts in the cluster.
+      SAMPLE:
+        InternalURLs:
+          "http://example.host:12345": {}
+          SAMPLE: {}
+        ExternalURL: "-"
+
       RailsAPI:
         InternalURLs: {}
         ExternalURL: "-"
@@ -54,10 +64,57 @@ Clusters:
         ExternalURL: ""
       WebDAV:
         InternalURLs: {}
+        # Base URL for Workbench inline preview.  If blank, use
+        # WebDAVDownload instead, and disable inline preview.
+        # If both are empty, downloading collections from workbench
+        # will be impossible.
+        #
+        # It is important to properly configure the download service
+        # to migitate cross-site-scripting (XSS) attacks.  A HTML page
+        # can be stored in collection.  If an attacker causes a victim
+        # to visit that page through Workbench, it will be rendered by
+        # the browser.  If all collections are served at the same
+        # domain, the browser will consider collections as coming from
+        # the same origin and having access to the same browsing data,
+        # enabling malicious Javascript on that page to access Arvados
+        # on behalf of the victim.
+        #
+        # This is mitigating by having separate domains for each
+        # collection, or limiting preview to circumstances where the
+        # collection is not accessed with the user's regular
+        # full-access token.
+        #
+        # Serve preview links using uuid or pdh in subdomain
+        # (requires wildcard DNS and TLS certificate)
+        #   https://*.collections.uuid_prefix.arvadosapi.com
+        #
+        # Serve preview links using uuid or pdh in main domain
+        # (requires wildcard DNS and TLS certificate)
+        #   https://*--collections.uuid_prefix.arvadosapi.com
+        #
+        # Serve preview links by setting uuid or pdh in the path.
+        # This configuration only allows previews of public data or
+        # collection-sharing links, because these use the anonymous
+        # user token or the token is already embedded in the URL.
+        # Other data must be handled as downloads via WebDAVDownload:
+        #   https://collections.uuid_prefix.arvadosapi.com
+        #
         ExternalURL: ""
+
       WebDAVDownload:
         InternalURLs: {}
+        # Base URL for download links. If blank, serve links to WebDAV
+        # with disposition=attachment query param.  Unlike preview links,
+        # browsers do not render attachments, so there is no risk of XSS.
+        #
+        # If WebDAVDownload is blank, and WebDAV uses a
+        # single-origin form, then Workbench will show an error page
+        #
+        # Serve download links by setting uuid or pdh in the path:
+        #   https://download.uuid_prefix.arvadosapi.com
+        #
         ExternalURL: ""
+
       Keepstore:
         InternalURLs: {}
         ExternalURL: "-"
@@ -66,6 +123,14 @@ Clusters:
         ExternalURL: ""
       WebShell:
         InternalURLs: {}
+        # ShellInABox service endpoint URL for a given VM.  If empty, do not
+        # offer web shell logins.
+        #
+        # E.g., using a path-based proxy server to forward connections to shell hosts:
+        # https://webshell.uuid_prefix.arvadosapi.com
+        #
+        # E.g., using a name-based proxy server to forward connections to shell hosts:
+        # https://*.webshell.uuid_prefix.arvadosapi.com
         ExternalURL: ""
       Workbench1:
         InternalURLs: {}
@@ -91,6 +156,7 @@ Clusters:
         user: ""
         password: ""
         dbname: ""
+        SAMPLE: ""
     API:
       # Maximum size (in bytes) allowed for a single API request.  This
       # limit is published in the discovery document for use by clients.
@@ -176,6 +242,11 @@ Clusters:
       NewUserNotificationRecipients: []
       NewInactiveUserNotificationRecipients: []
 
+      # Set anonymous_user_token to enable anonymous user access. You can get
+      # the token by running "bundle exec ./script/get_anonymous_user_token.rb"
+      # in the directory where your API server is running.
+      AnonymousUserToken: ""
+
     AuditLogs:
       # Time to keep audit logs, in seconds. (An audit log is a row added
       # to the "logs" table in the PostgreSQL database each time an
@@ -231,7 +302,7 @@ Clusters:
       # one another!
       BlobSigning: true
 
-      # blob_signing_key is a string of alphanumeric characters used to
+      # BlobSigningKey is a string of alphanumeric characters used to
       # generate permission signatures for Keep locators. It must be
       # identical to the permission key given to Keep. IMPORTANT: This is
       # a site secret. It should be at least 50 characters.
@@ -299,6 +370,19 @@ Clusters:
       ManagedProperties:
         SAMPLE: {Function: original_owner, Protected: true}
 
+      # In "trust all content" mode, Workbench will redirect download
+      # requests to WebDAV preview link, even in the cases when
+      # WebDAV would have to expose XSS vulnerabilities in order to
+      # handle the redirect (see discussion on Services.WebDAV).
+      #
+      # This setting has no effect in the recommended configuration,
+      # where the WebDAV is configured to have a separate domain for
+      # every collection; in this case XSS protection is provided by
+      # browsers' same-origin policy.
+      #
+      # The default setting (false) is appropriate for a multi-user site.
+      TrustAllContent: false
+
     Login:
       # These settings are provided by your OAuth2 provider (e.g.,
       # sso-provider).
@@ -506,7 +590,7 @@ Clusters:
         # Shell command to execute on each worker to determine whether
         # the worker is booted and ready to run containers. It should
         # exit zero if the worker is ready.
-        BootProbeCommand: "docker ps"
+        BootProbeCommand: "docker ps -q"
 
         # Minimum interval between consecutive probes to a single
         # worker.
@@ -619,11 +703,15 @@ Clusters:
     Mail:
       MailchimpAPIKey: ""
       MailchimpListID: ""
-      SendUserSetupNotificationEmail: ""
-      IssueReporterEmailFrom: ""
-      IssueReporterEmailTo: ""
-      SupportEmailAddress: ""
-      EmailFrom: ""
+      SendUserSetupNotificationEmail: true
+
+      # Bug/issue report notification to and from addresses
+      IssueReporterEmailFrom: "arvados@example.com"
+      IssueReporterEmailTo: "arvados@example.com"
+      SupportEmailAddress: "arvados@example.com"
+
+      # Generic issue email from
+      EmailFrom: "arvados@example.com"
     RemoteClusters:
       "*":
         Host: ""
@@ -657,16 +745,63 @@ Clusters:
       ArvadosDocsite: https://doc.arvados.org
       ArvadosPublicDataDocURL: https://playground.arvados.org/projects/public
       ShowUserAgreementInline: false
-      SecretToken: ""
       SecretKeyBase: ""
+
+      # Scratch directory used by the remote repository browsing
+      # feature. If it doesn't exist, it (and any missing parents) will be
+      # created using mkdir_p.
       RepositoryCache: /var/www/arvados-workbench/current/tmp/git
+
+      # Below is a sample setting of user_profile_form_fields config parameter.
+      # This configuration parameter should be set to either false (to disable) or
+      # to a map as shown below.
+      # Configure the map of input fields to be displayed in the profile page
+      # using the attribute "key" for each of the input fields.
+      # This sample shows configuration with one required and one optional form fields.
+      # For each of these input fields:
+      #   You can specify "Type" as "text" or "select".
+      #   List the "Options" to be displayed for each of the "select" menu.
+      #   Set "Required" as "true" for any of these fields to make them required.
+      # If any of the required fields are missing in the user's profile, the user will be
+      # redirected to the profile page before they can access any Workbench features.
       UserProfileFormFields:
         SAMPLE:
-          Type: text
-          FormFieldTitle: ""
-          FormFieldDescription: ""
-          Required: true
+          Type: select
+          FormFieldTitle: Best color
+          FormFieldDescription: your favorite color
+          Required: false
+          Position: 1
+          Options:
+            red: {}
+            blue: {}
+            green: {}
+            SAMPLE: {}
+
+        # exampleTextValue:  # key that will be set in properties
+        #   Type: text  #
+        #   FormFieldTitle: ""
+        #   FormFieldDescription: ""
+        #   Required: true
+        #   Position: 1
+        # exampleOptionsValue:
+        #   Type: select
+        #   FormFieldTitle: ""
+        #   FormFieldDescription: ""
+        #   Required: true
+        #   Position: 1
+        #   Options:
+        #     red: {}
+        #     blue: {}
+        #     yellow: {}
+
+      # Use "UserProfileFormMessage to configure the message you want
+      # to display on the profile page.
       UserProfileFormMessage: 'Welcome to Arvados. All <span style="color:red">required fields</span> must be completed before you can proceed.'
+
+      # Mimetypes of applications for which the view icon
+      # would be enabled in a collection's show page.
+      # It is sufficient to list only applications here.
+      # No need to list text and image types.
       ApplicationMimetypesWithViewIcon:
         cwl: {}
         fasta: {}
@@ -683,20 +818,56 @@ Clusters:
         vnd.realvnc.bed: {}
         xml: {}
         xsl: {}
+        SAMPLE: {}
+
+      # The maximum number of bytes to load in the log viewer
       LogViewerMaxBytes: 1M
+
+      # When anonymous_user_token is configured, show public projects page
       EnablePublicProjectsPage: true
+
+      # By default, disable the "Getting Started" popup which is specific to Arvados playground
       EnableGettingStartedPopup: false
+
+      # Ask Arvados API server to compress its response payloads.
       APIResponseCompression: true
+
+      # Timeouts for API requests.
       APIClientConnectTimeout: 2m
       APIClientReceiveTimeout: 5m
+
+      # Maximum number of historic log records of a running job to fetch
+      # and display in the Log tab, while subscribing to web sockets.
       RunningJobLogRecordsToFetch: 2000
+
+      # In systems with many shared projects, loading of dashboard and topnav
+      # cab be slow due to collections indexing; use the following parameters
+      # to suppress these properties
       ShowRecentCollectionsOnDashboard: true
       ShowUserNotifications: true
-      MultiSiteSearch: false
+
+      # Enable/disable "multi-site search" in top nav ("true"/"false"), or
+      # a link to the multi-site search page on a "home" Workbench site.
+      #
+      # Example:
+      #   https://workbench.qr1hi.arvadosapi.com/collections/multisite
+      MultiSiteSearch: ""
+
+      # Should workbench allow management of local git repositories? Set to false if
+      # the jobs api is disabled and there are no local git repositories.
       Repositories: true
+
       SiteName: Arvados Workbench
+      ProfilingEnabled: false
+
+      # This is related to obsolete Google OpenID 1.0 login
+      # but some workbench stuff still expects it to be set.
+      DefaultOpenIdPrefix: "https://www.google.com/accounts/o8/id"
 
       # Workbench2 configs
       VocabularyURL: ""
       FileViewersConfigURL: ""
+
+    # Use experimental controller code (see https://dev.arvados.org/issues/14287)
+    EnableBetaController14287: false
 `)
index a0c769537487cce179ff74d65035144ef647b561..168c1aa22a8554ef649cc65463b10b8437970494 100644 (file)
@@ -8,6 +8,7 @@ import (
        "bytes"
        "encoding/json"
        "errors"
+       "flag"
        "fmt"
        "io"
        "io/ioutil"
@@ -17,37 +18,125 @@ import (
        "git.curoverse.com/arvados.git/sdk/go/arvados"
        "github.com/ghodss/yaml"
        "github.com/imdario/mergo"
+       "github.com/sirupsen/logrus"
 )
 
-type logger interface {
-       Warnf(string, ...interface{})
+var ErrNoClustersDefined = errors.New("config does not define any clusters")
+
+type Loader struct {
+       Stdin          io.Reader
+       Logger         logrus.FieldLogger
+       SkipDeprecated bool // Don't load legacy/deprecated config keys/files
+
+       Path          string
+       KeepstorePath string
+
+       configdata []byte
 }
 
-func loadFileOrStdin(path string, stdin io.Reader, log logger) (*arvados.Config, error) {
-       if path == "-" {
-               return load(stdin, log, true)
-       } else {
-               return LoadFile(path, log)
+// NewLoader returns a new Loader with Stdin and Logger set to the
+// given values, and all config paths set to their default values.
+func NewLoader(stdin io.Reader, logger logrus.FieldLogger) *Loader {
+       ldr := &Loader{Stdin: stdin, Logger: logger}
+       // Calling SetupFlags on a throwaway FlagSet has the side
+       // effect of assigning default values to the configurable
+       // fields.
+       ldr.SetupFlags(flag.NewFlagSet("", flag.ContinueOnError))
+       return ldr
+}
+
+// SetupFlags configures a flagset so arguments like -config X can be
+// used to change the loader's Path fields.
+//
+//     ldr := NewLoader(os.Stdin, logrus.New())
+//     flagset := flag.NewFlagSet("", flag.ContinueOnError)
+//     ldr.SetupFlags(flagset)
+//     // ldr.Path == "/etc/arvados/config.yml"
+//     flagset.Parse([]string{"-config", "/tmp/c.yaml"})
+//     // ldr.Path == "/tmp/c.yaml"
+func (ldr *Loader) SetupFlags(flagset *flag.FlagSet) {
+       flagset.StringVar(&ldr.Path, "config", arvados.DefaultConfigFile, "Site configuration `file` (default may be overridden by setting an ARVADOS_CONFIG environment variable)")
+       flagset.StringVar(&ldr.KeepstorePath, "legacy-keepstore-config", defaultKeepstoreConfigPath, "Legacy keepstore configuration `file`")
+}
+
+// MungeLegacyConfigArgs checks args for a -config flag whose argument
+// is a regular file (or a symlink to one), but doesn't have a
+// top-level "Clusters" key and therefore isn't a valid cluster
+// configuration file. If it finds such a flag, it replaces -config
+// with legacyConfigArg (e.g., "-legacy-keepstore-config").
+//
+// This is used by programs that still need to accept "-config" as a
+// way to specify a per-component config file until their config has
+// been migrated.
+//
+// If any errors are encountered while reading or parsing a config
+// file, the given args are not munged. We presume the same errors
+// will be encountered again and reported later on when trying to load
+// cluster configuration from the same file, regardless of which
+// struct we end up using.
+func (ldr *Loader) MungeLegacyConfigArgs(lgr logrus.FieldLogger, args []string, legacyConfigArg string) []string {
+       munged := append([]string(nil), args...)
+       for i := 0; i < len(args); i++ {
+               if !strings.HasPrefix(args[i], "-") || strings.SplitN(strings.TrimPrefix(args[i], "-"), "=", 2)[0] != "config" {
+                       continue
+               }
+               var operand string
+               if strings.Contains(args[i], "=") {
+                       operand = strings.SplitN(args[i], "=", 2)[1]
+               } else if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
+                       i++
+                       operand = args[i]
+               } else {
+                       continue
+               }
+               if fi, err := os.Stat(operand); err != nil || !fi.Mode().IsRegular() {
+                       continue
+               }
+               f, err := os.Open(operand)
+               if err != nil {
+                       continue
+               }
+               defer f.Close()
+               buf, err := ioutil.ReadAll(f)
+               if err != nil {
+                       continue
+               }
+               var cfg arvados.Config
+               err = yaml.Unmarshal(buf, &cfg)
+               if err != nil {
+                       continue
+               }
+               if len(cfg.Clusters) == 0 {
+                       lgr.Warnf("%s is not a cluster config file -- interpreting %s as %s (please migrate your config!)", operand, "-config", legacyConfigArg)
+                       if operand == args[i] {
+                               munged[i-1] = legacyConfigArg
+                       } else {
+                               munged[i] = legacyConfigArg + "=" + operand
+                       }
+               }
        }
+       return munged
 }
 
-func LoadFile(path string, log logger) (*arvados.Config, error) {
+func (ldr *Loader) loadBytes(path string) ([]byte, error) {
+       if path == "-" {
+               return ioutil.ReadAll(ldr.Stdin)
+       }
        f, err := os.Open(path)
        if err != nil {
                return nil, err
        }
        defer f.Close()
-       return Load(f, log)
+       return ioutil.ReadAll(f)
 }
 
-func Load(rdr io.Reader, log logger) (*arvados.Config, error) {
-       return load(rdr, log, true)
-}
-
-func load(rdr io.Reader, log logger, useDeprecated bool) (*arvados.Config, error) {
-       buf, err := ioutil.ReadAll(rdr)
-       if err != nil {
-               return nil, err
+func (ldr *Loader) Load() (*arvados.Config, error) {
+       if ldr.configdata == nil {
+               buf, err := ldr.loadBytes(ldr.Path)
+               if err != nil {
+                       return nil, err
+               }
+               ldr.configdata = buf
        }
 
        // Load the config into a dummy map to get the cluster ID
@@ -57,12 +146,12 @@ func load(rdr io.Reader, log logger, useDeprecated bool) (*arvados.Config, error
        var dummy struct {
                Clusters map[string]struct{}
        }
-       err = yaml.Unmarshal(buf, &dummy)
+       err := yaml.Unmarshal(ldr.configdata, &dummy)
        if err != nil {
                return nil, err
        }
        if len(dummy.Clusters) == 0 {
-               return nil, errors.New("config does not define any clusters")
+               return nil, ErrNoClustersDefined
        }
 
        // We can't merge deep structs here; instead, we unmarshal the
@@ -82,11 +171,11 @@ func load(rdr io.Reader, log logger, useDeprecated bool) (*arvados.Config, error
                }
        }
        var src map[string]interface{}
-       err = yaml.Unmarshal(buf, &src)
+       err = yaml.Unmarshal(ldr.configdata, &src)
        if err != nil {
                return nil, fmt.Errorf("loading config data: %s", err)
        }
-       logExtraKeys(log, merged, src, "")
+       ldr.logExtraKeys(merged, src, "")
        removeSampleKeys(merged)
        err = mergo.Merge(&merged, src, mergo.WithOverride)
        if err != nil {
@@ -109,11 +198,18 @@ func load(rdr io.Reader, log logger, useDeprecated bool) (*arvados.Config, error
                return nil, fmt.Errorf("transcoding config data: %s", err)
        }
 
-       if useDeprecated {
-               err = applyDeprecatedConfig(&cfg, buf, log)
+       if !ldr.SkipDeprecated {
+               err = ldr.applyDeprecatedConfig(&cfg)
                if err != nil {
                        return nil, err
                }
+               for _, err := range []error{
+                       ldr.loadOldKeepstoreConfig(&cfg),
+               } {
+                       if err != nil {
+                               return nil, err
+                       }
+               }
        }
 
        // Check for known mistakes
@@ -147,8 +243,8 @@ func removeSampleKeys(m map[string]interface{}) {
        }
 }
 
-func logExtraKeys(log logger, expected, supplied map[string]interface{}, prefix string) {
-       if log == nil {
+func (ldr *Loader) logExtraKeys(expected, supplied map[string]interface{}, prefix string) {
+       if ldr.Logger == nil {
                return
        }
        allowed := map[string]interface{}{}
@@ -156,11 +252,15 @@ func logExtraKeys(log logger, expected, supplied map[string]interface{}, prefix
                allowed[strings.ToLower(k)] = v
        }
        for k, vsupp := range supplied {
+               if k == "SAMPLE" {
+                       // entry will be dropped in removeSampleKeys anyway
+                       continue
+               }
                vexp, ok := allowed[strings.ToLower(k)]
-               if !ok && expected["SAMPLE"] != nil {
+               if expected["SAMPLE"] != nil {
                        vexp = expected["SAMPLE"]
                } else if !ok {
-                       log.Warnf("deprecated or unknown config entry: %s%s", prefix, k)
+                       ldr.Logger.Warnf("deprecated or unknown config entry: %s%s", prefix, k)
                        continue
                }
                if vsupp, ok := vsupp.(map[string]interface{}); !ok {
@@ -168,9 +268,9 @@ func logExtraKeys(log logger, expected, supplied map[string]interface{}, prefix
                        // will be caught elsewhere; see TestBadType.
                        continue
                } else if vexp, ok := vexp.(map[string]interface{}); !ok {
-                       log.Warnf("unexpected object in config entry: %s%s", prefix, k)
+                       ldr.Logger.Warnf("unexpected object in config entry: %s%s", prefix, k)
                } else {
-                       logExtraKeys(log, vexp, vsupp, prefix+k+".")
+                       ldr.logExtraKeys(vexp, vsupp, prefix+k+".")
                }
        }
 }
index 6b014476b6d9f31d6cf23197bcde6fac2fda0bc3..340eb0a0a7e6900cc090bd170dea373a9100b664 100644 (file)
@@ -6,9 +6,12 @@ package config
 
 import (
        "bytes"
+       "fmt"
        "io"
+       "io/ioutil"
        "os"
        "os/exec"
+       "reflect"
        "strings"
        "testing"
 
@@ -26,16 +29,31 @@ func Test(t *testing.T) {
 
 var _ = check.Suite(&LoadSuite{})
 
+// Return a new Loader that reads cluster config from configdata
+// (instead of the usual default /etc/arvados/config.yml), and logs to
+// logdst or (if that's nil) c.Log.
+func testLoader(c *check.C, configdata string, logdst io.Writer) *Loader {
+       logger := ctxlog.TestLogger(c)
+       if logdst != nil {
+               lgr := logrus.New()
+               lgr.Out = logdst
+               logger = lgr
+       }
+       ldr := NewLoader(bytes.NewBufferString(configdata), logger)
+       ldr.Path = "-"
+       return ldr
+}
+
 type LoadSuite struct{}
 
 func (s *LoadSuite) TestEmpty(c *check.C) {
-       cfg, err := Load(&bytes.Buffer{}, ctxlog.TestLogger(c))
+       cfg, err := testLoader(c, "", nil).Load()
        c.Check(cfg, check.IsNil)
        c.Assert(err, check.ErrorMatches, `config does not define any clusters`)
 }
 
 func (s *LoadSuite) TestNoConfigs(c *check.C) {
-       cfg, err := Load(bytes.NewBufferString(`Clusters: {"z1111": {}}`), ctxlog.TestLogger(c))
+       cfg, err := testLoader(c, `Clusters: {"z1111": {}}`, nil).Load()
        c.Assert(err, check.IsNil)
        c.Assert(cfg.Clusters, check.HasLen, 1)
        cc, err := cfg.GetCluster("z1111")
@@ -45,12 +63,99 @@ func (s *LoadSuite) TestNoConfigs(c *check.C) {
        c.Check(cc.API.MaxItemsPerResponse, check.Equals, 1000)
 }
 
+func (s *LoadSuite) TestMungeLegacyConfigArgs(c *check.C) {
+       f, err := ioutil.TempFile("", "")
+       c.Check(err, check.IsNil)
+       defer os.Remove(f.Name())
+       io.WriteString(f, "Debug: true\n")
+       oldfile := f.Name()
+
+       f, err = ioutil.TempFile("", "")
+       c.Check(err, check.IsNil)
+       defer os.Remove(f.Name())
+       io.WriteString(f, "Clusters: {aaaaa: {}}\n")
+       newfile := f.Name()
+
+       for _, trial := range []struct {
+               argsIn  []string
+               argsOut []string
+       }{
+               {
+                       []string{"-config", oldfile},
+                       []string{"-old-config", oldfile},
+               },
+               {
+                       []string{"-config=" + oldfile},
+                       []string{"-old-config=" + oldfile},
+               },
+               {
+                       []string{"-config", newfile},
+                       []string{"-config", newfile},
+               },
+               {
+                       []string{"-config=" + newfile},
+                       []string{"-config=" + newfile},
+               },
+               {
+                       []string{"-foo", oldfile},
+                       []string{"-foo", oldfile},
+               },
+               {
+                       []string{"-foo=" + oldfile},
+                       []string{"-foo=" + oldfile},
+               },
+               {
+                       []string{"-foo", "-config=" + oldfile},
+                       []string{"-foo", "-old-config=" + oldfile},
+               },
+               {
+                       []string{"-foo", "bar", "-config=" + oldfile},
+                       []string{"-foo", "bar", "-old-config=" + oldfile},
+               },
+               {
+                       []string{"-foo=bar", "baz", "-config=" + oldfile},
+                       []string{"-foo=bar", "baz", "-old-config=" + oldfile},
+               },
+               {
+                       []string{"-config=/dev/null"},
+                       []string{"-config=/dev/null"},
+               },
+               {
+                       []string{"-config=-"},
+                       []string{"-config=-"},
+               },
+               {
+                       []string{"-config="},
+                       []string{"-config="},
+               },
+               {
+                       []string{"-foo=bar", "baz", "-config"},
+                       []string{"-foo=bar", "baz", "-config"},
+               },
+               {
+                       []string{},
+                       nil,
+               },
+       } {
+               var logbuf bytes.Buffer
+               logger := logrus.New()
+               logger.Out = &logbuf
+
+               var ldr Loader
+               args := ldr.MungeLegacyConfigArgs(logger, trial.argsIn, "-old-config")
+               c.Check(args, check.DeepEquals, trial.argsOut)
+               if fmt.Sprintf("%v", trial.argsIn) != fmt.Sprintf("%v", trial.argsOut) {
+                       c.Check(logbuf.String(), check.Matches, `.*`+oldfile+` is not a cluster config file -- interpreting -config as -old-config.*\n`)
+               }
+       }
+}
+
 func (s *LoadSuite) TestSampleKeys(c *check.C) {
        for _, yaml := range []string{
                `{"Clusters":{"z1111":{}}}`,
                `{"Clusters":{"z1111":{"InstanceTypes":{"Foo":{"RAM": "12345M"}}}}}`,
        } {
-               cfg, err := Load(bytes.NewBufferString(yaml), ctxlog.TestLogger(c))
+               cfg, err := testLoader(c, yaml, nil).Load()
                c.Assert(err, check.IsNil)
                cc, err := cfg.GetCluster("z1111")
                _, hasSample := cc.InstanceTypes["SAMPLE"]
@@ -63,7 +168,7 @@ func (s *LoadSuite) TestSampleKeys(c *check.C) {
 }
 
 func (s *LoadSuite) TestMultipleClusters(c *check.C) {
-       cfg, err := Load(bytes.NewBufferString(`{"Clusters":{"z1111":{},"z2222":{}}}`), ctxlog.TestLogger(c))
+       cfg, err := testLoader(c, `{"Clusters":{"z1111":{},"z2222":{}}}`, nil).Load()
        c.Assert(err, check.IsNil)
        c1, err := cfg.GetCluster("z1111")
        c.Assert(err, check.IsNil)
@@ -75,9 +180,7 @@ func (s *LoadSuite) TestMultipleClusters(c *check.C) {
 
 func (s *LoadSuite) TestDeprecatedOrUnknownWarning(c *check.C) {
        var logbuf bytes.Buffer
-       logger := logrus.New()
-       logger.Out = &logbuf
-       _, err := Load(bytes.NewBufferString(`
+       _, err := testLoader(c, `
 Clusters:
   zzzzz:
     postgresql: {}
@@ -88,7 +191,7 @@ Clusters:
         Host: z2222.arvadosapi.com
         Proxy: true
         BadKey: badValue
-`), logger)
+`, &logbuf).Load()
        c.Assert(err, check.IsNil)
        logs := strings.Split(strings.TrimSuffix(logbuf.String(), "\n"), "\n")
        for _, log := range logs {
@@ -97,13 +200,51 @@ Clusters:
        c.Check(logs, check.HasLen, 2)
 }
 
+func (s *LoadSuite) checkSAMPLEKeys(c *check.C, path string, x interface{}) {
+       v := reflect.Indirect(reflect.ValueOf(x))
+       switch v.Kind() {
+       case reflect.Map:
+               var stringKeys, sampleKey bool
+               iter := v.MapRange()
+               for iter.Next() {
+                       k := iter.Key()
+                       if k.Kind() == reflect.String {
+                               stringKeys = true
+                               if k.String() == "SAMPLE" || k.String() == "xxxxx" {
+                                       sampleKey = true
+                                       s.checkSAMPLEKeys(c, path+"."+k.String(), iter.Value().Interface())
+                               }
+                       }
+               }
+               if stringKeys && !sampleKey {
+                       c.Errorf("%s is a map with string keys (type %T) but config.default.yml has no SAMPLE key", path, x)
+               }
+               return
+       case reflect.Struct:
+               for i := 0; i < v.NumField(); i++ {
+                       val := v.Field(i)
+                       if val.CanInterface() {
+                               s.checkSAMPLEKeys(c, path+"."+v.Type().Field(i).Name, val.Interface())
+                       }
+               }
+       }
+}
+
+func (s *LoadSuite) TestDefaultConfigHasAllSAMPLEKeys(c *check.C) {
+       var logbuf bytes.Buffer
+       loader := testLoader(c, string(DefaultYAML), &logbuf)
+       cfg, err := loader.Load()
+       c.Assert(err, check.IsNil)
+       s.checkSAMPLEKeys(c, "", cfg)
+}
+
 func (s *LoadSuite) TestNoUnrecognizedKeysInDefaultConfig(c *check.C) {
        var logbuf bytes.Buffer
-       logger := logrus.New()
-       logger.Out = &logbuf
        var supplied map[string]interface{}
        yaml.Unmarshal(DefaultYAML, &supplied)
-       cfg, err := Load(bytes.NewBuffer(DefaultYAML), logger)
+
+       loader := testLoader(c, string(DefaultYAML), &logbuf)
+       cfg, err := loader.Load()
        c.Assert(err, check.IsNil)
        var loaded map[string]interface{}
        buf, err := yaml.Marshal(cfg)
@@ -111,7 +252,7 @@ func (s *LoadSuite) TestNoUnrecognizedKeysInDefaultConfig(c *check.C) {
        err = yaml.Unmarshal(buf, &loaded)
        c.Assert(err, check.IsNil)
 
-       logExtraKeys(logger, loaded, supplied, "")
+       loader.logExtraKeys(loaded, supplied, "")
        c.Check(logbuf.String(), check.Equals, "")
 }
 
@@ -119,25 +260,25 @@ func (s *LoadSuite) TestNoWarningsForDumpedConfig(c *check.C) {
        var logbuf bytes.Buffer
        logger := logrus.New()
        logger.Out = &logbuf
-       cfg, err := Load(bytes.NewBufferString(`{"Clusters":{"zzzzz":{}}}`), logger)
+       cfg, err := testLoader(c, `{"Clusters":{"zzzzz":{}}}`, &logbuf).Load()
        c.Assert(err, check.IsNil)
        yaml, err := yaml.Marshal(cfg)
        c.Assert(err, check.IsNil)
-       cfgDumped, err := Load(bytes.NewBuffer(yaml), logger)
+       cfgDumped, err := testLoader(c, string(yaml), &logbuf).Load()
        c.Assert(err, check.IsNil)
        c.Check(cfg, check.DeepEquals, cfgDumped)
        c.Check(logbuf.String(), check.Equals, "")
 }
 
 func (s *LoadSuite) TestPostgreSQLKeyConflict(c *check.C) {
-       _, err := Load(bytes.NewBufferString(`
+       _, err := testLoader(c, `
 Clusters:
  zzzzz:
   postgresql:
    connection:
      DBName: dbname
      Host: host
-`), ctxlog.TestLogger(c))
+`, nil).Load()
        c.Check(err, check.ErrorMatches, `Clusters.zzzzz.PostgreSQL.Connection: multiple entries for "(dbname|host)".*`)
 }
 
@@ -169,7 +310,7 @@ Clusters:
 `,
        } {
                c.Log(data)
-               v, err := Load(bytes.NewBufferString(data), ctxlog.TestLogger(c))
+               v, err := testLoader(c, data, nil).Load()
                if v != nil {
                        c.Logf("%#v", v.Clusters["zzzzz"].PostgreSQL.ConnectionPool)
                }
@@ -210,9 +351,9 @@ Clusters:
 }
 
 func (s *LoadSuite) checkEquivalent(c *check.C, goty, expectedy string) {
-       got, err := Load(bytes.NewBufferString(goty), ctxlog.TestLogger(c))
+       got, err := testLoader(c, goty, nil).Load()
        c.Assert(err, check.IsNil)
-       expected, err := Load(bytes.NewBufferString(expectedy), ctxlog.TestLogger(c))
+       expected, err := testLoader(c, expectedy, nil).Load()
        c.Assert(err, check.IsNil)
        if !c.Check(got, check.DeepEquals, expected) {
                cmd := exec.Command("diff", "-u", "--label", "expected", "--label", "got", "/dev/fd/3", "/dev/fd/4")
diff --git a/lib/controller/federation/conn.go b/lib/controller/federation/conn.go
new file mode 100644 (file)
index 0000000..e094953
--- /dev/null
@@ -0,0 +1,310 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package federation
+
+import (
+       "context"
+       "crypto/md5"
+       "errors"
+       "fmt"
+       "net/http"
+       "net/url"
+       "regexp"
+       "strings"
+
+       "git.curoverse.com/arvados.git/lib/controller/railsproxy"
+       "git.curoverse.com/arvados.git/lib/controller/rpc"
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "git.curoverse.com/arvados.git/sdk/go/auth"
+       "git.curoverse.com/arvados.git/sdk/go/ctxlog"
+)
+
+type Conn struct {
+       cluster *arvados.Cluster
+       local   backend
+       remotes map[string]backend
+}
+
+func New(cluster *arvados.Cluster) arvados.API {
+       local := railsproxy.NewConn(cluster)
+       remotes := map[string]backend{}
+       for id, remote := range cluster.RemoteClusters {
+               if !remote.Proxy {
+                       continue
+               }
+               remotes[id] = rpc.NewConn(id, &url.URL{Scheme: remote.Scheme, Host: remote.Host}, remote.Insecure, saltedTokenProvider(local, id))
+       }
+
+       return &Conn{
+               cluster: cluster,
+               local:   local,
+               remotes: remotes,
+       }
+}
+
+// Return a new rpc.TokenProvider that takes the client-provided
+// tokens from an incoming request context, determines whether they
+// should (and can) be salted for the given remoteID, and returns the
+// resulting tokens.
+func saltedTokenProvider(local backend, remoteID string) rpc.TokenProvider {
+       return func(ctx context.Context) ([]string, error) {
+               var tokens []string
+               incoming, ok := auth.FromContext(ctx)
+               if !ok {
+                       return nil, errors.New("no token provided")
+               }
+               for _, token := range incoming.Tokens {
+                       salted, err := auth.SaltToken(token, remoteID)
+                       switch err {
+                       case nil:
+                               tokens = append(tokens, salted)
+                       case auth.ErrSalted:
+                               tokens = append(tokens, token)
+                       case auth.ErrObsoleteToken:
+                               ctx := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{token}})
+                               aca, err := local.APIClientAuthorizationCurrent(ctx, arvados.GetOptions{})
+                               if errStatus(err) == http.StatusUnauthorized {
+                                       // pass through unmodified
+                                       tokens = append(tokens, token)
+                                       continue
+                               } else if err != nil {
+                                       return nil, err
+                               }
+                               salted, err := auth.SaltToken(aca.TokenV2(), remoteID)
+                               if err != nil {
+                                       return nil, err
+                               }
+                               tokens = append(tokens, salted)
+                       default:
+                               return nil, err
+                       }
+               }
+               return tokens, nil
+       }
+}
+
+// Return suitable backend for a query about the given cluster ID
+// ("aaaaa") or object UUID ("aaaaa-dz642-abcdefghijklmno").
+func (conn *Conn) chooseBackend(id string) backend {
+       if len(id) == 27 {
+               id = id[:5]
+       } else if len(id) != 5 {
+               // PDH or bogus ID
+               return conn.local
+       }
+       if id == conn.cluster.ClusterID {
+               return conn.local
+       } else if be, ok := conn.remotes[id]; ok {
+               return be
+       } else {
+               // TODO: return an "always error" backend?
+               return conn.local
+       }
+}
+
+// Call fn with the local backend; then, if fn returned 404, call fn
+// on the available remote backends (possibly concurrently) until one
+// succeeds.
+//
+// The second argument to fn is the cluster ID of the remote backend,
+// or "" for the local backend.
+//
+// A non-nil error means all backends failed.
+func (conn *Conn) tryLocalThenRemotes(ctx context.Context, fn func(context.Context, string, backend) error) error {
+       if err := fn(ctx, "", conn.local); err == nil || errStatus(err) != http.StatusNotFound {
+               return err
+       }
+
+       ctx, cancel := context.WithCancel(ctx)
+       defer cancel()
+       errchan := make(chan error, len(conn.remotes))
+       for remoteID, be := range conn.remotes {
+               remoteID, be := remoteID, be
+               go func() {
+                       errchan <- fn(ctx, remoteID, be)
+               }()
+       }
+       all404 := true
+       var errs []error
+       for i := 0; i < cap(errchan); i++ {
+               err := <-errchan
+               if err == nil {
+                       return nil
+               }
+               all404 = all404 && errStatus(err) == http.StatusNotFound
+               errs = append(errs, err)
+       }
+       if all404 {
+               return notFoundError{}
+       }
+       // FIXME: choose appropriate HTTP status
+       return fmt.Errorf("errors: %v", errs)
+}
+
+func (conn *Conn) CollectionCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Collection, error) {
+       return conn.chooseBackend(options.ClusterID).CollectionCreate(ctx, options)
+}
+
+func (conn *Conn) CollectionUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Collection, error) {
+       return conn.chooseBackend(options.UUID).CollectionUpdate(ctx, options)
+}
+
+func rewriteManifest(mt, remoteID string) string {
+       return regexp.MustCompile(` [0-9a-f]{32}\+[^ ]*`).ReplaceAllStringFunc(mt, func(tok string) string {
+               return strings.Replace(tok, "+A", "+R"+remoteID+"-", -1)
+       })
+}
+
+// this could be in sdk/go/arvados
+func portableDataHash(mt string) string {
+       h := md5.New()
+       blkRe := regexp.MustCompile(`^ [0-9a-f]{32}\+\d+`)
+       size := 0
+       _ = regexp.MustCompile(` ?[^ ]*`).ReplaceAllFunc([]byte(mt), func(tok []byte) []byte {
+               if m := blkRe.Find(tok); m != nil {
+                       // write hash+size, ignore remaining block hints
+                       tok = m
+               }
+               n, err := h.Write(tok)
+               if err != nil {
+                       panic(err)
+               }
+               size += n
+               return nil
+       })
+       return fmt.Sprintf("%x+%d", h.Sum(nil), size)
+}
+
+func (conn *Conn) CollectionGet(ctx context.Context, options arvados.GetOptions) (arvados.Collection, error) {
+       if len(options.UUID) == 27 {
+               // UUID is really a UUID
+               c, err := conn.chooseBackend(options.UUID).CollectionGet(ctx, options)
+               if err == nil && options.UUID[:5] != conn.cluster.ClusterID {
+                       c.ManifestText = rewriteManifest(c.ManifestText, options.UUID[:5])
+               }
+               return c, err
+       } else {
+               // UUID is a PDH
+               first := make(chan arvados.Collection, 1)
+               err := conn.tryLocalThenRemotes(ctx, func(ctx context.Context, remoteID string, be backend) error {
+                       c, err := be.CollectionGet(ctx, options)
+                       if err != nil {
+                               return err
+                       }
+                       // options.UUID is either hash+size or
+                       // hash+size+hints; only hash+size need to
+                       // match the computed PDH.
+                       if pdh := portableDataHash(c.ManifestText); pdh != options.UUID && !strings.HasPrefix(options.UUID, pdh+"+") {
+                               ctxlog.FromContext(ctx).Warnf("bad portable data hash %q received from remote %q (expected %q)", pdh, remoteID, options.UUID)
+                               return notFoundError{}
+                       }
+                       if remoteID != "" {
+                               c.ManifestText = rewriteManifest(c.ManifestText, remoteID)
+                       }
+                       select {
+                       case first <- c:
+                               return nil
+                       default:
+                               // lost race, return value doesn't matter
+                               return nil
+                       }
+               })
+               if err != nil {
+                       return arvados.Collection{}, err
+               }
+               return <-first, nil
+       }
+}
+
+func (conn *Conn) CollectionList(ctx context.Context, options arvados.ListOptions) (arvados.CollectionList, error) {
+       return conn.local.CollectionList(ctx, options)
+}
+
+func (conn *Conn) CollectionProvenance(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) {
+       return conn.chooseBackend(options.UUID).CollectionProvenance(ctx, options)
+}
+
+func (conn *Conn) CollectionUsedBy(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) {
+       return conn.chooseBackend(options.UUID).CollectionUsedBy(ctx, options)
+}
+
+func (conn *Conn) CollectionDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Collection, error) {
+       return conn.chooseBackend(options.UUID).CollectionDelete(ctx, options)
+}
+
+func (conn *Conn) CollectionTrash(ctx context.Context, options arvados.DeleteOptions) (arvados.Collection, error) {
+       return conn.chooseBackend(options.UUID).CollectionTrash(ctx, options)
+}
+
+func (conn *Conn) CollectionUntrash(ctx context.Context, options arvados.UntrashOptions) (arvados.Collection, error) {
+       return conn.chooseBackend(options.UUID).CollectionUntrash(ctx, options)
+}
+
+func (conn *Conn) ContainerCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Container, error) {
+       return conn.chooseBackend(options.ClusterID).ContainerCreate(ctx, options)
+}
+
+func (conn *Conn) ContainerUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Container, error) {
+       return conn.chooseBackend(options.UUID).ContainerUpdate(ctx, options)
+}
+
+func (conn *Conn) ContainerGet(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
+       return conn.chooseBackend(options.UUID).ContainerGet(ctx, options)
+}
+
+func (conn *Conn) ContainerList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerList, error) {
+       return conn.local.ContainerList(ctx, options)
+}
+
+func (conn *Conn) ContainerDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Container, error) {
+       return conn.chooseBackend(options.UUID).ContainerDelete(ctx, options)
+}
+
+func (conn *Conn) ContainerLock(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
+       return conn.chooseBackend(options.UUID).ContainerLock(ctx, options)
+}
+
+func (conn *Conn) ContainerUnlock(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
+       return conn.chooseBackend(options.UUID).ContainerUnlock(ctx, options)
+}
+
+func (conn *Conn) SpecimenCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Specimen, error) {
+       return conn.chooseBackend(options.ClusterID).SpecimenCreate(ctx, options)
+}
+
+func (conn *Conn) SpecimenUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Specimen, error) {
+       return conn.chooseBackend(options.UUID).SpecimenUpdate(ctx, options)
+}
+
+func (conn *Conn) SpecimenGet(ctx context.Context, options arvados.GetOptions) (arvados.Specimen, error) {
+       return conn.chooseBackend(options.UUID).SpecimenGet(ctx, options)
+}
+
+func (conn *Conn) SpecimenList(ctx context.Context, options arvados.ListOptions) (arvados.SpecimenList, error) {
+       return conn.local.SpecimenList(ctx, options)
+}
+
+func (conn *Conn) SpecimenDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Specimen, error) {
+       return conn.chooseBackend(options.UUID).SpecimenDelete(ctx, options)
+}
+
+func (conn *Conn) APIClientAuthorizationCurrent(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) {
+       return conn.chooseBackend(options.UUID).APIClientAuthorizationCurrent(ctx, options)
+}
+
+type backend interface{ arvados.API }
+
+type notFoundError struct{}
+
+func (notFoundError) HTTPStatus() int { return http.StatusNotFound }
+func (notFoundError) Error() string   { return "not found" }
+
+func errStatus(err error) int {
+       if httpErr, ok := err.(interface{ HTTPStatus() int }); ok {
+               return httpErr.HTTPStatus()
+       } else {
+               return http.StatusInternalServerError
+       }
+}
index 7d8e7a4334ae98bd727ed62725d1acea09746ae9..169b1f79614bd9b0391542ae71771dac87e0d95d 100644 (file)
@@ -6,6 +6,7 @@ package controller
 
 import (
        "bytes"
+       "context"
        "encoding/json"
        "fmt"
        "io"
@@ -39,7 +40,8 @@ type FederationSuite struct {
        // provided by the integration test environment.
        remoteServer *httpserver.Server
        // remoteMock ("zmock") appends each incoming request to
-       // remoteMockRequests, and returns an empty 200 response.
+       // remoteMockRequests, and returns 200 with an empty JSON
+       // object.
        remoteMock         *httpserver.Server
        remoteMockRequests []http.Request
 }
@@ -55,8 +57,9 @@ func (s *FederationSuite) SetUpTest(c *check.C) {
        c.Assert(s.remoteMock.Start(), check.IsNil)
 
        cluster := &arvados.Cluster{
-               ClusterID:  "zhome",
-               PostgreSQL: integrationTestCluster().PostgreSQL,
+               ClusterID:                 "zhome",
+               PostgreSQL:                integrationTestCluster().PostgreSQL,
+               EnableBetaController14287: enableBetaController14287,
        }
        cluster.TLS.Insecure = true
        cluster.API.MaxItemsPerResponse = 1000
@@ -65,7 +68,9 @@ func (s *FederationSuite) SetUpTest(c *check.C) {
        arvadostest.SetServiceURL(&cluster.Services.Controller, "http://localhost:/")
        s.testHandler = &Handler{Cluster: cluster}
        s.testServer = newServerFromIntegrationTestEnv(c)
-       s.testServer.Server.Handler = httpserver.AddRequestIDs(httpserver.LogRequests(s.log, s.testHandler))
+       s.testServer.Server.Handler = httpserver.HandlerWithContext(
+               ctxlog.Context(context.Background(), s.log),
+               httpserver.AddRequestIDs(httpserver.LogRequests(s.testHandler)))
 
        cluster.RemoteClusters = map[string]arvados.RemoteCluster{
                "zzzzz": {
@@ -91,6 +96,8 @@ func (s *FederationSuite) remoteMockHandler(w http.ResponseWriter, req *http.Req
        req.Body.Close()
        req.Body = ioutil.NopCloser(b)
        s.remoteMockRequests = append(s.remoteMockRequests, *req)
+       // Repond 200 with a valid JSON object
+       fmt.Fprint(w, "{}")
 }
 
 func (s *FederationSuite) TearDownTest(c *check.C) {
@@ -102,15 +109,15 @@ func (s *FederationSuite) TearDownTest(c *check.C) {
        }
 }
 
-func (s *FederationSuite) testRequest(req *http.Request) *http.Response {
+func (s *FederationSuite) testRequest(req *http.Request) *httptest.ResponseRecorder {
        resp := httptest.NewRecorder()
        s.testServer.Server.Handler.ServeHTTP(resp, req)
-       return resp.Result()
+       return resp
 }
 
 func (s *FederationSuite) TestLocalRequest(c *check.C) {
        req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+strings.Replace(arvadostest.WorkflowWithDefinitionYAMLUUID, "zzzzz-", "zhome-", 1), nil)
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        s.checkHandledLocally(c, resp)
 }
 
@@ -125,31 +132,31 @@ func (s *FederationSuite) checkHandledLocally(c *check.C, resp *http.Response) {
 
 func (s *FederationSuite) TestNoAuth(c *check.C) {
        req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, nil)
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusUnauthorized)
-       s.checkJSONErrorMatches(c, resp, `Not logged in`)
+       s.checkJSONErrorMatches(c, resp, `Not logged in.*`)
 }
 
 func (s *FederationSuite) TestBadAuth(c *check.C) {
        req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, nil)
        req.Header.Set("Authorization", "Bearer aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusUnauthorized)
-       s.checkJSONErrorMatches(c, resp, `Not logged in`)
+       s.checkJSONErrorMatches(c, resp, `Not logged in.*`)
 }
 
 func (s *FederationSuite) TestNoAccess(c *check.C) {
        req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, nil)
        req.Header.Set("Authorization", "Bearer "+arvadostest.SpectatorToken)
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusNotFound)
-       s.checkJSONErrorMatches(c, resp, `.*not found`)
+       s.checkJSONErrorMatches(c, resp, `.*not found.*`)
 }
 
 func (s *FederationSuite) TestGetUnknownRemote(c *check.C) {
        req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+strings.Replace(arvadostest.WorkflowWithDefinitionYAMLUUID, "zzzzz-", "zz404-", 1), nil)
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusNotFound)
        s.checkJSONErrorMatches(c, resp, `.*no proxy available for cluster zz404`)
 }
@@ -161,7 +168,7 @@ func (s *FederationSuite) TestRemoteError(c *check.C) {
 
        req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, nil)
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusBadGateway)
        s.checkJSONErrorMatches(c, resp, `.*HTTP response to HTTPS client`)
 }
@@ -169,7 +176,7 @@ func (s *FederationSuite) TestRemoteError(c *check.C) {
 func (s *FederationSuite) TestGetRemoteWorkflow(c *check.C) {
        req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, nil)
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusOK)
        var wf arvados.Workflow
        c.Check(json.NewDecoder(resp.Body).Decode(&wf), check.IsNil)
@@ -180,7 +187,7 @@ func (s *FederationSuite) TestGetRemoteWorkflow(c *check.C) {
 func (s *FederationSuite) TestOptionsMethod(c *check.C) {
        req := httptest.NewRequest("OPTIONS", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, nil)
        req.Header.Set("Origin", "https://example.com")
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusOK)
        body, err := ioutil.ReadAll(resp.Body)
        c.Check(err, check.IsNil)
@@ -196,7 +203,7 @@ func (s *FederationSuite) TestOptionsMethod(c *check.C) {
 
 func (s *FederationSuite) TestRemoteWithTokenInQuery(c *check.C) {
        req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+strings.Replace(arvadostest.WorkflowWithDefinitionYAMLUUID, "zzzzz-", "zmock-", 1)+"?api_token="+arvadostest.ActiveToken, nil)
-       s.testRequest(req)
+       s.testRequest(req).Result()
        c.Assert(s.remoteMockRequests, check.HasLen, 1)
        pr := s.remoteMockRequests[0]
        // Token is salted and moved from query to Authorization header.
@@ -205,39 +212,58 @@ func (s *FederationSuite) TestRemoteWithTokenInQuery(c *check.C) {
 }
 
 func (s *FederationSuite) TestLocalTokenSalted(c *check.C) {
-       req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+strings.Replace(arvadostest.WorkflowWithDefinitionYAMLUUID, "zzzzz-", "zmock-", 1), nil)
-       req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-       s.testRequest(req)
-       c.Assert(s.remoteMockRequests, check.HasLen, 1)
-       pr := s.remoteMockRequests[0]
-       // The salted token here has a "zzzzz-" UUID instead of a
-       // "ztest-" UUID because ztest's local database has the
-       // "zzzzz-" test fixtures. The "secret" part is HMAC(sha1,
-       // arvadostest.ActiveToken, "zmock") = "7fd3...".
-       c.Check(pr.Header.Get("Authorization"), check.Equals, "Bearer v2/zzzzz-gj3su-077z32aux8dg2s1/7fd31b61f39c0e82a4155592163218272cedacdc")
+       defer s.localServiceReturns404(c).Close()
+       for _, path := range []string{
+               // During the transition to the strongly typed
+               // controller implementation (#14287), workflows and
+               // collections test different code paths.
+               "/arvados/v1/workflows/" + strings.Replace(arvadostest.WorkflowWithDefinitionYAMLUUID, "zzzzz-", "zmock-", 1),
+               "/arvados/v1/collections/" + strings.Replace(arvadostest.UserAgreementCollection, "zzzzz-", "zmock-", 1),
+       } {
+               c.Log("testing path ", path)
+               s.remoteMockRequests = nil
+               req := httptest.NewRequest("GET", path, nil)
+               req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
+               s.testRequest(req).Result()
+               c.Assert(s.remoteMockRequests, check.HasLen, 1)
+               pr := s.remoteMockRequests[0]
+               // The salted token here has a "zzzzz-" UUID instead of a
+               // "ztest-" UUID because ztest's local database has the
+               // "zzzzz-" test fixtures. The "secret" part is HMAC(sha1,
+               // arvadostest.ActiveToken, "zmock") = "7fd3...".
+               c.Check(pr.Header.Get("Authorization"), check.Equals, "Bearer v2/zzzzz-gj3su-077z32aux8dg2s1/7fd31b61f39c0e82a4155592163218272cedacdc")
+       }
 }
 
 func (s *FederationSuite) TestRemoteTokenNotSalted(c *check.C) {
+       defer s.localServiceReturns404(c).Close()
        // remoteToken can be any v1 token that doesn't appear in
        // ztest's local db.
        remoteToken := "abcdef00000000000000000000000000000000000000000000"
-       req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+strings.Replace(arvadostest.WorkflowWithDefinitionYAMLUUID, "zzzzz-", "zmock-", 1), nil)
-       req.Header.Set("Authorization", "Bearer "+remoteToken)
-       s.testRequest(req)
-       c.Assert(s.remoteMockRequests, check.HasLen, 1)
-       pr := s.remoteMockRequests[0]
-       c.Check(pr.Header.Get("Authorization"), check.Equals, "Bearer "+remoteToken)
+
+       for _, path := range []string{
+               // During the transition to the strongly typed
+               // controller implementation (#14287), workflows and
+               // collections test different code paths.
+               "/arvados/v1/workflows/" + strings.Replace(arvadostest.WorkflowWithDefinitionYAMLUUID, "zzzzz-", "zmock-", 1),
+               "/arvados/v1/collections/" + strings.Replace(arvadostest.UserAgreementCollection, "zzzzz-", "zmock-", 1),
+       } {
+               c.Log("testing path ", path)
+               s.remoteMockRequests = nil
+               req := httptest.NewRequest("GET", path, nil)
+               req.Header.Set("Authorization", "Bearer "+remoteToken)
+               s.testRequest(req).Result()
+               c.Assert(s.remoteMockRequests, check.HasLen, 1)
+               pr := s.remoteMockRequests[0]
+               c.Check(pr.Header.Get("Authorization"), check.Equals, "Bearer "+remoteToken)
+       }
 }
 
 func (s *FederationSuite) TestWorkflowCRUD(c *check.C) {
-       wf := arvados.Workflow{
-               Description: "TestCRUD",
-       }
+       var wf arvados.Workflow
        {
-               body := &strings.Builder{}
-               json.NewEncoder(body).Encode(&wf)
                req := httptest.NewRequest("POST", "/arvados/v1/workflows", strings.NewReader(url.Values{
-                       "workflow": {body.String()},
+                       "workflow": {`{"description": "TestCRUD"}`},
                }.Encode()))
                req.Header.Set("Content-type", "application/x-www-form-urlencoded")
                req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
@@ -268,7 +294,7 @@ func (s *FederationSuite) TestWorkflowCRUD(c *check.C) {
                req := httptest.NewRequest(method, "/arvados/v1/workflows/"+wf.UUID, strings.NewReader(form.Encode()))
                req.Header.Set("Content-type", "application/x-www-form-urlencoded")
                req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-               resp := s.testRequest(req)
+               resp := s.testRequest(req).Result()
                s.checkResponseOK(c, resp)
                err := json.NewDecoder(resp.Body).Decode(&wf)
                c.Check(err, check.IsNil)
@@ -278,7 +304,7 @@ func (s *FederationSuite) TestWorkflowCRUD(c *check.C) {
        {
                req := httptest.NewRequest("DELETE", "/arvados/v1/workflows/"+wf.UUID, nil)
                req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-               resp := s.testRequest(req)
+               resp := s.testRequest(req).Result()
                s.checkResponseOK(c, resp)
                err := json.NewDecoder(resp.Body).Decode(&wf)
                c.Check(err, check.IsNil)
@@ -286,7 +312,7 @@ func (s *FederationSuite) TestWorkflowCRUD(c *check.C) {
        {
                req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+wf.UUID, nil)
                req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-               resp := s.testRequest(req)
+               resp := s.testRequest(req).Result()
                c.Check(resp.StatusCode, check.Equals, http.StatusNotFound)
        }
 }
@@ -320,7 +346,15 @@ func (s *FederationSuite) localServiceHandler(c *check.C, h http.Handler) *https
 
 func (s *FederationSuite) localServiceReturns404(c *check.C) *httpserver.Server {
        return s.localServiceHandler(c, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
-               w.WriteHeader(404)
+               if req.URL.Path == "/arvados/v1/api_client_authorizations/current" {
+                       if req.Header.Get("Authorization") == "Bearer "+arvadostest.ActiveToken {
+                               json.NewEncoder(w).Encode(arvados.APIClientAuthorization{UUID: arvadostest.ActiveTokenUUID, APIToken: arvadostest.ActiveToken})
+                       } else {
+                               w.WriteHeader(http.StatusUnauthorized)
+                       }
+               } else {
+                       w.WriteHeader(404)
+               }
        }))
 }
 
@@ -332,7 +366,7 @@ func (s *FederationSuite) TestGetLocalCollection(c *check.C) {
 
        req := httptest.NewRequest("GET", "/arvados/v1/collections/"+arvadostest.UserAgreementCollection, nil)
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
 
        c.Check(resp.StatusCode, check.Equals, http.StatusOK)
        var col arvados.Collection
@@ -349,7 +383,7 @@ func (s *FederationSuite) TestGetLocalCollection(c *check.C) {
        }).Encode()))
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
        req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
-       resp = s.testRequest(req)
+       resp = s.testRequest(req).Result()
 
        c.Check(resp.StatusCode, check.Equals, http.StatusOK)
        col = arvados.Collection{}
@@ -365,7 +399,7 @@ func (s *FederationSuite) TestGetRemoteCollection(c *check.C) {
 
        req := httptest.NewRequest("GET", "/arvados/v1/collections/"+arvadostest.UserAgreementCollection, nil)
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusOK)
        var col arvados.Collection
        c.Check(json.NewDecoder(resp.Body).Decode(&col), check.IsNil)
@@ -380,7 +414,7 @@ func (s *FederationSuite) TestGetRemoteCollectionError(c *check.C) {
 
        req := httptest.NewRequest("GET", "/arvados/v1/collections/zzzzz-4zz18-fakefakefakefak", nil)
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusNotFound)
 }
 
@@ -402,7 +436,7 @@ func (s *FederationSuite) TestGetLocalCollectionByPDH(c *check.C) {
 
        req := httptest.NewRequest("GET", "/arvados/v1/collections/"+arvadostest.UserAgreementPDH, nil)
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
 
        c.Check(resp.StatusCode, check.Equals, http.StatusOK)
        var col arvados.Collection
@@ -418,7 +452,7 @@ func (s *FederationSuite) TestGetRemoteCollectionByPDH(c *check.C) {
 
        req := httptest.NewRequest("GET", "/arvados/v1/collections/"+arvadostest.UserAgreementPDH, nil)
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
 
        c.Check(resp.StatusCode, check.Equals, http.StatusOK)
 
@@ -436,7 +470,7 @@ func (s *FederationSuite) TestGetCollectionByPDHError(c *check.C) {
        req := httptest.NewRequest("GET", "/arvados/v1/collections/99999999999999999999999999999999+99", nil)
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
 
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        defer resp.Body.Close()
 
        c.Check(resp.StatusCode, check.Equals, http.StatusNotFound)
@@ -475,7 +509,7 @@ func (s *FederationSuite) TestGetCollectionByPDHErrorBadHash(c *check.C) {
        req := httptest.NewRequest("GET", "/arvados/v1/collections/99999999999999999999999999999999+99", nil)
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
 
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        defer resp.Body.Close()
 
        c.Check(resp.StatusCode, check.Equals, http.StatusNotFound)
@@ -486,7 +520,7 @@ func (s *FederationSuite) TestSaltedTokenGetCollectionByPDH(c *check.C) {
 
        req := httptest.NewRequest("GET", "/arvados/v1/collections/"+arvadostest.UserAgreementPDH, nil)
        req.Header.Set("Authorization", "Bearer v2/zzzzz-gj3su-077z32aux8dg2s1/282d7d172b6cfdce364c5ed12ddf7417b2d00065")
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
 
        c.Check(resp.StatusCode, check.Equals, http.StatusOK)
        var col arvados.Collection
@@ -502,7 +536,7 @@ func (s *FederationSuite) TestSaltedTokenGetCollectionByPDHError(c *check.C) {
 
        req := httptest.NewRequest("GET", "/arvados/v1/collections/99999999999999999999999999999999+99", nil)
        req.Header.Set("Authorization", "Bearer v2/zzzzz-gj3su-077z32aux8dg2s1/282d7d172b6cfdce364c5ed12ddf7417b2d00065")
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
 
        c.Check(resp.StatusCode, check.Equals, http.StatusNotFound)
 }
@@ -511,7 +545,7 @@ func (s *FederationSuite) TestGetRemoteContainerRequest(c *check.C) {
        defer s.localServiceReturns404(c).Close()
        req := httptest.NewRequest("GET", "/arvados/v1/container_requests/"+arvadostest.QueuedContainerRequestUUID, nil)
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusOK)
        var cr arvados.ContainerRequest
        c.Check(json.NewDecoder(resp.Body).Decode(&cr), check.IsNil)
@@ -526,7 +560,7 @@ func (s *FederationSuite) TestUpdateRemoteContainerRequest(c *check.C) {
                        strings.NewReader(fmt.Sprintf(`{"container_request": {"priority": %d}}`, pri)))
                req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
                req.Header.Set("Content-type", "application/json")
-               resp := s.testRequest(req)
+               resp := s.testRequest(req).Result()
                c.Check(resp.StatusCode, check.Equals, http.StatusOK)
                var cr arvados.ContainerRequest
                c.Check(json.NewDecoder(resp.Body).Decode(&cr), check.IsNil)
@@ -554,7 +588,7 @@ func (s *FederationSuite) TestCreateRemoteContainerRequest(c *check.C) {
 `))
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
        req.Header.Set("Content-type", "application/json")
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusOK)
        var cr arvados.ContainerRequest
        c.Check(json.NewDecoder(resp.Body).Decode(&cr), check.IsNil)
@@ -586,7 +620,7 @@ func (s *FederationSuite) TestCreateRemoteContainerRequestCheckRuntimeToken(c *c
        arvadostest.SetServiceURL(&s.testHandler.Cluster.Services.RailsAPI, "https://"+os.Getenv("ARVADOS_TEST_API_HOST"))
        s.testHandler.Cluster.ClusterID = "zzzzz"
 
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusOK)
        var cr struct {
                arvados.ContainerRequest `json:"container_request"`
@@ -617,7 +651,7 @@ func (s *FederationSuite) TestCreateRemoteContainerRequestCheckSetRuntimeToken(c
 `))
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
        req.Header.Set("Content-type", "application/json")
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusOK)
        var cr struct {
                arvados.ContainerRequest `json:"container_request"`
@@ -646,7 +680,7 @@ func (s *FederationSuite) TestCreateRemoteContainerRequestRuntimeTokenFromAuth(c
 `))
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveTokenV2+"/zzzzz-dz642-parentcontainer")
        req.Header.Set("Content-type", "application/json")
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusOK)
        var cr struct {
                arvados.ContainerRequest `json:"container_request"`
@@ -672,7 +706,7 @@ func (s *FederationSuite) TestCreateRemoteContainerRequestError(c *check.C) {
 `))
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
        req.Header.Set("Content-type", "application/json")
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusNotFound)
 }
 
@@ -681,7 +715,7 @@ func (s *FederationSuite) TestGetRemoteContainer(c *check.C) {
        req := httptest.NewRequest("GET", "/arvados/v1/containers/"+arvadostest.QueuedContainerUUID, nil)
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
        resp := s.testRequest(req)
-       c.Check(resp.StatusCode, check.Equals, http.StatusOK)
+       c.Check(resp.Code, check.Equals, http.StatusOK)
        var cn arvados.Container
        c.Check(json.NewDecoder(resp.Body).Decode(&cn), check.IsNil)
        c.Check(cn.UUID, check.Equals, arvadostest.QueuedContainerUUID)
@@ -692,10 +726,11 @@ func (s *FederationSuite) TestListRemoteContainer(c *check.C) {
        req := httptest.NewRequest("GET", "/arvados/v1/containers?count=none&filters="+
                url.QueryEscape(fmt.Sprintf(`[["uuid", "in", ["%v"]]]`, arvadostest.QueuedContainerUUID)), nil)
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusOK)
        var cn arvados.ContainerList
        c.Check(json.NewDecoder(resp.Body).Decode(&cn), check.IsNil)
+       c.Assert(cn.Items, check.HasLen, 1)
        c.Check(cn.Items[0].UUID, check.Equals, arvadostest.QueuedContainerUUID)
 }
 
@@ -712,7 +747,7 @@ func (s *FederationSuite) TestListMultiRemoteContainers(c *check.C) {
                url.QueryEscape(`["uuid", "command"]`)),
                nil)
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusOK)
        var cn arvados.ContainerList
        c.Check(json.NewDecoder(resp.Body).Decode(&cn), check.IsNil)
@@ -735,7 +770,7 @@ func (s *FederationSuite) TestListMultiRemoteContainerError(c *check.C) {
                url.QueryEscape(`["uuid", "command"]`)),
                nil)
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusBadGateway)
        s.checkJSONErrorMatches(c, resp, `error fetching from zhome \(404 Not Found\): EOF`)
 }
@@ -761,7 +796,7 @@ func (s *FederationSuite) TestListMultiRemoteContainersPaged(c *check.C) {
                        arvadostest.QueuedContainerUUID))),
                nil)
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusOK)
        c.Check(callCount, check.Equals, 2)
        var cn arvados.ContainerList
@@ -797,7 +832,7 @@ func (s *FederationSuite) TestListMultiRemoteContainersMissing(c *check.C) {
                        arvadostest.QueuedContainerUUID))),
                nil)
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusOK)
        c.Check(callCount, check.Equals, 2)
        var cn arvados.ContainerList
@@ -818,7 +853,7 @@ func (s *FederationSuite) TestListMultiRemoteContainerPageSizeError(c *check.C)
                        arvadostest.QueuedContainerUUID))),
                nil)
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusBadRequest)
        s.checkJSONErrorMatches(c, resp, `Federated multi-object request for 2 objects which is more than max page size 1.`)
 }
@@ -829,7 +864,7 @@ func (s *FederationSuite) TestListMultiRemoteContainerLimitError(c *check.C) {
                        arvadostest.QueuedContainerUUID))),
                nil)
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusBadRequest)
        s.checkJSONErrorMatches(c, resp, `Federated multi-object may not provide 'limit', 'offset' or 'order'.`)
 }
@@ -840,7 +875,7 @@ func (s *FederationSuite) TestListMultiRemoteContainerOffsetError(c *check.C) {
                        arvadostest.QueuedContainerUUID))),
                nil)
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusBadRequest)
        s.checkJSONErrorMatches(c, resp, `Federated multi-object may not provide 'limit', 'offset' or 'order'.`)
 }
@@ -851,7 +886,7 @@ func (s *FederationSuite) TestListMultiRemoteContainerOrderError(c *check.C) {
                        arvadostest.QueuedContainerUUID))),
                nil)
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusBadRequest)
        s.checkJSONErrorMatches(c, resp, `Federated multi-object may not provide 'limit', 'offset' or 'order'.`)
 }
@@ -863,7 +898,7 @@ func (s *FederationSuite) TestListMultiRemoteContainerSelectError(c *check.C) {
                url.QueryEscape(`["command"]`)),
                nil)
        req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-       resp := s.testRequest(req)
+       resp := s.testRequest(req).Result()
        c.Check(resp.StatusCode, check.Equals, http.StatusBadRequest)
        s.checkJSONErrorMatches(c, resp, `Federated multi-object request must include 'uuid' in 'select'`)
 }
index 12faacdd4398211f8466a4ed7e971283190b9871..d524195e4429a2358ea560a784acf469350f7751 100644 (file)
@@ -18,6 +18,8 @@ import (
        "time"
 
        "git.curoverse.com/arvados.git/lib/config"
+       "git.curoverse.com/arvados.git/lib/controller/railsproxy"
+       "git.curoverse.com/arvados.git/lib/controller/router"
        "git.curoverse.com/arvados.git/sdk/go/arvados"
        "git.curoverse.com/arvados.git/sdk/go/health"
        "git.curoverse.com/arvados.git/sdk/go/httpserver"
@@ -63,7 +65,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 
 func (h *Handler) CheckHealth() error {
        h.setupOnce.Do(h.setup)
-       _, _, err := findRailsAPI(h.Cluster)
+       _, _, err := railsproxy.FindRailsAPI(h.Cluster)
        return err
 }
 
@@ -88,6 +90,12 @@ func (h *Handler) setup() {
                io.Copy(w, &buf)
        }))
 
+       if h.Cluster.EnableBetaController14287 {
+               rtr := router.New(h.Cluster)
+               mux.Handle("/arvados/v1/collections", rtr)
+               mux.Handle("/arvados/v1/collections/", rtr)
+       }
+
        hs := http.NotFoundHandler()
        hs = prepend(hs, h.proxyRailsAPI)
        hs = h.setupProxyRemoteCluster(hs)
@@ -141,7 +149,7 @@ func prepend(next http.Handler, middleware middlewareFunc) http.Handler {
 }
 
 func (h *Handler) localClusterRequest(req *http.Request) (*http.Response, error) {
-       urlOut, insecure, err := findRailsAPI(h.Cluster)
+       urlOut, insecure, err := railsproxy.FindRailsAPI(h.Cluster)
        if err != nil {
                return nil, err
        }
index 9b0ff2764be620bd847dc03c2da2f0848b008f07..fbfb037d36dad0d4716c3422c452aec0a118049b 100644 (file)
@@ -22,9 +22,13 @@ import (
        check "gopkg.in/check.v1"
 )
 
+var enableBetaController14287 bool
+
 // Gocheck boilerplate
 func Test(t *testing.T) {
-       check.TestingT(t)
+       for _, enableBetaController14287 = range []bool{false, true} {
+               check.TestingT(t)
+       }
 }
 
 var _ = check.Suite(&HandlerSuite{})
@@ -42,6 +46,8 @@ func (s *HandlerSuite) SetUpTest(c *check.C) {
        s.cluster = &arvados.Cluster{
                ClusterID:  "zzzzz",
                PostgreSQL: integrationTestCluster().PostgreSQL,
+
+               EnableBetaController14287: enableBetaController14287,
        }
        s.cluster.TLS.Insecure = true
        arvadostest.SetServiceURL(&s.cluster.Services.RailsAPI, "https://"+os.Getenv("ARVADOS_TEST_API_HOST"))
index c0b94c2b5f76d604e738c2d9bc43d3a01f8bf5dc..9eac9362c94f9e387b657e0a8cf57754d33e2143 100644 (file)
@@ -25,20 +25,23 @@ func (h HTTPError) Error() string {
        return h.Message
 }
 
-// headers that shouldn't be forwarded when proxying. See
-// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
 var dropHeaders = map[string]bool{
+       // Headers that shouldn't be forwarded when proxying. See
+       // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
        "Connection":          true,
        "Keep-Alive":          true,
        "Proxy-Authenticate":  true,
        "Proxy-Authorization": true,
-       // this line makes gofmt 1.10 and 1.11 agree
-       "TE":                true,
-       "Trailer":           true,
-       "Transfer-Encoding": true, // *-Encoding headers interfer with Go's automatic compression/decompression
-       "Content-Encoding":  true,
+       // (comment/space here makes gofmt1.10 agree with gofmt1.11)
+       "TE":      true,
+       "Trailer": true,
+       "Upgrade": true,
+
+       // Headers that would interfere with Go's automatic
+       // compression/decompression if we forwarded them.
        "Accept-Encoding":   true,
-       "Upgrade":           true,
+       "Content-Encoding":  true,
+       "Transfer-Encoding": true,
 }
 
 type ResponseFilter func(*http.Response, error) (*http.Response, error)
diff --git a/lib/controller/railsproxy/railsproxy.go b/lib/controller/railsproxy/railsproxy.go
new file mode 100644 (file)
index 0000000..576e603
--- /dev/null
@@ -0,0 +1,52 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+// Package railsproxy implements Arvados APIs by proxying to the
+// RailsAPI server on the local machine.
+package railsproxy
+
+import (
+       "context"
+       "errors"
+       "fmt"
+       "net/url"
+       "strings"
+
+       "git.curoverse.com/arvados.git/lib/controller/rpc"
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "git.curoverse.com/arvados.git/sdk/go/auth"
+)
+
+// For now, FindRailsAPI always uses the rails API running on this
+// node.
+func FindRailsAPI(cluster *arvados.Cluster) (*url.URL, bool, error) {
+       var best *url.URL
+       for target := range cluster.Services.RailsAPI.InternalURLs {
+               target := url.URL(target)
+               best = &target
+               if strings.HasPrefix(target.Host, "localhost:") || strings.HasPrefix(target.Host, "127.0.0.1:") || strings.HasPrefix(target.Host, "[::1]:") {
+                       break
+               }
+       }
+       if best == nil {
+               return nil, false, fmt.Errorf("Services.RailsAPI.InternalURLs is empty")
+       }
+       return best, cluster.TLS.Insecure, nil
+}
+
+func NewConn(cluster *arvados.Cluster) *rpc.Conn {
+       url, insecure, err := FindRailsAPI(cluster)
+       if err != nil {
+               panic(err)
+       }
+       return rpc.NewConn(cluster.ClusterID, url, insecure, provideIncomingToken)
+}
+
+func provideIncomingToken(ctx context.Context) ([]string, error) {
+       incoming, ok := auth.FromContext(ctx)
+       if !ok {
+               return nil, errors.New("no token provided")
+       }
+       return incoming.Tokens, nil
+}
diff --git a/lib/controller/router/checker_test.go b/lib/controller/router/checker_test.go
new file mode 100644 (file)
index 0000000..93d51fa
--- /dev/null
@@ -0,0 +1,32 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package router
+
+import (
+       "reflect"
+       "runtime"
+
+       check "gopkg.in/check.v1"
+)
+
+// a Gocheck checker for testing the name of a function. Used with
+// (*arvadostest.APIStub).Calls() to check that an HTTP request has
+// been routed to the correct arvados.API method.
+//
+//     c.Check(bytes.NewBuffer().Read, isMethodNamed, "Read")
+var isMethodNamed check.Checker = &chkIsMethodNamed{
+       CheckerInfo: &check.CheckerInfo{
+               Name:   "isMethodNamed",
+               Params: []string{"obtained", "expected"},
+       },
+}
+
+type chkIsMethodNamed struct{ *check.CheckerInfo }
+
+func (*chkIsMethodNamed) Check(params []interface{}, names []string) (bool, string) {
+       methodName := runtime.FuncForPC(reflect.ValueOf(params[0]).Pointer()).Name()
+       regex := `.*\)\.` + params[1].(string) + `(-.*)?`
+       return check.Matches.Check([]interface{}{methodName, regex}, names)
+}
diff --git a/lib/controller/router/error.go b/lib/controller/router/error.go
new file mode 100644 (file)
index 0000000..6db5f31
--- /dev/null
@@ -0,0 +1,18 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package router
+
+type errorWithStatus struct {
+       code int
+       error
+}
+
+func (err errorWithStatus) HTTPStatus() int {
+       return err.code
+}
+
+func httpError(code int, err error) error {
+       return errorWithStatus{code: code, error: err}
+}
diff --git a/lib/controller/router/request.go b/lib/controller/router/request.go
new file mode 100644 (file)
index 0000000..377f724
--- /dev/null
@@ -0,0 +1,173 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package router
+
+import (
+       "encoding/json"
+       "fmt"
+       "io"
+       "mime"
+       "net/http"
+       "strconv"
+       "strings"
+
+       "github.com/julienschmidt/httprouter"
+)
+
+// Parse req as an Arvados V1 API request and return the request
+// parameters.
+//
+// If the request has a parameter whose name is attrsKey (e.g.,
+// "collection"), it is renamed to "attrs".
+func (rtr *router) loadRequestParams(req *http.Request, attrsKey string) (map[string]interface{}, error) {
+       err := req.ParseForm()
+       if err != nil {
+               return nil, httpError(http.StatusBadRequest, err)
+       }
+       params := map[string]interface{}{}
+
+       // Load parameters from req.Form, which (after
+       // req.ParseForm()) includes the query string and -- when
+       // Content-Type is application/x-www-form-urlencoded -- the
+       // request body.
+       for k, values := range req.Form {
+               // All of these form values arrive as strings, so we
+               // need some type-guessing to accept non-string
+               // inputs:
+               //
+               // Values for parameters that take ints (limit=1) or
+               // bools (include_trash=1) are parsed accordingly.
+               //
+               // "null" and "" are nil.
+               //
+               // Values that look like JSON objects, arrays, or
+               // strings are parsed as JSON.
+               //
+               // The rest are left as strings.
+               for _, v := range values {
+                       switch {
+                       case intParams[k]:
+                               params[k], err = strconv.ParseInt(v, 10, 64)
+                               if err != nil {
+                                       return nil, err
+                               }
+                       case boolParams[k]:
+                               params[k] = stringToBool(v)
+                       case v == "null" || v == "":
+                               params[k] = nil
+                       case strings.HasPrefix(v, "["):
+                               var j []interface{}
+                               err := json.Unmarshal([]byte(v), &j)
+                               if err != nil {
+                                       return nil, err
+                               }
+                               params[k] = j
+                       case strings.HasPrefix(v, "{"):
+                               var j map[string]interface{}
+                               err := json.Unmarshal([]byte(v), &j)
+                               if err != nil {
+                                       return nil, err
+                               }
+                               params[k] = j
+                       case strings.HasPrefix(v, "\""):
+                               var j string
+                               err := json.Unmarshal([]byte(v), &j)
+                               if err != nil {
+                                       return nil, err
+                               }
+                               params[k] = j
+                       default:
+                               params[k] = v
+                       }
+                       // TODO: Need to accept "?foo[]=bar&foo[]=baz"
+                       // as foo=["bar","baz"]?
+               }
+       }
+
+       // Decode body as JSON if Content-Type request header is
+       // missing or application/json.
+       mt := req.Header.Get("Content-Type")
+       if ct, _, err := mime.ParseMediaType(mt); err != nil && mt != "" {
+               return nil, fmt.Errorf("error parsing media type %q: %s", mt, err)
+       } else if (ct == "application/json" || mt == "") && req.ContentLength != 0 {
+               jsonParams := map[string]interface{}{}
+               err := json.NewDecoder(req.Body).Decode(&jsonParams)
+               if err != nil {
+                       return nil, httpError(http.StatusBadRequest, err)
+               }
+               for k, v := range jsonParams {
+                       params[k] = v
+               }
+               if attrsKey != "" && params[attrsKey] == nil {
+                       // Copy top-level parameters from JSON request
+                       // body into params[attrsKey]. Some SDKs rely
+                       // on this Rails API feature; see
+                       // https://api.rubyonrails.org/v5.2.1/classes/ActionController/ParamsWrapper.html
+                       params[attrsKey] = jsonParams
+               }
+       }
+
+       routeParams, _ := req.Context().Value(httprouter.ParamsKey).(httprouter.Params)
+       for _, p := range routeParams {
+               params[p.Key] = p.Value
+       }
+
+       if v, ok := params[attrsKey]; ok && attrsKey != "" {
+               params["attrs"] = v
+               delete(params, attrsKey)
+       }
+
+       if order, ok := params["order"].(string); ok {
+               // We must accept strings ("foo, bar desc") and arrays
+               // (["foo", "bar desc"]) because RailsAPI does.
+               // Convert to an array here before trying to unmarshal
+               // into options structs.
+               if order == "" {
+                       delete(params, "order")
+               } else {
+                       params["order"] = strings.Split(order, ",")
+               }
+       }
+
+       return params, nil
+}
+
+// Copy src to dst, using json as an intermediate format in order to
+// invoke src's json-marshaling and dst's json-unmarshaling behaviors.
+func (rtr *router) transcode(src interface{}, dst interface{}) error {
+       var errw error
+       pr, pw := io.Pipe()
+       go func() {
+               defer pw.Close()
+               errw = json.NewEncoder(pw).Encode(src)
+       }()
+       defer pr.Close()
+       err := json.NewDecoder(pr).Decode(dst)
+       if errw != nil {
+               return errw
+       }
+       return err
+}
+
+var intParams = map[string]bool{
+       "limit":  true,
+       "offset": true,
+}
+
+var boolParams = map[string]bool{
+       "distinct":             true,
+       "ensure_unique_name":   true,
+       "include_trash":        true,
+       "include_old_versions": true,
+}
+
+func stringToBool(s string) bool {
+       switch s {
+       case "", "false", "0":
+               return false
+       default:
+               return true
+       }
+}
diff --git a/lib/controller/router/request_test.go b/lib/controller/router/request_test.go
new file mode 100644 (file)
index 0000000..89238f6
--- /dev/null
@@ -0,0 +1,206 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package router
+
+import (
+       "bytes"
+       "encoding/json"
+       "io"
+       "net/http"
+       "net/http/httptest"
+       "net/url"
+
+       "git.curoverse.com/arvados.git/sdk/go/arvadostest"
+       check "gopkg.in/check.v1"
+)
+
+type testReq struct {
+       method   string
+       path     string
+       token    string // default is ActiveTokenV2; use noToken to omit
+       param    map[string]interface{}
+       attrs    map[string]interface{}
+       attrsKey string
+       header   http.Header
+
+       // variations on request formatting
+       json            bool
+       jsonAttrsTop    bool
+       jsonStringParam bool
+       tokenInBody     bool
+       tokenInQuery    bool
+       noContentType   bool
+
+       body *bytes.Buffer
+}
+
+const noToken = "(no token)"
+
+func (tr *testReq) Request() *http.Request {
+       param := map[string]interface{}{}
+       for k, v := range tr.param {
+               param[k] = v
+       }
+
+       if tr.body != nil {
+               // caller provided a buffer
+       } else if tr.json {
+               if tr.jsonAttrsTop {
+                       for k, v := range tr.attrs {
+                               param[k] = v
+                       }
+               } else if tr.attrs != nil {
+                       param[tr.attrsKey] = tr.attrs
+               }
+               tr.body = bytes.NewBuffer(nil)
+               err := json.NewEncoder(tr.body).Encode(param)
+               if err != nil {
+                       panic(err)
+               }
+       } else {
+               values := make(url.Values)
+               for k, v := range param {
+                       if vs, ok := v.(string); ok && !tr.jsonStringParam {
+                               values.Set(k, vs)
+                       } else {
+                               jv, err := json.Marshal(v)
+                               if err != nil {
+                                       panic(err)
+                               }
+                               values.Set(k, string(jv))
+                       }
+               }
+               if tr.attrs != nil {
+                       jattrs, err := json.Marshal(tr.attrs)
+                       if err != nil {
+                               panic(err)
+                       }
+                       values.Set(tr.attrsKey, string(jattrs))
+               }
+               tr.body = bytes.NewBuffer(nil)
+               io.WriteString(tr.body, values.Encode())
+       }
+       method := tr.method
+       if method == "" {
+               method = "GET"
+       }
+       path := tr.path
+       if path == "" {
+               path = "example/test/path"
+       }
+       req := httptest.NewRequest(method, "https://an.example/"+path, tr.body)
+       token := tr.token
+       if token == "" {
+               token = arvadostest.ActiveTokenV2
+       }
+       if token != noToken {
+               req.Header.Set("Authorization", "Bearer "+token)
+       }
+       if tr.json {
+               req.Header.Set("Content-Type", "application/json")
+       } else {
+               req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+       }
+       for k, v := range tr.header {
+               req.Header[k] = append([]string(nil), v...)
+       }
+       return req
+}
+
+func (tr *testReq) bodyContent() string {
+       return string(tr.body.Bytes())
+}
+
+func (s *RouterSuite) TestAttrsInBody(c *check.C) {
+       attrs := map[string]interface{}{"foo": "bar"}
+       for _, tr := range []testReq{
+               {attrsKey: "model_name", json: true, attrs: attrs},
+               {attrsKey: "model_name", json: true, attrs: attrs, jsonAttrsTop: true},
+       } {
+               c.Logf("tr: %#v", tr)
+               req := tr.Request()
+               params, err := s.rtr.loadRequestParams(req, tr.attrsKey)
+               c.Logf("params: %#v", params)
+               c.Assert(err, check.IsNil)
+               c.Check(params, check.NotNil)
+               c.Assert(params["attrs"], check.FitsTypeOf, map[string]interface{}{})
+               c.Check(params["attrs"].(map[string]interface{})["foo"], check.Equals, "bar")
+       }
+}
+
+func (s *RouterSuite) TestBoolParam(c *check.C) {
+       testKey := "ensure_unique_name"
+
+       for i, tr := range []testReq{
+               {method: "POST", param: map[string]interface{}{testKey: false}, json: true},
+               {method: "POST", param: map[string]interface{}{testKey: false}},
+               {method: "POST", param: map[string]interface{}{testKey: "false"}},
+               {method: "POST", param: map[string]interface{}{testKey: "0"}},
+               {method: "POST", param: map[string]interface{}{testKey: ""}},
+       } {
+               c.Logf("#%d, tr: %#v", i, tr)
+               req := tr.Request()
+               c.Logf("tr.body: %s", tr.bodyContent())
+               params, err := s.rtr.loadRequestParams(req, tr.attrsKey)
+               c.Logf("params: %#v", params)
+               c.Assert(err, check.IsNil)
+               c.Check(params, check.NotNil)
+               c.Check(params[testKey], check.Equals, false)
+       }
+
+       for i, tr := range []testReq{
+               {method: "POST", param: map[string]interface{}{testKey: true}, json: true},
+               {method: "POST", param: map[string]interface{}{testKey: true}},
+               {method: "POST", param: map[string]interface{}{testKey: "true"}},
+               {method: "POST", param: map[string]interface{}{testKey: "1"}},
+       } {
+               c.Logf("#%d, tr: %#v", i, tr)
+               req := tr.Request()
+               c.Logf("tr.body: %s", tr.bodyContent())
+               params, err := s.rtr.loadRequestParams(req, tr.attrsKey)
+               c.Logf("params: %#v", params)
+               c.Assert(err, check.IsNil)
+               c.Check(params, check.NotNil)
+               c.Check(params[testKey], check.Equals, true)
+       }
+}
+
+func (s *RouterSuite) TestOrderParam(c *check.C) {
+       for i, tr := range []testReq{
+               {method: "POST", param: map[string]interface{}{"order": ""}, json: true},
+               {method: "POST", param: map[string]interface{}{"order": ""}, json: false},
+               {method: "POST", param: map[string]interface{}{"order": []string{}}, json: true},
+               {method: "POST", param: map[string]interface{}{"order": []string{}}, json: false},
+               {method: "POST", param: map[string]interface{}{}, json: true},
+               {method: "POST", param: map[string]interface{}{}, json: false},
+       } {
+               c.Logf("#%d, tr: %#v", i, tr)
+               req := tr.Request()
+               params, err := s.rtr.loadRequestParams(req, tr.attrsKey)
+               c.Assert(err, check.IsNil)
+               c.Assert(params, check.NotNil)
+               if order, ok := params["order"]; ok && order != nil {
+                       c.Check(order, check.DeepEquals, []interface{}{})
+               }
+       }
+
+       for i, tr := range []testReq{
+               {method: "POST", param: map[string]interface{}{"order": "foo,bar desc"}, json: true},
+               {method: "POST", param: map[string]interface{}{"order": "foo,bar desc"}, json: false},
+               {method: "POST", param: map[string]interface{}{"order": "[\"foo\", \"bar desc\"]"}, json: false},
+               {method: "POST", param: map[string]interface{}{"order": []string{"foo", "bar desc"}}, json: true},
+               {method: "POST", param: map[string]interface{}{"order": []string{"foo", "bar desc"}}, json: false},
+       } {
+               c.Logf("#%d, tr: %#v", i, tr)
+               req := tr.Request()
+               params, err := s.rtr.loadRequestParams(req, tr.attrsKey)
+               c.Assert(err, check.IsNil)
+               if _, ok := params["order"].([]string); ok {
+                       c.Check(params["order"], check.DeepEquals, []string{"foo", "bar desc"})
+               } else {
+                       c.Check(params["order"], check.DeepEquals, []interface{}{"foo", "bar desc"})
+               }
+       }
+}
diff --git a/lib/controller/router/response.go b/lib/controller/router/response.go
new file mode 100644 (file)
index 0000000..aa3af1f
--- /dev/null
@@ -0,0 +1,151 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package router
+
+import (
+       "encoding/json"
+       "fmt"
+       "net/http"
+       "regexp"
+       "strings"
+       "time"
+
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "git.curoverse.com/arvados.git/sdk/go/httpserver"
+)
+
+const rfc3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00"
+
+type responseOptions struct {
+       Select []string
+       Count  string
+}
+
+func (rtr *router) responseOptions(opts interface{}) (responseOptions, error) {
+       var rOpts responseOptions
+       switch opts := opts.(type) {
+       case *arvados.GetOptions:
+               rOpts.Select = opts.Select
+       case *arvados.ListOptions:
+               rOpts.Select = opts.Select
+               rOpts.Count = opts.Count
+       }
+       return rOpts, nil
+}
+
+func applySelectParam(selectParam []string, orig map[string]interface{}) map[string]interface{} {
+       if len(selectParam) == 0 {
+               return orig
+       }
+       selected := map[string]interface{}{}
+       for _, attr := range selectParam {
+               if v, ok := orig[attr]; ok {
+                       selected[attr] = v
+               }
+       }
+       // Preserve "kind" even if not requested
+       if v, ok := orig["kind"]; ok {
+               selected["kind"] = v
+       }
+       return selected
+}
+
+func (rtr *router) sendResponse(w http.ResponseWriter, resp interface{}, opts responseOptions) {
+       var tmp map[string]interface{}
+
+       err := rtr.transcode(resp, &tmp)
+       if err != nil {
+               rtr.sendError(w, err)
+               return
+       }
+
+       respKind := kind(resp)
+       if respKind != "" {
+               tmp["kind"] = respKind
+       }
+       defaultItemKind := ""
+       if strings.HasSuffix(respKind, "List") {
+               defaultItemKind = strings.TrimSuffix(respKind, "List")
+       }
+
+       if items, ok := tmp["items"].([]interface{}); ok {
+               for i, item := range items {
+                       // Fill in "kind" by inspecting UUID/PDH if
+                       // possible; fall back on assuming each
+                       // Items[] entry in an "arvados#fooList"
+                       // response should have kind="arvados#foo".
+                       item, _ := item.(map[string]interface{})
+                       infix := ""
+                       if uuid, _ := item["uuid"].(string); len(uuid) == 27 {
+                               infix = uuid[6:11]
+                       }
+                       if k := kind(infixMap[infix]); k != "" {
+                               item["kind"] = k
+                       } else if pdh, _ := item["portable_data_hash"].(string); pdh != "" {
+                               item["kind"] = "arvados#collection"
+                       } else if defaultItemKind != "" {
+                               item["kind"] = defaultItemKind
+                       }
+                       items[i] = applySelectParam(opts.Select, item)
+               }
+               if opts.Count == "none" {
+                       delete(tmp, "items_available")
+               }
+       } else {
+               tmp = applySelectParam(opts.Select, tmp)
+       }
+
+       // Format non-nil timestamps as rfc3339NanoFixed (by default
+       // they will have been encoded to time.RFC3339Nano, which
+       // omits trailing zeroes).
+       for k, v := range tmp {
+               if !strings.HasSuffix(k, "_at") {
+                       continue
+               }
+               switch tv := v.(type) {
+               case *time.Time:
+                       if tv == nil {
+                               break
+                       }
+                       tmp[k] = tv.Format(rfc3339NanoFixed)
+               case time.Time:
+                       tmp[k] = tv.Format(rfc3339NanoFixed)
+               case string:
+                       t, err := time.Parse(time.RFC3339Nano, tv)
+                       if err != nil {
+                               break
+                       }
+                       tmp[k] = t.Format(rfc3339NanoFixed)
+               }
+       }
+       w.Header().Set("Content-Type", "application/json")
+       json.NewEncoder(w).Encode(tmp)
+}
+
+func (rtr *router) sendError(w http.ResponseWriter, err error) {
+       code := http.StatusInternalServerError
+       if err, ok := err.(interface{ HTTPStatus() int }); ok {
+               code = err.HTTPStatus()
+       }
+       httpserver.Error(w, err.Error(), code)
+}
+
+var infixMap = map[string]interface{}{
+       "4zz18": arvados.Collection{},
+       "j7d0g": arvados.Group{},
+}
+
+var mungeKind = regexp.MustCompile(`\..`)
+
+func kind(resp interface{}) string {
+       t := fmt.Sprintf("%T", resp)
+       if !strings.HasPrefix(t, "arvados.") {
+               return ""
+       }
+       return mungeKind.ReplaceAllStringFunc(t, func(s string) string {
+               // "arvados.CollectionList" => "arvados#collectionList"
+               return "#" + strings.ToLower(s[1:])
+       })
+}
diff --git a/lib/controller/router/router.go b/lib/controller/router/router.go
new file mode 100644 (file)
index 0000000..f37c7ea
--- /dev/null
@@ -0,0 +1,283 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package router
+
+import (
+       "context"
+       "fmt"
+       "net/http"
+       "strings"
+
+       "git.curoverse.com/arvados.git/lib/controller/federation"
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "git.curoverse.com/arvados.git/sdk/go/auth"
+       "git.curoverse.com/arvados.git/sdk/go/ctxlog"
+       "git.curoverse.com/arvados.git/sdk/go/httpserver"
+       "github.com/julienschmidt/httprouter"
+       "github.com/sirupsen/logrus"
+)
+
+type router struct {
+       mux *httprouter.Router
+       fed arvados.API
+}
+
+func New(cluster *arvados.Cluster) *router {
+       rtr := &router{
+               mux: httprouter.New(),
+               fed: federation.New(cluster),
+       }
+       rtr.addRoutes()
+       return rtr
+}
+
+type routableFunc func(ctx context.Context, opts interface{}) (interface{}, error)
+
+func (rtr *router) addRoutes() {
+       for _, route := range []struct {
+               endpoint    arvados.APIEndpoint
+               defaultOpts func() interface{}
+               exec        routableFunc
+       }{
+               {
+                       arvados.EndpointCollectionCreate,
+                       func() interface{} { return &arvados.CreateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.CollectionCreate(ctx, *opts.(*arvados.CreateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointCollectionUpdate,
+                       func() interface{} { return &arvados.UpdateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.CollectionUpdate(ctx, *opts.(*arvados.UpdateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointCollectionGet,
+                       func() interface{} { return &arvados.GetOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.CollectionGet(ctx, *opts.(*arvados.GetOptions))
+                       },
+               },
+               {
+                       arvados.EndpointCollectionList,
+                       func() interface{} { return &arvados.ListOptions{Limit: -1} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.CollectionList(ctx, *opts.(*arvados.ListOptions))
+                       },
+               },
+               {
+                       arvados.EndpointCollectionProvenance,
+                       func() interface{} { return &arvados.GetOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.CollectionProvenance(ctx, *opts.(*arvados.GetOptions))
+                       },
+               },
+               {
+                       arvados.EndpointCollectionUsedBy,
+                       func() interface{} { return &arvados.GetOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.CollectionUsedBy(ctx, *opts.(*arvados.GetOptions))
+                       },
+               },
+               {
+                       arvados.EndpointCollectionDelete,
+                       func() interface{} { return &arvados.DeleteOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.CollectionDelete(ctx, *opts.(*arvados.DeleteOptions))
+                       },
+               },
+               {
+                       arvados.EndpointCollectionTrash,
+                       func() interface{} { return &arvados.DeleteOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.CollectionTrash(ctx, *opts.(*arvados.DeleteOptions))
+                       },
+               },
+               {
+                       arvados.EndpointCollectionUntrash,
+                       func() interface{} { return &arvados.UntrashOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.CollectionUntrash(ctx, *opts.(*arvados.UntrashOptions))
+                       },
+               },
+               {
+                       arvados.EndpointContainerCreate,
+                       func() interface{} { return &arvados.CreateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.ContainerCreate(ctx, *opts.(*arvados.CreateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointContainerUpdate,
+                       func() interface{} { return &arvados.UpdateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.ContainerUpdate(ctx, *opts.(*arvados.UpdateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointContainerGet,
+                       func() interface{} { return &arvados.GetOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.ContainerGet(ctx, *opts.(*arvados.GetOptions))
+                       },
+               },
+               {
+                       arvados.EndpointContainerList,
+                       func() interface{} { return &arvados.ListOptions{Limit: -1} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.ContainerList(ctx, *opts.(*arvados.ListOptions))
+                       },
+               },
+               {
+                       arvados.EndpointContainerDelete,
+                       func() interface{} { return &arvados.DeleteOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.ContainerDelete(ctx, *opts.(*arvados.DeleteOptions))
+                       },
+               },
+               {
+                       arvados.EndpointContainerLock,
+                       func() interface{} {
+                               return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
+                       },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.ContainerLock(ctx, *opts.(*arvados.GetOptions))
+                       },
+               },
+               {
+                       arvados.EndpointContainerUnlock,
+                       func() interface{} {
+                               return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
+                       },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.ContainerUnlock(ctx, *opts.(*arvados.GetOptions))
+                       },
+               },
+               {
+                       arvados.EndpointSpecimenCreate,
+                       func() interface{} { return &arvados.CreateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.SpecimenCreate(ctx, *opts.(*arvados.CreateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointSpecimenUpdate,
+                       func() interface{} { return &arvados.UpdateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.SpecimenUpdate(ctx, *opts.(*arvados.UpdateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointSpecimenGet,
+                       func() interface{} { return &arvados.GetOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.SpecimenGet(ctx, *opts.(*arvados.GetOptions))
+                       },
+               },
+               {
+                       arvados.EndpointSpecimenList,
+                       func() interface{} { return &arvados.ListOptions{Limit: -1} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.SpecimenList(ctx, *opts.(*arvados.ListOptions))
+                       },
+               },
+               {
+                       arvados.EndpointSpecimenDelete,
+                       func() interface{} { return &arvados.DeleteOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.SpecimenDelete(ctx, *opts.(*arvados.DeleteOptions))
+                       },
+               },
+       } {
+               rtr.addRoute(route.endpoint, route.defaultOpts, route.exec)
+               if route.endpoint.Method == "PATCH" {
+                       // Accept PUT as a synonym for PATCH.
+                       endpointPUT := route.endpoint
+                       endpointPUT.Method = "PUT"
+                       rtr.addRoute(endpointPUT, route.defaultOpts, route.exec)
+               }
+       }
+       rtr.mux.NotFound = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+               httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusNotFound)
+       })
+       rtr.mux.MethodNotAllowed = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+               httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusMethodNotAllowed)
+       })
+}
+
+func (rtr *router) addRoute(endpoint arvados.APIEndpoint, defaultOpts func() interface{}, exec routableFunc) {
+       rtr.mux.HandlerFunc(endpoint.Method, "/"+endpoint.Path, func(w http.ResponseWriter, req *http.Request) {
+               logger := ctxlog.FromContext(req.Context())
+               params, err := rtr.loadRequestParams(req, endpoint.AttrsKey)
+               if err != nil {
+                       logger.WithFields(logrus.Fields{
+                               "req":      req,
+                               "method":   endpoint.Method,
+                               "endpoint": endpoint,
+                       }).WithError(err).Debug("error loading request params")
+                       rtr.sendError(w, err)
+                       return
+               }
+               opts := defaultOpts()
+               err = rtr.transcode(params, opts)
+               if err != nil {
+                       logger.WithField("params", params).WithError(err).Debugf("error transcoding params to %T", opts)
+                       rtr.sendError(w, err)
+                       return
+               }
+               respOpts, err := rtr.responseOptions(opts)
+               if err != nil {
+                       logger.WithField("opts", opts).WithError(err).Debugf("error getting response options from %T", opts)
+                       rtr.sendError(w, err)
+                       return
+               }
+
+               creds := auth.CredentialsFromRequest(req)
+               if rt, _ := params["reader_tokens"].([]interface{}); len(rt) > 0 {
+                       for _, t := range rt {
+                               if t, ok := t.(string); ok {
+                                       creds.Tokens = append(creds.Tokens, t)
+                               }
+                       }
+               }
+               ctx := auth.NewContext(req.Context(), creds)
+               ctx = arvados.ContextWithRequestID(ctx, req.Header.Get("X-Request-Id"))
+               logger.WithFields(logrus.Fields{
+                       "apiEndpoint": endpoint,
+                       "apiOptsType": fmt.Sprintf("%T", opts),
+                       "apiOpts":     opts,
+               }).Debug("exec")
+               resp, err := exec(ctx, opts)
+               if err != nil {
+                       logger.WithError(err).Debugf("returning error type %T", err)
+                       rtr.sendError(w, err)
+                       return
+               }
+               rtr.sendResponse(w, resp, respOpts)
+       })
+}
+
+func (rtr *router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+       switch strings.SplitN(strings.TrimLeft(r.URL.Path, "/"), "/", 2)[0] {
+       case "login", "logout", "auth":
+       default:
+               w.Header().Set("Access-Control-Allow-Origin", "*")
+               w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, PUT, POST, DELETE")
+               w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
+               w.Header().Set("Access-Control-Max-Age", "86486400")
+       }
+       if r.Method == "OPTIONS" {
+               return
+       }
+       r.ParseForm()
+       if m := r.FormValue("_method"); m != "" {
+               r2 := *r
+               r = &r2
+               r.Method = m
+       }
+       rtr.mux.ServeHTTP(w, r)
+}
diff --git a/lib/controller/router/router_test.go b/lib/controller/router/router_test.go
new file mode 100644 (file)
index 0000000..4e6b161
--- /dev/null
@@ -0,0 +1,395 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package router
+
+import (
+       "bytes"
+       "encoding/json"
+       "io"
+       "net/http"
+       "net/http/httptest"
+       "os"
+       "strings"
+       "testing"
+       "time"
+
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "git.curoverse.com/arvados.git/sdk/go/arvadostest"
+       "github.com/julienschmidt/httprouter"
+       check "gopkg.in/check.v1"
+)
+
+// Gocheck boilerplate
+func Test(t *testing.T) {
+       check.TestingT(t)
+}
+
+var _ = check.Suite(&RouterSuite{})
+
+type RouterSuite struct {
+       rtr  *router
+       stub arvadostest.APIStub
+}
+
+func (s *RouterSuite) SetUpTest(c *check.C) {
+       s.stub = arvadostest.APIStub{}
+       s.rtr = &router{
+               mux: httprouter.New(),
+               fed: &s.stub,
+       }
+       s.rtr.addRoutes()
+}
+
+func (s *RouterSuite) TestOptions(c *check.C) {
+       token := arvadostest.ActiveToken
+       for _, trial := range []struct {
+               method       string
+               path         string
+               header       http.Header
+               body         string
+               shouldStatus int // zero value means 200
+               shouldCall   string
+               withOptions  interface{}
+       }{
+               {
+                       method:      "GET",
+                       path:        "/arvados/v1/collections/" + arvadostest.FooCollection,
+                       shouldCall:  "CollectionGet",
+                       withOptions: arvados.GetOptions{UUID: arvadostest.FooCollection},
+               },
+               {
+                       method:      "PUT",
+                       path:        "/arvados/v1/collections/" + arvadostest.FooCollection,
+                       shouldCall:  "CollectionUpdate",
+                       withOptions: arvados.UpdateOptions{UUID: arvadostest.FooCollection},
+               },
+               {
+                       method:      "PATCH",
+                       path:        "/arvados/v1/collections/" + arvadostest.FooCollection,
+                       shouldCall:  "CollectionUpdate",
+                       withOptions: arvados.UpdateOptions{UUID: arvadostest.FooCollection},
+               },
+               {
+                       method:      "DELETE",
+                       path:        "/arvados/v1/collections/" + arvadostest.FooCollection,
+                       shouldCall:  "CollectionDelete",
+                       withOptions: arvados.DeleteOptions{UUID: arvadostest.FooCollection},
+               },
+               {
+                       method:      "POST",
+                       path:        "/arvados/v1/collections",
+                       shouldCall:  "CollectionCreate",
+                       withOptions: arvados.CreateOptions{},
+               },
+               {
+                       method:      "GET",
+                       path:        "/arvados/v1/collections",
+                       shouldCall:  "CollectionList",
+                       withOptions: arvados.ListOptions{Limit: -1},
+               },
+               {
+                       method:      "GET",
+                       path:        "/arvados/v1/collections?limit=123&offset=456&include_trash=true&include_old_versions=1",
+                       shouldCall:  "CollectionList",
+                       withOptions: arvados.ListOptions{Limit: 123, Offset: 456, IncludeTrash: true, IncludeOldVersions: true},
+               },
+               {
+                       method:      "POST",
+                       path:        "/arvados/v1/collections?limit=123&_method=GET",
+                       body:        `{"offset":456,"include_trash":true,"include_old_versions":true}`,
+                       shouldCall:  "CollectionList",
+                       withOptions: arvados.ListOptions{Limit: 123, Offset: 456, IncludeTrash: true, IncludeOldVersions: true},
+               },
+               {
+                       method:      "POST",
+                       path:        "/arvados/v1/collections?limit=123",
+                       body:        "offset=456&include_trash=true&include_old_versions=1&_method=GET",
+                       header:      http.Header{"Content-Type": {"application/x-www-form-urlencoded"}},
+                       shouldCall:  "CollectionList",
+                       withOptions: arvados.ListOptions{Limit: 123, Offset: 456, IncludeTrash: true, IncludeOldVersions: true},
+               },
+               {
+                       method:       "PATCH",
+                       path:         "/arvados/v1/collections",
+                       shouldStatus: http.StatusMethodNotAllowed,
+               },
+               {
+                       method:       "PUT",
+                       path:         "/arvados/v1/collections",
+                       shouldStatus: http.StatusMethodNotAllowed,
+               },
+               {
+                       method:       "DELETE",
+                       path:         "/arvados/v1/collections",
+                       shouldStatus: http.StatusMethodNotAllowed,
+               },
+       } {
+               // Reset calls captured in previous trial
+               s.stub = arvadostest.APIStub{}
+
+               c.Logf("trial: %#v", trial)
+               _, rr, _ := doRequest(c, s.rtr, token, trial.method, trial.path, trial.header, bytes.NewBufferString(trial.body))
+               if trial.shouldStatus == 0 {
+                       c.Check(rr.Code, check.Equals, http.StatusOK)
+               } else {
+                       c.Check(rr.Code, check.Equals, trial.shouldStatus)
+               }
+               calls := s.stub.Calls(nil)
+               if trial.shouldCall == "" {
+                       c.Check(calls, check.HasLen, 0)
+               } else if len(calls) != 1 {
+                       c.Check(calls, check.HasLen, 1)
+               } else {
+                       c.Check(calls[0].Method, isMethodNamed, trial.shouldCall)
+                       c.Check(calls[0].Options, check.DeepEquals, trial.withOptions)
+               }
+       }
+}
+
+var _ = check.Suite(&RouterIntegrationSuite{})
+
+type RouterIntegrationSuite struct {
+       rtr *router
+}
+
+func (s *RouterIntegrationSuite) SetUpTest(c *check.C) {
+       cluster := &arvados.Cluster{}
+       cluster.TLS.Insecure = true
+       arvadostest.SetServiceURL(&cluster.Services.RailsAPI, "https://"+os.Getenv("ARVADOS_TEST_API_HOST"))
+       s.rtr = New(cluster)
+}
+
+func (s *RouterIntegrationSuite) TearDownSuite(c *check.C) {
+       err := arvados.NewClientFromEnv().RequestAndDecode(nil, "POST", "database/reset", nil, nil)
+       c.Check(err, check.IsNil)
+}
+
+func (s *RouterIntegrationSuite) TestCollectionResponses(c *check.C) {
+       token := arvadostest.ActiveTokenV2
+
+       // Check "get collection" response has "kind" key
+       _, rr, jresp := doRequest(c, s.rtr, token, "GET", `/arvados/v1/collections`, nil, bytes.NewBufferString(`{"include_trash":true}`))
+       c.Check(rr.Code, check.Equals, http.StatusOK)
+       c.Check(jresp["items"], check.FitsTypeOf, []interface{}{})
+       c.Check(jresp["kind"], check.Equals, "arvados#collectionList")
+       c.Check(jresp["items"].([]interface{})[0].(map[string]interface{})["kind"], check.Equals, "arvados#collection")
+
+       // Check items in list response have a "kind" key regardless
+       // of whether a uuid/pdh is selected.
+       for _, selectj := range []string{
+               ``,
+               `,"select":["portable_data_hash"]`,
+               `,"select":["name"]`,
+               `,"select":["uuid"]`,
+       } {
+               _, rr, jresp = doRequest(c, s.rtr, token, "GET", `/arvados/v1/collections`, nil, bytes.NewBufferString(`{"where":{"uuid":["`+arvadostest.FooCollection+`"]}`+selectj+`}`))
+               c.Check(rr.Code, check.Equals, http.StatusOK)
+               c.Check(jresp["items"], check.FitsTypeOf, []interface{}{})
+               c.Check(jresp["items_available"], check.FitsTypeOf, float64(0))
+               c.Check(jresp["kind"], check.Equals, "arvados#collectionList")
+               item0 := jresp["items"].([]interface{})[0].(map[string]interface{})
+               c.Check(item0["kind"], check.Equals, "arvados#collection")
+               if selectj == "" || strings.Contains(selectj, "portable_data_hash") {
+                       c.Check(item0["portable_data_hash"], check.Equals, arvadostest.FooCollectionPDH)
+               } else {
+                       c.Check(item0["portable_data_hash"], check.IsNil)
+               }
+               if selectj == "" || strings.Contains(selectj, "name") {
+                       c.Check(item0["name"], check.FitsTypeOf, "")
+               } else {
+                       c.Check(item0["name"], check.IsNil)
+               }
+               if selectj == "" || strings.Contains(selectj, "uuid") {
+                       c.Check(item0["uuid"], check.Equals, arvadostest.FooCollection)
+               } else {
+                       c.Check(item0["uuid"], check.IsNil)
+               }
+       }
+
+       // Check "create collection" response has "kind" key
+       _, rr, jresp = doRequest(c, s.rtr, token, "POST", `/arvados/v1/collections`, http.Header{"Content-Type": {"application/x-www-form-urlencoded"}}, bytes.NewBufferString(`ensure_unique_name=true`))
+       c.Check(rr.Code, check.Equals, http.StatusOK)
+       c.Check(jresp["uuid"], check.FitsTypeOf, "")
+       c.Check(jresp["kind"], check.Equals, "arvados#collection")
+}
+
+func (s *RouterIntegrationSuite) TestContainerList(c *check.C) {
+       token := arvadostest.ActiveTokenV2
+
+       _, rr, jresp := doRequest(c, s.rtr, token, "GET", `/arvados/v1/containers?limit=0`, nil, nil)
+       c.Check(rr.Code, check.Equals, http.StatusOK)
+       c.Check(jresp["items_available"], check.FitsTypeOf, float64(0))
+       c.Check(jresp["items_available"].(float64) > 2, check.Equals, true)
+       c.Check(jresp["items"], check.HasLen, 0)
+
+       _, rr, jresp = doRequest(c, s.rtr, token, "GET", `/arvados/v1/containers?limit=2&select=["uuid","command"]`, nil, nil)
+       c.Check(rr.Code, check.Equals, http.StatusOK)
+       c.Check(jresp["items_available"], check.FitsTypeOf, float64(0))
+       c.Check(jresp["items_available"].(float64) > 2, check.Equals, true)
+       c.Check(jresp["items"], check.HasLen, 2)
+       item0 := jresp["items"].([]interface{})[0].(map[string]interface{})
+       c.Check(item0["uuid"], check.HasLen, 27)
+       c.Check(item0["command"], check.FitsTypeOf, []interface{}{})
+       c.Check(item0["command"].([]interface{})[0], check.FitsTypeOf, "")
+       c.Check(item0["mounts"], check.IsNil)
+
+       _, rr, jresp = doRequest(c, s.rtr, token, "GET", `/arvados/v1/containers`, nil, nil)
+       c.Check(rr.Code, check.Equals, http.StatusOK)
+       c.Check(jresp["items_available"], check.FitsTypeOf, float64(0))
+       c.Check(jresp["items_available"].(float64) > 2, check.Equals, true)
+       avail := int(jresp["items_available"].(float64))
+       c.Check(jresp["items"], check.HasLen, avail)
+       item0 = jresp["items"].([]interface{})[0].(map[string]interface{})
+       c.Check(item0["uuid"], check.HasLen, 27)
+       c.Check(item0["command"], check.FitsTypeOf, []interface{}{})
+       c.Check(item0["command"].([]interface{})[0], check.FitsTypeOf, "")
+       c.Check(item0["mounts"], check.NotNil)
+}
+
+func (s *RouterIntegrationSuite) TestContainerLock(c *check.C) {
+       uuid := arvadostest.QueuedContainerUUID
+       token := arvadostest.AdminToken
+       _, rr, jresp := doRequest(c, s.rtr, token, "POST", "/arvados/v1/containers/"+uuid+"/lock", nil, nil)
+       c.Check(rr.Code, check.Equals, http.StatusOK)
+       c.Check(jresp["uuid"], check.HasLen, 27)
+       c.Check(jresp["state"], check.Equals, "Locked")
+       _, rr, jresp = doRequest(c, s.rtr, token, "POST", "/arvados/v1/containers/"+uuid+"/lock", nil, nil)
+       c.Check(rr.Code, check.Equals, http.StatusUnprocessableEntity)
+       c.Check(rr.Body.String(), check.Not(check.Matches), `.*"uuid":.*`)
+       _, rr, jresp = doRequest(c, s.rtr, token, "POST", "/arvados/v1/containers/"+uuid+"/unlock", nil, nil)
+       c.Check(rr.Code, check.Equals, http.StatusOK)
+       c.Check(jresp["uuid"], check.HasLen, 27)
+       c.Check(jresp["state"], check.Equals, "Queued")
+       c.Check(jresp["environment"], check.IsNil)
+       _, rr, jresp = doRequest(c, s.rtr, token, "POST", "/arvados/v1/containers/"+uuid+"/unlock", nil, nil)
+       c.Check(rr.Code, check.Equals, http.StatusUnprocessableEntity)
+       c.Check(jresp["uuid"], check.IsNil)
+}
+
+func (s *RouterIntegrationSuite) TestFullTimestampsInResponse(c *check.C) {
+       uuid := arvadostest.CollectionReplicationDesired2Confirmed2UUID
+       token := arvadostest.ActiveTokenV2
+
+       _, rr, jresp := doRequest(c, s.rtr, token, "GET", `/arvados/v1/collections/`+uuid, nil, nil)
+       c.Check(rr.Code, check.Equals, http.StatusOK)
+       c.Check(jresp["uuid"], check.Equals, uuid)
+       expectNS := map[string]int{
+               "created_at":  596506000, // fixture says 596506247, but truncated by postgresql
+               "modified_at": 596338000, // fixture says 596338465, but truncated by postgresql
+       }
+       for key, ns := range expectNS {
+               mt, ok := jresp[key].(string)
+               c.Logf("jresp[%q] == %q", key, mt)
+               c.Assert(ok, check.Equals, true)
+               t, err := time.Parse(time.RFC3339Nano, mt)
+               c.Check(err, check.IsNil)
+               c.Check(t.Nanosecond(), check.Equals, ns)
+       }
+}
+
+func (s *RouterIntegrationSuite) TestSelectParam(c *check.C) {
+       uuid := arvadostest.QueuedContainerUUID
+       token := arvadostest.ActiveTokenV2
+       for _, sel := range [][]string{
+               {"uuid", "command"},
+               {"uuid", "command", "uuid"},
+               {"", "command", "uuid"},
+       } {
+               j, err := json.Marshal(sel)
+               c.Assert(err, check.IsNil)
+               _, rr, resp := doRequest(c, s.rtr, token, "GET", "/arvados/v1/containers/"+uuid+"?select="+string(j), nil, nil)
+               c.Check(rr.Code, check.Equals, http.StatusOK)
+
+               c.Check(resp["kind"], check.Equals, "arvados#container")
+               c.Check(resp["uuid"], check.HasLen, 27)
+               c.Check(resp["command"], check.HasLen, 2)
+               c.Check(resp["mounts"], check.IsNil)
+               _, hasMounts := resp["mounts"]
+               c.Check(hasMounts, check.Equals, false)
+       }
+}
+
+func (s *RouterIntegrationSuite) TestRouteNotFound(c *check.C) {
+       token := arvadostest.ActiveTokenV2
+       req := (&testReq{
+               method: "POST",
+               path:   "arvados/v1/collections/" + arvadostest.FooCollection + "/error404pls",
+               token:  token,
+       }).Request()
+       rr := httptest.NewRecorder()
+       s.rtr.ServeHTTP(rr, req)
+       c.Check(rr.Code, check.Equals, http.StatusNotFound)
+       c.Logf("body: %q", rr.Body.String())
+       var j map[string]interface{}
+       err := json.Unmarshal(rr.Body.Bytes(), &j)
+       c.Check(err, check.IsNil)
+       c.Logf("decoded: %v", j)
+       c.Assert(j["errors"], check.FitsTypeOf, []interface{}{})
+       c.Check(j["errors"].([]interface{})[0], check.Equals, "API endpoint not found")
+}
+
+func (s *RouterIntegrationSuite) TestCORS(c *check.C) {
+       token := arvadostest.ActiveTokenV2
+       req := (&testReq{
+               method: "OPTIONS",
+               path:   "arvados/v1/collections/" + arvadostest.FooCollection,
+               header: http.Header{"Origin": {"https://example.com"}},
+               token:  token,
+       }).Request()
+       rr := httptest.NewRecorder()
+       s.rtr.ServeHTTP(rr, req)
+       c.Check(rr.Code, check.Equals, http.StatusOK)
+       c.Check(rr.Body.String(), check.HasLen, 0)
+       c.Check(rr.Result().Header.Get("Access-Control-Allow-Origin"), check.Equals, "*")
+       for _, hdr := range []string{"Authorization", "Content-Type"} {
+               c.Check(rr.Result().Header.Get("Access-Control-Allow-Headers"), check.Matches, ".*"+hdr+".*")
+       }
+       for _, method := range []string{"GET", "HEAD", "PUT", "POST", "DELETE"} {
+               c.Check(rr.Result().Header.Get("Access-Control-Allow-Methods"), check.Matches, ".*"+method+".*")
+       }
+
+       for _, unsafe := range []string{"login", "logout", "auth", "auth/foo", "login/?blah"} {
+               req := (&testReq{
+                       method: "OPTIONS",
+                       path:   unsafe,
+                       header: http.Header{"Origin": {"https://example.com"}},
+                       token:  token,
+               }).Request()
+               rr := httptest.NewRecorder()
+               s.rtr.ServeHTTP(rr, req)
+               c.Check(rr.Code, check.Equals, http.StatusOK)
+               c.Check(rr.Body.String(), check.HasLen, 0)
+               c.Check(rr.Result().Header.Get("Access-Control-Allow-Origin"), check.Equals, "")
+               c.Check(rr.Result().Header.Get("Access-Control-Allow-Methods"), check.Equals, "")
+               c.Check(rr.Result().Header.Get("Access-Control-Allow-Headers"), check.Equals, "")
+
+               req = (&testReq{
+                       method: "POST",
+                       path:   unsafe,
+                       header: http.Header{"Origin": {"https://example.com"}},
+                       token:  token,
+               }).Request()
+               rr = httptest.NewRecorder()
+               s.rtr.ServeHTTP(rr, req)
+               c.Check(rr.Result().Header.Get("Access-Control-Allow-Origin"), check.Equals, "")
+               c.Check(rr.Result().Header.Get("Access-Control-Allow-Methods"), check.Equals, "")
+               c.Check(rr.Result().Header.Get("Access-Control-Allow-Headers"), check.Equals, "")
+       }
+}
+
+func doRequest(c *check.C, rtr http.Handler, token, method, path string, hdrs http.Header, body io.Reader) (*http.Request, *httptest.ResponseRecorder, map[string]interface{}) {
+       req := httptest.NewRequest(method, path, body)
+       for k, v := range hdrs {
+               req.Header[k] = v
+       }
+       req.Header.Set("Authorization", "Bearer "+token)
+       rr := httptest.NewRecorder()
+       rtr.ServeHTTP(rr, req)
+       c.Logf("response body: %s", rr.Body.String())
+       var jresp map[string]interface{}
+       err := json.Unmarshal(rr.Body.Bytes(), &jresp)
+       c.Check(err, check.IsNil)
+       return req, rr, jresp
+}
diff --git a/lib/controller/rpc/conn.go b/lib/controller/rpc/conn.go
new file mode 100644 (file)
index 0000000..e07eaf4
--- /dev/null
@@ -0,0 +1,266 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package rpc
+
+import (
+       "context"
+       "crypto/tls"
+       "encoding/json"
+       "fmt"
+       "io"
+       "net"
+       "net/http"
+       "net/url"
+       "strings"
+       "time"
+
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
+)
+
+type TokenProvider func(context.Context) ([]string, error)
+
+type Conn struct {
+       clusterID     string
+       httpClient    http.Client
+       baseURL       url.URL
+       tokenProvider TokenProvider
+}
+
+func NewConn(clusterID string, url *url.URL, insecure bool, tp TokenProvider) *Conn {
+       transport := http.DefaultTransport
+       if insecure {
+               // It's not safe to copy *http.DefaultTransport
+               // because it has a mutex (which might be locked)
+               // protecting a private map (which might not be nil).
+               // So we build our own, using the Go 1.12 default
+               // values, ignoring any changes the application has
+               // made to http.DefaultTransport.
+               transport = &http.Transport{
+                       DialContext: (&net.Dialer{
+                               Timeout:   30 * time.Second,
+                               KeepAlive: 30 * time.Second,
+                               DualStack: true,
+                       }).DialContext,
+                       MaxIdleConns:          100,
+                       IdleConnTimeout:       90 * time.Second,
+                       TLSHandshakeTimeout:   10 * time.Second,
+                       ExpectContinueTimeout: 1 * time.Second,
+                       TLSClientConfig:       &tls.Config{InsecureSkipVerify: true},
+               }
+       }
+       return &Conn{
+               clusterID:     clusterID,
+               httpClient:    http.Client{Transport: transport},
+               baseURL:       *url,
+               tokenProvider: tp,
+       }
+}
+
+func (conn *Conn) requestAndDecode(ctx context.Context, dst interface{}, ep arvados.APIEndpoint, body io.Reader, opts interface{}) error {
+       aClient := arvados.Client{
+               Client:  &conn.httpClient,
+               Scheme:  conn.baseURL.Scheme,
+               APIHost: conn.baseURL.Host,
+       }
+       tokens, err := conn.tokenProvider(ctx)
+       if err != nil {
+               return err
+       } else if len(tokens) > 0 {
+               ctx = arvados.ContextWithAuthorization(ctx, "Bearer "+tokens[0])
+       } else {
+               // Use a non-empty auth string to ensure we override
+               // any default token set on aClient -- and to avoid
+               // having the remote prompt us to send a token by
+               // responding 401.
+               ctx = arvados.ContextWithAuthorization(ctx, "Bearer -")
+       }
+
+       // Encode opts to JSON and decode from there to a
+       // map[string]interface{}, so we can munge the query params
+       // using the JSON key names specified by opts' struct tags.
+       j, err := json.Marshal(opts)
+       if err != nil {
+               return fmt.Errorf("%T: requestAndDecode: Marshal opts: %s", conn, err)
+       }
+       var params map[string]interface{}
+       err = json.Unmarshal(j, &params)
+       if err != nil {
+               return fmt.Errorf("%T: requestAndDecode: Unmarshal opts: %s", conn, err)
+       }
+       if attrs, ok := params["attrs"]; ok && ep.AttrsKey != "" {
+               params[ep.AttrsKey] = attrs
+               delete(params, "attrs")
+       }
+       if limit, ok := params["limit"].(float64); ok && limit < 0 {
+               // Negative limit means "not specified" here, but some
+               // servers/versions do not accept that, so we need to
+               // remove it entirely.
+               delete(params, "limit")
+       }
+       if len(tokens) > 1 {
+               params["reader_tokens"] = tokens[1:]
+       }
+       path := ep.Path
+       if strings.Contains(ep.Path, "/:uuid") {
+               uuid, _ := params["uuid"].(string)
+               path = strings.Replace(path, "/:uuid", "/"+uuid, 1)
+               delete(params, "uuid")
+       }
+       return aClient.RequestAndDecodeContext(ctx, dst, ep.Method, path, body, params)
+}
+
+func (conn *Conn) CollectionCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Collection, error) {
+       ep := arvados.EndpointCollectionCreate
+       var resp arvados.Collection
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) CollectionUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Collection, error) {
+       ep := arvados.EndpointCollectionUpdate
+       var resp arvados.Collection
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) CollectionGet(ctx context.Context, options arvados.GetOptions) (arvados.Collection, error) {
+       ep := arvados.EndpointCollectionGet
+       var resp arvados.Collection
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) CollectionList(ctx context.Context, options arvados.ListOptions) (arvados.CollectionList, error) {
+       ep := arvados.EndpointCollectionList
+       var resp arvados.CollectionList
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) CollectionProvenance(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) {
+       ep := arvados.EndpointCollectionProvenance
+       var resp map[string]interface{}
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) CollectionUsedBy(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) {
+       ep := arvados.EndpointCollectionUsedBy
+       var resp map[string]interface{}
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) CollectionDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Collection, error) {
+       ep := arvados.EndpointCollectionDelete
+       var resp arvados.Collection
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) CollectionTrash(ctx context.Context, options arvados.DeleteOptions) (arvados.Collection, error) {
+       ep := arvados.EndpointCollectionTrash
+       var resp arvados.Collection
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) CollectionUntrash(ctx context.Context, options arvados.UntrashOptions) (arvados.Collection, error) {
+       ep := arvados.EndpointCollectionUntrash
+       var resp arvados.Collection
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) ContainerCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Container, error) {
+       ep := arvados.EndpointContainerCreate
+       var resp arvados.Container
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) ContainerUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Container, error) {
+       ep := arvados.EndpointContainerUpdate
+       var resp arvados.Container
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) ContainerGet(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
+       ep := arvados.EndpointContainerGet
+       var resp arvados.Container
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) ContainerList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerList, error) {
+       ep := arvados.EndpointContainerList
+       var resp arvados.ContainerList
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) ContainerDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Container, error) {
+       ep := arvados.EndpointContainerDelete
+       var resp arvados.Container
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) ContainerLock(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
+       ep := arvados.EndpointContainerLock
+       var resp arvados.Container
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) ContainerUnlock(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
+       ep := arvados.EndpointContainerUnlock
+       var resp arvados.Container
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) SpecimenCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Specimen, error) {
+       ep := arvados.EndpointSpecimenCreate
+       var resp arvados.Specimen
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) SpecimenUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Specimen, error) {
+       ep := arvados.EndpointSpecimenUpdate
+       var resp arvados.Specimen
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) SpecimenGet(ctx context.Context, options arvados.GetOptions) (arvados.Specimen, error) {
+       ep := arvados.EndpointSpecimenGet
+       var resp arvados.Specimen
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) SpecimenList(ctx context.Context, options arvados.ListOptions) (arvados.SpecimenList, error) {
+       ep := arvados.EndpointSpecimenList
+       var resp arvados.SpecimenList
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) SpecimenDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Specimen, error) {
+       ep := arvados.EndpointSpecimenDelete
+       var resp arvados.Specimen
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
+func (conn *Conn) APIClientAuthorizationCurrent(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) {
+       ep := arvados.EndpointAPIClientAuthorizationCurrent
+       var resp arvados.APIClientAuthorization
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
diff --git a/lib/controller/rpc/conn_test.go b/lib/controller/rpc/conn_test.go
new file mode 100644 (file)
index 0000000..80e90a0
--- /dev/null
@@ -0,0 +1,79 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package rpc
+
+import (
+       "context"
+       "net/url"
+       "os"
+       "testing"
+
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "git.curoverse.com/arvados.git/sdk/go/arvadostest"
+       "git.curoverse.com/arvados.git/sdk/go/ctxlog"
+       "github.com/sirupsen/logrus"
+       check "gopkg.in/check.v1"
+)
+
+// Gocheck boilerplate
+func Test(t *testing.T) {
+       check.TestingT(t)
+}
+
+var _ = check.Suite(&RPCSuite{})
+
+const contextKeyTestTokens = "testTokens"
+
+type RPCSuite struct {
+       log  logrus.FieldLogger
+       ctx  context.Context
+       conn *Conn
+}
+
+func (s *RPCSuite) SetUpTest(c *check.C) {
+       ctx := ctxlog.Context(context.Background(), ctxlog.TestLogger(c))
+       s.ctx = context.WithValue(ctx, contextKeyTestTokens, []string{arvadostest.ActiveToken})
+       s.conn = NewConn("zzzzz", &url.URL{Scheme: "https", Host: os.Getenv("ARVADOS_TEST_API_HOST")}, true, func(ctx context.Context) ([]string, error) {
+               return ctx.Value(contextKeyTestTokens).([]string), nil
+       })
+}
+
+func (s *RPCSuite) TestCollectionCreate(c *check.C) {
+       coll, err := s.conn.CollectionCreate(s.ctx, arvados.CreateOptions{Attrs: map[string]interface{}{
+               "owner_uuid":         arvadostest.ActiveUserUUID,
+               "portable_data_hash": "d41d8cd98f00b204e9800998ecf8427e+0",
+       }})
+       c.Check(err, check.IsNil)
+       c.Check(coll.UUID, check.HasLen, 27)
+}
+
+func (s *RPCSuite) TestSpecimenCRUD(c *check.C) {
+       sp, err := s.conn.SpecimenCreate(s.ctx, arvados.CreateOptions{Attrs: map[string]interface{}{
+               "owner_uuid": arvadostest.ActiveUserUUID,
+               "properties": map[string]string{"foo": "bar"},
+       }})
+       c.Check(err, check.IsNil)
+       c.Check(sp.UUID, check.HasLen, 27)
+       c.Check(sp.Properties, check.HasLen, 1)
+       c.Check(sp.Properties["foo"], check.Equals, "bar")
+
+       spGet, err := s.conn.SpecimenGet(s.ctx, arvados.GetOptions{UUID: sp.UUID})
+       c.Check(spGet.UUID, check.Equals, sp.UUID)
+       c.Check(spGet.Properties["foo"], check.Equals, "bar")
+
+       spList, err := s.conn.SpecimenList(s.ctx, arvados.ListOptions{Limit: -1, Filters: []arvados.Filter{{"uuid", "=", sp.UUID}}})
+       c.Check(spList.ItemsAvailable, check.Equals, 1)
+       c.Assert(spList.Items, check.HasLen, 1)
+       c.Check(spList.Items[0].UUID, check.Equals, sp.UUID)
+       c.Check(spList.Items[0].Properties["foo"], check.Equals, "bar")
+
+       anonCtx := context.WithValue(context.Background(), contextKeyTestTokens, []string{arvadostest.AnonymousToken})
+       spList, err = s.conn.SpecimenList(anonCtx, arvados.ListOptions{Limit: -1, Filters: []arvados.Filter{{"uuid", "=", sp.UUID}}})
+       c.Check(spList.ItemsAvailable, check.Equals, 0)
+       c.Check(spList.Items, check.HasLen, 0)
+
+       spDel, err := s.conn.SpecimenDelete(s.ctx, arvados.DeleteOptions{UUID: sp.UUID})
+       c.Check(spDel.UUID, check.Equals, sp.UUID)
+}
index ae7f138b1b6862ab43022ed91b0fbdd360b3dc36..edc5fd117de33f4b96dd3fa53f815ab382b13bf1 100644 (file)
@@ -5,6 +5,7 @@
 package controller
 
 import (
+       "context"
        "net/http"
        "os"
        "path/filepath"
@@ -36,6 +37,8 @@ func newServerFromIntegrationTestEnv(c *check.C) *httpserver.Server {
        handler := &Handler{Cluster: &arvados.Cluster{
                ClusterID:  "zzzzz",
                PostgreSQL: integrationTestCluster().PostgreSQL,
+
+               EnableBetaController14287: enableBetaController14287,
        }}
        handler.Cluster.TLS.Insecure = true
        arvadostest.SetServiceURL(&handler.Cluster.Services.RailsAPI, "https://"+os.Getenv("ARVADOS_TEST_API_HOST"))
@@ -43,7 +46,9 @@ func newServerFromIntegrationTestEnv(c *check.C) *httpserver.Server {
 
        srv := &httpserver.Server{
                Server: http.Server{
-                       Handler: httpserver.AddRequestIDs(httpserver.LogRequests(log, handler)),
+                       Handler: httpserver.HandlerWithContext(
+                               ctxlog.Context(context.Background(), log),
+                               httpserver.AddRequestIDs(httpserver.LogRequests(handler))),
                },
                Addr: ":",
        }
index daf7977ad50c5af0380ee44282bf81ff439e4d3b..e817a0cc7b65459ac753394aae0c3eb503303e59 100644 (file)
@@ -74,7 +74,7 @@ func (suite *IntegrationSuite) TestGetLockUnlockCancel(c *check.C) {
                        defer wg.Done()
                        err := cq.Unlock(uuid)
                        c.Check(err, check.NotNil)
-                       c.Check(err, check.ErrorMatches, ".*cannot unlock when Queued*.")
+                       c.Check(err, check.ErrorMatches, ".*cannot unlock when Queued.*")
 
                        err = cq.Lock(uuid)
                        c.Check(err, check.IsNil)
index 07fdc672282d5475308a9f010555f9a0efc241c6..4a5ca3017afdab64ac288fcfbc7147dcf90e3027 100644 (file)
@@ -107,7 +107,7 @@ func (s *DispatcherSuite) TearDownTest(c *check.C) {
 // a fake queue and cloud driver. The fake cloud driver injects
 // artificial errors in order to exercise a variety of code paths.
 func (s *DispatcherSuite) TestDispatchToStubDriver(c *check.C) {
-       drivers["test"] = s.stubDriver
+       Drivers["test"] = s.stubDriver
        s.disp.setupOnce.Do(s.disp.initialize)
        queue := &test.Queue{
                ChooseType: func(ctr *arvados.Container) (arvados.InstanceType, error) {
@@ -211,7 +211,7 @@ func (s *DispatcherSuite) TestDispatchToStubDriver(c *check.C) {
 
 func (s *DispatcherSuite) TestAPIPermissions(c *check.C) {
        s.cluster.ManagementToken = "abcdefgh"
-       drivers["test"] = s.stubDriver
+       Drivers["test"] = s.stubDriver
        s.disp.setupOnce.Do(s.disp.initialize)
        s.disp.queue = &test.Queue{}
        go s.disp.run()
@@ -233,7 +233,7 @@ func (s *DispatcherSuite) TestAPIPermissions(c *check.C) {
 
 func (s *DispatcherSuite) TestAPIDisabled(c *check.C) {
        s.cluster.ManagementToken = ""
-       drivers["test"] = s.stubDriver
+       Drivers["test"] = s.stubDriver
        s.disp.setupOnce.Do(s.disp.initialize)
        s.disp.queue = &test.Queue{}
        go s.disp.run()
@@ -252,7 +252,7 @@ func (s *DispatcherSuite) TestAPIDisabled(c *check.C) {
 func (s *DispatcherSuite) TestInstancesAPI(c *check.C) {
        s.cluster.ManagementToken = "abcdefgh"
        s.cluster.Containers.CloudVMs.TimeoutBooting = arvados.Duration(time.Second)
-       drivers["test"] = s.stubDriver
+       Drivers["test"] = s.stubDriver
        s.disp.setupOnce.Do(s.disp.initialize)
        s.disp.queue = &test.Queue{}
        go s.disp.run()
index 6162c81b63bc4589df0d4beb3bbe6bc35a4330e8..f1ae68c001b24e7c8c613db9680105b57889edb1 100644 (file)
@@ -17,13 +17,16 @@ import (
        "golang.org/x/crypto/ssh"
 )
 
-var drivers = map[string]cloud.Driver{
+// Map of available cloud drivers.
+// Clusters.*.Containers.CloudVMs.Driver configuration values
+// correspond to keys in this map.
+var Drivers = map[string]cloud.Driver{
        "azure": azure.Driver,
        "ec2":   ec2.Driver,
 }
 
 func newInstanceSet(cluster *arvados.Cluster, setID cloud.InstanceSetID, logger logrus.FieldLogger, reg *prometheus.Registry) (cloud.InstanceSet, error) {
-       driver, ok := drivers[cluster.Containers.CloudVMs.Driver]
+       driver, ok := Drivers[cluster.Containers.CloudVMs.Driver]
        if !ok {
                return nil, fmt.Errorf("unsupported cloud driver %q", cluster.Containers.CloudVMs.Driver)
        }
@@ -85,7 +88,7 @@ func (is defaultTaggingInstanceSet) Create(it arvados.InstanceType, image cloud.
        return is.InstanceSet.Create(it, image, allTags, init, pk)
 }
 
-// Filters the instances returned by the wrapped InstanceSet's
+// Filter the instances returned by the wrapped InstanceSet's
 // Instances() method (in case the wrapped InstanceSet didn't do this
 // itself).
 type filteringInstanceSet struct {
index feed1c2a78b82a84821f22eee99e39e960dbd431..d608763cf5100d46ca0b1cb2b50a6759a157b1e3 100644 (file)
@@ -173,14 +173,13 @@ func (exr *Executor) sshClient(create bool) (*ssh.Client, error) {
        return exr.client, exr.clientErr
 }
 
-// Create a new SSH client.
-func (exr *Executor) setupSSHClient() (*ssh.Client, error) {
-       target := exr.Target()
-       addr := target.Address()
+func (exr *Executor) TargetHostPort() (string, string) {
+       addr := exr.Target().Address()
        if addr == "" {
-               return nil, errors.New("instance has no address")
+               return "", ""
        }
-       if h, p, err := net.SplitHostPort(addr); err != nil || p == "" {
+       h, p, err := net.SplitHostPort(addr)
+       if err != nil || p == "" {
                // Target address does not specify a port.  Use
                // targetPort, or "ssh".
                if h == "" {
@@ -189,11 +188,19 @@ func (exr *Executor) setupSSHClient() (*ssh.Client, error) {
                if p = exr.targetPort; p == "" {
                        p = "ssh"
                }
-               addr = net.JoinHostPort(h, p)
+       }
+       return h, p
+}
+
+// Create a new SSH client.
+func (exr *Executor) setupSSHClient() (*ssh.Client, error) {
+       addr := net.JoinHostPort(exr.TargetHostPort())
+       if addr == ":" {
+               return nil, errors.New("instance has no address")
        }
        var receivedKey ssh.PublicKey
        client, err := ssh.Dial("tcp", addr, &ssh.ClientConfig{
-               User: target.RemoteUser(),
+               User: exr.Target().RemoteUser(),
                Auth: []ssh.AuthMethod{
                        ssh.PublicKeys(exr.signers...),
                },
@@ -210,7 +217,7 @@ func (exr *Executor) setupSSHClient() (*ssh.Client, error) {
        }
 
        if exr.hostKey == nil || !bytes.Equal(exr.hostKey.Marshal(), receivedKey.Marshal()) {
-               err = target.VerifyHostKey(receivedKey, client)
+               err = exr.Target().VerifyHostKey(receivedKey, client)
                if err != nil {
                        return nil, err
                }
index 97ca7f60a2a916fd6feece03e016c7fe3673e22d..13b7fd29343772201db02f3a0937df89074adc99 100644 (file)
@@ -292,7 +292,7 @@ func (wp *Pool) Create(it arvados.InstanceType) bool {
                        wp.tagKeyPrefix + tagKeyIdleBehavior:   string(IdleBehaviorRun),
                        wp.tagKeyPrefix + tagKeyInstanceSecret: secret,
                }
-               initCmd := cloud.InitCommand(fmt.Sprintf("umask 0177 && echo -n %q >%s", secret, instanceSecretFilename))
+               initCmd := TagVerifier{nil, secret}.InitCommand()
                inst, err := wp.instanceSet.Create(it, wp.imageID, tags, initCmd, wp.installPublicKey)
                wp.mtx.Lock()
                defer wp.mtx.Unlock()
@@ -346,7 +346,7 @@ func (wp *Pool) SetIdleBehavior(id cloud.InstanceID, idleBehavior IdleBehavior)
 // Caller must have lock.
 func (wp *Pool) updateWorker(inst cloud.Instance, it arvados.InstanceType) (*worker, bool) {
        secret := inst.Tags()[wp.tagKeyPrefix+tagKeyInstanceSecret]
-       inst = tagVerifier{inst, secret}
+       inst = TagVerifier{inst, secret}
        id := inst.ID()
        if wkr := wp.workers[id]; wkr != nil {
                wkr.executor.SetTarget(inst)
index 330071951425c1c382f8be4e53f436d758d032f6..c718702101bdcf08502c252f3c59e7d88eeb6735 100644 (file)
@@ -21,13 +21,17 @@ var (
        instanceSecretLength   = 40 // hex digits
 )
 
-type tagVerifier struct {
+type TagVerifier struct {
        cloud.Instance
-       secret string
+       Secret string
 }
 
-func (tv tagVerifier) VerifyHostKey(pubKey ssh.PublicKey, client *ssh.Client) error {
-       if err := tv.Instance.VerifyHostKey(pubKey, client); err != cloud.ErrNotImplemented || tv.secret == "" {
+func (tv TagVerifier) InitCommand() cloud.InitCommand {
+       return cloud.InitCommand(fmt.Sprintf("umask 0177 && echo -n %q >%s", tv.Secret, instanceSecretFilename))
+}
+
+func (tv TagVerifier) VerifyHostKey(pubKey ssh.PublicKey, client *ssh.Client) error {
+       if err := tv.Instance.VerifyHostKey(pubKey, client); err != cloud.ErrNotImplemented || tv.Secret == "" {
                // If the wrapped instance indicates it has a way to
                // verify the key, return that decision.
                return err
@@ -49,7 +53,7 @@ func (tv tagVerifier) VerifyHostKey(pubKey ssh.PublicKey, client *ssh.Client) er
        if err != nil {
                return err
        }
-       if stdout.String() != tv.secret {
+       if stdout.String() != tv.Secret {
                return errBadInstanceSecret
        }
        return nil
index 94021163e469fd87c6eb58dc29041ba00b95b65a..b6737bc553d61258373d578fdac416452105ec43 100644 (file)
@@ -10,7 +10,6 @@ import (
        "flag"
        "fmt"
        "io"
-       "io/ioutil"
        "net"
        "net/http"
        "net/url"
@@ -62,20 +61,25 @@ func (c *command) RunCommand(prog string, args []string, stdin io.Reader, stdout
                        log.WithError(err).Info("exiting")
                }
        }()
+
        flags := flag.NewFlagSet("", flag.ContinueOnError)
        flags.SetOutput(stderr)
-       configFile := flags.String("config", arvados.DefaultConfigFile, "Site configuration `file`")
+
+       loader := config.NewLoader(stdin, log)
+       loader.SetupFlags(flags)
+       versionFlag := flags.Bool("version", false, "Write version information to stdout and exit 0")
+
        err = flags.Parse(args)
        if err == flag.ErrHelp {
                err = nil
                return 0
        } else if err != nil {
                return 2
+       } else if *versionFlag {
+               return cmd.Version.RunCommand(prog, args, stdin, stdout, stderr)
        }
-       // Logged warnings are discarded for now: the config template
-       // is incomplete, which causes extra warnings about keys that
-       // are really OK.
-       cfg, err := config.LoadFile(*configFile, ctxlog.New(ioutil.Discard, "json", "error"))
+
+       cfg, err := loader.Load()
        if err != nil {
                return 1
        }
@@ -116,7 +120,8 @@ func (c *command) RunCommand(prog string, args []string, stdin io.Reader, stdout
        }
        srv := &httpserver.Server{
                Server: http.Server{
-                       Handler: httpserver.AddRequestIDs(httpserver.LogRequests(log, handler)),
+                       Handler: httpserver.HandlerWithContext(ctx,
+                               httpserver.AddRequestIDs(httpserver.LogRequests(handler))),
                },
                Addr: listen,
        }
index 252ca57d47bb30ad1834e7070b3cfdf1e0ffdbb1..c1ea586b0475db4c5b1a780fea5eab11bb02d11f 100644 (file)
@@ -133,10 +133,10 @@ class CollectionFsAccess(cwltool.stdfsaccess.StdFsAccess):
         patternsegments = rest.split("/")
         return sorted(self._match(collection, patternsegments, "keep:" + collection.manifest_locator()))
 
-    def open(self, fn, mode):
+    def open(self, fn, mode, encoding=None):
         collection, rest = self.get_collection(fn)
         if collection is not None:
-            return collection.open(rest, mode)
+            return collection.open(rest, mode, encoding=encoding)
         else:
             return super(CollectionFsAccess, self).open(self._abs(fn), mode)
 
@@ -225,7 +225,7 @@ class CollectionFetcher(DefaultFetcher):
 
     def fetch_text(self, url):
         if url.startswith("keep:"):
-            with self.fsaccess.open(url, "r") as f:
+            with self.fsaccess.open(url, "r", encoding="utf-8") as f:
                 return f.read()
         if url.startswith("arvwf:"):
             record = self.api_client.workflows().get(uuid=url[6:]).execute(num_retries=self.num_retries)
index ceea719efe3f3f8a539dd9c9079d67bb93b588e4..b59df35c777ee1da5d125ca8504742f7fbaff04b 100644 (file)
@@ -14,6 +14,11 @@ README = os.path.join(SETUP_DIR, 'README.rst')
 
 import arvados_version
 version = arvados_version.get_version(SETUP_DIR, "arvados_cwl")
+if os.environ.get('ARVADOS_BUILDING_VERSION', False):
+    pysdk_dep = "=={}".format(version)
+else:
+    # On dev releases, arvados-python-client may have a different timestamp
+    pysdk_dep = "<={}".format(version)
 
 setup(name='arvados-cwl-runner',
       version=version,
@@ -37,7 +42,7 @@ setup(name='arvados-cwl-runner',
           'schema-salad==4.2.20190417121603',
           'typing >= 3.6.4',
           'ruamel.yaml >=0.15.54, <=0.15.77',
-          'arvados-python-client>=1.3.0.20190205182514',
+          'arvados-python-client{}'.format(pysdk_dep),
           'setuptools',
           'ciso8601 >= 2.0.0',
           'networkx < 2.3'
diff --git a/sdk/go/arvados/api.go b/sdk/go/arvados/api.go
new file mode 100644 (file)
index 0000000..7126575
--- /dev/null
@@ -0,0 +1,105 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package arvados
+
+import "context"
+
+type APIEndpoint struct {
+       Method string
+       Path   string
+       // "new attributes" key for create/update requests
+       AttrsKey string
+}
+
+var (
+       EndpointCollectionCreate              = APIEndpoint{"POST", "arvados/v1/collections", "collection"}
+       EndpointCollectionUpdate              = APIEndpoint{"PATCH", "arvados/v1/collections/:uuid", "collection"}
+       EndpointCollectionGet                 = APIEndpoint{"GET", "arvados/v1/collections/:uuid", ""}
+       EndpointCollectionList                = APIEndpoint{"GET", "arvados/v1/collections", ""}
+       EndpointCollectionProvenance          = APIEndpoint{"GET", "arvados/v1/collections/:uuid/provenance", ""}
+       EndpointCollectionUsedBy              = APIEndpoint{"GET", "arvados/v1/collections/:uuid/used_by", ""}
+       EndpointCollectionDelete              = APIEndpoint{"DELETE", "arvados/v1/collections/:uuid", ""}
+       EndpointCollectionTrash               = APIEndpoint{"POST", "arvados/v1/collections/:uuid/trash", ""}
+       EndpointCollectionUntrash             = APIEndpoint{"POST", "arvados/v1/collections/:uuid/untrash", ""}
+       EndpointSpecimenCreate                = APIEndpoint{"POST", "arvados/v1/specimens", "specimen"}
+       EndpointSpecimenUpdate                = APIEndpoint{"PATCH", "arvados/v1/specimens/:uuid", "specimen"}
+       EndpointSpecimenGet                   = APIEndpoint{"GET", "arvados/v1/specimens/:uuid", ""}
+       EndpointSpecimenList                  = APIEndpoint{"GET", "arvados/v1/specimens", ""}
+       EndpointSpecimenDelete                = APIEndpoint{"DELETE", "arvados/v1/specimens/:uuid", ""}
+       EndpointContainerCreate               = APIEndpoint{"POST", "arvados/v1/containers", "container"}
+       EndpointContainerUpdate               = APIEndpoint{"PATCH", "arvados/v1/containers/:uuid", "container"}
+       EndpointContainerGet                  = APIEndpoint{"GET", "arvados/v1/containers/:uuid", ""}
+       EndpointContainerList                 = APIEndpoint{"GET", "arvados/v1/containers", ""}
+       EndpointContainerDelete               = APIEndpoint{"DELETE", "arvados/v1/containers/:uuid", ""}
+       EndpointContainerLock                 = APIEndpoint{"POST", "arvados/v1/containers/:uuid/lock", ""}
+       EndpointContainerUnlock               = APIEndpoint{"POST", "arvados/v1/containers/:uuid/unlock", ""}
+       EndpointAPIClientAuthorizationCurrent = APIEndpoint{"GET", "arvados/v1/api_client_authorizations/current", ""}
+)
+
+type GetOptions struct {
+       UUID         string   `json:"uuid"`
+       Select       []string `json:"select"`
+       IncludeTrash bool     `json:"include_trash"`
+}
+
+type UntrashOptions struct {
+       UUID             string `json:"uuid"`
+       EnsureUniqueName bool   `json:"ensure_unique_name"`
+}
+
+type ListOptions struct {
+       ClusterID          string                 `json:"cluster_id"`
+       Select             []string               `json:"select"`
+       Filters            []Filter               `json:"filters"`
+       Where              map[string]interface{} `json:"where"`
+       Limit              int                    `json:"limit"`
+       Offset             int                    `json:"offset"`
+       Order              []string               `json:"order"`
+       Distinct           bool                   `json:"distinct"`
+       Count              string                 `json:"count"`
+       IncludeTrash       bool                   `json:"include_trash"`
+       IncludeOldVersions bool                   `json:"include_old_versions"`
+}
+
+type CreateOptions struct {
+       ClusterID        string                 `json:"cluster_id"`
+       EnsureUniqueName bool                   `json:"ensure_unique_name"`
+       Select           []string               `json:"select"`
+       Attrs            map[string]interface{} `json:"attrs"`
+}
+
+type UpdateOptions struct {
+       UUID  string                 `json:"uuid"`
+       Attrs map[string]interface{} `json:"attrs"`
+}
+
+type DeleteOptions struct {
+       UUID string `json:"uuid"`
+}
+
+type API interface {
+       CollectionCreate(ctx context.Context, options CreateOptions) (Collection, error)
+       CollectionUpdate(ctx context.Context, options UpdateOptions) (Collection, error)
+       CollectionGet(ctx context.Context, options GetOptions) (Collection, error)
+       CollectionList(ctx context.Context, options ListOptions) (CollectionList, error)
+       CollectionProvenance(ctx context.Context, options GetOptions) (map[string]interface{}, error)
+       CollectionUsedBy(ctx context.Context, options GetOptions) (map[string]interface{}, error)
+       CollectionDelete(ctx context.Context, options DeleteOptions) (Collection, error)
+       CollectionTrash(ctx context.Context, options DeleteOptions) (Collection, error)
+       CollectionUntrash(ctx context.Context, options UntrashOptions) (Collection, error)
+       ContainerCreate(ctx context.Context, options CreateOptions) (Container, error)
+       ContainerUpdate(ctx context.Context, options UpdateOptions) (Container, error)
+       ContainerGet(ctx context.Context, options GetOptions) (Container, error)
+       ContainerList(ctx context.Context, options ListOptions) (ContainerList, error)
+       ContainerDelete(ctx context.Context, options DeleteOptions) (Container, error)
+       ContainerLock(ctx context.Context, options GetOptions) (Container, error)
+       ContainerUnlock(ctx context.Context, options GetOptions) (Container, error)
+       SpecimenCreate(ctx context.Context, options CreateOptions) (Specimen, error)
+       SpecimenUpdate(ctx context.Context, options UpdateOptions) (Specimen, error)
+       SpecimenGet(ctx context.Context, options GetOptions) (Specimen, error)
+       SpecimenList(ctx context.Context, options ListOptions) (SpecimenList, error)
+       SpecimenDelete(ctx context.Context, options DeleteOptions) (Specimen, error)
+       APIClientAuthorizationCurrent(ctx context.Context, options GetOptions) (APIClientAuthorization, error)
+}
index 17cff235db82fba55fa12c6ff08fe0a114dff27b..7c17cdef04debdf4540b6c2de761673c34d01883 100644 (file)
@@ -6,10 +6,10 @@ package arvados
 
 // APIClientAuthorization is an arvados#apiClientAuthorization resource.
 type APIClientAuthorization struct {
-       UUID      string   `json:"uuid,omitempty"`
-       APIToken  string   `json:"api_token,omitempty"`
-       ExpiresAt string   `json:"expires_at,omitempty"`
-       Scopes    []string `json:"scopes,omitempty"`
+       UUID      string   `json:"uuid"`
+       APIToken  string   `json:"api_token"`
+       ExpiresAt string   `json:"expires_at"`
+       Scopes    []string `json:"scopes"`
 }
 
 // APIClientAuthorizationList is an arvados#apiClientAuthorizationList resource.
index cbc2ca72f035f150fce46613fa015d299a9bbd7b..a5815987b192a86c9ee646205bcc9ea0f7986dcc 100644 (file)
@@ -13,7 +13,6 @@ import (
        "io"
        "io/ioutil"
        "log"
-       "math"
        "net/http"
        "net/url"
        "os"
@@ -35,6 +34,9 @@ type Client struct {
        // DefaultSecureClient or InsecureHTTPClient will be used.
        Client *http.Client `json:"-"`
 
+       // Protocol scheme: "http", "https", or "" (https)
+       Scheme string
+
        // Hostname (or host:port) of Arvados API server.
        APIHost string
 
@@ -79,6 +81,7 @@ func NewClientFromConfig(cluster *Cluster) (*Client, error) {
                return nil, fmt.Errorf("no host in config Services.Controller.ExternalURL: %v", ctrlURL)
        }
        return &Client{
+               Scheme:   ctrlURL.Scheme,
                APIHost:  ctrlURL.Host,
                Insecure: cluster.TLS.Insecure,
        }, nil
@@ -105,6 +108,7 @@ func NewClientFromEnv() *Client {
                insecure = true
        }
        return &Client{
+               Scheme:          "https",
                APIHost:         os.Getenv("ARVADOS_API_HOST"),
                AuthToken:       os.Getenv("ARVADOS_API_TOKEN"),
                Insecure:        insecure,
@@ -117,12 +121,17 @@ var reqIDGen = httpserver.IDGenerator{Prefix: "req-"}
 // Do adds Authorization and X-Request-Id headers and then calls
 // (*http.Client)Do().
 func (c *Client) Do(req *http.Request) (*http.Response, error) {
-       if c.AuthToken != "" {
+       if auth, _ := req.Context().Value(contextKeyAuthorization{}).(string); auth != "" {
+               req.Header.Add("Authorization", auth)
+       } else if c.AuthToken != "" {
                req.Header.Add("Authorization", "OAuth2 "+c.AuthToken)
        }
 
        if req.Header.Get("X-Request-Id") == "" {
-               reqid, _ := c.context().Value(contextKeyRequestID).(string)
+               reqid, _ := req.Context().Value(contextKeyRequestID{}).(string)
+               if reqid == "" {
+                       reqid, _ = c.context().Value(contextKeyRequestID{}).(string)
+               }
                if reqid == "" {
                        reqid = reqIDGen.Next()
                }
@@ -178,7 +187,9 @@ func anythingToValues(params interface{}) (url.Values, error) {
                return nil, err
        }
        var generic map[string]interface{}
-       err = json.Unmarshal(j, &generic)
+       dec := json.NewDecoder(bytes.NewBuffer(j))
+       dec.UseNumber()
+       err = dec.Decode(&generic)
        if err != nil {
                return nil, err
        }
@@ -188,21 +199,29 @@ func anythingToValues(params interface{}) (url.Values, error) {
                        urlValues.Set(k, v)
                        continue
                }
-               if v, ok := v.(float64); ok {
-                       // Unmarshal decodes all numbers as float64,
-                       // which can be written as 1.2345e4 in JSON,
-                       // but this form is not accepted for ints in
-                       // url params. If a number fits in an int64,
-                       // encode it as int64 rather than float64.
-                       if v, frac := math.Modf(v); frac == 0 && v <= math.MaxInt64 && v >= math.MinInt64 {
-                               urlValues.Set(k, fmt.Sprintf("%d", int64(v)))
-                               continue
+               if v, ok := v.(json.Number); ok {
+                       urlValues.Set(k, v.String())
+                       continue
+               }
+               if v, ok := v.(bool); ok {
+                       if v {
+                               urlValues.Set(k, "true")
+                       } else {
+                               // "foo=false", "foo=0", and "foo="
+                               // are all taken as true strings, so
+                               // don't send false values at all --
+                               // rely on the default being false.
                        }
+                       continue
                }
                j, err := json.Marshal(v)
                if err != nil {
                        return nil, err
                }
+               if bytes.Equal(j, []byte("null")) {
+                       // don't add it to urlValues at all
+                       continue
+               }
                urlValues.Set(k, string(j))
        }
        return urlValues, nil
@@ -216,6 +235,10 @@ func anythingToValues(params interface{}) (url.Values, error) {
 //
 // path must not contain a query string.
 func (c *Client) RequestAndDecode(dst interface{}, method, path string, body io.Reader, params interface{}) error {
+       return c.RequestAndDecodeContext(c.context(), dst, method, path, body, params)
+}
+
+func (c *Client) RequestAndDecodeContext(ctx context.Context, dst interface{}, method, path string, body io.Reader, params interface{}) error {
        if body, ok := body.(io.Closer); ok {
                // Ensure body is closed even if we error out early
                defer body.Close()
@@ -243,6 +266,7 @@ func (c *Client) RequestAndDecode(dst interface{}, method, path string, body io.
        if err != nil {
                return err
        }
+       req = req.WithContext(ctx)
        req.Header.Set("Content-type", "application/x-www-form-urlencoded")
        return c.DoAndDecode(dst, req)
 }
@@ -265,13 +289,13 @@ func (c *Client) UpdateBody(rsc resource) io.Reader {
        return bytes.NewBufferString(v.Encode())
 }
 
-type contextKey string
-
-var contextKeyRequestID contextKey = "X-Request-Id"
-
+// WithRequestID returns a new shallow copy of c that sends the given
+// X-Request-Id value (instead of a new randomly generated one) with
+// each subsequent request that doesn't provide its own via context or
+// header.
 func (c *Client) WithRequestID(reqid string) *Client {
        cc := *c
-       cc.ctx = context.WithValue(cc.context(), contextKeyRequestID, reqid)
+       cc.ctx = ContextWithRequestID(cc.context(), reqid)
        return &cc
 }
 
@@ -294,7 +318,11 @@ func (c *Client) httpClient() *http.Client {
 }
 
 func (c *Client) apiURL(path string) string {
-       return "https://" + c.APIHost + "/" + path
+       scheme := c.Scheme
+       if scheme == "" {
+               scheme = "https"
+       }
+       return scheme + "://" + c.APIHost + "/" + path
 }
 
 // DiscoveryDocument is the Arvados server's description of itself.
index 5b613006077baf2657b87b60820c05e7f4537963..5b919bea74a325173109c5f2cc12a56053116f56 100644 (file)
@@ -15,23 +15,25 @@ import (
 
 // Collection is an arvados#collection resource.
 type Collection struct {
-       UUID                      string     `json:"uuid,omitempty"`
-       OwnerUUID                 string     `json:"owner_uuid,omitempty"`
-       TrashAt                   *time.Time `json:"trash_at,omitempty"`
-       ManifestText              string     `json:"manifest_text"`
-       UnsignedManifestText      string     `json:"unsigned_manifest_text,omitempty"`
-       Name                      string     `json:"name,omitempty"`
-       CreatedAt                 *time.Time `json:"created_at,omitempty"`
-       ModifiedAt                *time.Time `json:"modified_at,omitempty"`
-       PortableDataHash          string     `json:"portable_data_hash,omitempty"`
-       ReplicationConfirmed      *int       `json:"replication_confirmed,omitempty"`
-       ReplicationConfirmedAt    *time.Time `json:"replication_confirmed_at,omitempty"`
-       ReplicationDesired        *int       `json:"replication_desired,omitempty"`
-       StorageClassesDesired     []string   `json:"storage_classes_desired,omitempty"`
-       StorageClassesConfirmed   []string   `json:"storage_classes_confirmed,omitempty"`
-       StorageClassesConfirmedAt *time.Time `json:"storage_classes_confirmed_at,omitempty"`
-       DeleteAt                  *time.Time `json:"delete_at,omitempty"`
-       IsTrashed                 bool       `json:"is_trashed,omitempty"`
+       UUID                      string                 `json:"uuid"`
+       Etag                      string                 `json:"etag"`
+       OwnerUUID                 string                 `json:"owner_uuid"`
+       TrashAt                   *time.Time             `json:"trash_at"`
+       ManifestText              string                 `json:"manifest_text"`
+       UnsignedManifestText      string                 `json:"unsigned_manifest_text"`
+       Name                      string                 `json:"name"`
+       CreatedAt                 *time.Time             `json:"created_at"`
+       ModifiedAt                *time.Time             `json:"modified_at"`
+       PortableDataHash          string                 `json:"portable_data_hash"`
+       ReplicationConfirmed      *int                   `json:"replication_confirmed"`
+       ReplicationConfirmedAt    *time.Time             `json:"replication_confirmed_at"`
+       ReplicationDesired        *int                   `json:"replication_desired"`
+       StorageClassesDesired     []string               `json:"storage_classes_desired"`
+       StorageClassesConfirmed   []string               `json:"storage_classes_confirmed"`
+       StorageClassesConfirmedAt *time.Time             `json:"storage_classes_confirmed_at"`
+       DeleteAt                  *time.Time             `json:"delete_at"`
+       IsTrashed                 bool                   `json:"is_trashed"`
+       Properties                map[string]interface{} `json:"properties"`
 }
 
 func (c Collection) resourceName() string {
@@ -73,7 +75,6 @@ func (c *Collection) SizedDigests() ([]SizedDigest, error) {
        return sds, scanner.Err()
 }
 
-// CollectionList is an arvados#collectionList resource.
 type CollectionList struct {
        Items          []Collection `json:"items"`
        ItemsAvailable int          `json:"items_available"`
index d7e92e6ed98ca1edb0131391d2891f096aa1d9d8..c8206c7da437c48ff963d563e976cc77cdb4ac3b 100644 (file)
@@ -9,11 +9,18 @@ import (
        "errors"
        "fmt"
        "net/url"
+       "os"
 
        "git.curoverse.com/arvados.git/sdk/go/config"
 )
 
-const DefaultConfigFile = "/etc/arvados/config.yml"
+var DefaultConfigFile = func() string {
+       if path := os.Getenv("ARVADOS_CONFIG"); path != "" {
+               return path
+       } else {
+               return "/etc/arvados/config.yml"
+       }
+}()
 
 type Config struct {
        Clusters map[string]Cluster
@@ -76,15 +83,20 @@ type Cluster struct {
                UnloggedAttributes []string
        }
        Collections struct {
-               BlobSigning           bool
-               BlobSigningKey        string
-               BlobSigningTTL        Duration
-               CollectionVersioning  bool
-               DefaultTrashLifetime  Duration
-               DefaultReplication    int
-               ManagedProperties     map[string]interface{}
+               BlobSigning          bool
+               BlobSigningKey       string
+               BlobSigningTTL       Duration
+               CollectionVersioning bool
+               DefaultTrashLifetime Duration
+               DefaultReplication   int
+               ManagedProperties    map[string]struct {
+                       Value     interface{}
+                       Function  string
+                       Protected bool
+               }
                PreserveVersionIfIdle Duration
                TrashSweepInterval    Duration
+               TrustAllContent       bool
        }
        Git struct {
                Repositories string
@@ -96,7 +108,7 @@ type Cluster struct {
        Mail struct {
                MailchimpAPIKey                string
                MailchimpListID                string
-               SendUserSetupNotificationEmail string
+               SendUserSetupNotificationEmail bool
                IssueReporterEmailFrom         string
                IssueReporterEmailTo           string
                SupportEmailAddress            string
@@ -113,6 +125,7 @@ type Cluster struct {
                Insecure    bool
        }
        Users struct {
+               AnonymousUserToken                    string
                AdminNotifierEmailFrom                string
                AutoAdminFirstUser                    bool
                AutoAdminUserWithEmail                string
@@ -135,16 +148,17 @@ type Cluster struct {
                ApplicationMimetypesWithViewIcon map[string]struct{}
                ArvadosDocsite                   string
                ArvadosPublicDataDocURL          string
+               DefaultOpenIdPrefix              string
                EnableGettingStartedPopup        bool
                EnablePublicProjectsPage         bool
                FileViewersConfigURL             string
                LogViewerMaxBytes                ByteSize
-               MultiSiteSearch                  bool
+               MultiSiteSearch                  string
+               ProfilingEnabled                 bool
                Repositories                     bool
                RepositoryCache                  string
                RunningJobLogRecordsToFetch      int
                SecretKeyBase                    string
-               SecretToken                      string
                ShowRecentCollectionsOnDashboard bool
                ShowUserAgreementInline          bool
                ShowUserNotifications            bool
@@ -155,10 +169,14 @@ type Cluster struct {
                        FormFieldTitle       string
                        FormFieldDescription string
                        Required             bool
+                       Position             int
+                       Options              map[string]struct{}
                }
                UserProfileFormMessage string
                VocabularyURL          string
        }
+
+       EnableBetaController14287 bool
 }
 
 type Services struct {
diff --git a/sdk/go/arvados/context.go b/sdk/go/arvados/context.go
new file mode 100644 (file)
index 0000000..6ecf85b
--- /dev/null
@@ -0,0 +1,24 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package arvados
+
+import (
+       "context"
+)
+
+type contextKeyRequestID struct{}
+type contextKeyAuthorization struct{}
+
+func ContextWithRequestID(ctx context.Context, reqid string) context.Context {
+       return context.WithValue(ctx, contextKeyRequestID{}, reqid)
+}
+
+// ContextWithAuthorization returns a child context that (when used
+// with (*Client)RequestAndDecodeContext) sends the given
+// Authorization header value instead of the Client's default
+// AuthToken.
+func ContextWithAuthorization(ctx context.Context, value string) context.Context {
+       return context.WithValue(ctx, contextKeyAuthorization{}, value)
+}
index 9a04855784a76eee88bb4119b1483fa3505b67f2..5329a5146ad75e616446be87c6755a61176d7b7f 100644 (file)
@@ -31,6 +31,10 @@ func (e TransactionError) Error() (s string) {
        return
 }
 
+func (e TransactionError) HTTPStatus() int {
+       return e.StatusCode
+}
+
 func newTransactionError(req *http.Request, resp *http.Response, buf []byte) *TransactionError {
        var e TransactionError
        if json.Unmarshal(buf, &e) != nil {
index 9ae0fc3a5f4dc2a1e674325b5c4d9f86f19e5afa..c8308aea59e94d06be2e94235b2f0bda4f16f8c0 100644 (file)
@@ -26,5 +26,4 @@ type keepClient interface {
 
 type apiClient interface {
        RequestAndDecode(dst interface{}, method, path string, body io.Reader, params interface{}) error
-       UpdateBody(rsc resource) io.Reader
 }
index 6644f4cfb8e93ef7d601e667cee21a9dbce5d39b..972b3979fcfa4dd7fdb3cde62a90eacd37b27c56 100644 (file)
@@ -131,7 +131,12 @@ func (fs *collectionFileSystem) Sync() error {
                UUID:         fs.uuid,
                ManifestText: txt,
        }
-       err = fs.RequestAndDecode(nil, "PUT", "arvados/v1/collections/"+fs.uuid, fs.UpdateBody(coll), map[string]interface{}{"select": []string{"uuid"}})
+       err = fs.RequestAndDecode(nil, "PUT", "arvados/v1/collections/"+fs.uuid, nil, map[string]interface{}{
+               "collection": map[string]string{
+                       "manifest_text": coll.ManifestText,
+               },
+               "select": []string{"uuid"},
+       })
        if err != nil {
                return fmt.Errorf("sync failed: update %s: %s", fs.uuid, err)
        }
index 49e7d675f8b5c6729b61d94d0f271f615c68e924..91b8222cdc631d6ac80bf005c3f07c84fe641b92 100644 (file)
@@ -118,20 +118,24 @@ func (s *SiteFSSuite) TestProjectReaddirAfterLoadOne(c *check.C) {
 }
 
 func (s *SiteFSSuite) TestSlashInName(c *check.C) {
-       badCollection := Collection{
-               Name:      "bad/collection",
-               OwnerUUID: fixtureAProjectUUID,
-       }
-       err := s.client.RequestAndDecode(&badCollection, "POST", "arvados/v1/collections", s.client.UpdateBody(&badCollection), nil)
+       var badCollection Collection
+       err := s.client.RequestAndDecode(&badCollection, "POST", "arvados/v1/collections", nil, map[string]interface{}{
+               "collection": map[string]string{
+                       "name":       "bad/collection",
+                       "owner_uuid": fixtureAProjectUUID,
+               },
+       })
        c.Assert(err, check.IsNil)
        defer s.client.RequestAndDecode(nil, "DELETE", "arvados/v1/collections/"+badCollection.UUID, nil, nil)
 
-       badProject := Group{
-               Name:       "bad/project",
-               GroupClass: "project",
-               OwnerUUID:  fixtureAProjectUUID,
-       }
-       err = s.client.RequestAndDecode(&badProject, "POST", "arvados/v1/groups", s.client.UpdateBody(&badProject), nil)
+       var badProject Group
+       err = s.client.RequestAndDecode(&badProject, "POST", "arvados/v1/groups", nil, map[string]interface{}{
+               "group": map[string]string{
+                       "name":        "bad/project",
+                       "group_class": "project",
+                       "owner_uuid":  fixtureAProjectUUID,
+               },
+       })
        c.Assert(err, check.IsNil)
        defer s.client.RequestAndDecode(nil, "DELETE", "arvados/v1/groups/"+badProject.UUID, nil, nil)
 
@@ -154,11 +158,13 @@ func (s *SiteFSSuite) TestProjectUpdatedByOther(c *check.C) {
        _, err = s.fs.Open("/home/A Project/oob")
        c.Check(err, check.NotNil)
 
-       oob := Collection{
-               Name:      "oob",
-               OwnerUUID: fixtureAProjectUUID,
-       }
-       err = s.client.RequestAndDecode(&oob, "POST", "arvados/v1/collections", s.client.UpdateBody(&oob), nil)
+       var oob Collection
+       err = s.client.RequestAndDecode(&oob, "POST", "arvados/v1/collections", nil, map[string]interface{}{
+               "collection": map[string]string{
+                       "name":       "oob",
+                       "owner_uuid": fixtureAProjectUUID,
+               },
+       })
        c.Assert(err, check.IsNil)
        defer s.client.RequestAndDecode(nil, "DELETE", "arvados/v1/collections/"+oob.UUID, nil, nil)
 
@@ -179,8 +185,13 @@ func (s *SiteFSSuite) TestProjectUpdatedByOther(c *check.C) {
        c.Check(err, check.IsNil)
 
        // Delete test.txt behind s.fs's back by updating the
-       // collection record with the old (empty) ManifestText.
-       err = s.client.RequestAndDecode(nil, "PATCH", "arvados/v1/collections/"+oob.UUID, s.client.UpdateBody(&oob), nil)
+       // collection record with an empty ManifestText.
+       err = s.client.RequestAndDecode(nil, "PATCH", "arvados/v1/collections/"+oob.UUID, nil, map[string]interface{}{
+               "collection": map[string]string{
+                       "manifest_text":      "",
+                       "portable_data_hash": "d41d8cd98f00b204e9800998ecf8427e+0",
+               },
+       })
        c.Assert(err, check.IsNil)
 
        err = project.Sync()
index 6b5718a6c740e69b0fd5c4fc8f19106c7dddef11..bf2fe72ff880b9a3885482bbd326aab4255c0691 100644 (file)
@@ -6,9 +6,9 @@ package arvados
 
 // Group is an arvados#group record
 type Group struct {
-       UUID       string `json:"uuid,omitempty"`
-       Name       string `json:"name,omitempty"`
-       OwnerUUID  string `json:"owner_uuid,omitempty"`
+       UUID       string `json:"uuid"`
+       Name       string `json:"name"`
+       OwnerUUID  string `json:"owner_uuid"`
        GroupClass string `json:"group_class"`
 }
 
index dee13556e1098f1788a36a55c49eb39df44c78a1..fbd699f30653035ff8c23ad1f62223c5ca54adc9 100644 (file)
@@ -7,13 +7,13 @@ package arvados
 // Link is an arvados#link record
 type Link struct {
        UUID      string `json:"uuid,omiempty"`
-       OwnerUUID string `json:"owner_uuid,omitempty"`
-       Name      string `json:"name,omitempty"`
-       LinkClass string `json:"link_class,omitempty"`
-       HeadUUID  string `json:"head_uuid,omitempty"`
-       HeadKind  string `json:"head_kind,omitempty"`
-       TailUUID  string `json:"tail_uuid,omitempty"`
-       TailKind  string `json:"tail_kind,omitempty"`
+       OwnerUUID string `json:"owner_uuid"`
+       Name      string `json:"name"`
+       LinkClass string `json:"link_class"`
+       HeadUUID  string `json:"head_uuid"`
+       HeadKind  string `json:"head_kind"`
+       TailUUID  string `json:"tail_uuid"`
+       TailKind  string `json:"tail_kind"`
 }
 
 // UserList is an arvados#userList resource.
index 6f72bf7c6c9f46d931aaafbe9c4f8594ac470984..6f72634e5457e7379ee6660297be9aced63b91a0 100644 (file)
@@ -10,14 +10,14 @@ import (
 
 // Log is an arvados#log record
 type Log struct {
-       ID              uint64                 `json:"id,omitempty"`
-       UUID            string                 `json:"uuid,omitempty"`
-       ObjectUUID      string                 `json:"object_uuid,omitempty"`
-       ObjectOwnerUUID string                 `json:"object_owner_uuid,omitempty"`
-       EventType       string                 `json:"event_type,omitempty"`
-       EventAt         *time.Time             `json:"event,omitempty"`
-       Properties      map[string]interface{} `json:"properties,omitempty"`
-       CreatedAt       *time.Time             `json:"created_at,omitempty"`
+       ID              uint64                 `json:"id"`
+       UUID            string                 `json:"uuid"`
+       ObjectUUID      string                 `json:"object_uuid"`
+       ObjectOwnerUUID string                 `json:"object_owner_uuid"`
+       EventType       string                 `json:"event_type"`
+       EventAt         *time.Time             `json:"event"`
+       Properties      map[string]interface{} `json:"properties"`
+       CreatedAt       *time.Time             `json:"created_at"`
 }
 
 // LogList is an arvados#logList resource.
index cc844fe8253c1684dcec9b006659b8a08e84e8cf..97466eb8ae8bc44863cf4711582268108772bbf6 100644 (file)
@@ -12,10 +12,10 @@ type Node struct {
        Domain     string         `json:"domain"`
        Hostname   string         `json:"hostname"`
        IPAddress  string         `json:"ip_address"`
-       LastPingAt *time.Time     `json:"last_ping_at,omitempty"`
+       LastPingAt *time.Time     `json:"last_ping_at"`
        SlotNumber int            `json:"slot_number"`
        Status     string         `json:"status"`
-       JobUUID    string         `json:"job_uuid,omitempty"`
+       JobUUID    string         `json:"job_uuid"`
        Properties NodeProperties `json:"properties"`
 }
 
index 14ce098cfc1a54f0f0de74aa9cf60ca8274a693e..d1a25c438a9eeb72e147cbd5658ee1bb340ee344 100644 (file)
@@ -4,7 +4,10 @@
 
 package arvados
 
-import "encoding/json"
+import (
+       "encoding/json"
+       "fmt"
+)
 
 // ResourceListParams expresses which results are requested in a
 // list/index API.
@@ -27,7 +30,35 @@ type Filter struct {
        Operand  interface{}
 }
 
-// MarshalJSON encodes a Filter in the form expected by the API.
+// MarshalJSON encodes a Filter to a JSON array.
 func (f *Filter) MarshalJSON() ([]byte, error) {
        return json.Marshal([]interface{}{f.Attr, f.Operator, f.Operand})
 }
+
+// UnmarshalJSON decodes a JSON array to a Filter.
+func (f *Filter) UnmarshalJSON(data []byte) error {
+       var elements []interface{}
+       err := json.Unmarshal(data, &elements)
+       if err != nil {
+               return err
+       }
+       if len(elements) != 3 {
+               return fmt.Errorf("invalid filter %q: must have 3 elements", data)
+       }
+       attr, ok := elements[0].(string)
+       if !ok {
+               return fmt.Errorf("invalid filter attr %q", elements[0])
+       }
+       op, ok := elements[1].(string)
+       if !ok {
+               return fmt.Errorf("invalid filter operator %q", elements[1])
+       }
+       operand := elements[2]
+       switch operand.(type) {
+       case string, float64, []interface{}, nil:
+       default:
+               return fmt.Errorf("invalid filter operand %q", elements[2])
+       }
+       *f = Filter{attr, op, operand}
+       return nil
+}
index 5642599b4c5664a16a7b1342887748863f9c2715..4e09c5375db980c60171f9209714c81f55aef152 100644 (file)
@@ -23,3 +23,14 @@ func TestMarshalFiltersWithNanoseconds(t *testing.T) {
                t.Errorf("Encoded as %q, expected %q", buf, expect)
        }
 }
+
+func TestMarshalFiltersWithNil(t *testing.T) {
+       buf, err := json.Marshal([]Filter{
+               {Attr: "modified_at", Operator: "=", Operand: nil}})
+       if err != nil {
+               t.Fatal(err)
+       }
+       if expect := []byte(`[["modified_at","=",null]]`); 0 != bytes.Compare(buf, expect) {
+               t.Errorf("Encoded as %q, expected %q", buf, expect)
+       }
+}
diff --git a/sdk/go/arvados/specimen.go b/sdk/go/arvados/specimen.go
new file mode 100644 (file)
index 0000000..e320ca2
--- /dev/null
@@ -0,0 +1,23 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package arvados
+
+import "time"
+
+type Specimen struct {
+       UUID       string                 `json:"uuid"`
+       OwnerUUID  string                 `json:"owner_uuid"`
+       CreatedAt  time.Time              `json:"created_at"`
+       ModifiedAt time.Time              `json:"modified_at"`
+       UpdatedAt  time.Time              `json:"updated_at"`
+       Properties map[string]interface{} `json:"properties"`
+}
+
+type SpecimenList struct {
+       Items          []Specimen `json:"items"`
+       ItemsAvailable int        `json:"items_available"`
+       Offset         int        `json:"offset"`
+       Limit          int        `json:"limit"`
+}
index 3a36e5ebaee6614ba6ae64be3f353bd3edef6dba..27d2b28a42b6c5c4312d0aa16624e8061103ac5d 100644 (file)
@@ -6,11 +6,11 @@ package arvados
 
 // User is an arvados#user record
 type User struct {
-       UUID     string `json:"uuid,omitempty"`
+       UUID     string `json:"uuid"`
        IsActive bool   `json:"is_active"`
        IsAdmin  bool   `json:"is_admin"`
-       Username string `json:"username,omitempty"`
-       Email    string `json:"email,omitempty"`
+       Username string `json:"username"`
+       Email    string `json:"email"`
 }
 
 // UserList is an arvados#userList resource.
index 09c8c71e8ef5e7c1c03c5f33aa6f3a6c923cf22d..5ddc8732df16b09ad81f21686b25f05cca9eb873 100644 (file)
@@ -8,13 +8,13 @@ import "time"
 
 // Workflow is an arvados#workflow resource.
 type Workflow struct {
-       UUID        string     `json:"uuid,omitempty"`
-       OwnerUUID   string     `json:"owner_uuid,omitempty"`
-       Name        string     `json:"name,omitempty"`
-       Description string     `json:"description,omitempty"`
-       Definition  string     `json:"definition,omitempty"`
-       CreatedAt   *time.Time `json:"created_at,omitempty"`
-       ModifiedAt  *time.Time `json:"modified_at,omitempty"`
+       UUID        string     `json:"uuid"`
+       OwnerUUID   string     `json:"owner_uuid"`
+       Name        string     `json:"name"`
+       Description string     `json:"description"`
+       Definition  string     `json:"definition"`
+       CreatedAt   *time.Time `json:"created_at"`
+       ModifiedAt  *time.Time `json:"modified_at"`
 }
 
 // WorkflowList is an arvados#workflowList resource.
diff --git a/sdk/go/arvadostest/api.go b/sdk/go/arvadostest/api.go
new file mode 100644 (file)
index 0000000..a3cacf3
--- /dev/null
@@ -0,0 +1,135 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package arvadostest
+
+import (
+       "context"
+       "errors"
+       "sync"
+
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
+)
+
+var ErrStubUnimplemented = errors.New("stub unimplemented")
+
+type APIStub struct {
+       // The error to return from every stubbed API method.
+       Error error
+       calls []APIStubCall
+       mtx   sync.Mutex
+}
+
+func (as *APIStub) CollectionCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Collection, error) {
+       as.appendCall(as.CollectionCreate, ctx, options)
+       return arvados.Collection{}, as.Error
+}
+func (as *APIStub) CollectionUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Collection, error) {
+       as.appendCall(as.CollectionUpdate, ctx, options)
+       return arvados.Collection{}, as.Error
+}
+func (as *APIStub) CollectionGet(ctx context.Context, options arvados.GetOptions) (arvados.Collection, error) {
+       as.appendCall(as.CollectionGet, ctx, options)
+       return arvados.Collection{}, as.Error
+}
+func (as *APIStub) CollectionList(ctx context.Context, options arvados.ListOptions) (arvados.CollectionList, error) {
+       as.appendCall(as.CollectionList, ctx, options)
+       return arvados.CollectionList{}, as.Error
+}
+func (as *APIStub) CollectionProvenance(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) {
+       as.appendCall(as.CollectionProvenance, ctx, options)
+       return nil, as.Error
+}
+func (as *APIStub) CollectionUsedBy(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) {
+       as.appendCall(as.CollectionUsedBy, ctx, options)
+       return nil, as.Error
+}
+func (as *APIStub) CollectionDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Collection, error) {
+       as.appendCall(as.CollectionDelete, ctx, options)
+       return arvados.Collection{}, as.Error
+}
+func (as *APIStub) CollectionTrash(ctx context.Context, options arvados.DeleteOptions) (arvados.Collection, error) {
+       as.appendCall(as.CollectionTrash, ctx, options)
+       return arvados.Collection{}, as.Error
+}
+func (as *APIStub) CollectionUntrash(ctx context.Context, options arvados.UntrashOptions) (arvados.Collection, error) {
+       as.appendCall(as.CollectionUntrash, ctx, options)
+       return arvados.Collection{}, as.Error
+}
+func (as *APIStub) ContainerCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Container, error) {
+       as.appendCall(as.ContainerCreate, ctx, options)
+       return arvados.Container{}, as.Error
+}
+func (as *APIStub) ContainerUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Container, error) {
+       as.appendCall(as.ContainerUpdate, ctx, options)
+       return arvados.Container{}, as.Error
+}
+func (as *APIStub) ContainerGet(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
+       as.appendCall(as.ContainerGet, ctx, options)
+       return arvados.Container{}, as.Error
+}
+func (as *APIStub) ContainerList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerList, error) {
+       as.appendCall(as.ContainerList, ctx, options)
+       return arvados.ContainerList{}, as.Error
+}
+func (as *APIStub) ContainerDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Container, error) {
+       as.appendCall(as.ContainerDelete, ctx, options)
+       return arvados.Container{}, as.Error
+}
+func (as *APIStub) ContainerLock(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
+       as.appendCall(as.ContainerLock, ctx, options)
+       return arvados.Container{}, as.Error
+}
+func (as *APIStub) ContainerUnlock(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) {
+       as.appendCall(as.ContainerUnlock, ctx, options)
+       return arvados.Container{}, as.Error
+}
+func (as *APIStub) SpecimenCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Specimen, error) {
+       as.appendCall(as.SpecimenCreate, ctx, options)
+       return arvados.Specimen{}, as.Error
+}
+func (as *APIStub) SpecimenUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Specimen, error) {
+       as.appendCall(as.SpecimenUpdate, ctx, options)
+       return arvados.Specimen{}, as.Error
+}
+func (as *APIStub) SpecimenGet(ctx context.Context, options arvados.GetOptions) (arvados.Specimen, error) {
+       as.appendCall(as.SpecimenGet, ctx, options)
+       return arvados.Specimen{}, as.Error
+}
+func (as *APIStub) SpecimenList(ctx context.Context, options arvados.ListOptions) (arvados.SpecimenList, error) {
+       as.appendCall(as.SpecimenList, ctx, options)
+       return arvados.SpecimenList{}, as.Error
+}
+func (as *APIStub) SpecimenDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Specimen, error) {
+       as.appendCall(as.SpecimenDelete, ctx, options)
+       return arvados.Specimen{}, as.Error
+}
+func (as *APIStub) APIClientAuthorizationCurrent(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) {
+       as.appendCall(as.APIClientAuthorizationCurrent, ctx, options)
+       return arvados.APIClientAuthorization{}, as.Error
+}
+
+func (as *APIStub) appendCall(method interface{}, ctx context.Context, options interface{}) {
+       as.mtx.Lock()
+       defer as.mtx.Unlock()
+       as.calls = append(as.calls, APIStubCall{method, ctx, options})
+}
+
+func (as *APIStub) Calls(method interface{}) []APIStubCall {
+       as.mtx.Lock()
+       defer as.mtx.Unlock()
+       var calls []APIStubCall
+       for _, call := range as.calls {
+               if method == nil || call.Method == method {
+                       calls = append(calls, call)
+               }
+       }
+       return calls
+}
+
+type APIStubCall struct {
+       Method  interface{}
+       Context context.Context
+       Options interface{}
+}
index 95b83265a05a4835363975cf8720157c0e9171da..be29bc23ef394df21ac95a19ea169873d83af4ca 100644 (file)
@@ -32,7 +32,7 @@ const (
        ASubprojectUUID = "zzzzz-j7d0g-axqo7eu9pwvna1x"
 
        FooAndBarFilesInDirUUID = "zzzzz-4zz18-foonbarfilesdir"
-       FooAndBarFilesInDirPDH  = "6bbac24198d09a93975f60098caf0bdf+62"
+       FooAndBarFilesInDirPDH  = "870369fc72738603c2fad16664e50e2d+58"
 
        Dispatch1Token    = "kwi8oowusvbutahacwk2geulqewy5oaqmpalczfna4b6bb0hfw"
        Dispatch1AuthUUID = "zzzzz-gj3su-k9dvestay1plssr"
@@ -55,6 +55,8 @@ const (
        FooCollectionSharingToken     = "iknqgmunrhgsyfok8uzjlwun9iscwm3xacmzmg65fa1j1lpdss"
 
        WorkflowWithDefinitionYAMLUUID = "zzzzz-7fd4e-validworkfloyml"
+
+       CollectionReplicationDesired2Confirmed2UUID = "zzzzz-4zz18-434zv1tnnf2rygp"
 )
 
 // PathologicalManifest : A valid manifest designed to test
index 3c266e0d3afda2254df6b3c7ccad7157a121bc6c..c2f6a0e8f0885e68a98f7e62a4ee4f17d0d930d2 100644 (file)
@@ -5,6 +5,7 @@
 package auth
 
 import (
+       "context"
        "encoding/base64"
        "net/http"
        "net/url"
@@ -19,8 +20,17 @@ func NewCredentials() *Credentials {
        return &Credentials{Tokens: []string{}}
 }
 
+func NewContext(ctx context.Context, c *Credentials) context.Context {
+       return context.WithValue(ctx, contextKeyCredentials{}, c)
+}
+
+func FromContext(ctx context.Context) (*Credentials, bool) {
+       c, ok := ctx.Value(contextKeyCredentials{}).(*Credentials)
+       return c, ok
+}
+
 func CredentialsFromRequest(r *http.Request) *Credentials {
-       if c, ok := r.Context().Value(contextKeyCredentials).(*Credentials); ok {
+       if c, ok := FromContext(r.Context()); ok {
                // preloaded by middleware
                return c
        }
index ad1fa5141a1cf268729e33bdef3cacc3fb14d76c..b638f7982516b431e15322adde0b55b0637c8a3f 100644 (file)
@@ -9,17 +9,15 @@ import (
        "net/http"
 )
 
-type contextKey string
-
-var contextKeyCredentials contextKey = "credentials"
+type contextKeyCredentials struct{}
 
 // LoadToken wraps the next handler, adding credentials to the request
 // context so subsequent handlers can access them efficiently via
 // CredentialsFromRequest.
 func LoadToken(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-               if _, ok := r.Context().Value(contextKeyCredentials).(*Credentials); !ok {
-                       r = r.WithContext(context.WithValue(r.Context(), contextKeyCredentials, CredentialsFromRequest(r)))
+               if _, ok := r.Context().Value(contextKeyCredentials{}).(*Credentials); !ok {
+                       r = r.WithContext(context.WithValue(r.Context(), contextKeyCredentials{}, CredentialsFromRequest(r)))
                }
                next.ServeHTTP(w, r)
        })
index 1ccf8c04782fbf57aedfe6cb20f75c50ef53cb9d..b222e18ea1159e67b9069c086207dbc3585c8e26 100644 (file)
@@ -14,10 +14,7 @@ type ErrorResponse struct {
 }
 
 func Error(w http.ResponseWriter, error string, code int) {
-       w.Header().Set("Content-Type", "application/json")
-       w.Header().Set("X-Content-Type-Options", "nosniff")
-       w.WriteHeader(code)
-       json.NewEncoder(w).Encode(ErrorResponse{Errors: []string{error}})
+       Errors(w, []string{error}, code)
 }
 
 func Errors(w http.ResponseWriter, errors []string, code int) {
index 357daee269f3784dd650107ef081d689cd3639de..f64708454c2b1e12cb5a75906d7d43676629cfb5 100644 (file)
@@ -9,6 +9,7 @@ import (
        "net/http"
        "time"
 
+       "git.curoverse.com/arvados.git/sdk/go/ctxlog"
        "git.curoverse.com/arvados.git/sdk/go/stats"
        "github.com/sirupsen/logrus"
 )
@@ -19,18 +20,23 @@ type contextKey struct {
 
 var (
        requestTimeContextKey = contextKey{"requestTime"}
-       loggerContextKey      = contextKey{"logger"}
 )
 
+// HandlerWithContext returns an http.Handler that changes the request
+// context to ctx (replacing http.Server's default
+// context.Background()), then calls next.
+func HandlerWithContext(ctx context.Context, next http.Handler) http.Handler {
+       return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+               next.ServeHTTP(w, r.WithContext(ctx))
+       })
+}
+
 // LogRequests wraps an http.Handler, logging each request and
-// response via logger.
-func LogRequests(logger logrus.FieldLogger, h http.Handler) http.Handler {
-       if logger == nil {
-               logger = logrus.StandardLogger()
-       }
+// response.
+func LogRequests(h http.Handler) http.Handler {
        return http.HandlerFunc(func(wrapped http.ResponseWriter, req *http.Request) {
                w := &responseTimer{ResponseWriter: WrapResponseWriter(wrapped)}
-               lgr := logger.WithFields(logrus.Fields{
+               lgr := ctxlog.FromContext(req.Context()).WithFields(logrus.Fields{
                        "RequestID":       req.Header.Get("X-Request-Id"),
                        "remoteAddr":      req.RemoteAddr,
                        "reqForwardedFor": req.Header.Get("X-Forwarded-For"),
@@ -42,7 +48,7 @@ func LogRequests(logger logrus.FieldLogger, h http.Handler) http.Handler {
                })
                ctx := req.Context()
                ctx = context.WithValue(ctx, &requestTimeContextKey, time.Now())
-               ctx = context.WithValue(ctx, &loggerContextKey, lgr)
+               ctx = ctxlog.Context(ctx, lgr)
                req = req.WithContext(ctx)
 
                logRequest(w, req, lgr)
@@ -52,11 +58,7 @@ func LogRequests(logger logrus.FieldLogger, h http.Handler) http.Handler {
 }
 
 func Logger(req *http.Request) logrus.FieldLogger {
-       if lgr, ok := req.Context().Value(&loggerContextKey).(logrus.FieldLogger); ok {
-               return lgr
-       } else {
-               return logrus.StandardLogger()
-       }
+       return ctxlog.FromContext(req.Context())
 }
 
 func logRequest(w *responseTimer, req *http.Request, lgr *logrus.Entry) {
index 8386db9276935c9dbd565ea657357b15302111cb..3b2bc7758069b44345b3da522b8f80cc303c52fe 100644 (file)
@@ -6,12 +6,14 @@ package httpserver
 
 import (
        "bytes"
+       "context"
        "encoding/json"
        "net/http"
        "net/http/httptest"
        "testing"
        "time"
 
+       "git.curoverse.com/arvados.git/sdk/go/ctxlog"
        "github.com/sirupsen/logrus"
        check "gopkg.in/check.v1"
 )
@@ -31,15 +33,19 @@ func (s *Suite) TestLogRequests(c *check.C) {
        log.Formatter = &logrus.JSONFormatter{
                TimestampFormat: time.RFC3339Nano,
        }
+       ctx := ctxlog.Context(context.Background(), log)
+
+       h := AddRequestIDs(LogRequests(
+               http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+                       w.Write([]byte("hello world"))
+               })))
 
-       h := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
-               w.Write([]byte("hello world"))
-       })
        req, err := http.NewRequest("GET", "https://foo.example/bar", nil)
        req.Header.Set("X-Forwarded-For", "1.2.3.4:12345")
        c.Assert(err, check.IsNil)
        resp := httptest.NewRecorder()
-       AddRequestIDs(LogRequests(log, h)).ServeHTTP(resp, req)
+
+       HandlerWithContext(ctx, h).ServeHTTP(resp, req)
 
        dec := json.NewDecoder(captured)
 
index 032093f8d8aab842a1a3af194089c1ee9b4980e4..fab6c3f11801a4a6ec52de6f1f86f17a438e246f 100644 (file)
@@ -104,7 +104,7 @@ func (m *metrics) ServeAPI(token string, next http.Handler) http.Handler {
 //
 // For the metrics to be accurate, the caller must ensure every
 // request passed to the Handler also passes through
-// LogRequests(logger, ...), and vice versa.
+// LogRequests(...), and vice versa.
 //
 // If registry is nil, a new registry is created.
 //
index ab610d65e71453ba8abc2287c5321a6a41dee217..c8dd09de86fbfeab54b32a71978ccbc7be026048 100644 (file)
@@ -551,7 +551,7 @@ func (kc *KeepClient) httpClient() HTTPClient {
                // It's not safe to copy *http.DefaultTransport
                // because it has a mutex (which might be locked)
                // protecting a private map (which might not be nil).
-               // So we build our own, using the Go 1.10 default
+               // So we build our own, using the Go 1.12 default
                // values, ignoring any changes the application has
                // made to http.DefaultTransport.
                Transport: &http.Transport{
@@ -563,7 +563,7 @@ func (kc *KeepClient) httpClient() HTTPClient {
                        MaxIdleConns:          100,
                        IdleConnTimeout:       90 * time.Second,
                        TLSHandshakeTimeout:   tlsTimeout,
-                       ExpectContinueTimeout: time.Second,
+                       ExpectContinueTimeout: 1 * time.Second,
                        TLSClientConfig:       arvadosclient.MakeTLSConfig(kc.Arvados.ApiInsecure),
                },
        }
index 9bc2cef60169b674a51d6ae3cffc24662aaa78df..063ca2625c094cddab39694718f3a111e7d84f07 100755 (executable)
@@ -17,6 +17,11 @@ README = os.path.join(SETUP_DIR, 'README.rst')
 
 import arvados_version
 version = arvados_version.get_version(SETUP_DIR, "arvados_pam")
+if os.environ.get('ARVADOS_BUILDING_VERSION', False):
+    pysdk_dep = "=={}".format(version)
+else:
+    # On dev releases, arvados-python-client may have a different timestamp
+    pysdk_dep = "<={}".format(version)
 
 short_tests_only = False
 if '--short-tests-only' in sys.argv:
@@ -44,7 +49,7 @@ setup(name='arvados-pam',
           ('share/doc/arvados-pam/examples', glob.glob('examples/*')),
       ],
       install_requires=[
-          'arvados-python-client>=0.1.20150801000000',
+          'arvados-python-client{}'.format(pysdk_dep),
       ],
       test_suite='tests',
       tests_require=['pbr<1.7.0', 'mock>=1.0', 'python-pam'],
index 1ef3b00c665e89c61aaa7853c7b0b455c944259a..a7b8bacdc340bcbd9c7286187fca1bfce3309d44 100644 (file)
@@ -8,7 +8,7 @@ events {
 }
 http {
   log_format customlog
-    '[$time_local] $server_name $status $body_bytes_sent $request_time $request_method "$scheme://$http_host$request_uri" $remote_addr:$remote_port '
+    '[$time_local] "$http_x_request_id" $server_name $status $body_bytes_sent $request_time $request_method "$scheme://$http_host$request_uri" $remote_addr:$remote_port '
     '"$http_referer" "$http_user_agent"';
   access_log "{{ACCESSLOG}}" customlog;
   client_body_temp_path "{{TMPDIR}}";
index 0edfab15183e1d848c5787620a5f1f9ab6e896dd..3063ea6473c8f1cd5d496d8a7c88ba3cf1b00a03 100644 (file)
@@ -409,9 +409,13 @@ def run_controller():
         f.write("""
 Clusters:
   zzzzz:
+    EnableBetaController14287: {beta14287}
     ManagementToken: e687950a23c3a9bceec28c6223a06c79
     API:
       RequestTimeout: 30s
+    Logging:
+        Level: "{loglevel}"
+    HTTPRequestTimeout: 30s
     PostgreSQL:
       ConnectionPool: 32
       Connection:
@@ -429,9 +433,11 @@ Clusters:
         InternalURLs:
           "https://localhost:{railsport}": {{}}
         """.format(
+            beta14287=('true' if '14287' in os.environ.get('ARVADOS_EXPERIMENTAL', '') else 'false'),
+            loglevel=('info' if os.environ.get('ARVADOS_DEBUG', '') in ['','0'] else 'debug'),
             dbhost=_dbconfig('host'),
-            dbname=_dbconfig('database'),
-            dbuser=_dbconfig('username'),
+            dbname=_dbconfig('dbname'),
+            dbuser=_dbconfig('user'),
             dbpass=_dbconfig('password'),
             controllerport=port,
             railsport=rails_api_port,
@@ -474,8 +480,8 @@ Postgres:
                    port,
                    ('info' if os.environ.get('ARVADOS_DEBUG', '') in ['','0'] else 'debug'),
                    _dbconfig('host'),
-                   _dbconfig('database'),
-                   _dbconfig('username'),
+                   _dbconfig('dbname'),
+                   _dbconfig('user'),
                    _dbconfig('password')))
     logf = open(_logfilename('ws'), 'a')
     ws = subprocess.Popen(
@@ -743,9 +749,14 @@ def _getport(program):
 def _dbconfig(key):
     global _cached_db_config
     if not _cached_db_config:
-        _cached_db_config = yaml.safe_load(open(os.path.join(
-            SERVICES_SRC_DIR, 'api', 'config', 'database.yml')))
-    return _cached_db_config['test'][key]
+        if "ARVADOS_CONFIG" in os.environ:
+            _cached_db_config = list(yaml.safe_load(open(os.environ["ARVADOS_CONFIG"]))["Clusters"].values())[0]["PostgreSQL"]["Connection"]
+        else:
+            _cached_db_config = yaml.safe_load(open(os.path.join(
+                SERVICES_SRC_DIR, 'api', 'config', 'database.yml')))["test"]
+            _cached_db_config["dbname"] = _cached_db_config["database"]
+            _cached_db_config["user"] = _cached_db_config["username"]
+    return _cached_db_config[key]
 
 def _apiconfig(key):
     global _cached_config
index e07a5aca79b5a310d320c6901df9c5a82326465a..b23515dda4528b1e7f44433a8fcfa006d7ee3486 100644 (file)
@@ -140,8 +140,13 @@ class ApplicationController < ActionController::Base
 
   def render_error(e)
     logger.error e.inspect
-    if !e.is_a? RequestError and (e.respond_to? :backtrace and e.backtrace)
-      logger.error e.backtrace.collect { |x| x + "\n" }.join('')
+    if e.respond_to? :backtrace and e.backtrace
+      # This will be cleared by lograge after adding it to the log.
+      # Usually lograge would get the exceptions, but in our case we're catching
+      # all of them with exception handlers that cannot re-raise them because they
+      # don't get propagated.
+      Thread.current[:exception] = e.inspect
+      Thread.current[:backtrace] = e.backtrace.collect { |x| x + "\n" }.join('')
     end
     if (@object.respond_to? :errors and
         @object.errors.andand.full_messages.andand.any?)
@@ -183,6 +188,9 @@ class ApplicationController < ActionController::Base
       err = {}
     end
     err[:errors] ||= args
+    err[:errors].map! do |err|
+      err += " (" + Thread.current[:request_id] + ")"
+    end
     err[:error_token] = [Time.now.utc.to_i, "%08x" % rand(16 ** 8)].join("+")
     status = err.delete(:status) || 422
     logger.error "Error #{err[:error_token]}: #{status}"
index e0532fc715caa7003bea91fc8fd90c0ae95474b7..d502d5a698e647c7806a6be0ff5497aa6b0f43a8 100644 (file)
@@ -37,6 +37,9 @@ class Arvados::V1::GroupsController < ApplicationController
               recursive: {
                 type: 'boolean', required: false, description: 'Include contents from child groups recursively.'
               },
+              include: {
+                type: 'string', required: false, description: 'Include objects referred to by listed field in "included" (only owner_uuid)'
+              }
             })
     params.delete(:select)
     params
index 39d56997a09596f64a86a2842a96667b048a6f92..ac00e5d39c2cce78c1dd26cfca3fba285810b80e 100644 (file)
@@ -27,6 +27,7 @@ class Collection < ArvadosModel
   before_validation :check_manifest_validity
   before_validation :check_signatures
   before_validation :strip_signatures_and_update_replication_confirmed
+  before_validation :name_null_if_empty
   validate :ensure_pdh_matches_manifest_text
   validate :ensure_storage_classes_desired_is_not_empty
   validate :ensure_storage_classes_contain_non_empty_strings
@@ -38,7 +39,7 @@ class Collection < ArvadosModel
   around_update :manage_versioning, unless: :is_past_version?
 
   api_accessible :user, extend: :common do |t|
-    t.add :name
+    t.add lambda { |x| x.name || "" }, as: :name
     t.add :description
     t.add :properties
     t.add :portable_data_hash
@@ -77,6 +78,7 @@ class Collection < ArvadosModel
                 # correct timestamp in signed_manifest_text.
                 'manifest_text' => ['manifest_text', 'trash_at', 'is_trashed'],
                 'unsigned_manifest_text' => ['manifest_text'],
+                'name' => ['name'],
                 )
   end
 
@@ -195,6 +197,12 @@ class Collection < ArvadosModel
     end
   end
 
+  def name_null_if_empty
+    if name == ""
+      self.name = nil
+    end
+  end
+
   def set_file_names
     if self.manifest_text_changed?
       self.file_names = manifest_files
index 22a8fed58e2fc584333f5f3684e822a7a20ac21c..cf4b842c4a93cef5900c66aa67cbfd259bb031db 100644 (file)
@@ -17,6 +17,7 @@
 # delete application.yml and database.yml.
 
 require 'config_loader'
+require 'open3'
 
 begin
   # If secret_token.rb exists here, we need to load it first.
@@ -42,34 +43,31 @@ EOS
   WARNED_OMNIAUTH_CONFIG = true
 end
 
-# Load the defaults
-$arvados_config_defaults = ConfigLoader.load "#{::Rails.root.to_s}/config/config.default.yml"
-if $arvados_config_defaults.empty?
-  raise "Missing #{::Rails.root.to_s}/config/config.default.yml"
-end
-
-def remove_sample_entries(h)
-  return unless h.is_a? Hash
-  h.delete("SAMPLE")
-  h.each { |k, v| remove_sample_entries(v) }
+# Load the defaults, used by config:migrate and fallback loading
+# legacy application.yml
+Open3.popen2("arvados-server", "config-dump", "-config=-") do |stdin, stdout, status_thread|
+  stdin.write("Clusters: {xxxxx: {}}")
+  stdin.close
+  confs = YAML.load(stdout, deserialize_symbols: false)
+  clusterID, clusterConfig = confs["Clusters"].first
+  $arvados_config_defaults = clusterConfig
+  $arvados_config_defaults["ClusterID"] = clusterID
 end
-remove_sample_entries($arvados_config_defaults)
-
-clusterID, clusterConfig = $arvados_config_defaults["Clusters"].first
-$arvados_config_defaults = clusterConfig
-$arvados_config_defaults["ClusterID"] = clusterID
-
-# Initialize the global config with the defaults
-$arvados_config_global = $arvados_config_defaults.deep_dup
 
 # Load the global config file
-confs = ConfigLoader.load "/etc/arvados/config.yml"
-if !confs.empty?
-  clusterID, clusterConfig = confs["Clusters"].first
-  $arvados_config_global["ClusterID"] = clusterID
-
-  # Copy the cluster config over the defaults
-  $arvados_config_global.deep_merge!(clusterConfig)
+Open3.popen2("arvados-server", "config-dump") do |stdin, stdout, status_thread|
+  confs = YAML.load(stdout, deserialize_symbols: false)
+  if confs && !confs.empty?
+    # config-dump merges defaults with user configuration, so every
+    # key should be set.
+    clusterID, clusterConfig = confs["Clusters"].first
+    $arvados_config_global = clusterConfig
+    $arvados_config_global["ClusterID"] = clusterID
+  else
+    # config-dump failed, assume we will be loading from legacy
+    # application.yml, initialize with defaults.
+    $arvados_config_global = $arvados_config_defaults.deep_dup
+  end
 end
 
 # Now make a copy
diff --git a/services/api/config/config.default.yml b/services/api/config/config.default.yml
deleted file mode 120000 (symlink)
index f039aa0..0000000
+++ /dev/null
@@ -1 +0,0 @@
-../../../lib/config/config.default.yml
\ No newline at end of file
index 07dba3aef4ff9bb64db8c1236a73c0c97c15a794..9b422462b193a400a58375bc31cf76486132d023 100644 (file)
@@ -13,6 +13,21 @@ Server::Application.configure do
       client_ipaddr: event.payload[:client_ipaddr],
       client_auth: event.payload[:client_auth],
     }
+
+    # Lograge adds exceptions not being rescued to event.payload, but we're
+    # catching all errors on ApplicationController so we look for backtraces
+    # elsewhere.
+    if !Thread.current[:backtrace].nil?
+      payload.merge!(
+        {
+          exception: Thread.current[:exception],
+          exception_backtrace: Thread.current[:backtrace],
+        }
+      )
+      Thread.current[:exception] = nil
+      Thread.current[:backtrace] = nil
+    end
+
     exceptions = %w(controller action format id)
     params = event.payload[:params].except(*exceptions)
 
index 1d5891ed62832e4fc761e7005122f5d2b0785fcd..47e1e4bb178f2d0bd3f6d99dec1c23458bcd2ff0 100644 (file)
@@ -6,9 +6,9 @@ fpm_depends+=('git >= 1.7.10')
 
 case "$TARGET" in
     centos*)
-        fpm_depends+=(libcurl-devel postgresql-devel)
+        fpm_depends+=(libcurl-devel postgresql-devel arvados-server)
         ;;
     debian* | ubuntu*)
-        fpm_depends+=(libcurl-ssl-dev libpq-dev g++)
+        fpm_depends+=(libcurl-ssl-dev libpq-dev g++ arvados-server)
         ;;
 esac
index c84e479e48fbe6118a1297a903013addf68e928e..1503f6bc0147ebde726fb449ee5af734637fdca1 100644 (file)
@@ -101,7 +101,7 @@ baz_file:
 w_a_z_file:
   uuid: zzzzz-4zz18-25k12570yk134b3
   current_version_uuid: zzzzz-4zz18-25k12570yk134b3
-  portable_data_hash: 8706aadd12a0ebc07d74cae88762ba9e+56
+  portable_data_hash: 44a8da9ec82098323895cd14e178386f+56
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-09T10:53:38Z
   modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
@@ -129,7 +129,7 @@ w_a_z_file_version_1:
 multilevel_collection_1:
   uuid: zzzzz-4zz18-pyw8yp9g3pr7irn
   current_version_uuid: zzzzz-4zz18-pyw8yp9g3pr7irn
-  portable_data_hash: 1fd08fc162a5c6413070a8bd0bffc818+150
+  portable_data_hash: f9ddda46bb293b6847da984e3aa735db+290
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
   modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
@@ -143,7 +143,7 @@ multilevel_collection_2:
   uuid: zzzzz-4zz18-45xf9hw1sxkhl6q
   current_version_uuid: zzzzz-4zz18-45xf9hw1sxkhl6q
   # All of this collection's files are deep in subdirectories.
-  portable_data_hash: 80cf6dd2cf079dd13f272ec4245cb4a8+48
+  portable_data_hash: 8591cc5caeca80fc62fd529ba1d63bf3+118
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
   modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
@@ -394,7 +394,7 @@ unique_expired_collection:
 unique_expired_collection2:
   uuid: zzzzz-4zz18-mto52zx1s7sn3jr
   current_version_uuid: zzzzz-4zz18-mto52zx1s7sn3jr
-  portable_data_hash: 4ad199f90029935844dc3f098f4fca2b+49
+  portable_data_hash: 64a2bed1ef0f40fe3a7d39bcf2584cb8+50
   owner_uuid: zzzzz-tpzed-000000000000000
   created_at: 2014-02-03T17:22:54Z
   modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
@@ -404,7 +404,7 @@ unique_expired_collection2:
   is_trashed: true
   trash_at: 2001-01-01T00:00:00Z
   delete_at: 2038-01-01T00:00:00Z
-  manifest_text: ". 29d7797f1888013986899bc9083783fa+3 0:3:expired\n"
+  manifest_text: ". 29d7797f1888013986899bc9083783fa+3 0:3:expired2\n"
   name: unique_expired_collection2
 
 # a collection with a log file that can be parsed by the log viewer
@@ -474,14 +474,14 @@ collection_with_files_in_subdir:
   uuid: zzzzz-4zz18-filesinsubdir00
   current_version_uuid: zzzzz-4zz18-filesinsubdir00
   name: collection_files_in_subdir
-  portable_data_hash: 85877ca2d7e05498dd3d109baf2df106+95
+  portable_data_hash: 7eb64275355980ebc93411b44050c137+281
   owner_uuid: zzzzz-tpzed-user1withloadab
   created_at: 2014-02-03T17:22:54Z
   modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
   modified_by_user_uuid: zzzzz-tpzed-user1withloadab
   modified_at: 2014-02-03T17:22:54Z
   updated_at: 2014-02-03T17:22:54Z
-  manifest_text: ". 85877ca2d7e05498dd3d109baf2df106+95 0:95:file_in_subdir1\n./subdir2/subdir3 2bbc341c702df4d8f42ec31f16c10120+64 0:32:file1_in_subdir3.txt 32:32:file2_in_subdir3.txt\n./subdir2/subdir3/subdir4 2bbc341c702df4d8f42ec31f16c10120+64 0:32:file1_in_subdir4.txt 32:32:file2_in_subdir4.txt"
+  manifest_text: ". 85877ca2d7e05498dd3d109baf2df106+95 0:95:file_in_subdir1\n./subdir2/subdir3 2bbc341c702df4d8f42ec31f16c10120+64 0:32:file1_in_subdir3.txt 32:32:file2_in_subdir3.txt\n./subdir2/subdir3/subdir4 2bbc341c702df4d8f42ec31f16c10120+64 0:32:file1_in_subdir4.txt 32:32:file2_in_subdir4.txt\n"
 
 graph_test_collection1:
   uuid: zzzzz-4zz18-bv31uwvy3neko22
@@ -722,7 +722,7 @@ collection_with_one_property:
 collection_with_repeated_filenames_and_contents_in_two_dirs_1:
   uuid: zzzzz-4zz18-duplicatenames1
   current_version_uuid: zzzzz-4zz18-duplicatenames1
-  portable_data_hash: f3a67fad3a19c31c658982fb8158fa58+144
+  portable_data_hash: ce437b12aa73ab34f7af5227f556c9e6+142
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
   modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
@@ -748,7 +748,7 @@ collection_with_repeated_filenames_and_contents_in_two_dirs_2:
 foo_and_bar_files_in_dir:
   uuid: zzzzz-4zz18-foonbarfilesdir
   current_version_uuid: zzzzz-4zz18-foonbarfilesdir
-  portable_data_hash: 6bbac24198d09a93975f60098caf0bdf+62
+  portable_data_hash: 870369fc72738603c2fad16664e50e2d+58
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
   modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
@@ -801,20 +801,20 @@ collection_with_several_unsupported_file_types:
 collection_not_readable_by_active:
   uuid: zzzzz-4zz18-cd42uwvy3neko21
   current_version_uuid: zzzzz-4zz18-cd42uwvy3neko21
-  portable_data_hash: bb89eb5140e2848d39b416daeef4ffc5+45
+  portable_data_hash: b9e51a238ce08a698e7d7f8f101aee18+55
   owner_uuid: zzzzz-tpzed-000000000000000
   created_at: 2014-02-03T17:22:54Z
   modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
   modified_by_user_uuid: zzzzz-tpzed-d9tiejq69daie8f
   modified_at: 2014-02-03T17:22:54Z
   updated_at: 2014-02-03T17:22:54Z
-  manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
+  manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar 0:0:empty\n"
   name: collection_not_readable_by_active
 
 collection_to_remove_and_rename_files:
   uuid: zzzzz-4zz18-a21ux3541sxa8sf
   current_version_uuid: zzzzz-4zz18-a21ux3541sxa8sf
-  portable_data_hash: 80cf6dd2cf079dd13f272ec4245cb4a8+48
+  portable_data_hash: 21aed8fd508bd6263704b673455949ba+57
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
   modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
@@ -843,7 +843,7 @@ collection_with_tags_owned_by_active:
 trashed_collection_to_test_name_conflict_on_untrash:
   uuid: zzzzz-4zz18-trashedcolnamec
   current_version_uuid: zzzzz-4zz18-trashedcolnamec
-  portable_data_hash: 80cf6dd2cf079dd13f272ec4245cb4a8+48
+  portable_data_hash: 21aed8fd508bd6263704b673455949ba+57
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
   modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
@@ -859,7 +859,7 @@ trashed_collection_to_test_name_conflict_on_untrash:
 same_name_as_trashed_coll_to_test_name_conflict_on_untrash:
   uuid: zzzzz-4zz18-namesameastrash
   current_version_uuid: zzzzz-4zz18-namesameastrash
-  portable_data_hash: 80cf6dd2cf079dd13f272ec4245cb4a8+48
+  portable_data_hash: 21aed8fd508bd6263704b673455949ba+57
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
   modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
@@ -872,7 +872,7 @@ same_name_as_trashed_coll_to_test_name_conflict_on_untrash:
 collection_in_trashed_subproject:
   uuid: zzzzz-4zz18-trashedproj2col
   current_version_uuid: zzzzz-4zz18-trashedproj2col
-  portable_data_hash: 80cf6dd2cf079dd13f272ec4245cb4a8+48
+  portable_data_hash: 21aed8fd508bd6263704b673455949ba+57
   owner_uuid: zzzzz-j7d0g-trashedproject2
   created_at: 2014-02-03T17:22:54Z
   modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
index dea98887e9843866b182a7ca054aa60628223fd7..ea86dca1784834d7ca0c37838c743aa785812a7b 100644 (file)
@@ -322,7 +322,7 @@ completed_with_input_mounts:
           basename: bar
           class: File
           location: "keep:fa7aeb5140e2848d39b416daeef4ffc5+45/bar"
-    /var/lib/cwl/workflow.json: "keep:1fd08fc162a5c6413070a8bd0bffc818+150"
+    /var/lib/cwl/workflow.json: "keep:f9ddda46bb293b6847da984e3aa735db+290"
 
 uncommitted:
   uuid: zzzzz-xvhdp-cr4uncommittedc
index b5f71acb536aa9bdd2bbf1ba67bbd6aba55560af..175a8f71ea0544e2253754f607a2217a441d63cc 100644 (file)
@@ -24,11 +24,16 @@ class ApplicationControllerTest < ActionController::TestCase
     token_time = token.split('+', 2).first.to_i
     assert_operator(token_time, :>=, @start_stamp, "error token too old")
     assert_operator(token_time, :<=, now_timestamp, "error token too new")
+    json_response['errors'].each do |err|
+      assert_match(/req-[a-z0-9]{20}/, err, "X-Request-Id value missing on error message")
+    end
   end
 
   def check_404(errmsg="Path not found")
     assert_response 404
-    assert_equal([errmsg], json_response['errors'])
+    json_response['errors'].each do |err|
+      assert(err.include?(errmsg), "error message '#{err}' expected to include '#{errmsg}'")
+    end
     check_error_token
   end
 
@@ -116,4 +121,16 @@ class ApplicationControllerTest < ActionController::TestCase
       end
     end
   end
+
+  test "exceptions with backtraces get logged at exception_backtrace key" do
+    Group.stubs(:new).raises(Exception, 'Whoops')
+    Rails.logger.expects(:info).with(any_parameters) do |param|
+      param.include?('Whoops') and param.include?('"exception_backtrace":')
+    end
+    @controller = Arvados::V1::GroupsController.new
+    authorize_with :active
+    post :create, params: {
+      group: {},
+    }
+  end
 end
index e62faa3314e3a3bed9fd1fa207d58ea73c75d128..53421a4cbcf249871b09389fc618c1790f22a776 100644 (file)
@@ -84,7 +84,7 @@ class Arvados::V1::SchemaControllerTest < ActionController::TestCase
     group_index_params = discovery_doc['resources']['groups']['methods']['index']['parameters']
     group_contents_params = discovery_doc['resources']['groups']['methods']['contents']['parameters']
 
-    assert_equal group_contents_params.keys.sort, (group_index_params.keys - ['select'] + ['uuid', 'recursive']).sort
+    assert_equal group_contents_params.keys.sort, (group_index_params.keys - ['select'] + ['uuid', 'recursive', 'include']).sort
 
     recursive_param = group_contents_params['recursive']
     assert_equal 'boolean', recursive_param['type']
index 8691030e9d3c09e14a909ae0f0a3bd647a293212..f0741fcfde9f2fe27297720f5bbc4bb88af9b418 100644 (file)
@@ -10,7 +10,9 @@ class LoginWorkflowTest < ActionDispatch::IntegrationTest
       params: {specimen: {}},
       headers: {'HTTP_ACCEPT' => ''})
     assert_response 401
-    assert_includes(json_response['errors'], "Not logged in")
+    json_response['errors'].each do |err|
+      assert(err.include?("Not logged in"), "error message '#{err}' expected to include 'Not logged in'")
+    end
   end
 
   test "login prompt respects JSON Accept header" do
@@ -18,7 +20,9 @@ class LoginWorkflowTest < ActionDispatch::IntegrationTest
       params: {specimen: {}},
       headers: {'HTTP_ACCEPT' => 'application/json'})
     assert_response 401
-    assert_includes(json_response['errors'], "Not logged in")
+    json_response['errors'].each do |err|
+      assert(err.include?("Not logged in"), "error message '#{err}' expected to include 'Not logged in'")
+    end
   end
 
   test "login prompt respects HTML Accept header" do
index c2bf94fe73a2d85f45c23ca1dbc7a8dcda25a8c9..2dd6eedcfbc4b20c9a386d2473f6d90e0a415fbe 100644 (file)
@@ -1013,6 +1013,23 @@ class CollectionTest < ActiveSupport::TestCase
     assert_empty Collection.where(uuid: uuid)
   end
 
+  test "empty names are exempt from name uniqueness" do
+    act_as_user users(:active) do
+      c1 = Collection.new(name: nil, manifest_text: '', owner_uuid: groups(:aproject).uuid)
+      assert c1.save
+      c2 = Collection.new(name: '', manifest_text: '', owner_uuid: groups(:aproject).uuid)
+      assert c2.save
+      c3 = Collection.new(name: '', manifest_text: '', owner_uuid: groups(:aproject).uuid)
+      assert c3.save
+      c4 = Collection.new(name: 'c4', manifest_text: '', owner_uuid: groups(:aproject).uuid)
+      assert c4.save
+      c5 = Collection.new(name: 'c4', manifest_text: '', owner_uuid: groups(:aproject).uuid)
+      assert_raises(ActiveRecord::RecordNotUnique) do
+        c5.save
+      end
+    end
+  end
+
   test "create collections with managed properties" do
     Rails.configuration.Collections.ManagedProperties = {
       'default_prop1' => {'Value' => 'prop1_value'},
index 84b578a3e21ee6a1b9b70f1adf48709154452bb9..3261291b53650c516f7b58ca50668bfd27dca964 100644 (file)
@@ -987,7 +987,7 @@ func (runner *ContainerRunner) AttachStreams() (err error) {
                go func() {
                        _, err := io.Copy(response.Conn, stdinRdr)
                        if err != nil {
-                               runner.CrunchLog.Printf("While writing stdin collection to docker container %q", err)
+                               runner.CrunchLog.Printf("While writing stdin collection to docker container: %v", err)
                                runner.stop(nil)
                        }
                        stdinRdr.Close()
@@ -997,7 +997,7 @@ func (runner *ContainerRunner) AttachStreams() (err error) {
                go func() {
                        _, err := io.Copy(response.Conn, bytes.NewReader(stdinJson))
                        if err != nil {
-                               runner.CrunchLog.Printf("While writing stdin json to docker container %q", err)
+                               runner.CrunchLog.Printf("While writing stdin json to docker container: %v", err)
                                runner.stop(nil)
                        }
                        response.CloseWrite()
index 0bc4a0b653120bdb4763454447c60369430a8600..ae1aa019bbb43f55aee277afea61cc309ca8cbd7 100644 (file)
@@ -15,6 +15,11 @@ README = os.path.join(SETUP_DIR, 'README.rst')
 
 import arvados_version
 version = arvados_version.get_version(SETUP_DIR, "arvados_fuse")
+if os.environ.get('ARVADOS_BUILDING_VERSION', False):
+    pysdk_dep = "=={}".format(version)
+else:
+    # On dev releases, arvados-python-client may have a different timestamp
+    pysdk_dep = "<={}".format(version)
 
 short_tests_only = False
 if '--short-tests-only' in sys.argv:
@@ -38,8 +43,8 @@ setup(name='arvados_fuse',
           ('share/doc/arvados_fuse', ['agpl-3.0.txt', 'README.rst']),
       ],
       install_requires=[
-        'arvados-python-client >= 0.1.20151118035730',
-        'llfuse >=1.2, <=1.3.6',
+        'arvados-python-client{}'.format(pysdk_dep),
+        'llfuse >= 1.3.6',
         'future',
         'python-daemon',
         'ciso8601 >= 2.0.0',
index 2f66b2461ebb81c7ee94399cc4dc426ac2315c33..a06a4f884d2365da279e0a27b399fac409e25e7c 100644 (file)
@@ -16,7 +16,7 @@ import (
 
 var (
        version             = "dev"
-       command cmd.Handler = service.Command(arvados.ServiceNameController, newHandler)
+       command cmd.Handler = service.Command(arvados.ServiceNameHealth, newHandler)
 )
 
 func newHandler(ctx context.Context, cluster *arvados.Cluster, _ string) service.Handler {
index a548b1ff9f338fb47f7998fe3423c06f5b2f2ad4..6aaf07abae395241fdbd5f26be8ae111f14aac1f 100644 (file)
@@ -30,7 +30,6 @@ func (s *integrationSuite) TestIdenticalTimestamps(c *check.C) {
                        var lastMod time.Time
                        sawUUID := make(map[string]bool)
                        err := EachCollection(&s.config.Client, pageSize, func(c arvados.Collection) error {
-                               got[trial] = append(got[trial], c.UUID)
                                if c.ModifiedAt == nil {
                                        return nil
                                }
@@ -38,6 +37,7 @@ func (s *integrationSuite) TestIdenticalTimestamps(c *check.C) {
                                        // dup
                                        return nil
                                }
+                               got[trial] = append(got[trial], c.UUID)
                                sawUUID[c.UUID] = true
                                if lastMod == *c.ModifiedAt {
                                        streak++
index 894056c9f27756c9f452f904568d53f88f433c74..e2f13a425ed8dfabc729649d98aa7e4ed977899a 100644 (file)
@@ -5,6 +5,7 @@
 package main
 
 import (
+       "context"
        "fmt"
        "net/http"
        "os"
@@ -14,6 +15,7 @@ import (
 
        "git.curoverse.com/arvados.git/sdk/go/arvados"
        "git.curoverse.com/arvados.git/sdk/go/auth"
+       "git.curoverse.com/arvados.git/sdk/go/ctxlog"
        "git.curoverse.com/arvados.git/sdk/go/httpserver"
        "github.com/sirupsen/logrus"
 )
@@ -127,11 +129,13 @@ func (srv *Server) start() error {
        if srv.config.Listen == "" {
                return nil
        }
+       ctx := ctxlog.Context(context.Background(), srv.Logger)
        server := &httpserver.Server{
                Server: http.Server{
-                       Handler: httpserver.LogRequests(srv.Logger,
-                               auth.RequireLiteralToken(srv.config.ManagementToken,
-                                       srv.metrics.Handler(srv.Logger))),
+                       Handler: httpserver.HandlerWithContext(ctx,
+                               httpserver.LogRequests(
+                                       auth.RequireLiteralToken(srv.config.ManagementToken,
+                                               srv.metrics.Handler(srv.Logger)))),
                },
                Addr: srv.config.Listen,
        }
index 8336b78f9ea9614af2796211d9ed89d58da741e8..b9a1f3069f9d3e8bd03563c6785cb0b00d388582 100644 (file)
@@ -157,7 +157,11 @@ func (c *cache) Update(client *arvados.Client, coll arvados.Collection, fs arvad
        }
        var updated arvados.Collection
        defer c.pdhs.Remove(coll.UUID)
-       err := client.RequestAndDecode(&updated, "PATCH", "arvados/v1/collections/"+coll.UUID, client.UpdateBody(coll), nil)
+       err := client.RequestAndDecode(&updated, "PATCH", "arvados/v1/collections/"+coll.UUID, nil, map[string]interface{}{
+               "collection": map[string]string{
+                       "manifest_text": coll.ManifestText,
+               },
+       })
        if err == nil {
                c.collections.Add(client.AuthToken+"\000"+coll.PortableDataHash, &cachedCollection{
                        expire:     time.Now().Add(time.Duration(c.TTL)),
index 1c93a2b91c0981840c5ac2dde998a55adb1d9b51..9d9e314fcaf7e25710f1fdd341ca13c7491413f0 100644 (file)
@@ -9,7 +9,6 @@ import (
        "fmt"
        "io"
        "io/ioutil"
-       "net/url"
        "os"
        "os/exec"
        "path/filepath"
@@ -74,7 +73,7 @@ func (s *IntegrationSuite) testCadaver(c *check.C, password string, pathFunc fun
        var newCollection arvados.Collection
        arv := arvados.NewClientFromEnv()
        arv.AuthToken = arvadostest.ActiveToken
-       err = arv.RequestAndDecode(&newCollection, "POST", "arvados/v1/collections", bytes.NewBufferString(url.Values{"collection": {"{}"}}.Encode()), nil)
+       err = arv.RequestAndDecode(&newCollection, "POST", "arvados/v1/collections", nil, map[string]interface{}{"collection": map[string]interface{}{}})
        c.Assert(err, check.IsNil)
 
        readPath, writePath, pdhPath := pathFunc(newCollection)
index 040638623748f8aa57150b314886871703157287..93259f74cd9c2f2692555bc4a995cf3ce261241a 100644 (file)
@@ -465,8 +465,12 @@ func (s *IntegrationSuite) TestSpecialCharsInPath(c *check.C) {
        f.Close()
        mtxt, err := fs.MarshalManifest(".")
        c.Assert(err, check.IsNil)
-       coll := arvados.Collection{ManifestText: mtxt}
-       err = client.RequestAndDecode(&coll, "POST", "arvados/v1/collections", client.UpdateBody(coll), nil)
+       var coll arvados.Collection
+       err = client.RequestAndDecode(&coll, "POST", "arvados/v1/collections", nil, map[string]interface{}{
+               "collection": map[string]string{
+                       "manifest_text": mtxt,
+               },
+       })
        c.Assert(err, check.IsNil)
 
        u, _ := url.Parse("http://download.example.com/c=" + coll.UUID + "/")
@@ -773,11 +777,14 @@ func (s *IntegrationSuite) TestDirectoryListing(c *check.C) {
 func (s *IntegrationSuite) TestDeleteLastFile(c *check.C) {
        arv := arvados.NewClientFromEnv()
        var newCollection arvados.Collection
-       err := arv.RequestAndDecode(&newCollection, "POST", "arvados/v1/collections", arv.UpdateBody(&arvados.Collection{
-               OwnerUUID:    arvadostest.ActiveUserUUID,
-               ManifestText: ". acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:foo.txt 0:3:bar.txt\n",
-               Name:         "keep-web test collection",
-       }), map[string]bool{"ensure_unique_name": true})
+       err := arv.RequestAndDecode(&newCollection, "POST", "arvados/v1/collections", nil, map[string]interface{}{
+               "collection": map[string]string{
+                       "owner_uuid":    arvadostest.ActiveUserUUID,
+                       "manifest_text": ". acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:foo.txt 0:3:bar.txt\n",
+                       "name":          "keep-web test collection",
+               },
+               "ensure_unique_name": true,
+       })
        c.Assert(err, check.IsNil)
        defer arv.RequestAndDecode(&newCollection, "DELETE", "arvados/v1/collections/"+newCollection.UUID, nil, nil)
 
index f70dd1a71f6ae92ecdc3f2979e2296f33238e28f..167fbbe5b85cf93f012d072e1fd97af3f5bd7106 100644 (file)
@@ -5,10 +5,13 @@
 package main
 
 import (
+       "context"
        "net/http"
 
+       "git.curoverse.com/arvados.git/sdk/go/ctxlog"
        "git.curoverse.com/arvados.git/sdk/go/httpserver"
        "github.com/prometheus/client_golang/prometheus"
+       "github.com/sirupsen/logrus"
 )
 
 type server struct {
@@ -20,7 +23,8 @@ func (srv *server) Start() error {
        h := &handler{Config: srv.Config}
        reg := prometheus.NewRegistry()
        h.Config.Cache.registry = reg
-       mh := httpserver.Instrument(reg, nil, httpserver.AddRequestIDs(httpserver.LogRequests(nil, h)))
+       ctx := ctxlog.Context(context.Background(), logrus.StandardLogger())
+       mh := httpserver.Instrument(reg, nil, httpserver.HandlerWithContext(ctx, httpserver.AddRequestIDs(httpserver.LogRequests(h))))
        h.MetricsAPI = mh.ServeAPI(h.Config.ManagementToken, http.NotFoundHandler())
        srv.Handler = mh
        srv.Addr = srv.Config.Listen
index c6fd99b9d8ed2f70b264b342ed041d5062eeb0a8..f8aa6c4aa7db3df87e7e598aaa901e8e3e91763c 100644 (file)
@@ -182,7 +182,7 @@ func main() {
 
        // Start serving requests.
        router = MakeRESTRouter(!cfg.DisableGet, !cfg.DisablePut, kc, time.Duration(cfg.Timeout), cfg.ManagementToken)
-       http.Serve(listener, httpserver.AddRequestIDs(httpserver.LogRequests(nil, router)))
+       http.Serve(listener, httpserver.AddRequestIDs(httpserver.LogRequests(router)))
 
        log.Println("shutting down")
 }
index 9a4d02df850fab836cdafaa4e21abb070b492782..72088e2b5ead5726e02bc06c6d8f84e6b5817fa5 100644 (file)
@@ -21,6 +21,7 @@ import (
        "time"
 
        "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "git.curoverse.com/arvados.git/sdk/go/ctxlog"
        "git.curoverse.com/arvados.git/sdk/go/health"
        "git.curoverse.com/arvados.git/sdk/go/httpserver"
        "github.com/gorilla/mux"
@@ -93,8 +94,10 @@ func MakeRESTRouter(cluster *arvados.Cluster, reg *prometheus.Registry) http.Han
        rtr.metrics.setupWorkQueueMetrics(trashq, "trash")
        rtr.metrics.setupRequestMetrics(rtr.limiter)
 
-       instrumented := httpserver.Instrument(rtr.metrics.reg, nil,
-               httpserver.AddRequestIDs(httpserver.LogRequests(nil, rtr.limiter)))
+       instrumented := httpserver.Instrument(rtr.metrics.reg, log,
+               httpserver.HandlerWithContext(
+                       ctxlog.Context(context.Background(), log),
+                       httpserver.AddRequestIDs(httpserver.LogRequests(rtr.limiter))))
        return instrumented.ServeAPI(theConfig.ManagementToken, instrumented)
 }
 
index d03512d5989b4f950d7169db58e6cd6feefbf2ed..eff59d2f72104407615302109bd6967e3a9b3fff 100644 (file)
@@ -1,7 +1,7 @@
 PATH
   remote: .
   specs:
-    arvados-login-sync (1.3.3.20190528194843)
+    arvados-login-sync (1.4.0.20190701162225)
       arvados (~> 1.3.0, >= 1.3.0)
 
 GEM
@@ -60,8 +60,8 @@ GEM
     mocha (1.8.0)
       metaclass (~> 0.0.1)
     multi_json (1.13.1)
-    multipart-post (2.1.1)
-    os (1.0.1)
+    multipart-post (2.0.0)
+    os (1.0.0)
     public_suffix (3.0.3)
     rake (12.3.2)
     retriable (1.4.1)
index ef05467810a79a88e1e4f27149fd30531804d318..fcfd36339a1550c24fde0ea6d53d3bcb72a523ea 100644 (file)
@@ -15,6 +15,11 @@ README = os.path.join(SETUP_DIR, 'README.rst')
 
 import arvados_version
 version = arvados_version.get_version(SETUP_DIR, "arvnodeman")
+if os.environ.get('ARVADOS_BUILDING_VERSION', False):
+    pysdk_dep = "=={}".format(version)
+else:
+    # On dev releases, arvados-python-client may have a different timestamp
+    pysdk_dep = "<={}".format(version)
 
 short_tests_only = False
 if '--short-tests-only' in sys.argv:
@@ -36,7 +41,7 @@ setup(name='arvados-node-manager',
       ],
       install_requires=[
           'apache-libcloud>=2.3.1.dev1',
-          'arvados-python-client>=0.1.20170731145219',
+          'arvados-python-client{}'.format(pysdk_dep),
           'future',
           'pykka < 2',
           'python-daemon',
index 7585bc5e17e017dc095a141d550e4e609c877c94..19e006744c9a7f1849595153529d55e60d819b26 100644 (file)
@@ -208,8 +208,8 @@ func (s *v0Suite) TestTrashedCollection(c *check.C) {
        ac := arvados.NewClientFromEnv()
        ac.AuthToken = s.token
 
-       coll := &arvados.Collection{ManifestText: ""}
-       err := ac.RequestAndDecode(coll, "POST", "arvados/v1/collections", s.jsonBody("collection", coll), map[string]interface{}{"ensure_unique_name": true})
+       var coll arvados.Collection
+       err := ac.RequestAndDecode(&coll, "POST", "arvados/v1/collections", s.jsonBody("collection", `{"manifest_text":""}`), map[string]interface{}{"ensure_unique_name": true})
        c.Assert(err, check.IsNil)
        s.ignoreLogID = s.lastLogID(c)
 
@@ -290,7 +290,7 @@ func (s *v0Suite) emitEvents(uuidChan chan<- string) {
        wf := &arvados.Workflow{
                Name: "ws_test",
        }
-       err := ac.RequestAndDecode(wf, "POST", "arvados/v1/workflows", s.jsonBody("workflow", wf), map[string]interface{}{"ensure_unique_name": true})
+       err := ac.RequestAndDecode(wf, "POST", "arvados/v1/workflows", s.jsonBody("workflow", `{"name":"ws_test"}`), map[string]interface{}{"ensure_unique_name": true})
        if err != nil {
                panic(err)
        }
@@ -298,17 +298,17 @@ func (s *v0Suite) emitEvents(uuidChan chan<- string) {
                uuidChan <- wf.UUID
        }
        lg := &arvados.Log{}
-       err = ac.RequestAndDecode(lg, "POST", "arvados/v1/logs", s.jsonBody("log", &arvados.Log{
-               ObjectUUID: wf.UUID,
-               EventType:  "blip",
-               Properties: map[string]interface{}{
+       err = ac.RequestAndDecode(lg, "POST", "arvados/v1/logs", s.jsonBody("log", map[string]interface{}{
+               "object_uuid": wf.UUID,
+               "event_type":  "blip",
+               "properties": map[string]interface{}{
                        "beep": "boop",
                },
        }), nil)
        if err != nil {
                panic(err)
        }
-       err = ac.RequestAndDecode(wf, "PUT", "arvados/v1/workflows/"+wf.UUID, s.jsonBody("workflow", wf), nil)
+       err = ac.RequestAndDecode(wf, "PUT", "arvados/v1/workflows/"+wf.UUID, s.jsonBody("workflow", `{"name":"ws_test"}`), nil)
        if err != nil {
                panic(err)
        }
@@ -316,12 +316,16 @@ func (s *v0Suite) emitEvents(uuidChan chan<- string) {
 }
 
 func (s *v0Suite) jsonBody(rscName string, ob interface{}) io.Reader {
-       j, err := json.Marshal(ob)
-       if err != nil {
-               panic(err)
+       val, ok := ob.(string)
+       if !ok {
+               j, err := json.Marshal(ob)
+               if err != nil {
+                       panic(err)
+               }
+               val = string(j)
        }
        v := url.Values{}
-       v[rscName] = []string{string(j)}
+       v[rscName] = []string{val}
        return bytes.NewBufferString(v.Encode())
 }
 
index efcad7f44c976e5118027e87e4e4d8d0ac100f5d..0a0d82e71b59c4c0290457e85aaf2b33480b28c2 100755 (executable)
@@ -560,7 +560,18 @@ case "$subcmd" in
 
     clone)
         if test -n "$2" ; then
-            cp -r "$ARVBOX_BASE/$1" "$ARVBOX_BASE/$2"
+           mkdir -p "$ARVBOX_BASE/$2"
+            cp -a "$ARVBOX_BASE/$1/passenger" \
+              "$ARVBOX_BASE/$1/gems" \
+              "$ARVBOX_BASE/$1/pip" \
+              "$ARVBOX_BASE/$1/npm" \
+              "$ARVBOX_BASE/$1/gopath" \
+              "$ARVBOX_BASE/$1/Rlibs" \
+              "$ARVBOX_BASE/$1/arvados" \
+              "$ARVBOX_BASE/$1/sso-devise-omniauth-provider" \
+              "$ARVBOX_BASE/$1/composer" \
+              "$ARVBOX_BASE/$1/workbench2" \
+              "$ARVBOX_BASE/$2"
             echo "Created new arvbox $2"
             echo "export ARVBOX_CONTAINER=$2"
         else
index c07c70ba9b6f961ba8c2b3721d7193cf8d716f71..e920a39eb94caa8ebe7fb49faa8003cd62f59b9b 100644 (file)
@@ -40,7 +40,7 @@ ENV GEM_HOME /var/lib/gems
 ENV GEM_PATH /var/lib/gems
 ENV PATH $PATH:/var/lib/gems/bin
 
-ENV GOVERSION 1.11.5
+ENV GOVERSION 1.12.7
 
 # Install golang binary
 RUN curl -f http://storage.googleapis.com/golang/go${GOVERSION}.linux-amd64.tar.gz | \
@@ -108,7 +108,7 @@ ADD crunch-setup.sh gitolite.rc \
     keep-setup.sh common.sh createusers.sh \
     logger runsu.sh waitforpostgres.sh \
     yml_override.py api-setup.sh \
-    go-setup.sh devenv.sh \
+    go-setup.sh devenv.sh cluster-config.sh \
     /usr/local/lib/arvbox/
 
 ADD runit /etc/runit
index c81eb908b104dd597aabf0d481b1d40cf1b59839..d60f19627b75f79cb22f26b01f5ddd9139b8b175 100755 (executable)
@@ -7,6 +7,7 @@ exec 2>&1
 set -ex -o pipefail
 
 . /usr/local/lib/arvbox/common.sh
+. /usr/local/lib/arvbox/go-setup.sh
 
 cd /usr/src/arvados/services/api
 
@@ -44,6 +45,20 @@ else
     echo $vm_uuid > /var/lib/arvados/vm-uuid
 fi
 
+if ! test -f /var/lib/arvados/api_database_pw ; then
+    ruby -e 'puts rand(2**128).to_s(36)' > /var/lib/arvados/api_database_pw
+fi
+database_pw=$(cat /var/lib/arvados/api_database_pw)
+
+if ! (psql postgres -c "\du" | grep "^ arvados ") >/dev/null ; then
+    psql postgres -c "create user arvados with password '$database_pw'"
+fi
+psql postgres -c "ALTER USER arvados WITH SUPERUSER;"
+
+if test -a /usr/src/arvados/services/api/config/arvados_config.rb ; then
+    rm -f config/application.yml config/database.yml
+    flock /var/lib/arvados/cluster_config.yml.lock /usr/local/lib/arvbox/cluster-config.sh
+else
 cat >config/application.yml <<EOF
 $RAILS_ENV:
   uuid_prefix: $uuid_prefix
@@ -69,18 +84,8 @@ $RAILS_ENV:
 EOF
 
 (cd config && /usr/local/lib/arvbox/yml_override.py application.yml)
-
-if ! test -f /var/lib/arvados/api_database_pw ; then
-    ruby -e 'puts rand(2**128).to_s(36)' > /var/lib/arvados/api_database_pw
-fi
-database_pw=$(cat /var/lib/arvados/api_database_pw)
-
-if ! (psql postgres -c "\du" | grep "^ arvados ") >/dev/null ; then
-    psql postgres -c "create user arvados with password '$database_pw'"
-fi
-psql postgres -c "ALTER USER arvados WITH SUPERUSER;"
-
 sed "s/password:.*/password: $database_pw/" <config/database.yml.example >config/database.yml
+fi
 
 if ! test -f /var/lib/arvados/api_database_setup ; then
    bundle exec rake db:setup
diff --git a/tools/arvbox/lib/arvbox/docker/cluster-config.sh b/tools/arvbox/lib/arvbox/docker/cluster-config.sh
new file mode 100755 (executable)
index 0000000..1ec2796
--- /dev/null
@@ -0,0 +1,91 @@
+#!/bin/bash
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+exec 2>&1
+set -ex -o pipefail
+
+if [[ -s /etc/arvados/config.yml ]] ; then
+   exit
+fi
+
+uuid_prefix=$(cat /var/lib/arvados/api_uuid_prefix)
+secret_token=$(cat /var/lib/arvados/api_secret_token)
+blob_signing_key=$(cat /var/lib/arvados/blob_signing_key)
+management_token=$(cat /var/lib/arvados/management_token)
+sso_app_secret=$(cat /var/lib/arvados/sso_app_secret)
+vm_uuid=$(cat /var/lib/arvados/vm-uuid)
+database_pw=$(cat /var/lib/arvados/api_database_pw)
+
+workbench_secret_key_base=$(cat /var/lib/arvados/workbench_secret_token)
+
+if test -s /var/lib/arvados/api_rails_env ; then
+  database_env=$(cat /var/lib/arvados/api_rails_env)
+else
+  database_env=development
+fi
+
+cat >/var/lib/arvados/cluster_config.yml <<EOF
+Clusters:
+  ${uuid_prefix}:
+    ManagementToken: $management_token
+    Services:
+      Workbench1:
+        ExternalURL: "https://$localip:${services[workbench]}"
+      Workbench2:
+        ExternalURL: "https://$localip:${services[workbench2-ssl]}"
+      SSO:
+        ExternalURL: "https://$localip:${services[sso]}"
+      Websocket:
+        ExternalURL: "wss://$localip:${services[websockets-ssl]}/websocket"
+      GitSSH:
+        ExternalURL: "ssh://git@$localip:"
+      GitHTTP:
+        ExternalURL: "http://$localip:${services[arv-git-httpd]}/"
+      WebDAV:
+        ExternalURL: "https://$localip:${services[keep-web-ssl]}/"
+      Composer:
+        ExternalURL: "http://$localip:${services[composer]}"
+      Controller:
+        ExternalURL: "https://$localip:${services[controller-ssl]}"
+    NodeProfiles:  # to be deprecated in favor of "Services" section
+      "*":
+        arvados-controller:
+          Listen: ":${services[controller]}" # choose a port
+        arvados-api-server:
+          Listen: ":${services[api]}" # must match Rails server port in your Nginx config
+    PostgreSQL:
+      ConnectionPool: 32 # max concurrent connections per arvados server daemon
+      Connection:
+        # All parameters here are passed to the PG client library in a connection string;
+        # see https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-PARAMKEYWORDS
+        host: localhost
+        user: arvados
+        password: ${database_pw}
+        dbname: arvados_${database_env}
+        client_encoding: utf8
+    API:
+      RailsSessionSecretToken: $secret_token
+    Collections:
+      BlobSigningKey: $blob_signing_key
+      DefaultReplication: 1
+    Containers:
+      SupportedDockerImageFormats: ["v2"]
+    Login:
+      ProviderAppSecret: $sso_app_secret
+      ProviderAppID: arvados-server
+    Users:
+      NewUsersAreActive: true
+      AutoAdminFirstUser: true
+      AutoSetupNewUsers: true
+      AutoSetupNewUsersWithVmUUID: $vm_uuid
+      AutoSetupNewUsersWithRepository: true
+    Workbench:
+      SecretKeyBase: $workbench_secret_key_base
+      ArvadosDocsite: http://$localip:${services[doc]}/
+EOF
+
+/usr/local/lib/arvbox/yml_override.py /var/lib/arvados/cluster_config.yml
+
+cp /var/lib/arvados/cluster_config.yml /etc/arvados/config.yml
index 36ff49db51b3b011dc6dea0346c530cb27cb6dd9..8e4e74ca0f6ca2dec0c4782d56f6d49b56a1d18a 100644 (file)
@@ -18,6 +18,11 @@ else
     localip=$(ip addr show $defaultdev | grep 'inet ' | sed 's/ *inet \(.*\)\/.*/\1/')
 fi
 
+root_cert=/var/lib/arvados/root-cert.pem
+root_cert_key=/var/lib/arvados/root-cert.key
+server_cert=/var/lib/arvados/server-cert-${localip}.pem
+server_cert_key=/var/lib/arvados/server-cert-${localip}.key
+
 declare -A services
 services=(
   [workbench]=443
index c6270457d5af8f1e9b8bab9ac2de004014744d55..58fb413582e0a513c1819f66a36ccf47a3f36306 100755 (executable)
@@ -53,6 +53,8 @@ export R_LIBS=/var/lib/Rlibs
 export GOPATH=/var/lib/gopath
 EOF
 
+    mkdir -p /etc/arvados
+    chown -R arvbox:arvbox /etc/arvados
 fi
 
 if ! grep "^fuse:" /etc/group >/dev/null 2>/dev/null ; then
index f068ce68422cb974851f9faea4697976d755d16c..cdd7298da000376a7755258edf38f9de01438de8 100644 (file)
@@ -14,3 +14,6 @@ flock /var/lib/gopath/gopath.lock go get -t github.com/kardianos/govendor
 cd "$GOPATH/src/git.curoverse.com/arvados.git"
 flock /var/lib/gopath/gopath.lock go get -v -d ...
 flock /var/lib/gopath/gopath.lock "$GOPATH/bin/govendor" sync
+
+flock /var/lib/gopath/gopath.lock go get -t "git.curoverse.com/arvados.git/cmd/arvados-server"
+install $GOPATH/bin/arvados-server /usr/local/bin
index 8e5e1ed772606dd0cb82e4ebbc9273ff379d779e..f951eef18d0e8e2690b4d4c64c446f2f52ae4ff2 100755 (executable)
@@ -10,7 +10,7 @@ set -ex -o pipefail
 
 uuid_prefix=$(cat /var/lib/arvados/api_uuid_prefix)
 
-if test ! -s /var/lib/arvados/root-cert.pem ; then
+if ! openssl verify -CAfile $root_cert $root_cert ; then
     # req           signing request sub-command
     # -new          new certificate request
     # -nodes        "no des" don't encrypt key
@@ -32,13 +32,19 @@ if test ! -s /var/lib/arvados/root-cert.pem ; then
            -extensions x509_ext \
            -config <(cat /etc/ssl/openssl.cnf \
                          <(printf "\n[x509_ext]\nbasicConstraints=critical,CA:true,pathlen:0\nkeyUsage=critical,keyCertSign,cRLSign")) \
-            -out /var/lib/arvados/root-cert.pem \
-            -keyout /var/lib/arvados/root-cert.key \
+            -out $root_cert \
+            -keyout $root_cert_key \
             -days 365
-    chown arvbox:arvbox /var/lib/arvados/root-cert.*
+    chown arvbox:arvbox $root_cert $root_cert_key
+    rm -f $server_cert $server_cert_key
 fi
 
-if test ! -s /var/lib/arvados/server-cert-${localip}.pem ; then
+cp $root_cert /usr/local/share/ca-certificates/arvados-testing-cert.crt
+update-ca-certificates
+
+if ! openssl verify -CAfile $root_cert $server_cert ; then
+
+    rm -f $server_cert $server_cert_key
 
     if [[ $localip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
        san=IP:$localip
@@ -67,25 +73,22 @@ if test ! -s /var/lib/arvados/server-cert-${localip}.pem ; then
            -config <(cat /etc/ssl/openssl.cnf \
                          <(printf "\n[x509_ext]\nkeyUsage=critical,digitalSignature,keyEncipherment\nsubjectAltName=DNS:localhost,$san")) \
             -out /var/lib/arvados/server-cert-${localip}.csr \
-            -keyout /var/lib/arvados/server-cert-${localip}.key \
+            -keyout $server_cert_key \
             -days 365
 
     openssl x509 \
            -req \
            -in /var/lib/arvados/server-cert-${localip}.csr \
-           -CA /var/lib/arvados/root-cert.pem \
-           -CAkey /var/lib/arvados/root-cert.key \
-           -out /var/lib/arvados/server-cert-${localip}.pem \
+           -CA $root_cert \
+           -CAkey $root_cert_key \
+           -out $server_cert \
            -set_serial $RANDOM$RANDOM \
            -extfile <(cat /etc/ssl/openssl.cnf \
                          <(printf "\n[x509_ext]\nkeyUsage=critical,digitalSignature,keyEncipherment\nsubjectAltName=DNS:localhost,$san")) \
            -extensions x509_ext \
            -days 365
 
-    chown arvbox:arvbox /var/lib/arvados/server-cert-${localip}.*
+    chown arvbox:arvbox $server_cert $server_cert_key
 fi
 
-cp /var/lib/arvados/root-cert.pem /usr/local/share/ca-certificates/arvados-testing-cert.crt
-update-ca-certificates
-
 sv stop certificate
index 986ad84966b53554550f78db6f52df96000a1793..56deed782e88da2c92c9d5fa46656135dd1e017b 100755 (executable)
@@ -9,85 +9,12 @@ set -ex -o pipefail
 . /usr/local/lib/arvbox/common.sh
 . /usr/local/lib/arvbox/go-setup.sh
 
-flock /var/lib/gopath/gopath.lock go get -t "git.curoverse.com/arvados.git/cmd/arvados-server"
-install $GOPATH/bin/arvados-server /usr/local/bin
 (cd /usr/local/bin && ln -sf arvados-server arvados-controller)
 
 if test "$1" = "--only-deps" ; then
     exit
 fi
 
-uuid_prefix=$(cat /var/lib/arvados/api_uuid_prefix)
-secret_token=$(cat /var/lib/arvados/api_secret_token)
-blob_signing_key=$(cat /var/lib/arvados/blob_signing_key)
-management_token=$(cat /var/lib/arvados/management_token)
-sso_app_secret=$(cat /var/lib/arvados/sso_app_secret)
-vm_uuid=$(cat /var/lib/arvados/vm-uuid)
-database_pw=$(cat /var/lib/arvados/api_database_pw)
-
-if test -s /var/lib/arvados/api_rails_env ; then
-  database_env=$(cat /var/lib/arvados/api_rails_env)
-else
-  database_env=development
-fi
-
-mkdir -p /etc/arvados
-
-cat >/var/lib/arvados/cluster_config.yml <<EOF
-Clusters:
-  ${uuid_prefix}:
-    ManagementToken: $management_token
-    Services:
-      Workbench1:
-        ExternalURL: "https://$localip:${services[workbench]}"
-      Workbench2:
-        ExternalURL: "https://$localip:${services[workbench2-ssl]}"
-      SSO:
-        ExternalURL: "https://$localip:${services[sso]}"
-      Websocket:
-        ExternalURL: "wss://$localip:${services[websockets-ssl]}/websocket"
-      GitSSH:
-        ExternalURL: "ssh://git@$localip:"
-      GitHTTP:
-        ExternalURL: "http://$localip:${services[arv-git-httpd]}/"
-      WebDAV:
-        ExternalURL: "https://$localip:${services[keep-web-ssl]}/"
-    NodeProfiles:  # to be deprecated in favor of "Services" section
-      "*":
-        arvados-controller:
-          Listen: ":${services[controller]}" # choose a port
-        arvados-api-server:
-          Listen: ":${services[api]}" # must match Rails server port in your Nginx config
-    PostgreSQL:
-      ConnectionPool: 32 # max concurrent connections per arvados server daemon
-      Connection:
-        # All parameters here are passed to the PG client library in a connection string;
-        # see https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-PARAMKEYWORDS
-        host: localhost
-        user: arvados
-        password: ${database_pw}
-        dbname: arvados_${database_env}
-        client_encoding: utf8
-    API:
-      RailsSessionSecretToken: $secret_token
-    Collections:
-      BlobSigningKey: $blob_signing_key
-      DefaultReplication: 1
-    Containers:
-      SupportedDockerImageFormats: ["v2"]
-    Login:
-      ProviderAppSecret: $sso_app_secret
-      ProviderAppID: arvados-server
-    Users:
-      NewUsersAreActive: true
-      AutoAdminFirstUser: true
-      AutoSetupNewUsers: true
-      AutoSetupNewUsersWithVmUUID: $vm_uuid
-      AutoSetupNewUsersWithRepository: true
-EOF
-
-/usr/local/lib/arvbox/yml_override.py /var/lib/arvados/cluster_config.yml
-
-cp /var/lib/arvados/cluster_config.yml /etc/arvados/config.yml
+flock /var/lib/arvados/cluster_config.yml.lock /usr/local/lib/arvbox/cluster-config.sh
 
 exec /usr/local/lib/arvbox/runsu.sh /usr/local/bin/arvados-controller
index 2353e949f7090093a02501afa57779f0dce6f649..18c56ce9dd3708e4e7230757f8a8a571ce64bcc1 100755 (executable)
@@ -8,6 +8,8 @@ set -ex -o pipefail
 
 . /usr/local/lib/arvbox/common.sh
 
+openssl verify -CAfile $root_cert $server_cert
+
 cat <<EOF >/var/lib/arvados/nginx.conf
 worker_processes auto;
 pid /var/lib/arvados/nginx.pid;
@@ -46,8 +48,8 @@ http {
   server {
     listen *:${services[controller-ssl]} ssl default_server;
     server_name controller;
-    ssl_certificate "/var/lib/arvados/server-cert-${localip}.pem";
-    ssl_certificate_key "/var/lib/arvados/server-cert-${localip}.key";
+    ssl_certificate "${server_cert}";
+    ssl_certificate_key "${server_cert_key}";
     location  / {
       proxy_pass http://controller;
       proxy_set_header Host \$http_host;
@@ -68,8 +70,8 @@ server {
   proxy_read_timeout    300s;
 
   ssl                   on;
-  ssl_certificate "/var/lib/arvados/server-cert-${localip}.pem";
-  ssl_certificate_key "/var/lib/arvados/server-cert-${localip}.key";
+  ssl_certificate "${server_cert}";
+  ssl_certificate_key "${server_cert_key}";
 
   location / {
     proxy_pass          http://arvados-ws;
@@ -86,8 +88,8 @@ server {
   server {
     listen *:${services[workbench2-ssl]} ssl default_server;
     server_name workbench2;
-    ssl_certificate "/var/lib/arvados/server-cert-${localip}.pem";
-    ssl_certificate_key "/var/lib/arvados/server-cert-${localip}.key";
+    ssl_certificate "${server_cert}";
+    ssl_certificate_key "${server_cert_key}";
     location  / {
       proxy_pass http://workbench2;
       proxy_set_header Host \$http_host;
@@ -110,8 +112,8 @@ server {
   server {
     listen *:${services[keep-web-ssl]} ssl default_server;
     server_name keep-web;
-    ssl_certificate "/var/lib/arvados/server-cert-${localip}.pem";
-    ssl_certificate_key "/var/lib/arvados/server-cert-${localip}.key";
+    ssl_certificate "${server_cert}";
+    ssl_certificate_key "${server_cert_key}";
     location  / {
       proxy_pass http://keep-web;
       proxy_set_header Host \$http_host;
index cbd3b2fbef2089dfd21d0b40e57cce7c130f2677..a7d3b1ca2eee874e09392cce97d521a47db39041 100755 (executable)
@@ -35,7 +35,7 @@ if ! test -s /var/lib/arvados/sso_secret_token ; then
 fi
 secret_token=$(cat /var/lib/arvados/sso_secret_token)
 
-test -s /var/lib/arvados/server-cert-${localip}.pem
+openssl verify -CAfile $root_cert $server_cert
 
 cat >config/application.yml <<EOF
 $RAILS_ENV:
index e65801b447a6819ce4be7f112f2dbbe5aa6e39a9..e163493781f1a16531dc7bb355137aed941843fa 100755 (executable)
@@ -22,6 +22,7 @@ else
 fi
 
 if test "$1" != "--only-deps" ; then
+    openssl verify -CAfile $root_cert $server_cert
     exec bundle exec passenger start --port=${services[workbench]} \
         --ssl --ssl-certificate=/var/lib/arvados/server-cert-${localip}.pem \
         --ssl-certificate-key=/var/lib/arvados/server-cert-${localip}.key \
index 6f13ee0278f8c67c333b03f338c998c741a8d9a8..9b139500b7fbe8f9f07be3b7538aafe49c768a91 100755 (executable)
@@ -20,10 +20,17 @@ run_bundler --without=development
 bundle exec passenger-config build-native-support
 bundle exec passenger-config install-standalone-runtime
 mkdir -p /usr/src/arvados/apps/workbench/tmp
-RAILS_GROUPS=assets bundle exec rake npm:install
 
 if test "$1" = "--only-deps" ; then
-    exit
+   # Workaround for validation that asserts there's a download URL
+   # configured, which breaks rake if it is missing.
+cat >config/application.yml <<EOF
+$RAILS_ENV:
+  keep_web_url: https://example.com/c=%{uuid_or_pdh}
+EOF
+   RAILS_GROUPS=assets bundle exec rake npm:install
+   rm config/application.yml
+   exit
 fi
 
 set -u
@@ -33,6 +40,9 @@ if ! test -s /var/lib/arvados/workbench_secret_token ; then
 fi
 secret_token=$(cat /var/lib/arvados/workbench_secret_token)
 
+if test -a /usr/src/arvados/apps/workbench/config/arvados_config.rb ; then
+    rm -f config/application.yml
+else
 cat >config/application.yml <<EOF
 $RAILS_ENV:
   secret_token: $secret_token
@@ -47,6 +57,8 @@ $RAILS_ENV:
   workbench2_url: https://$localip:${services[workbench2-ssl]}
 EOF
 
-bundle exec rake assets:precompile
-
 (cd config && /usr/local/lib/arvbox/yml_override.py application.yml)
+fi
+
+RAILS_GROUPS=assets bundle exec rake npm:install
+bundle exec rake assets:precompile
index 6df440a14e37f87f8fcea5cac7c57ca1269915b4..10f1f26f46610dac96ed2b9f1c5cc5e8db0ba528 100644 (file)
@@ -13,21 +13,34 @@ class DygraphsChart(crunchstat_summary.webchart.WebChart):
     def headHTML(self):
         return '<link rel="stylesheet" href="{}">\n'.format(self.CSS)
 
-    def chartdata(self, label, tasks, stat):
+    def chartdata(self, label, tasks, stats):
+        '''For Crunch2, label is the name of container request,
+        tasks is the top level container and
+        stats is index by a tuple of (category, metric).
+        '''
         return {
-            'data': self._collate_data(tasks, stat),
+            'data': self._collate_data(tasks, stats),
             'options': {
+                'legend': 'always',
                 'connectSeparatedPoints': True,
-                'labels': ['elapsed']+[uuid for uuid, _ in tasks.items()],
-                'title': '{}: {} {}'.format(label, stat[0], stat[1]),
+                'labels': ['elapsed'] +  stats[1],
+                'title': '{}: {}'.format(label, stats[0]),
             },
         }
 
-    def _collate_data(self, tasks, stat):
+    def _collate_data(self, tasks, stats):
         data = []
         nulls = []
+        # uuid is category for crunch2
         for uuid, task in tasks.items():
-            for pt in task.series[stat]:
-                data.append([pt[0].total_seconds()] + nulls + [pt[1]])
+            # All stats in a category are assumed to have the same time base and same number of samples
+            category = stats[0]
+            series_names = stats[1]
+            sn0 = series_names[0]
+            series = task.series[(category,sn0)]
+            for i in range(len(series)):
+                pt = series[i]
+                vals = [task.series[(category,stat)][i][1] for stat in series_names[1:]]
+                data.append([pt[0].total_seconds()] + nulls + [pt[1]] + vals)
             nulls.append(None)
         return sorted(data)
index 884f16b4a7db36f64d95bc256938352837f91412..e962ced31404bfe26a7da36d034871941663ef23 100644 (file)
@@ -129,13 +129,14 @@ class Summarizer(object):
                 try:
                     self.label = m.group('job_uuid')
                 except IndexError:
-                    self.label = 'container'
-            if m.group('category').endswith(':'):
+                    self.label = 'label #1'
+            category = m.group('category')
+            if category.endswith(':'):
                 # "stderr crunchstat: notice: ..."
                 continue
-            elif m.group('category') in ('error', 'caught'):
+            elif category in ('error', 'caught'):
                 continue
-            elif m.group('category') in ('read', 'open', 'cgroup', 'CID', 'Running'):
+            elif category in ('read', 'open', 'cgroup', 'CID', 'Running'):
                 # "stderr crunchstat: read /proc/1234/net/dev: ..."
                 # (old logs are less careful with unprefixed error messages)
                 continue
@@ -221,11 +222,11 @@ class Summarizer(object):
                     if group == 'interval' and this_interval_s:
                             stat = stat + '__rate'
                             val = val / this_interval_s
-                            if stat in ['user+sys__rate', 'tx+rx__rate']:
+                            if stat in ['user+sys__rate', 'user__rate', 'sys__rate', 'tx+rx__rate', 'rx__rate', 'tx__rate']:
                                 task.series[category, stat].append(
                                     (timestamp - self.starttime, val))
                     else:
-                        if stat in ['rss']:
+                        if stat in ['rss','used','total']:
                             task.series[category, stat].append(
                                 (timestamp - self.starttime, val))
                         self.task_stats[task_id][category][stat] = val
@@ -315,7 +316,13 @@ class Summarizer(object):
                  (float(self.job_tot['blkio:0:0']['read']) /
                  float(self.job_tot['net:keep0']['rx']))
                  if self.job_tot['net:keep0']['rx'] > 0 else 0,
-                 lambda x: x * 100.0)):
+                 lambda x: x * 100.0),
+               ('Temp disk utilization {}%',
+                 (float(self.job_tot['statfs']['used']) /
+                 float(self.job_tot['statfs']['total']))
+                 if self.job_tot['statfs']['total'] > 0 else 0,
+                 lambda x: x * 100.0),
+                ):
             format_string, val, transform = args
             if val == float('-Inf'):
                 continue
@@ -328,7 +335,9 @@ class Summarizer(object):
         return itertools.chain(
             self._recommend_cpu(),
             self._recommend_ram(),
-            self._recommend_keep_cache())
+            self._recommend_keep_cache(),
+            self._recommend_temp_disk(),
+            )
 
     def _recommend_cpu(self):
         """Recommend asking for 4 cores if max CPU usage was 333%"""
@@ -431,6 +440,21 @@ class Summarizer(object):
                 math.ceil(asked_cache * 2 / self._runtime_constraint_mem_unit()))
 
 
+    def _recommend_temp_disk(self):
+        """Recommend decreasing temp disk if utilization < 50%"""
+        total = float(self.job_tot['statfs']['total'])
+        utilization = (float(self.job_tot['statfs']['used']) / total) if total > 0 else 0.0
+
+        if utilization < 50.8 and total > 0:
+            yield (
+                '#!! {} max temp disk utilization was {:.0f}% of {:.0f} MiB -- '
+                'consider reducing "tmpdirMin" and/or "outdirMin"'
+            ).format(
+                self.label,
+                utilization * 100.0,
+                total / MB)
+
+
     def _format(self, val):
         """Return a string representation of a stat.
 
index cf0c1e67aa1ffdcf7853b2b1271bb2f03b16bae2..31afcf64e906166788bf06b9caa4ed191ead13c9 100644 (file)
@@ -45,10 +45,13 @@ class WebChart(object):
                 'label': s.long_label(),
                 'charts': [
                     self.chartdata(s.label, s.tasks, stat)
-                    for stat in (('cpu', 'user+sys__rate'),
-                                 ('mem', 'rss'),
-                                 ('net:eth0', 'tx+rx__rate'),
-                                 ('net:keep0', 'tx+rx__rate'))],
+                    for stat in (('cpu', ['user+sys__rate', 'user__rate', 'sys__rate']),
+                                 ('mem', ['rss']),
+                                 ('net:eth0', ['tx+rx__rate','rx__rate','tx__rate']),
+                                 ('net:keep0', ['tx+rx__rate','rx__rate','tx__rate']),
+                                 ('statfs', ['used', 'total']),
+                                 )
+                    ],
             }
             for s in self.summarizers]
 
index 0691e4f1ef4ea7e1604a0b7b73787f25b7dd7e58..f77059b82496f5825d9d634847a2b0537efaed72 100644 (file)
@@ -20,3 +20,4 @@ time  elapsed 10      -       10
 # Max network speed in a single interval: 0.00MB/s
 # Keep cache miss rate 0.00%
 # Keep cache utilization 0.00%
+# Temp disk utilization 0.00%
index c64c34c80ec6cd775e81031330070c047265a96d..b17c7005936cee279c69537cb94251845250d9cf 100644 (file)
@@ -23,4 +23,6 @@ time  elapsed 20      -       20
 # Max network speed in a single interval: 0.00MB/s
 # Keep cache miss rate 0.00%
 # Keep cache utilization 0.00%
-#!! container max RSS was 67 MiB -- try reducing runtime_constraints to "ram":1020054732
+# Temp disk utilization 1.21%
+#!! label #1 max RSS was 67 MiB -- try reducing runtime_constraints to "ram":1020054732
+#!! label #1 max temp disk utilization was 1% of 383960 MiB -- consider reducing "tmpdirMin" and/or "outdirMin"
index 3075c24b951020d1444311bc083d269b581219b2..5152e577f5c5a17f3ef57b0c644592f5de14fcb6 100644 (file)
@@ -34,4 +34,6 @@ time  elapsed 20      -       20
 # Max network speed in a single interval: 0.00MB/s
 # Keep cache miss rate 0.00%
 # Keep cache utilization 0.00%
+# Temp disk utilization 1.21%
 #!! container max RSS was 67 MiB -- try reducing runtime_constraints to "ram":1020054732
+#!! container max temp disk utilization was 1% of 383960 MiB -- consider reducing "tmpdirMin" and/or "outdirMin"
index 5e3ad152f7e0e48759312592344cdc936eb95f23..1fb56c7beba7a2e7345bc4580fd2e3d7962d6d1f 100644 (file)
@@ -31,4 +31,5 @@ time  elapsed 80      -       80
 # Max network speed in a single interval: 42.58MB/s
 # Keep cache miss rate 0.00%
 # Keep cache utilization 0.00%
+# Temp disk utilization 0.00%
 #!! 4xphq-8i9sb-jq0ekny1xou3zoh max RSS was 334 MiB -- try reducing runtime_constraints to "min_ram_mb_per_node":972
index e260ca5bdeeed232ee61e094c17fe1ccfad5063f..f567233fb7d6e0cd6fdd3716b1d7dd237a8824f6 100644 (file)
@@ -20,4 +20,5 @@ time  elapsed 2       -       4
 # Max network speed in a single interval: 0.00MB/s
 # Keep cache miss rate 0.00%
 # Keep cache utilization 0.00%
+# Temp disk utilization 0.00%
 #!! 4xphq-8i9sb-zvb2ocfycpomrup max RSS was 1 MiB -- try reducing runtime_constraints to "min_ram_mb_per_node":972
index ffe1072250123f2b05f67ddd62da2bf0881b35a1..ab0febbefa83fcddfe0519c3744f826be50eecc0 100644 (file)
@@ -20,4 +20,5 @@ time  elapsed 2       -       3
 # Max network speed in a single interval: 0.00MB/s
 # Keep cache miss rate 0.00%
 # Keep cache utilization 0.00%
+# Temp disk utilization 0.00%
 #!! 4xphq-8i9sb-v831jm2uq0g2g9x max RSS was 1 MiB -- try reducing runtime_constraints to "min_ram_mb_per_node":972
index 7603ea488c37e4a74f7946d3e108116329b0bad2..0270eaaec06d7fa521e8279022035b6a3bf5bd01 100644 (file)
@@ -102,7 +102,7 @@ class SummarizeContainer(ReportDiff):
                 return UTF8Decode(gzip.open(self.arvmountlog))
         mock_cr().open.side_effect = _open
         args = crunchstat_summary.command.ArgumentParser().parse_args(
-            ['--job', self.fake_request['uuid']])
+            ['--container', self.fake_request['uuid']])
         cmd = crunchstat_summary.command.Command(args)
         cmd.run()
         self.diff_known_report(self.reportfile, cmd)