[arvados] created: 2.7.0-689-ga7d13c98bb

git repository hosting git at public.arvados.org
Fri Sep 29 14:14:41 UTC 2023


        at  a7d13c98bbf2a79262f09f1054f8c1767a00f74a (commit)


commit a7d13c98bbf2a79262f09f1054f8c1767a00f74a
Author: Tom Clegg <tom at curii.com>
Date:   Fri Sep 29 10:11:44 2023 -0400

    20300: Remove obsolete auth code.
    
    The browser-facing parts are now handled by controller.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/app/controllers/user_sessions_controller.rb b/services/api/app/controllers/user_sessions_controller.rb
index 1a9cc797fc..c2fc2e1a03 100644
--- a/services/api/app/controllers/user_sessions_controller.rb
+++ b/services/api/app/controllers/user_sessions_controller.rb
@@ -11,24 +11,29 @@ class UserSessionsController < ApplicationController
 
   respond_to :html
 
+  def login
+    return send_error "Legacy code path no longer supported", status: 404
+  end
+
   # create a new session
   def create
-    if !Rails.configuration.Login.LoginCluster.empty? and Rails.configuration.Login.LoginCluster != Rails.configuration.ClusterID
-      raise "Local login disabled when LoginCluster is set"
-    end
-
-    max_expires_at = nil
-    if params[:provider] == 'controller'
-      if request.headers['Authorization'] != 'Bearer ' + Rails.configuration.SystemRootToken
-        return send_error('Invalid authorization header', status: 401)
-      end
-      # arvados-controller verified the user and is passing auth_info
-      # in request params.
-      authinfo = SafeJSON.load(params[:auth_info])
-      max_expires_at = authinfo["expires_at"]
-    else
+    remote, return_to_url = params[:return_to].split(',', 2)
+    if params[:provider] != 'controller' ||
+       return_to_url != 'https://controller.api.client.invalid'
       return send_error "Legacy code path no longer supported", status: 404
     end
+    if request.headers['Authorization'] != 'Bearer ' + Rails.configuration.SystemRootToken
+      return send_error('Invalid authorization header', status: 401)
+    end
+    if remote == ''
+      remote = nil
+    elsif remote !~ /^[0-9a-z]{5}$/
+      return send_error 'Invalid remote cluster id', status: 400
+    end
+    # arvados-controller verified the user and is passing auth_info
+    # in request params.
+    authinfo = SafeJSON.load(params[:auth_info])
+    max_expires_at = authinfo["expires_at"]
 
     if !authinfo['user_uuid'].blank?
       user = User.find_by_uuid(authinfo['user_uuid'])
@@ -49,40 +54,13 @@ class UserSessionsController < ApplicationController
     # For the benefit of functional and integration tests:
     @user = user
 
-    if user.uuid[0..4] != Rails.configuration.ClusterID
-      # Actually a remote user
-      # Send them to their home cluster's login
-      rh = Rails.configuration.RemoteClusters[user.uuid[0..4]]
-      remote, return_to_url = params[:return_to].split(',', 2)
-      @remotehomeurl = "#{rh.Scheme || "https"}://#{rh.Host}/login?remote=#{Rails.configuration.ClusterID}&return_to=#{return_to_url}"
-      render
-      return
-    end
-
     # prevent ArvadosModel#before_create and _update from throwing
     # "unauthorized":
     Thread.current[:user] = user
 
     user.save or raise Exception.new(user.errors.messages)
 
-    # Give the authenticated user a cookie for direct API access
-    session[:user_id] = user.id
-    session[:api_client_uuid] = nil
-    session[:api_client_trusted] = true # full permission to see user's secrets
-
-    @redirect_to = root_path
-    if params.has_key?(:return_to)
-      # return_to param's format is 'remote,return_to_url'. This comes from login()
-      # encoding the remote=zbbbb parameter passed by a client asking for a salted
-      # token.
-      remote, return_to_url = params[:return_to].split(',', 2)
-      if remote !~ /^[0-9a-z]{5}$/ && remote != ""
-        return send_error 'Invalid remote cluster id', status: 400
-      end
-      remote = nil if remote == ''
-      return send_api_token_to(return_to_url, user, remote, max_expires_at)
-    end
-    redirect_to @redirect_to
+    return send_api_token_to(return_to_url, user, remote, max_expires_at)
   end
 
   # Omniauth failure callback
@@ -90,51 +68,6 @@ class UserSessionsController < ApplicationController
     flash[:notice] = params[:message]
   end
 
-  # logout - this gets intercepted by controller, so this is probably
-  # mostly dead code at this point.
-  def logout
-    session[:user_id] = nil
-
-    flash[:notice] = 'You have logged off'
-    return_to = params[:return_to] || root_url
-    redirect_to return_to
-  end
-
-  # login.  Redirect to LoginCluster.
-  def login
-    if params[:remote] !~ /^[0-9a-z]{5}$/ && !params[:remote].nil?
-      return send_error 'Invalid remote cluster id', status: 400
-    end
-    if current_user && params[:return_to] == "https://controller.api.client.invalid"
-      # Already logged in; just need to send a token to the requesting
-      # API client. Note, although this response looks like it's meant
-      # to be sent to a web browser, in fact the only supported use
-      # case is where our client is arvados-controller, giving us the
-      # placeholder URL https://controller.api.client.invalid.
-      return send_api_token_to(params[:return_to], current_user, params[:remote])
-    end
-    p = []
-    p << "auth_provider=#{CGI.escape(params[:auth_provider])}" if params[:auth_provider]
-
-    if !Rails.configuration.Login.LoginCluster.empty? and Rails.configuration.Login.LoginCluster != Rails.configuration.ClusterID
-      host = ApiClientAuthorization.remote_host(uuid_prefix: Rails.configuration.Login.LoginCluster)
-      if not host
-        raise "LoginCluster #{Rails.configuration.Login.LoginCluster} missing from RemoteClusters"
-      end
-      scheme = "https"
-      cluster = Rails.configuration.RemoteClusters[Rails.configuration.Login.LoginCluster]
-      if cluster and cluster['Scheme'] and !cluster['Scheme'].empty?
-        scheme = cluster['Scheme']
-      end
-      login_cluster = "#{scheme}://#{host}"
-      p << "remote=#{CGI.escape(params[:remote])}" if params[:remote]
-      p << "return_to=#{CGI.escape(params[:return_to])}" if params[:return_to]
-      redirect_to "#{login_cluster}/login?#{p.join('&')}"
-    else
-      return send_error "Legacy code path no longer supported", status: 404
-    end
-  end
-
   def send_api_token_to(callback_url, user, remote=nil, token_expiration=nil)
     # Give the API client a token for making API calls on behalf of
     # the authenticated user
diff --git a/services/api/test/functional/user_sessions_controller_test.rb b/services/api/test/functional/user_sessions_controller_test.rb
index 66aff787bd..cf4c6e8b4d 100644
--- a/services/api/test/functional/user_sessions_controller_test.rb
+++ b/services/api/test/functional/user_sessions_controller_test.rb
@@ -6,124 +6,30 @@ require 'test_helper'
 
 class UserSessionsControllerTest < ActionController::TestCase
 
-  test "redirect to joshid" do
-    api_client_page = 'http://client.example.com/home'
-    get :login, params: {return_to: api_client_page}
-    # Not supported any more
-    assert_response 404
-  end
-
-  test "send token when user is already logged in" do
-    authorize_with :inactive
-    api_client_page = 'http://client.example.com/home'
-    get :login, params: {return_to: api_client_page}
-    assert_response :redirect
-    assert_equal(0, @response.redirect_url.index(api_client_page + '?'),
-                 'Redirect url ' + @response.redirect_url +
-                 ' should start with ' + api_client_page + '?')
-    assert_not_nil assigns(:api_client)
-  end
-
-  test "login creates token without expiration by default" do
-    assert_equal Rails.configuration.Login.TokenLifetime, 0
-    authorize_with :inactive
-    api_client_page = 'http://client.example.com/home'
-    get :login, params: {return_to: api_client_page}
-    assert_response :redirect
-    assert_not_nil assigns(:api_client)
-    assert_nil assigns(:api_client_auth).expires_at
-  end
-
-  test "login creates token with configured lifetime" do
-    token_lifetime = 1.hour
-    Rails.configuration.Login.TokenLifetime = token_lifetime
-    authorize_with :inactive
-    api_client_page = 'http://client.example.com/home'
-    get :login, params: {return_to: api_client_page}
-    assert_response :redirect
-    assert_not_nil assigns(:api_client)
-    api_client_auth = assigns(:api_client_auth)
-    assert_in_delta(api_client_auth.expires_at,
-                    api_client_auth.updated_at + token_lifetime,
-                    1.second)
-  end
-
-  [[0, 1.hour, 1.hour],
-  [1.hour, 2.hour, 1.hour],
-  [2.hour, 1.hour, 1.hour],
-  [2.hour, nil, 2.hour],
-  ].each do |config_lifetime, request_lifetime, expect_lifetime|
-    test "login with TokenLifetime=#{config_lifetime} and request has expires_at=#{ request_lifetime.nil? ? "nil" : request_lifetime }" do
-      Rails.configuration.Login.TokenLifetime = config_lifetime
-      expected_expiration_time =  Time.now() + expect_lifetime
-      authorize_with :inactive
-      @request.headers['Authorization'] = 'Bearer '+Rails.configuration.SystemRootToken
-      if request_lifetime.nil?
-        get :create, params: {provider: 'controller', auth_info: {email: "foo at bar.com"}, return_to: ',https://app.example'}
-      else
-        get :create, params: {provider: 'controller', auth_info: {email: "foo at bar.com", expires_at: Time.now() + request_lifetime}, return_to: ',https://app.example'}
-      end
-      assert_response :redirect
-      api_client_auth = assigns(:api_client_auth)
-      assert_not_nil api_client_auth
-      assert_not_nil assigns(:api_client)
-      assert_in_delta(api_client_auth.expires_at,
-                      expected_expiration_time,
-                      1.second)
-    end
-  end
-
-  test "login with remote param returns a salted token" do
-    authorize_with :inactive
-    api_client_page = 'http://client.example.com/home'
-    remote_prefix = 'zbbbb'
-    get :login, params: {return_to: api_client_page, remote: remote_prefix}
-    assert_response :redirect
-    api_client_auth = assigns(:api_client_auth)
-    assert_not_nil api_client_auth
-    assert_includes(@response.redirect_url, 'api_token='+api_client_auth.salted_token(remote: remote_prefix))
+  setup do
+    @allowed_return_to = ",https://controller.api.client.invalid"
   end
 
-  test "login with malformed remote param returns an error" do
-    authorize_with :inactive
-    api_client_page = 'http://client.example.com/home'
-    remote_prefix = 'invalid_cluster_id'
-    get :login, params: {return_to: api_client_page, remote: remote_prefix}
-    assert_response 400
-  end
-
-  test "login to LoginCluster" do
-    Rails.configuration.Login.LoginCluster = 'zbbbb'
-    Rails.configuration.RemoteClusters['zbbbb'] = ConfigLoader.to_OrderedOptions({'Host' => 'zbbbb.example.com'})
-    api_client_page = 'http://client.example.com/home'
-    get :login, params: {return_to: api_client_page}
-    assert_response :redirect
-    assert_equal("https://zbbbb.example.com/login?return_to=http%3A%2F%2Fclient.example.com%2Fhome", @response.redirect_url)
-    assert_nil assigns(:api_client)
-  end
-
-  test "don't go into redirect loop if LoginCluster is self" do
-    Rails.configuration.Login.LoginCluster = 'zzzzz'
-    api_client_page = 'http://client.example.com/home'
-    get :login, params: {return_to: api_client_page}
-    # Doesn't redirect, just fail.
+  test "login route deleted" do
+    @request.headers['Authorization'] = 'Bearer '+Rails.configuration.SystemRootToken
+    get :login, params: {provider: 'controller', return_to: @allowed_return_to}
     assert_response 404
   end
 
   test "controller cannot create session without SystemRootToken" do
-    get :create, params: {provider: 'controller', auth_info: {email: "foo at bar.com"}, return_to: ',https://app.example'}
+    get :create, params: {provider: 'controller', auth_info: {email: "foo at bar.com"}, return_to: @allowed_return_to}
     assert_response 401
   end
 
   test "controller cannot create session with wrong SystemRootToken" do
     @request.headers['Authorization'] = 'Bearer blah'
-    get :create, params: {provider: 'controller', auth_info: {email: "foo at bar.com"}, return_to: ',https://app.example'}
+    get :create, params: {provider: 'controller', auth_info: {email: "foo at bar.com"}, return_to: @allowed_return_to}
     assert_response 401
   end
 
   test "controller can create session using SystemRootToken" do
     @request.headers['Authorization'] = 'Bearer '+Rails.configuration.SystemRootToken
-    get :create, params: {provider: 'controller', auth_info: {email: "foo at bar.com"}, return_to: ',https://app.example'}
+    get :create, params: {provider: 'controller', auth_info: {email: "foo at bar.com"}, return_to: @allowed_return_to}
     assert_response :redirect
     api_client_auth = assigns(:api_client_auth)
     assert_not_nil api_client_auth
diff --git a/services/api/test/integration/user_sessions_test.rb b/services/api/test/integration/user_sessions_test.rb
index 76659f3207..eb49cf832e 100644
--- a/services/api/test/integration/user_sessions_test.rb
+++ b/services/api/test/integration/user_sessions_test.rb
@@ -8,7 +8,7 @@ class UserSessionsApiTest < ActionDispatch::IntegrationTest
   # remote prefix & return url packed into the return_to param passed around
   # between API and SSO provider.
   def client_url(remote: nil)
-    url = ',https://wb.example.com'
+    url = ',https://controller.api.client.invalid'
     url = "#{remote}#{url}" unless remote.nil?
     url
   end

commit 572c1aa397cd016bc349fb191928cde6082c962b
Author: Tom Clegg <tom at curii.com>
Date:   Thu Sep 28 10:57:19 2023 -0400

    20300: Remove superfluous wrapper funcs.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/lib/controller/localdb/conn.go b/lib/controller/localdb/conn.go
index 6ab9e1450b..4b00ef39c5 100644
--- a/lib/controller/localdb/conn.go
+++ b/lib/controller/localdb/conn.go
@@ -158,21 +158,6 @@ func (conn *Conn) VocabularyGet(ctx context.Context) (arvados.Vocabulary, error)
 	return *conn.vocabularyCache, nil
 }
 
-// Logout handles the logout of conn giving to the appropriate loginController
-func (conn *Conn) Logout(ctx context.Context, opts arvados.LogoutOptions) (arvados.LogoutResponse, error) {
-	return conn.loginController.Logout(ctx, opts)
-}
-
-// Login handles the login of conn giving to the appropriate loginController
-func (conn *Conn) Login(ctx context.Context, opts arvados.LoginOptions) (arvados.LoginResponse, error) {
-	return conn.loginController.Login(ctx, opts)
-}
-
-// UserAuthenticate handles the User Authentication of conn giving to the appropriate loginController
-func (conn *Conn) UserAuthenticate(ctx context.Context, opts arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) {
-	return conn.loginController.UserAuthenticate(ctx, opts)
-}
-
 var privateNetworks = func() (nets []*net.IPNet) {
 	for _, s := range []string{
 		"127.0.0.0/8",

commit 2d1d16aaae709b0f0b3c582c7cbb9c30f451842e
Author: Tom Clegg <tom at curii.com>
Date:   Tue Sep 26 09:07:51 2023 -0400

    20300: Fix flaky test.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/lib/controller/localdb/container_test.go b/lib/controller/localdb/container_test.go
index 65d9fac5bb..946eb32b9d 100644
--- a/lib/controller/localdb/container_test.go
+++ b/lib/controller/localdb/container_test.go
@@ -250,6 +250,9 @@ func (s *containerSuite) TestUpdatePriorityMultiLevelWorkflow(c *C) {
 						"state": "Cancelled",
 					},
 				})
+				if errors.Is(err, context.Canceled) {
+					break
+				}
 				c.Assert(err, IsNil)
 			}
 			time.Sleep(time.Second / 10)

commit c13ae783174624c9f1c48bf090e44fb4b9501320
Author: Tom Clegg <tom at curii.com>
Date:   Mon Sep 25 17:10:38 2023 -0400

    20300: Fix login callback redirect.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/lib/controller/federation_test.go b/lib/controller/federation_test.go
index 4fbb3440ed..599686e3e6 100644
--- a/lib/controller/federation_test.go
+++ b/lib/controller/federation_test.go
@@ -707,7 +707,7 @@ func (s *FederationSuite) TestCreateRemoteContainerRequestCheckRuntimeToken(c *c
 	s.testHandler.Cluster.API.MaxTokenLifetime = arvados.Duration(time.Hour)
 
 	resp := s.testRequest(req).Result()
-	c.Check(resp.StatusCode, check.Equals, http.StatusOK)
+	c.Assert(resp.StatusCode, check.Equals, http.StatusOK)
 
 	cr := s.getCRfromMockRequest(c)
 
diff --git a/services/api/app/controllers/user_sessions_controller.rb b/services/api/app/controllers/user_sessions_controller.rb
index ae34fa7600..1a9cc797fc 100644
--- a/services/api/app/controllers/user_sessions_controller.rb
+++ b/services/api/app/controllers/user_sessions_controller.rb
@@ -105,13 +105,12 @@ class UserSessionsController < ApplicationController
     if params[:remote] !~ /^[0-9a-z]{5}$/ && !params[:remote].nil?
       return send_error 'Invalid remote cluster id', status: 400
     end
-    if current_user and params[:return_to]
+    if current_user && params[:return_to] == "https://controller.api.client.invalid"
       # Already logged in; just need to send a token to the requesting
-      # API client.
-      #
-      # FIXME: if current_user has never authorized this app before,
-      # ask for confirmation here!
-
+      # API client. Note, although this response looks like it's meant
+      # to be sent to a web browser, in fact the only supported use
+      # case is where our client is arvados-controller, giving us the
+      # placeholder URL https://controller.api.client.invalid.
       return send_api_token_to(params[:return_to], current_user, params[:remote])
     end
     p = []
@@ -173,7 +172,7 @@ class UserSessionsController < ApplicationController
       token = @api_client_auth.salted_token(remote: remote)
     end
     callback_url += 'api_token=' + token
-    redirect_to callback_url
+    redirect_to callback_url, allow_other_host: true
   end
 
   def cross_origin_forbidden

commit 6d918ec7f9bb4ffc58d6e482a63031c8a51e20ed
Author: Tom Clegg <tom at curii.com>
Date:   Mon Sep 25 10:45:11 2023 -0400

    20300: Use Rails 7.0 config defaults.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/config/application.rb b/services/api/config/application.rb
index e91d681b5a..9b024c3a82 100644
--- a/services/api/config/application.rb
+++ b/services/api/config/application.rb
@@ -33,7 +33,7 @@ module Server
     require_relative "arvados_config.rb"
 
     # Initialize configuration defaults for specified Rails version.
-    config.load_defaults 6.1
+    config.load_defaults 7.0
 
     # Configuration for the application, engines, and railties goes here.
     #
diff --git a/services/api/config/initializers/new_framework_defaults_7_0.rb b/services/api/config/initializers/new_framework_defaults_7_0.rb
deleted file mode 100644
index fcba0035e1..0000000000
--- a/services/api/config/initializers/new_framework_defaults_7_0.rb
+++ /dev/null
@@ -1,143 +0,0 @@
-# Be sure to restart your server when you modify this file.
-#
-# This file eases your Rails 7.0 framework defaults upgrade.
-#
-# Uncomment each configuration one by one to switch to the new default.
-# Once your application is ready to run with all new defaults, you can remove
-# this file and set the `config.load_defaults` to `7.0`.
-#
-# Read the Guide for Upgrading Ruby on Rails for more info on each option.
-# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html
-
-# `button_to` view helper will render `<button>` element, regardless of whether
-# or not the content is passed as the first argument or as a block.
-Rails.application.config.action_view.button_to_generates_button_tag = true
-
-# `stylesheet_link_tag` view helper will not render the media attribute by default.
-Rails.application.config.action_view.apply_stylesheet_media_default = false
-
-# Change the digest class for the key generators to `OpenSSL::Digest::SHA256`.
-# Changing this default means invalidate all encrypted messages generated by
-# your application and, all the encrypted cookies. Only change this after you
-# rotated all the messages using the key rotator.
-#
-# See upgrading guide for more information on how to build a rotator.
-# https://guides.rubyonrails.org/v7.0/upgrading_ruby_on_rails.html
-Rails.application.config.active_support.key_generator_hash_digest_class = OpenSSL::Digest::SHA256
-
-# Change the digest class for ActiveSupport::Digest.
-# Changing this default means that for example Etags change and
-# various cache keys leading to cache invalidation.
-Rails.application.config.active_support.hash_digest_class = OpenSSL::Digest::SHA256
-
-# Don't override ActiveSupport::TimeWithZone.name and use the default Ruby
-# implementation.
-Rails.application.config.active_support.remove_deprecated_time_with_zone_name = true
-
-# Calls `Rails.application.executor.wrap` around test cases.
-# This makes test cases behave closer to an actual request or job.
-# Several features that are normally disabled in test, such as Active Record query cache
-# and asynchronous queries will then be enabled.
-Rails.application.config.active_support.executor_around_test_case = true
-
-# Set both the `:open_timeout` and `:read_timeout` values for `:smtp` delivery method.
-# Rails.application.config.action_mailer.smtp_timeout = 5
-
-# The ActiveStorage video previewer will now use scene change detection to generate
-# better preview images (rather than the previous default of using the first frame
-# of the video).
-# Rails.application.config.active_storage.video_preview_arguments =
-#   "-vf 'select=eq(n\\,0)+eq(key\\,1)+gt(scene\\,0.015),loop=loop=-1:size=2,trim=start_frame=1' -frames:v 1 -f image2"
-
-# Automatically infer `inverse_of` for associations with a scope.
-Rails.application.config.active_record.automatic_scope_inversing = true
-
-# Raise when running tests if fixtures contained foreign key violations
-Rails.application.config.active_record.verify_foreign_keys_for_fixtures = true
-
-# Disable partial inserts.
-# This default means that all columns will be referenced in INSERT queries
-# regardless of whether they have a default or not.
-Rails.application.config.active_record.partial_inserts = false
-
-# Protect from open redirect attacks in `redirect_back_or_to` and `redirect_to`.
-Rails.application.config.action_controller.raise_on_open_redirects = true
-
-# Change the variant processor for Active Storage.
-# Changing this default means updating all places in your code that
-# generate variants to use image processing macros and ruby-vips
-# operations. See the upgrading guide for detail on the changes required.
-# The `:mini_magick` option is not deprecated; it's fine to keep using it.
-# Rails.application.config.active_storage.variant_processor = :vips
-
-# Enable parameter wrapping for JSON.
-# Previously this was set in an initializer. It's fine to keep using that initializer if you've customized it.
-# To disable parameter wrapping entirely, set this config to `false`.
-Rails.application.config.action_controller.wrap_parameters_by_default = true
-
-# Specifies whether generated namespaced UUIDs follow the RFC 4122 standard for namespace IDs provided as a
-# `String` to `Digest::UUID.uuid_v3` or `Digest::UUID.uuid_v5` method calls.
-#
-# See https://guides.rubyonrails.org/configuring.html#config-active-support-use-rfc4122-namespaced-uuids for
-# more information.
-Rails.application.config.active_support.use_rfc4122_namespaced_uuids = true
-
-# Change the default headers to disable browsers' flawed legacy XSS protection.
-Rails.application.config.action_dispatch.default_headers = {
-  "X-Frame-Options" => "SAMEORIGIN",
-  "X-XSS-Protection" => "0",
-  "X-Content-Type-Options" => "nosniff",
-  "X-Download-Options" => "noopen",
-  "X-Permitted-Cross-Domain-Policies" => "none",
-  "Referrer-Policy" => "strict-origin-when-cross-origin"
-}
-
-
-# ** Please read carefully, this must be configured in config/application.rb **
-# Change the format of the cache entry.
-# Changing this default means that all new cache entries added to the cache
-# will have a different format that is not supported by Rails 6.1 applications.
-# Only change this value after your application is fully deployed to Rails 7.0
-# and you have no plans to rollback.
-# When you're ready to change format, add this to `config/application.rb` (NOT this file):
-#  config.active_support.cache_format_version = 7.0
-
-
-# Cookie serializer: 2 options
-#
-# If you're upgrading and haven't set `cookies_serializer` previously, your cookie serializer
-# is `:marshal`. The default for new apps is `:json`.
-#
-Rails.application.config.action_dispatch.cookies_serializer = :json
-#
-#
-# To migrate an existing application to the `:json` serializer, use the `:hybrid` option.
-#
-# Rails transparently deserializes existing (Marshal-serialized) cookies on read and
-# re-writes them in the JSON format.
-#
-# It is fine to use `:hybrid` long term; you should do that until you're confident *all* your cookies
-# have been converted to JSON. To keep using `:hybrid` long term, move this config to its own
-# initializer or to `config/application.rb`.
-#
-# Rails.application.config.action_dispatch.cookies_serializer = :hybrid
-#
-#
-# If your cookies can't yet be serialized to JSON, keep using `:marshal` for backward-compatibility.
-#
-# If you have configured the serializer elsewhere, you can remove this section of the file.
-#
-# See https://guides.rubyonrails.org/action_controller_overview.html#cookies for more information.
-
-# Change the return value of `ActionDispatch::Request#content_type` to the Content-Type header without modification.
-Rails.application.config.action_dispatch.return_only_request_media_type_on_content_type = false
-
-# Active Storage `has_many_attached` relationships will default to replacing the current collection instead of appending to it.
-# Thus, to support submitting an empty collection, the `file_field` helper will render an hidden field `include_hidden` by default when `multiple_file_field_include_hidden` is set to `true`.
-# See https://guides.rubyonrails.org/configuring.html#config-active-storage-multiple-file-field-include-hidden for more information.
-# Rails.application.config.active_storage.multiple_file_field_include_hidden = true
-
-# ** Please read carefully, this must be configured in config/application.rb (NOT this file) **
-# Disables the deprecated #to_s override in some Ruby core classes
-# See https://guides.rubyonrails.org/configuring.html#config-active-support-disable-to-s-conversion for more information.
-# config.active_support.disable_to_s_conversion = true

commit 030a7ee62740e5f7def3a7333d1c8996b3111fef
Author: Tom Clegg <tom at curii.com>
Date:   Mon Sep 25 10:43:32 2023 -0400

    20300: Bypass query cache when re-fetching record for race check.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/app/models/api_client_authorization.rb b/services/api/app/models/api_client_authorization.rb
index 756d835eff..ef73d79c17 100644
--- a/services/api/app/models/api_client_authorization.rb
+++ b/services/api/app/models/api_client_authorization.rb
@@ -389,7 +389,9 @@ class ApiClientAuthorization < ArvadosModel
         rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
           Rails.logger.debug("remote user #{remote_user['uuid']} already exists, retrying...")
           # Some other request won the race: retry fetching the user record.
-          user = User.find_by_uuid(remote_user['uuid'])
+          user = User.uncached do
+            User.find_by_uuid(remote_user['uuid'])
+          end
           if !user
             Rails.logger.warn("cannot find or create remote user #{remote_user['uuid']}")
             return nil

commit 4b58bfc5b58db18e4816102f9850757f0884a42e
Author: Tom Clegg <tom at curii.com>
Date:   Mon Sep 25 10:05:23 2023 -0400

    20300: Merge similar test cases and fix query cache sensitivity.
    
    Token expiration tests needlessly relied on the Rails < 7.0 behavior
    of disabling the query cache during tests, such that successive calls
    to db_current_time would never return equal times.
    
    Merged the admin and non-admin test cases, since they have the same
    structure and nearly the same expectations.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/test/integration/api_client_authorizations_api_test.rb b/services/api/test/integration/api_client_authorizations_api_test.rb
index 405e4bf687..1b5c563962 100644
--- a/services/api/test/integration/api_client_authorizations_api_test.rb
+++ b/services/api/test/integration/api_client_authorizations_api_test.rb
@@ -77,93 +77,49 @@ class ApiClientAuthorizationsApiTest < ActionDispatch::IntegrationTest
   end
 
   [nil, db_current_time + 2.hours].each do |desired_expiration|
-    test "expires_at gets clamped on non-admins when API.MaxTokenLifetime is set and desired expires_at #{desired_expiration.nil? ? 'is not set' : 'exceeds the limit'}" do
-      Rails.configuration.API.MaxTokenLifetime = 1.hour
-
-      # Test token creation
-      start_t = db_current_time
-      post "/arvados/v1/api_client_authorizations",
-        params: {
-          :format => :json,
-          :api_client_authorization => {
-            :owner_uuid => users(:active).uuid,
-            :expires_at => desired_expiration,
-          }
-        },
-        headers: {'HTTP_AUTHORIZATION' => "OAuth2 #{api_client_authorizations(:active_trustedclient).api_token}"}
-      end_t = db_current_time
-      assert_response 200
-      expiration_t = json_response['expires_at'].to_time
-      assert_operator expiration_t.to_f, :>, (start_t + Rails.configuration.API.MaxTokenLifetime).to_f
-      if !desired_expiration.nil?
-        assert_operator expiration_t.to_f, :<, desired_expiration.to_f
-      else
-        assert_operator expiration_t.to_f, :<, (end_t + Rails.configuration.API.MaxTokenLifetime).to_f
-      end
-
-      # Test token update
-      previous_expiration = expiration_t
-      token_uuid = json_response["uuid"]
-      start_t = db_current_time
-      put "/arvados/v1/api_client_authorizations/#{token_uuid}",
-        params: {
-          :api_client_authorization => {
-            :expires_at => desired_expiration
-          }
-        },
-        headers: {'HTTP_AUTHORIZATION' => "OAuth2 #{api_client_authorizations(:active_trustedclient).api_token}"}
-      end_t = db_current_time
-      assert_response 200
-      expiration_t = json_response['expires_at'].to_time
-      assert_operator previous_expiration.to_f, :<, expiration_t.to_f
-      assert_operator expiration_t.to_f, :>, (start_t + Rails.configuration.API.MaxTokenLifetime).to_f
-      if !desired_expiration.nil?
-        assert_operator expiration_t.to_f, :<, desired_expiration.to_f
-      else
-        assert_operator expiration_t.to_f, :<, (end_t + Rails.configuration.API.MaxTokenLifetime).to_f
-      end
-    end
-
-    test "behavior when expires_at is set to #{desired_expiration.nil? ? 'nil' : 'exceed the limit'} by admins when API.MaxTokenLifetime is set" do
-      Rails.configuration.API.MaxTokenLifetime = 1.hour
-
-      # Test token creation
-      post "/arvados/v1/api_client_authorizations",
-        params: {
-          :format => :json,
-          :api_client_authorization => {
-            :owner_uuid => users(:admin).uuid,
-            :expires_at => desired_expiration,
-          }
-        },
-        headers: {'HTTP_AUTHORIZATION' => "OAuth2 #{api_client_authorizations(:admin_trustedclient).api_token}"}
-      assert_response 200
-      if desired_expiration.nil?
-        # When expires_at is nil, default to MaxTokenLifetime
-        assert_operator (json_response['expires_at'].to_time.to_i - (db_current_time + Rails.configuration.API.MaxTokenLifetime).to_i).abs, :<, 2
-      else
-        assert_equal json_response['expires_at'].to_time.to_i, desired_expiration.to_i
-      end
-
-      # Test token update (reverse the above behavior)
-      token_uuid = json_response['uuid']
-      if desired_expiration.nil?
-        submitted_updated_expiration = db_current_time + Rails.configuration.API.MaxTokenLifetime + 1.hour
-      else
-        submitted_updated_expiration = nil
-      end
-      put "/arvados/v1/api_client_authorizations/#{token_uuid}",
-        params: {
-          :api_client_authorization => {
-            :expires_at => submitted_updated_expiration,
-          }
-        },
-        headers: {'HTTP_AUTHORIZATION' => "OAuth2 #{api_client_authorizations(:admin_trustedclient).api_token}"}
-      assert_response 200
-      if submitted_updated_expiration.nil?
-        assert_operator (json_response['expires_at'].to_time.to_i - (db_current_time + Rails.configuration.API.MaxTokenLifetime).to_i).abs, :<, 2
-      else
-        assert_equal json_response['expires_at'].to_time.to_i, submitted_updated_expiration.to_i
+    [false, true].each do |admin|
+      test "expires_at gets clamped on #{admin ? 'admins' : 'non-admins'} when API.MaxTokenLifetime is set and desired expires_at #{desired_expiration.nil? ? 'is not set' : 'exceeds the limit'}" do
+        Rails.configuration.API.MaxTokenLifetime = 1.hour
+        token = api_client_authorizations(admin ? :admin_trustedclient : :active_trustedclient).api_token
+
+        # Test token creation
+        start_t = db_current_time
+        post "/arvados/v1/api_client_authorizations",
+             params: {
+               :format => :json,
+               :api_client_authorization => {
+                 :owner_uuid => users(admin ? :admin : :active).uuid,
+                 :expires_at => desired_expiration,
+               }
+             },
+             headers: {'HTTP_AUTHORIZATION' => "OAuth2 #{token}"}
+        assert_response 200
+        expiration_t = json_response['expires_at'].to_time
+        if admin && desired_expiration
+          assert_in_delta desired_expiration.to_f, expiration_t.to_f, 1
+        else
+          assert_in_delta (start_t + Rails.configuration.API.MaxTokenLifetime).to_f, expiration_t.to_f, 2
+        end
+
+        # Test token update
+        previous_expiration = expiration_t
+        token_uuid = json_response["uuid"]
+
+        start_t = db_current_time
+        patch "/arvados/v1/api_client_authorizations/#{token_uuid}",
+            params: {
+              :api_client_authorization => {
+                :expires_at => desired_expiration
+              }
+            },
+            headers: {'HTTP_AUTHORIZATION' => "OAuth2 #{token}"}
+        assert_response 200
+        expiration_t = json_response['expires_at'].to_time
+        if admin && desired_expiration
+          assert_in_delta desired_expiration.to_f, expiration_t.to_f, 1
+        else
+          assert_in_delta (start_t + Rails.configuration.API.MaxTokenLifetime).to_f, expiration_t.to_f, 2
+        end
       end
     end
   end

commit 322ff98d31140a365faa48e9dc78fc079c4b1ee9
Author: Tom Clegg <tom at curii.com>
Date:   Thu Sep 21 16:37:05 2023 -0400

    20300: Turn off overrides of Rails 7.0 framework defaults.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/config/application.rb b/services/api/config/application.rb
index ca5eff1a47..e91d681b5a 100644
--- a/services/api/config/application.rb
+++ b/services/api/config/application.rb
@@ -59,6 +59,14 @@ module Server
     # from connecting to Rails internally via plain http.
     config.ssl_options = {redirect: false}
 
+    # This will change to 7.0 in a future release when there is no
+    # longer a possibility of rolling back to Arvados 2.7 (Rails 5.2)
+    # which cannot read 7.0-format cache files.
+    config.active_support.cache_format_version = 6.1
+
+    # Delete when switching to 7.0 framework defaults.
+    config.active_support.disable_to_s_conversion = true
+
     # Before using the filesystem backend for Rails.cache, check
     # whether we own the relevant directory. If we don't, using it is
     # likely to either fail or (if we're root) pollute it and cause
diff --git a/services/api/config/initializers/new_framework_defaults_7_0.rb b/services/api/config/initializers/new_framework_defaults_7_0.rb
index b13ef5ed16..fcba0035e1 100644
--- a/services/api/config/initializers/new_framework_defaults_7_0.rb
+++ b/services/api/config/initializers/new_framework_defaults_7_0.rb
@@ -11,10 +11,10 @@
 
 # `button_to` view helper will render `<button>` element, regardless of whether
 # or not the content is passed as the first argument or as a block.
-# Rails.application.config.action_view.button_to_generates_button_tag = true
+Rails.application.config.action_view.button_to_generates_button_tag = true
 
 # `stylesheet_link_tag` view helper will not render the media attribute by default.
-# Rails.application.config.action_view.apply_stylesheet_media_default = false
+Rails.application.config.action_view.apply_stylesheet_media_default = false
 
 # Change the digest class for the key generators to `OpenSSL::Digest::SHA256`.
 # Changing this default means invalidate all encrypted messages generated by
@@ -23,22 +23,22 @@
 #
 # See upgrading guide for more information on how to build a rotator.
 # https://guides.rubyonrails.org/v7.0/upgrading_ruby_on_rails.html
-# Rails.application.config.active_support.key_generator_hash_digest_class = OpenSSL::Digest::SHA256
+Rails.application.config.active_support.key_generator_hash_digest_class = OpenSSL::Digest::SHA256
 
 # Change the digest class for ActiveSupport::Digest.
 # Changing this default means that for example Etags change and
 # various cache keys leading to cache invalidation.
-# Rails.application.config.active_support.hash_digest_class = OpenSSL::Digest::SHA256
+Rails.application.config.active_support.hash_digest_class = OpenSSL::Digest::SHA256
 
 # Don't override ActiveSupport::TimeWithZone.name and use the default Ruby
 # implementation.
-# Rails.application.config.active_support.remove_deprecated_time_with_zone_name = true
+Rails.application.config.active_support.remove_deprecated_time_with_zone_name = true
 
 # Calls `Rails.application.executor.wrap` around test cases.
 # This makes test cases behave closer to an actual request or job.
 # Several features that are normally disabled in test, such as Active Record query cache
 # and asynchronous queries will then be enabled.
-# Rails.application.config.active_support.executor_around_test_case = true
+Rails.application.config.active_support.executor_around_test_case = true
 
 # Set both the `:open_timeout` and `:read_timeout` values for `:smtp` delivery method.
 # Rails.application.config.action_mailer.smtp_timeout = 5
@@ -50,18 +50,18 @@
 #   "-vf 'select=eq(n\\,0)+eq(key\\,1)+gt(scene\\,0.015),loop=loop=-1:size=2,trim=start_frame=1' -frames:v 1 -f image2"
 
 # Automatically infer `inverse_of` for associations with a scope.
-# Rails.application.config.active_record.automatic_scope_inversing = true
+Rails.application.config.active_record.automatic_scope_inversing = true
 
 # Raise when running tests if fixtures contained foreign key violations
-# Rails.application.config.active_record.verify_foreign_keys_for_fixtures = true
+Rails.application.config.active_record.verify_foreign_keys_for_fixtures = true
 
 # Disable partial inserts.
 # This default means that all columns will be referenced in INSERT queries
 # regardless of whether they have a default or not.
-# Rails.application.config.active_record.partial_inserts = false
+Rails.application.config.active_record.partial_inserts = false
 
 # Protect from open redirect attacks in `redirect_back_or_to` and `redirect_to`.
-# Rails.application.config.action_controller.raise_on_open_redirects = true
+Rails.application.config.action_controller.raise_on_open_redirects = true
 
 # Change the variant processor for Active Storage.
 # Changing this default means updating all places in your code that
@@ -73,24 +73,24 @@
 # Enable parameter wrapping for JSON.
 # Previously this was set in an initializer. It's fine to keep using that initializer if you've customized it.
 # To disable parameter wrapping entirely, set this config to `false`.
-# Rails.application.config.action_controller.wrap_parameters_by_default = true
+Rails.application.config.action_controller.wrap_parameters_by_default = true
 
 # Specifies whether generated namespaced UUIDs follow the RFC 4122 standard for namespace IDs provided as a
 # `String` to `Digest::UUID.uuid_v3` or `Digest::UUID.uuid_v5` method calls.
 #
 # See https://guides.rubyonrails.org/configuring.html#config-active-support-use-rfc4122-namespaced-uuids for
 # more information.
-# Rails.application.config.active_support.use_rfc4122_namespaced_uuids = true
+Rails.application.config.active_support.use_rfc4122_namespaced_uuids = true
 
 # Change the default headers to disable browsers' flawed legacy XSS protection.
-# Rails.application.config.action_dispatch.default_headers = {
-#   "X-Frame-Options" => "SAMEORIGIN",
-#   "X-XSS-Protection" => "0",
-#   "X-Content-Type-Options" => "nosniff",
-#   "X-Download-Options" => "noopen",
-#   "X-Permitted-Cross-Domain-Policies" => "none",
-#   "Referrer-Policy" => "strict-origin-when-cross-origin"
-# }
+Rails.application.config.action_dispatch.default_headers = {
+  "X-Frame-Options" => "SAMEORIGIN",
+  "X-XSS-Protection" => "0",
+  "X-Content-Type-Options" => "nosniff",
+  "X-Download-Options" => "noopen",
+  "X-Permitted-Cross-Domain-Policies" => "none",
+  "Referrer-Policy" => "strict-origin-when-cross-origin"
+}
 
 
 # ** Please read carefully, this must be configured in config/application.rb **
@@ -108,7 +108,7 @@
 # If you're upgrading and haven't set `cookies_serializer` previously, your cookie serializer
 # is `:marshal`. The default for new apps is `:json`.
 #
-# Rails.application.config.action_dispatch.cookies_serializer = :json
+Rails.application.config.action_dispatch.cookies_serializer = :json
 #
 #
 # To migrate an existing application to the `:json` serializer, use the `:hybrid` option.
@@ -130,7 +130,7 @@
 # See https://guides.rubyonrails.org/action_controller_overview.html#cookies for more information.
 
 # Change the return value of `ActionDispatch::Request#content_type` to the Content-Type header without modification.
-# Rails.application.config.action_dispatch.return_only_request_media_type_on_content_type = false
+Rails.application.config.action_dispatch.return_only_request_media_type_on_content_type = false
 
 # Active Storage `has_many_attached` relationships will default to replacing the current collection instead of appending to it.
 # Thus, to support submitting an empty collection, the `file_field` helper will render an hidden field `include_hidden` by default when `multiple_file_field_include_hidden` is set to `true`.

commit 4960b0e0b6af812e2c34c040163b1ebfa9506bb7
Author: Tom Clegg <tom at curii.com>
Date:   Thu Sep 21 15:57:53 2023 -0400

    20300: Fix relative import paths.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/lib/update_permissions.rb b/services/api/lib/update_permissions.rb
index 799bc40ba2..b06a19747c 100644
--- a/services/api/lib/update_permissions.rb
+++ b/services/api/lib/update_permissions.rb
@@ -2,7 +2,7 @@
 #
 # SPDX-License-Identifier: AGPL-3.0
 
-require '20200501150153_permission_table_constants'
+require_relative '20200501150153_permission_table_constants'
 
 REVOKE_PERM = 0
 CAN_MANAGE_PERM = 3
diff --git a/services/api/test/test_helper.rb b/services/api/test/test_helper.rb
index 843d4f1b23..08696a9983 100644
--- a/services/api/test/test_helper.rb
+++ b/services/api/test/test_helper.rb
@@ -2,7 +2,7 @@
 #
 # SPDX-License-Identifier: AGPL-3.0
 
-require 'update_permissions'
+require_relative '../lib/update_permissions'
 
 ENV["RAILS_ENV"] = "test"
 unless ENV["NO_COVERAGE_TEST"]

commit 401c2c71179825e6c3350177c0f46c503d66a061
Author: Tom Clegg <tom at curii.com>
Date:   Thu Sep 21 14:56:21 2023 -0400

    20300: Update exec_query bind vars usage for Rails 7.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/app/models/group.rb b/services/api/app/models/group.rb
index 5c0aeba589..b6970459b6 100644
--- a/services/api/app/models/group.rb
+++ b/services/api/app/models/group.rb
@@ -168,9 +168,9 @@ class Group < ArvadosModel
       "create temporary table #{temptable} on commit drop " +
       "as select * from project_subtree_with_trash_at($1, LEAST($2, $3)::timestamp)",
       "Group.update_trash.select",
-      [[nil, self.uuid],
-       [nil, TrashedGroup.find_by_group_uuid(self.owner_uuid).andand.trash_at],
-       [nil, self.trash_at]])
+      [self.uuid,
+       TrashedGroup.find_by_group_uuid(self.owner_uuid).andand.trash_at,
+       self.trash_at])
     frozen_descendants = ActiveRecord::Base.connection.exec_query(
       "select uuid from frozen_groups, #{temptable} where uuid = target_uuid",
       "Group.update_trash.check_frozen")
@@ -200,16 +200,16 @@ class Group < ArvadosModel
     ActiveRecord::Base.connection.exec_query(
       "create temporary table #{temptable} on commit drop as select * from project_subtree_with_is_frozen($1,$2)",
       "Group.update_frozen.select",
-      [[nil, self.uuid],
-       [nil, !self.frozen_by_uuid.nil?]])
+      [self.uuid,
+       !self.frozen_by_uuid.nil?])
     if frozen_by_uuid
       rows = ActiveRecord::Base.connection.exec_query(
         "select cr.uuid, cr.state from container_requests cr, #{temptable} frozen " +
         "where cr.owner_uuid = frozen.uuid and frozen.is_frozen " +
         "and cr.state not in ($1, $2) limit 1",
         "Group.update_frozen.check_container_requests",
-        [[nil, ContainerRequest::Uncommitted],
-         [nil, ContainerRequest::Final]])
+        [ContainerRequest::Uncommitted,
+         ContainerRequest::Final])
       if rows.any?
         raise ArgumentError.new("cannot freeze project containing container request #{rows.first['uuid']} with state = #{rows.first['state']}")
       end
@@ -240,11 +240,11 @@ class Group < ArvadosModel
     ActiveRecord::Base.connection.exec_delete(
       "delete from trashed_groups where group_uuid=$1",
       "Group.clear_permissions_trash_frozen",
-      [[nil, self.uuid]])
+      [self.uuid])
     ActiveRecord::Base.connection.exec_delete(
       "delete from frozen_groups where uuid=$1",
       "Group.clear_permissions_trash_frozen",
-      [[nil, self.uuid]])
+      [self.uuid])
   end
 
   def assign_name
diff --git a/services/api/app/models/node.rb b/services/api/app/models/node.rb
index 9e250acfb1..3f0e0b5616 100644
--- a/services/api/app/models/node.rb
+++ b/services/api/app/models/node.rb
@@ -163,8 +163,8 @@ class Node < ArvadosModel
                           LIMIT 1',
                           # query label:
                           'Node.available_slot_number',
-                          # [col_id, val] for $1 vars:
-                          [[nil, MAX_VMS]],
+                          # bind vars:
+                          [MAX_VMS],
                          ).rows.first.andand.first
   end
 
diff --git a/services/api/app/models/user.rb b/services/api/app/models/user.rb
index afc2d18b8a..37603a86e3 100644
--- a/services/api/app/models/user.rb
+++ b/services/api/app/models/user.rb
@@ -145,10 +145,10 @@ SELECT 1 FROM #{PERMISSION_VIEW}
 },
                   # "name" arg is a query label that appears in logs:
                    "user_can_query",
-                   [[nil, self.uuid],
-                    [nil, target_uuid],
-                    [nil, VAL_FOR_PERM[action]],
-                    [nil, target_owner_uuid]]
+                   [self.uuid,
+                    target_uuid,
+                    VAL_FOR_PERM[action],
+                    target_owner_uuid]
                   ).any?
         return false
       end
@@ -237,7 +237,7 @@ SELECT target_uuid, perm_level
                    # "name" arg is a query label that appears in logs:
                    "User.group_permissions",
                    # "binds" arg is an array of [col_id, value] for '$1' vars:
-                   [[nil, uuid]]).
+                   [uuid]).
         rows.each do |group_uuid, max_p_val|
         @group_perms[group_uuid] = PERMS_FOR_VAL[max_p_val.to_i]
       end
diff --git a/services/api/db/migrate/20180917205609_recompute_file_names_index.rb b/services/api/db/migrate/20180917205609_recompute_file_names_index.rb
index b321422143..ed6be3bfe1 100644
--- a/services/api/db/migrate/20180917205609_recompute_file_names_index.rb
+++ b/services/api/db/migrate/20180917205609_recompute_file_names_index.rb
@@ -8,7 +8,7 @@ class RecomputeFileNamesIndex < ActiveRecord::Migration[4.2]
     Collection.select(:portable_data_hash, :manifest_text).where(portable_data_hash: pdhs).distinct(:portable_data_hash).each do |c|
       ActiveRecord::Base.connection.exec_query("update collections set file_names=$1 where portable_data_hash=$2",
                                                "update file_names index",
-                                               [[nil, c.manifest_files], [nil, c.portable_data_hash]])
+                                               [c.manifest_files, c.portable_data_hash])
     end
     ActiveRecord::Base.connection.exec_query('COMMIT')
   end
diff --git a/services/api/db/migrate/20220726034131_write_via_all_users.rb b/services/api/db/migrate/20220726034131_write_via_all_users.rb
index 30e6463beb..f1280597f9 100644
--- a/services/api/db/migrate/20220726034131_write_via_all_users.rb
+++ b/services/api/db/migrate/20220726034131_write_via_all_users.rb
@@ -14,11 +14,11 @@ class WriteViaAllUsers < ActiveRecord::Migration[5.2]
     ActiveRecord::Base.connection.exec_query(
       "update links set name=$1 where link_class=$2 and name=$3 and tail_uuid like $4 and head_uuid = $5",
       "migrate", [
-        [nil, to],
-        [nil, "permission"],
-        [nil, from],
-        [nil, "_____-tpzed-_______________"],
-        [nil, all_users_group_uuid],
+        to,
+        "permission",
+        from,
+        "_____-tpzed-_______________",
+        all_users_group_uuid,
       ])
   end
 end
diff --git a/services/api/lib/can_be_an_owner.rb b/services/api/lib/can_be_an_owner.rb
index fc66f84bfc..e09037819c 100644
--- a/services/api/lib/can_be_an_owner.rb
+++ b/services/api/lib/can_be_an_owner.rb
@@ -62,7 +62,7 @@ module CanBeAnOwner
                   # "name" arg is a query label that appears in logs:
                   "descendant_project_uuids for #{self.uuid}",
                   # "binds" arg is an array of [col_id, value] for '$1' vars:
-                  [[nil, self.uuid], [nil, 'project']],
+                  [self.uuid, 'project'],
                   ).rows.map do |project_uuid,|
       project_uuid
     end
diff --git a/services/api/lib/migrate_yaml_to_json.rb b/services/api/lib/migrate_yaml_to_json.rb
index aa2af60b25..8987f3364c 100644
--- a/services/api/lib/migrate_yaml_to_json.rb
+++ b/services/api/lib/migrate_yaml_to_json.rb
@@ -8,7 +8,7 @@ module MigrateYAMLToJSON
     n = conn.update(
       "UPDATE #{table} SET #{column}=$1 WHERE #{column}=$2",
       "#{table}.#{column} convert YAML to JSON",
-      [[nil, "{}"], [nil, "--- {}\n"]])
+      ["{}", "--- {}\n"])
     Rails.logger.info("#{table}.#{column}: #{n} rows updated using empty hash")
     finished = false
     while !finished
@@ -16,14 +16,14 @@ module MigrateYAMLToJSON
       conn.exec_query(
         "SELECT id, #{column} FROM #{table} WHERE #{column} LIKE $1 LIMIT 100",
         "#{table}.#{column} check for YAML",
-        [[nil, '---%']],
+        ['---%'],
       ).rows.map do |id, yaml|
         n += 1
         json = SafeJSON.dump(YAML.safe_load(yaml))
         conn.exec_query(
           "UPDATE #{table} SET #{column}=$1 WHERE id=$2 AND #{column}=$3",
           "#{table}.#{column} convert YAML to JSON",
-          [[nil, json], [nil, id], [nil, yaml]])
+          [json, id, yaml])
       end
       Rails.logger.info("#{table}.#{column}: #{n} rows updated")
       finished = (n == 0)
diff --git a/services/api/lib/update_permissions.rb b/services/api/lib/update_permissions.rb
index b7e5476404..799bc40ba2 100644
--- a/services/api/lib/update_permissions.rb
+++ b/services/api/lib/update_permissions.rb
@@ -111,10 +111,10 @@ create temporary table #{temptable_perms} on commit drop
 as select * from compute_permission_subgraph($1, $2, $3, $4)
 },
                                              'update_permissions.select',
-                                             [[nil, perm_origin_uuid],
-                                              [nil, starting_uuid],
-                                              [nil, perm_level],
-                                              [nil, edge_id]]
+                                             [perm_origin_uuid,
+                                              starting_uuid,
+                                              perm_level,
+                                              edge_id]
 
     ActiveRecord::Base.connection.exec_query "SET LOCAL enable_mergejoin to true;"
 
diff --git a/services/api/lib/update_priorities.rb b/services/api/lib/update_priorities.rb
index 4183ac10b1..94115340df 100644
--- a/services/api/lib/update_priorities.rb
+++ b/services/api/lib/update_priorities.rb
@@ -17,7 +17,7 @@ UNION
           and container_requests.requesting_container_uuid is not NULL
 )
         order by containers.uuid for update
-  }, 'select_for_update_priorities', [[nil, container_uuid]]
+  }, 'select_for_update_priorities', [container_uuid]
 end
 
 def update_priorities starting_container_uuid
@@ -27,5 +27,5 @@ def update_priorities starting_container_uuid
   ActiveRecord::Base.connection.exec_query %{
 update containers set priority=computed.upd_priority from container_tree_priorities($1) as computed
  where containers.uuid = computed.pri_container_uuid and priority != computed.upd_priority
-}, 'update_priorities', [[nil, starting_container_uuid]]
+}, 'update_priorities', [starting_container_uuid]
 end

commit 55f6306112865521082eae53f8b42eb5bc874905
Author: Tom Clegg <tom at curii.com>
Date:   Thu Sep 21 11:50:19 2023 -0400

    20300: Update time.to_s(:db) to to_fs(:db).
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/test/fixtures/collections.yml b/services/api/test/fixtures/collections.yml
index a5c3e63dde..5a3242e4ff 100644
--- a/services/api/test/fixtures/collections.yml
+++ b/services/api/test/fixtures/collections.yml
@@ -1128,8 +1128,8 @@ collection_<%=i%>_of_10:
   uuid: zzzzz-4zz18-10gneyn6brkx<%= i.to_s.rjust(3, '0') %>
   current_version_uuid: zzzzz-4zz18-10gneyn6brkx<%= i.to_s.rjust(3, '0') %>
   owner_uuid: zzzzz-j7d0g-0010collections
-  created_at: <%= i.minute.ago.to_s(:db) %>
-  modified_at: <%= i.minute.ago.to_s(:db) %>
+  created_at: <%= i.minute.ago.to_fs(:db) %>
+  modified_at: <%= i.minute.ago.to_fs(:db) %>
 <% end %>
 
 # collections in project_with_201_collections
@@ -1141,8 +1141,8 @@ collection_<%=i%>_of_201:
   uuid: zzzzz-4zz18-201gneyn6brd<%= i.to_s.rjust(3, '0') %>
   current_version_uuid: zzzzz-4zz18-201gneyn6brd<%= i.to_s.rjust(3, '0') %>
   owner_uuid: zzzzz-j7d0g-0201collections
-  created_at: <%= i.minute.ago.to_s(:db) %>
-  modified_at: <%= i.minute.ago.to_s(:db) %>
+  created_at: <%= i.minute.ago.to_fs(:db) %>
+  modified_at: <%= i.minute.ago.to_fs(:db) %>
 <% end %>
 
 # Do not add your fixtures below this line as the rest of this file will be trimmed by test_helper
diff --git a/services/api/test/fixtures/container_requests.yml b/services/api/test/fixtures/container_requests.yml
index cc5aedf5e6..71c7a54df3 100644
--- a/services/api/test/fixtures/container_requests.yml
+++ b/services/api/test/fixtures/container_requests.yml
@@ -8,9 +8,9 @@ queued:
   name: queued
   state: Committed
   priority: 1
-  created_at: <%= 2.minute.ago.to_s(:db) %>
-  updated_at: <%= 1.minute.ago.to_s(:db) %>
-  modified_at: <%= 1.minute.ago.to_s(:db) %>
+  created_at: <%= 2.minute.ago.to_fs(:db) %>
+  updated_at: <%= 1.minute.ago.to_fs(:db) %>
+  modified_at: <%= 1.minute.ago.to_fs(:db) %>
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   container_image: test
   cwd: test
@@ -32,9 +32,9 @@ running:
   name: running
   state: Committed
   priority: 501
-  created_at: <%= 2.minute.ago.to_s(:db) %>
-  updated_at: <%= 1.minute.ago.to_s(:db) %>
-  modified_at: <%= 1.minute.ago.to_s(:db) %>
+  created_at: <%= 2.minute.ago.to_fs(:db) %>
+  updated_at: <%= 1.minute.ago.to_fs(:db) %>
+  modified_at: <%= 1.minute.ago.to_fs(:db) %>
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   container_image: test
   cwd: test
@@ -55,9 +55,9 @@ requester_for_running:
   name: requester_for_running_cr
   state: Committed
   priority: 1
-  created_at: <%= 2.minute.ago.to_s(:db) %>
-  updated_at: <%= 2.minute.ago.to_s(:db) %>
-  modified_at: <%= 2.minute.ago.to_s(:db) %>
+  created_at: <%= 2.minute.ago.to_fs(:db) %>
+  updated_at: <%= 2.minute.ago.to_fs(:db) %>
+  modified_at: <%= 2.minute.ago.to_fs(:db) %>
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   container_image: test
   cwd: test
@@ -102,9 +102,9 @@ completed:
   name: completed container request
   state: Final
   priority: 1
-  created_at: <%= 2.minute.ago.to_s(:db) %>
-  updated_at: <%= 1.minute.ago.to_s(:db) %>
-  modified_at: <%= 1.minute.ago.to_s(:db) %>
+  created_at: <%= 2.minute.ago.to_fs(:db) %>
+  updated_at: <%= 1.minute.ago.to_fs(:db) %>
+  modified_at: <%= 1.minute.ago.to_fs(:db) %>
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   container_image: test
   cwd: test
@@ -124,7 +124,7 @@ completed-older:
   name: completed
   state: Final
   priority: 1
-  created_at: <%= 30.minute.ago.to_s(:db) %>
+  created_at: <%= 30.minute.ago.to_fs(:db) %>
   updated_at: 2016-01-11 11:11:11.111111111 Z
   modified_at: 2016-01-11 11:11:11.111111111 Z
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
@@ -413,7 +413,7 @@ cr_for_requester2:
   name: requester_cr2
   state: Final
   priority: 1
-  created_at: <%= 30.minute.ago.to_s(:db) %>
+  created_at: <%= 30.minute.ago.to_fs(:db) %>
   updated_at: 2016-01-11 11:11:11.111111111 Z
   modified_at: 2016-01-11 11:11:11.111111111 Z
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
@@ -539,9 +539,9 @@ running_to_be_deleted:
   name: running to be deleted
   state: Committed
   priority: 1
-  created_at: <%= 2.days.ago.to_s(:db) %>
-  updated_at: <%= 1.days.ago.to_s(:db) %>
-  modified_at: <%= 1.days.ago.to_s(:db) %>
+  created_at: <%= 2.days.ago.to_fs(:db) %>
+  updated_at: <%= 1.days.ago.to_fs(:db) %>
+  modified_at: <%= 1.days.ago.to_fs(:db) %>
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   container_image: test
   cwd: test
@@ -562,9 +562,9 @@ completed_with_input_mounts:
   name: completed container request
   state: Final
   priority: 1
-  created_at: <%= 24.hour.ago.to_s(:db) %>
-  updated_at: <%= 24.hour.ago.to_s(:db) %>
-  modified_at: <%= 24.hour.ago.to_s(:db) %>
+  created_at: <%= 24.hour.ago.to_fs(:db) %>
+  updated_at: <%= 24.hour.ago.to_fs(:db) %>
+  modified_at: <%= 24.hour.ago.to_fs(:db) %>
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   container_image: test
   cwd: test
@@ -598,9 +598,9 @@ uncommitted:
   uuid: zzzzz-xvhdp-cr4uncommittedc
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   name: uncommitted
-  created_at: <%= 2.minute.ago.to_s(:db) %>
-  updated_at: <%= 1.minute.ago.to_s(:db) %>
-  modified_at: <%= 1.minute.ago.to_s(:db) %>
+  created_at: <%= 2.minute.ago.to_fs(:db) %>
+  updated_at: <%= 1.minute.ago.to_fs(:db) %>
+  modified_at: <%= 1.minute.ago.to_fs(:db) %>
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   command: ["arvados-cwl-runner", "--local", "--api=containers",
             "/var/lib/cwl/workflow.json", "/var/lib/cwl/cwl.input.json"]
@@ -1019,9 +1019,9 @@ cr_in_trashed_project:
   name: completed container request
   state: Final
   priority: 1
-  created_at: <%= 2.minute.ago.to_s(:db) %>
-  updated_at: <%= 1.minute.ago.to_s(:db) %>
-  modified_at: <%= 1.minute.ago.to_s(:db) %>
+  created_at: <%= 2.minute.ago.to_fs(:db) %>
+  updated_at: <%= 1.minute.ago.to_fs(:db) %>
+  modified_at: <%= 1.minute.ago.to_fs(:db) %>
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   container_image: test
   cwd: test
@@ -1041,9 +1041,9 @@ runtime_token:
   name: queued
   state: Committed
   priority: 1
-  created_at: <%= 2.minute.ago.to_s(:db) %>
-  updated_at: <%= 1.minute.ago.to_s(:db) %>
-  modified_at: <%= 1.minute.ago.to_s(:db) %>
+  created_at: <%= 2.minute.ago.to_fs(:db) %>
+  updated_at: <%= 1.minute.ago.to_fs(:db) %>
+  modified_at: <%= 1.minute.ago.to_fs(:db) %>
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   container_image: test
   cwd: test
@@ -1065,7 +1065,7 @@ runtime_token:
 <% for i in 1..60 do %>
 cr_<%=i%>_of_60:
   uuid: zzzzz-xvhdp-oneof60crs<%= i.to_s.rjust(5, '0') %>
-  created_at: <%= ((i+5)/5).hour.ago.to_s(:db) %>
+  created_at: <%= ((i+5)/5).hour.ago.to_fs(:db) %>
   owner_uuid: zzzzz-j7d0g-nnncrspipelines
   name: cr-<%= i.to_s %>
   output_path: test
diff --git a/services/api/test/fixtures/containers.yml b/services/api/test/fixtures/containers.yml
index 703d2aafbe..46bc1e50f9 100644
--- a/services/api/test/fixtures/containers.yml
+++ b/services/api/test/fixtures/containers.yml
@@ -33,9 +33,9 @@ running:
   owner_uuid: zzzzz-tpzed-000000000000000
   state: Running
   priority: 12
-  created_at: <%= 1.minute.ago.to_s(:db) %>
-  updated_at: <%= 1.minute.ago.to_s(:db) %>
-  started_at: <%= 1.minute.ago.to_s(:db) %>
+  created_at: <%= 1.minute.ago.to_fs(:db) %>
+  updated_at: <%= 1.minute.ago.to_fs(:db) %>
+  started_at: <%= 1.minute.ago.to_fs(:db) %>
   container_image: test
   cwd: /tmp
   output_path: /tmp
@@ -59,9 +59,9 @@ running_older:
   owner_uuid: zzzzz-tpzed-000000000000000
   state: Running
   priority: 1
-  created_at: <%= 2.minute.ago.to_s(:db) %>
-  updated_at: <%= 2.minute.ago.to_s(:db) %>
-  started_at: <%= 2.minute.ago.to_s(:db) %>
+  created_at: <%= 2.minute.ago.to_fs(:db) %>
+  updated_at: <%= 2.minute.ago.to_fs(:db) %>
+  started_at: <%= 2.minute.ago.to_fs(:db) %>
   container_image: test
   cwd: /tmp
   output_path: /tmp
@@ -82,9 +82,9 @@ locked:
   state: Locked
   locked_by_uuid: zzzzz-gj3su-k9dvestay1plssr
   priority: 0
-  created_at: <%= 2.minute.ago.to_s(:db) %>
-  updated_at: <%= 2.minute.ago.to_s(:db) %>
-  modified_at: <%= 2.minute.ago.to_s(:db) %>
+  created_at: <%= 2.minute.ago.to_fs(:db) %>
+  updated_at: <%= 2.minute.ago.to_fs(:db) %>
+  modified_at: <%= 2.minute.ago.to_fs(:db) %>
   container_image: test
   cwd: test
   output_path: test
@@ -353,8 +353,8 @@ ancient_container_with_logs:
   state: Complete
   exit_code: 0
   priority: 1
-  created_at: <%= 2.year.ago.to_s(:db) %>
-  updated_at: <%= 2.year.ago.to_s(:db) %>
+  created_at: <%= 2.year.ago.to_fs(:db) %>
+  updated_at: <%= 2.year.ago.to_fs(:db) %>
   container_image: test
   cwd: test
   output_path: test
@@ -362,7 +362,7 @@ ancient_container_with_logs:
   runtime_constraints:
     ram: 12000000000
     vcpus: 4
-  finished_at: <%= 2.year.ago.to_s(:db) %>
+  finished_at: <%= 2.year.ago.to_fs(:db) %>
   log: ea10d51bcf88862dbcc36eb292017dfd+45
   output: test
   secret_mounts: {}
@@ -374,8 +374,8 @@ previous_container_with_logs:
   state: Complete
   exit_code: 0
   priority: 1
-  created_at: <%= 1.month.ago.to_s(:db) %>
-  updated_at: <%= 1.month.ago.to_s(:db) %>
+  created_at: <%= 1.month.ago.to_fs(:db) %>
+  updated_at: <%= 1.month.ago.to_fs(:db) %>
   container_image: test
   cwd: test
   output_path: test
@@ -383,7 +383,7 @@ previous_container_with_logs:
   runtime_constraints:
     ram: 12000000000
     vcpus: 4
-  finished_at: <%= 1.month.ago.to_s(:db) %>
+  finished_at: <%= 1.month.ago.to_fs(:db) %>
   log: ea10d51bcf88862dbcc36eb292017dfd+45
   output: test
   secret_mounts: {}
@@ -394,8 +394,8 @@ running_container_with_logs:
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   state: Running
   priority: 1
-  created_at: <%= 1.hour.ago.to_s(:db) %>
-  updated_at: <%= 1.hour.ago.to_s(:db) %>
+  created_at: <%= 1.hour.ago.to_fs(:db) %>
+  updated_at: <%= 1.hour.ago.to_fs(:db) %>
   container_image: test
   cwd: test
   output_path: test
@@ -416,9 +416,9 @@ running_to_be_deleted:
   owner_uuid: zzzzz-tpzed-000000000000000
   state: Running
   priority: 1
-  created_at: <%= 1.minute.ago.to_s(:db) %>
-  updated_at: <%= 1.minute.ago.to_s(:db) %>
-  started_at: <%= 1.minute.ago.to_s(:db) %>
+  created_at: <%= 1.minute.ago.to_fs(:db) %>
+  updated_at: <%= 1.minute.ago.to_fs(:db) %>
+  started_at: <%= 1.minute.ago.to_fs(:db) %>
   container_image: test
   cwd: test
   output_path: test
diff --git a/services/api/test/fixtures/job_tasks.yml b/services/api/test/fixtures/job_tasks.yml
index 7131da6f5e..6a857a02f2 100644
--- a/services/api/test/fixtures/job_tasks.yml
+++ b/services/api/test/fixtures/job_tasks.yml
@@ -5,11 +5,11 @@
 running_job_task_1:
   uuid: zzzzz-ot0gb-runningjobtask1
   owner_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
-  created_at: <%= 3.minute.ago.to_s(:db) %>
+  created_at: <%= 3.minute.ago.to_fs(:db) %>
   job_uuid: zzzzz-8i9sb-with2components
 
 running_job_task_2:
   uuid: zzzzz-ot0gb-runningjobtask2
   owner_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
-  created_at: <%= 3.minute.ago.to_s(:db) %>
+  created_at: <%= 3.minute.ago.to_fs(:db) %>
   job_uuid: zzzzz-8i9sb-with2components
diff --git a/services/api/test/fixtures/jobs.yml b/services/api/test/fixtures/jobs.yml
index 9280aeab93..54b38259ba 100644
--- a/services/api/test/fixtures/jobs.yml
+++ b/services/api/test/fixtures/jobs.yml
@@ -8,8 +8,8 @@ running:
   cancelled_at: ~
   cancelled_by_user_uuid: ~
   cancelled_by_client_uuid: ~
-  created_at: <%= 2.7.minute.ago.to_s(:db) %>
-  started_at: <%= 2.7.minute.ago.to_s(:db) %>
+  created_at: <%= 2.7.minute.ago.to_fs(:db) %>
+  started_at: <%= 2.7.minute.ago.to_fs(:db) %>
   finished_at: ~
   script: hash
   repository: active/foo
@@ -32,11 +32,11 @@ running:
 running_cancelled:
   uuid: zzzzz-8i9sb-4cf0nhn6xte809j
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
-  cancelled_at: <%= 1.minute.ago.to_s(:db) %>
+  cancelled_at: <%= 1.minute.ago.to_fs(:db) %>
   cancelled_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   cancelled_by_client_uuid: zzzzz-ozdt8-obw7foaks3qjyej
-  created_at: <%= 4.minute.ago.to_s(:db) %>
-  started_at: <%= 3.minute.ago.to_s(:db) %>
+  created_at: <%= 4.minute.ago.to_fs(:db) %>
+  started_at: <%= 3.minute.ago.to_fs(:db) %>
   finished_at: ~
   script: hash
   repository: active/foo
@@ -63,9 +63,9 @@ uses_nonexistent_script_version:
   cancelled_by_user_uuid: ~
   cancelled_by_client_uuid: ~
   script_version: 7def43a4d3f20789dda4700f703b5514cc3ed250
-  created_at: <%= 5.minute.ago.to_s(:db) %>
-  started_at: <%= 3.minute.ago.to_s(:db) %>
-  finished_at: <%= 2.minute.ago.to_s(:db) %>
+  created_at: <%= 5.minute.ago.to_fs(:db) %>
+  started_at: <%= 3.minute.ago.to_fs(:db) %>
+  finished_at: <%= 2.minute.ago.to_fs(:db) %>
   script: hash
   repository: active/foo
   running: false
@@ -94,9 +94,9 @@ foobar:
   script_version: 7def43a4d3f20789dda4700f703b5514cc3ed250
   script_parameters:
     input: 1f4b0bc7583c2a7f9102c395f4ffc5e3+45
-  created_at: <%= 4.minute.ago.to_s(:db) %>
-  started_at: <%= 3.minute.ago.to_s(:db) %>
-  finished_at: <%= 2.minute.ago.to_s(:db) %>
+  created_at: <%= 4.minute.ago.to_fs(:db) %>
+  started_at: <%= 3.minute.ago.to_fs(:db) %>
+  finished_at: <%= 2.minute.ago.to_fs(:db) %>
   running: false
   success: true
   output: fa7aeb5140e2848d39b416daeef4ffc5+45
@@ -122,9 +122,9 @@ barbaz:
   script_parameters:
     input: fa7aeb5140e2848d39b416daeef4ffc5+45
     an_integer: 1
-  created_at: <%= 4.minute.ago.to_s(:db) %>
-  started_at: <%= 3.minute.ago.to_s(:db) %>
-  finished_at: <%= 2.minute.ago.to_s(:db) %>
+  created_at: <%= 4.minute.ago.to_fs(:db) %>
+  started_at: <%= 3.minute.ago.to_fs(:db) %>
+  finished_at: <%= 2.minute.ago.to_fs(:db) %>
   running: false
   success: true
   repository: active/foo
@@ -151,9 +151,9 @@ runningbarbaz:
   script_parameters:
     input: fa7aeb5140e2848d39b416daeef4ffc5+45
     an_integer: 1
-  created_at: <%= 4.minute.ago.to_s(:db) %>
-  started_at: <%= 3.minute.ago.to_s(:db) %>
-  finished_at: <%= 2.minute.ago.to_s(:db) %>
+  created_at: <%= 4.minute.ago.to_fs(:db) %>
+  started_at: <%= 3.minute.ago.to_fs(:db) %>
+  finished_at: <%= 2.minute.ago.to_fs(:db) %>
   running: true
   success: ~
   repository: active/foo
@@ -172,8 +172,8 @@ runningbarbaz:
 
 previous_job_run:
   uuid: zzzzz-8i9sb-cjs4pklxxjykqqq
-  created_at: <%= 14.minute.ago.to_s(:db) %>
-  finished_at: <%= 13.minutes.ago.to_s(:db) %>
+  created_at: <%= 14.minute.ago.to_fs(:db) %>
+  finished_at: <%= 13.minutes.ago.to_fs(:db) %>
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   repository: active/foo
   script: hash
@@ -189,8 +189,8 @@ previous_job_run:
 
 previous_job_run_nil_log:
   uuid: zzzzz-8i9sb-cjs4pklxxjykqq3
-  created_at: <%= 14.minute.ago.to_s(:db) %>
-  finished_at: <%= 13.minutes.ago.to_s(:db) %>
+  created_at: <%= 14.minute.ago.to_fs(:db) %>
+  finished_at: <%= 13.minutes.ago.to_fs(:db) %>
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   repository: active/foo
   script: hash
@@ -206,8 +206,8 @@ previous_job_run_nil_log:
 
 previous_ancient_job_run:
   uuid: zzzzz-8i9sb-ahd7cie8jah9qui
-  created_at: <%= 366.days.ago.to_s(:db) %>
-  finished_at: <%= 365.days.ago.to_s(:db) %>
+  created_at: <%= 366.days.ago.to_fs(:db) %>
+  finished_at: <%= 365.days.ago.to_fs(:db) %>
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   repository: active/foo
   script: hash
@@ -223,7 +223,7 @@ previous_ancient_job_run:
 
 previous_docker_job_run:
   uuid: zzzzz-8i9sb-k6emstgk4kw4yhi
-  created_at: <%= 14.minute.ago.to_s(:db) %>
+  created_at: <%= 14.minute.ago.to_fs(:db) %>
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   repository: active/foo
   script: hash
@@ -242,7 +242,7 @@ previous_docker_job_run:
 
 previous_ancient_docker_image_job_run:
   uuid: zzzzz-8i9sb-t3b460aolxxuldl
-  created_at: <%= 144.minute.ago.to_s(:db) %>
+  created_at: <%= 144.minute.ago.to_fs(:db) %>
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   repository: active/foo
   script: hash
@@ -260,7 +260,7 @@ previous_ancient_docker_image_job_run:
 
 previous_job_run_with_arvados_sdk_version:
   uuid: zzzzz-8i9sb-eoo0321or2dw2jg
-  created_at: <%= 14.minute.ago.to_s(:db) %>
+  created_at: <%= 14.minute.ago.to_fs(:db) %>
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   repository: active/foo
   script: hash
@@ -281,7 +281,7 @@ previous_job_run_with_arvados_sdk_version:
 
 previous_job_run_no_output:
   uuid: zzzzz-8i9sb-cjs4pklxxjykppp
-  created_at: <%= 14.minute.ago.to_s(:db) %>
+  created_at: <%= 14.minute.ago.to_fs(:db) %>
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   repository: active/foo
   script: hash
@@ -297,7 +297,7 @@ previous_job_run_no_output:
 previous_job_run_superseded_by_hash_branch:
   # This supplied_script_version is a branch name with later commits.
   uuid: zzzzz-8i9sb-aeviezu5dahph3e
-  created_at: <%= 15.minute.ago.to_s(:db) %>
+  created_at: <%= 15.minute.ago.to_fs(:db) %>
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   repository: active/shabranchnames
   script: testscript
@@ -311,7 +311,7 @@ previous_job_run_superseded_by_hash_branch:
 
 nondeterminisic_job_run:
   uuid: zzzzz-8i9sb-cjs4pklxxjykyyy
-  created_at: <%= 14.minute.ago.to_s(:db) %>
+  created_at: <%= 14.minute.ago.to_fs(:db) %>
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   repository: active/foo
   script: hash2
@@ -326,14 +326,14 @@ nondeterminisic_job_run:
 
 nearly_finished_job:
   uuid: zzzzz-8i9sb-2gx6rz0pjl033w3
-  created_at: <%= 14.minute.ago.to_s(:db) %>
+  created_at: <%= 14.minute.ago.to_fs(:db) %>
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   repository: arvados
   script: doesnotexist
   script_version: 309e25a64fe994867db8459543af372f850e25b9
   script_parameters:
     input: b519d9cb706a29fc7ea24dbea2f05851+249025
-  started_at: <%= 3.minute.ago.to_s(:db) %>
+  started_at: <%= 3.minute.ago.to_fs(:db) %>
   finished_at: ~
   running: true
   success: ~
@@ -348,7 +348,7 @@ nearly_finished_job:
 
 queued:
   uuid: zzzzz-8i9sb-grx15v5mjnsyxk7
-  created_at: <%= 1.minute.ago.to_s(:db) %>
+  created_at: <%= 1.minute.ago.to_fs(:db) %>
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   cancelled_at: ~
   cancelled_by_user_uuid: ~
@@ -382,11 +382,11 @@ job_with_real_log:
 cancelled:
   uuid: zzzzz-8i9sb-4cf0abc123e809j
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
-  cancelled_at: <%= 1.minute.ago.to_s(:db) %>
+  cancelled_at: <%= 1.minute.ago.to_fs(:db) %>
   cancelled_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   cancelled_by_client_uuid: zzzzz-ozdt8-obw7foaks3qjyej
-  created_at: <%= 4.minute.ago.to_s(:db) %>
-  started_at: <%= 3.minute.ago.to_s(:db) %>
+  created_at: <%= 4.minute.ago.to_fs(:db) %>
+  started_at: <%= 3.minute.ago.to_fs(:db) %>
   finished_at: ~
   script_version: 1de84a854e2b440dc53bf42f8548afa4c17da332
   running: false
@@ -432,8 +432,8 @@ running_will_be_completed:
   cancelled_at: ~
   cancelled_by_user_uuid: ~
   cancelled_by_client_uuid: ~
-  created_at: <%= 3.minute.ago.to_s(:db) %>
-  started_at: <%= 3.minute.ago.to_s(:db) %>
+  created_at: <%= 3.minute.ago.to_fs(:db) %>
+  started_at: <%= 3.minute.ago.to_fs(:db) %>
   finished_at: ~
   script_version: 1de84a854e2b440dc53bf42f8548afa4c17da332
   running: true
@@ -499,9 +499,9 @@ job_with_latest_version:
   supplied_script_version: main
   script_parameters:
     input: 1f4b0bc7583c2a7f9102c395f4ffc5e3+45
-  created_at: <%= 3.minute.ago.to_s(:db) %>
-  started_at: <%= 2.minute.ago.to_s(:db) %>
-  finished_at: <%= 1.minute.ago.to_s(:db) %>
+  created_at: <%= 3.minute.ago.to_fs(:db) %>
+  started_at: <%= 2.minute.ago.to_fs(:db) %>
+  finished_at: <%= 1.minute.ago.to_fs(:db) %>
   running: false
   success: true
   output: fa7aeb5140e2848d39b416daeef4ffc5+45
@@ -544,8 +544,8 @@ completed_job_in_publicly_accessible_project:
   log: zzzzz-4zz18-4en62shvi99lxd4
   output: b519d9cb706a29fc7ea24dbea2f05851+93
   script_parameters_digest: 02a085407e751d00b5dc88f1bd5e8247
-  started_at: <%= 10.minute.ago.to_s(:db) %>
-  finished_at: <%= 5.minute.ago.to_s(:db) %>
+  started_at: <%= 10.minute.ago.to_fs(:db) %>
+  finished_at: <%= 5.minute.ago.to_fs(:db) %>
 
 job_in_publicly_accessible_project_but_other_objects_elsewhere:
   uuid: zzzzz-8i9sb-jyq01muyhgr4ofj
@@ -568,8 +568,8 @@ running_job_with_components:
   cancelled_at: ~
   cancelled_by_user_uuid: ~
   cancelled_by_client_uuid: ~
-  created_at: <%= 3.minute.ago.to_s(:db) %>
-  started_at: <%= 3.minute.ago.to_s(:db) %>
+  created_at: <%= 3.minute.ago.to_fs(:db) %>
+  started_at: <%= 3.minute.ago.to_fs(:db) %>
   finished_at: ~
   script: hash
   repository: active/foo
@@ -599,8 +599,8 @@ running_job_with_components_at_level_1:
   cancelled_at: ~
   cancelled_by_user_uuid: ~
   cancelled_by_client_uuid: ~
-  created_at: <%= 12.hour.ago.to_s(:db) %>
-  started_at: <%= 12.hour.ago.to_s(:db) %>
+  created_at: <%= 12.hour.ago.to_fs(:db) %>
+  started_at: <%= 12.hour.ago.to_fs(:db) %>
   finished_at: ~
   repository: active/foo
   script: hash
@@ -630,8 +630,8 @@ running_job_with_components_at_level_2:
   cancelled_at: ~
   cancelled_by_user_uuid: ~
   cancelled_by_client_uuid: ~
-  created_at: <%= 12.hour.ago.to_s(:db) %>
-  started_at: <%= 12.hour.ago.to_s(:db) %>
+  created_at: <%= 12.hour.ago.to_fs(:db) %>
+  started_at: <%= 12.hour.ago.to_fs(:db) %>
   finished_at: ~
   repository: active/foo
   script: hash
@@ -660,8 +660,8 @@ running_job_1_with_components_at_level_3:
   cancelled_at: ~
   cancelled_by_user_uuid: ~
   cancelled_by_client_uuid: ~
-  created_at: <%= 12.hour.ago.to_s(:db) %>
-  started_at: <%= 12.hour.ago.to_s(:db) %>
+  created_at: <%= 12.hour.ago.to_fs(:db) %>
+  started_at: <%= 12.hour.ago.to_fs(:db) %>
   finished_at: ~
   repository: active/foo
   script: hash
@@ -687,8 +687,8 @@ running_job_2_with_components_at_level_3:
   cancelled_at: ~
   cancelled_by_user_uuid: ~
   cancelled_by_client_uuid: ~
-  created_at: <%= 12.hour.ago.to_s(:db) %>
-  started_at: <%= 12.hour.ago.to_s(:db) %>
+  created_at: <%= 12.hour.ago.to_fs(:db) %>
+  started_at: <%= 12.hour.ago.to_fs(:db) %>
   finished_at: ~
   repository: active/foo
   script: hash
@@ -715,8 +715,8 @@ running_job_1_with_circular_component_relationship:
   cancelled_at: ~
   cancelled_by_user_uuid: ~
   cancelled_by_client_uuid: ~
-  created_at: <%= 12.hour.ago.to_s(:db) %>
-  started_at: <%= 12.hour.ago.to_s(:db) %>
+  created_at: <%= 12.hour.ago.to_fs(:db) %>
+  started_at: <%= 12.hour.ago.to_fs(:db) %>
   finished_at: ~
   repository: active/foo
   script: hash
@@ -744,8 +744,8 @@ running_job_2_with_circular_component_relationship:
   cancelled_at: ~
   cancelled_by_user_uuid: ~
   cancelled_by_client_uuid: ~
-  created_at: <%= 12.hour.ago.to_s(:db) %>
-  started_at: <%= 12.hour.ago.to_s(:db) %>
+  created_at: <%= 12.hour.ago.to_fs(:db) %>
+  started_at: <%= 12.hour.ago.to_fs(:db) %>
   finished_at: ~
   repository: active/foo
   script: hash
diff --git a/services/api/test/fixtures/keep_disks.yml b/services/api/test/fixtures/keep_disks.yml
index e8424b26fa..5cccf498af 100644
--- a/services/api/test/fixtures/keep_disks.yml
+++ b/services/api/test/fixtures/keep_disks.yml
@@ -7,9 +7,9 @@ nonfull:
   owner_uuid: zzzzz-tpzed-d9tiejq69daie8f
   node_uuid: zzzzz-7ekkf-53y36l1lu5ijveb
   keep_service_uuid: zzzzz-bi6l4-6zhilxar6r8ey90
-  last_read_at: <%= 1.minute.ago.to_s(:db) %>
-  last_write_at: <%= 2.minute.ago.to_s(:db) %>
-  last_ping_at: <%= 3.minute.ago.to_s(:db) %>
+  last_read_at: <%= 1.minute.ago.to_fs(:db) %>
+  last_write_at: <%= 2.minute.ago.to_fs(:db) %>
+  last_ping_at: <%= 3.minute.ago.to_fs(:db) %>
   ping_secret: z9xz2tc69dho51g1dmkdy5fnupdhsprahcwxdbjs0zms4eo6i
 
 full:
@@ -17,9 +17,9 @@ full:
   owner_uuid: zzzzz-tpzed-d9tiejq69daie8f
   node_uuid: zzzzz-7ekkf-53y36l1lu5ijveb
   keep_service_uuid: zzzzz-bi6l4-6zhilxar6r8ey90
-  last_read_at: <%= 1.minute.ago.to_s(:db) %>
-  last_write_at: <%= 2.day.ago.to_s(:db) %>
-  last_ping_at: <%= 3.minute.ago.to_s(:db) %>
+  last_read_at: <%= 1.minute.ago.to_fs(:db) %>
+  last_write_at: <%= 2.day.ago.to_fs(:db) %>
+  last_ping_at: <%= 3.minute.ago.to_fs(:db) %>
   ping_secret: xx3ieejcufbjy4lli6yt5ig4e8w5l2hhgmbyzpzuq38gri6lj
 
 nonfull2:
@@ -27,7 +27,7 @@ nonfull2:
   owner_uuid: zzzzz-tpzed-d9tiejq69daie8f
   node_uuid: zzzzz-7ekkf-2z3mc76g2q73aio
   keep_service_uuid: zzzzz-bi6l4-rsnj3c76ndxb7o0
-  last_read_at: <%= 1.minute.ago.to_s(:db) %>
-  last_write_at: <%= 2.minute.ago.to_s(:db) %>
-  last_ping_at: <%= 3.minute.ago.to_s(:db) %>
+  last_read_at: <%= 1.minute.ago.to_fs(:db) %>
+  last_write_at: <%= 2.minute.ago.to_fs(:db) %>
+  last_ping_at: <%= 3.minute.ago.to_fs(:db) %>
   ping_secret: 4rs260ibhdum1d242xy23qv320rlerc0j7qg9vyqnchbgmjeek
diff --git a/services/api/test/fixtures/logs.yml b/services/api/test/fixtures/logs.yml
index 25f1efff62..3b41550ae7 100644
--- a/services/api/test/fixtures/logs.yml
+++ b/services/api/test/fixtures/logs.yml
@@ -8,8 +8,8 @@ noop: # nothing happened ...to the 'spectator' user
   owner_uuid: zzzzz-tpzed-000000000000000
   object_uuid: zzzzz-tpzed-l1s2piq4t4mps8r
   object_owner_uuid: zzzzz-tpzed-000000000000000
-  event_at: <%= 1.minute.ago.to_s(:db) %>
-  created_at: <%= 1.minute.ago.to_s(:db) %>
+  event_at: <%= 1.minute.ago.to_fs(:db) %>
+  created_at: <%= 1.minute.ago.to_fs(:db) %>
 
 admin_changes_repository2: # admin changes repository2, which is owned by active user
   id: 2
@@ -17,8 +17,8 @@ admin_changes_repository2: # admin changes repository2, which is owned by active
   owner_uuid: zzzzz-tpzed-d9tiejq69daie8f # admin user
   object_uuid: zzzzz-2x53u-382brsig8rp3667 # repository foo
   object_owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz # active user
-  created_at: <%= 2.minute.ago.to_s(:db) %>
-  event_at: <%= 2.minute.ago.to_s(:db) %>
+  created_at: <%= 2.minute.ago.to_fs(:db) %>
+  event_at: <%= 2.minute.ago.to_fs(:db) %>
   event_type: update
 
 admin_changes_specimen: # admin changes specimen owned_by_spectator
@@ -27,8 +27,8 @@ admin_changes_specimen: # admin changes specimen owned_by_spectator
   owner_uuid: zzzzz-tpzed-d9tiejq69daie8f # admin user
   object_uuid: zzzzz-2x53u-3b0xxwzlbzxq5yr # specimen owned_by_spectator
   object_owner_uuid: zzzzz-tpzed-l1s2piq4t4mps8r # spectator user
-  created_at: <%= 3.minute.ago.to_s(:db) %>
-  event_at: <%= 3.minute.ago.to_s(:db) %>
+  created_at: <%= 3.minute.ago.to_fs(:db) %>
+  event_at: <%= 3.minute.ago.to_fs(:db) %>
   event_type: update
 
 system_adds_foo_file: # foo collection added, readable by active through link
@@ -37,8 +37,8 @@ system_adds_foo_file: # foo collection added, readable by active through link
   owner_uuid: zzzzz-tpzed-000000000000000 # system user
   object_uuid: zzzzz-4zz18-znfnqtbbv4spc3w # foo file
   object_owner_uuid: zzzzz-tpzed-000000000000000 # system user
-  created_at: <%= 4.minute.ago.to_s(:db) %>
-  event_at: <%= 4.minute.ago.to_s(:db) %>
+  created_at: <%= 4.minute.ago.to_fs(:db) %>
+  event_at: <%= 4.minute.ago.to_fs(:db) %>
   event_type: create
 
 system_adds_baz: # baz collection added, readable by active and spectator through group 'all users' group membership
@@ -47,8 +47,8 @@ system_adds_baz: # baz collection added, readable by active and spectator throug
   owner_uuid: zzzzz-tpzed-000000000000000 # system user
   object_uuid: zzzzz-4zz18-y9vne9npefyxh8g # baz file
   object_owner_uuid: zzzzz-tpzed-000000000000000 # system user
-  created_at: <%= 5.minute.ago.to_s(:db) %>
-  event_at: <%= 5.minute.ago.to_s(:db) %>
+  created_at: <%= 5.minute.ago.to_fs(:db) %>
+  event_at: <%= 5.minute.ago.to_fs(:db) %>
   event_type: create
 
 log_owned_by_active:
@@ -57,7 +57,7 @@ log_owned_by_active:
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz # active user
   object_uuid: zzzzz-2x53u-382brsig8rp3667 # repository foo
   object_owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz # active user
-  event_at: <%= 2.minute.ago.to_s(:db) %>
+  event_at: <%= 2.minute.ago.to_fs(:db) %>
   summary: non-admin use can read own logs
 
 crunchstat_for_running_job:
@@ -162,16 +162,16 @@ stderr_for_ancient_container:
   modified_by_client_uuid: zzzzz-ozdt8-obw7foaks3qjyej
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   object_uuid: zzzzz-dz642-logscontainer01
-  event_at: <%= 2.year.ago.to_s(:db) %>
+  event_at: <%= 2.year.ago.to_fs(:db) %>
   event_type: stderr
   summary: ~
   properties:
     text: '2013-11-07_23:33:41 zzzzz-8i9sb-ahd7cie8jah9qui 29610 1 stderr crunchstat:
       cpu 1935.4300 user 59.4100 sys 8 cpus -- interval 10.0002 seconds 12.9900 user
       0.9900 sys'
-  created_at: <%= 2.year.ago.to_s(:db) %>
-  updated_at: <%= 2.year.ago.to_s(:db) %>
-  modified_at: <%= 2.year.ago.to_s(:db) %>
+  created_at: <%= 2.year.ago.to_fs(:db) %>
+  updated_at: <%= 2.year.ago.to_fs(:db) %>
+  modified_at: <%= 2.year.ago.to_fs(:db) %>
   object_owner_uuid: zzzzz-j7d0g-xurymjxw79nv3jz
 
 crunchstat_for_ancient_container:
@@ -181,16 +181,16 @@ crunchstat_for_ancient_container:
   modified_by_client_uuid: zzzzz-ozdt8-obw7foaks3qjyej
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   object_uuid: zzzzz-dz642-logscontainer01
-  event_at: <%= 2.year.ago.to_s(:db) %>
+  event_at: <%= 2.year.ago.to_fs(:db) %>
   event_type: crunchstat
   summary: ~
   properties:
     text: '2013-11-07_23:33:41 zzzzz-8i9sb-ahd7cie8jah9qui 29610 1 stderr crunchstat:
       cpu 1935.4300 user 59.4100 sys 8 cpus -- interval 10.0002 seconds 12.9900 user
       0.9900 sys'
-  created_at: <%= 2.year.ago.to_s(:db) %>
-  updated_at: <%= 2.year.ago.to_s(:db) %>
-  modified_at: <%= 2.year.ago.to_s(:db) %>
+  created_at: <%= 2.year.ago.to_fs(:db) %>
+  updated_at: <%= 2.year.ago.to_fs(:db) %>
+  modified_at: <%= 2.year.ago.to_fs(:db) %>
   object_owner_uuid: zzzzz-j7d0g-xurymjxw79nv3jz
 
 stderr_for_previous_container:
@@ -200,16 +200,16 @@ stderr_for_previous_container:
   modified_by_client_uuid: zzzzz-ozdt8-obw7foaks3qjyej
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   object_uuid: zzzzz-dz642-logscontainer02
-  event_at: <%= 1.month.ago.to_s(:db) %>
+  event_at: <%= 1.month.ago.to_fs(:db) %>
   event_type: stderr
   summary: ~
   properties:
     text: '2013-11-07_23:33:41 zzzzz-8i9sb-ahd7cie8jah9qui 29610 1 stderr crunchstat:
       cpu 1935.4300 user 59.4100 sys 8 cpus -- interval 10.0002 seconds 12.9900 user
       0.9900 sys'
-  created_at: <%= 1.month.ago.to_s(:db) %>
-  updated_at: <%= 1.month.ago.to_s(:db) %>
-  modified_at: <%= 1.month.ago.to_s(:db) %>
+  created_at: <%= 1.month.ago.to_fs(:db) %>
+  updated_at: <%= 1.month.ago.to_fs(:db) %>
+  modified_at: <%= 1.month.ago.to_fs(:db) %>
   object_owner_uuid: zzzzz-j7d0g-xurymjxw79nv3jz
 
 crunchstat_for_previous_container:
@@ -219,16 +219,16 @@ crunchstat_for_previous_container:
   modified_by_client_uuid: zzzzz-ozdt8-obw7foaks3qjyej
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   object_uuid: zzzzz-dz642-logscontainer02
-  event_at: <%= 1.month.ago.to_s(:db) %>
+  event_at: <%= 1.month.ago.to_fs(:db) %>
   event_type: crunchstat
   summary: ~
   properties:
     text: '2013-11-07_23:33:41 zzzzz-8i9sb-ahd7cie8jah9qui 29610 1 stderr crunchstat:
       cpu 1935.4300 user 59.4100 sys 8 cpus -- interval 10.0002 seconds 12.9900 user
       0.9900 sys'
-  created_at: <%= 1.month.ago.to_s(:db) %>
-  updated_at: <%= 1.month.ago.to_s(:db) %>
-  modified_at: <%= 1.month.ago.to_s(:db) %>
+  created_at: <%= 1.month.ago.to_fs(:db) %>
+  updated_at: <%= 1.month.ago.to_fs(:db) %>
+  modified_at: <%= 1.month.ago.to_fs(:db) %>
   object_owner_uuid: zzzzz-j7d0g-xurymjxw79nv3jz
 
 stderr_for_running_container:
@@ -238,16 +238,16 @@ stderr_for_running_container:
   modified_by_client_uuid: zzzzz-ozdt8-obw7foaks3qjyej
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   object_uuid: zzzzz-dz642-logscontainer03
-  event_at: <%= 1.hour.ago.to_s(:db) %>
+  event_at: <%= 1.hour.ago.to_fs(:db) %>
   event_type: crunchstat
   summary: ~
   properties:
     text: '2013-11-07_23:33:41 zzzzz-8i9sb-ahd7cie8jah9qui 29610 1 stderr crunchstat:
       cpu 1935.4300 user 59.4100 sys 8 cpus -- interval 10.0002 seconds 12.9900 user
       0.9900 sys'
-  created_at: <%= 1.hour.ago.to_s(:db) %>
-  updated_at: <%= 1.hour.ago.to_s(:db) %>
-  modified_at: <%= 1.hour.ago.to_s(:db) %>
+  created_at: <%= 1.hour.ago.to_fs(:db) %>
+  updated_at: <%= 1.hour.ago.to_fs(:db) %>
+  modified_at: <%= 1.hour.ago.to_fs(:db) %>
   object_owner_uuid: zzzzz-j7d0g-xurymjxw79nv3jz
 
 crunchstat_for_running_container:
@@ -257,14 +257,14 @@ crunchstat_for_running_container:
   modified_by_client_uuid: zzzzz-ozdt8-obw7foaks3qjyej
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   object_uuid: zzzzz-dz642-logscontainer03
-  event_at: <%= 1.hour.ago.to_s(:db) %>
+  event_at: <%= 1.hour.ago.to_fs(:db) %>
   event_type: crunchstat
   summary: ~
   properties:
     text: '2013-11-07_23:33:41 zzzzz-8i9sb-ahd7cie8jah9qui 29610 1 stderr crunchstat:
       cpu 1935.4300 user 59.4100 sys 8 cpus -- interval 10.0002 seconds 12.9900 user
       0.9900 sys'
-  created_at: <%= 1.hour.ago.to_s(:db) %>
-  updated_at: <%= 1.hour.ago.to_s(:db) %>
-  modified_at: <%= 1.hour.ago.to_s(:db) %>
+  created_at: <%= 1.hour.ago.to_fs(:db) %>
+  updated_at: <%= 1.hour.ago.to_fs(:db) %>
+  modified_at: <%= 1.hour.ago.to_fs(:db) %>
   object_owner_uuid: zzzzz-j7d0g-xurymjxw79nv3jz
diff --git a/services/api/test/fixtures/nodes.yml b/services/api/test/fixtures/nodes.yml
index 821a6b5e42..d4589ed705 100644
--- a/services/api/test/fixtures/nodes.yml
+++ b/services/api/test/fixtures/nodes.yml
@@ -9,8 +9,8 @@ busy:
   slot_number: 0
   domain: ""
   ip_address: 172.17.2.172
-  last_ping_at: <%= 1.minute.ago.to_s(:db) %>
-  first_ping_at: <%= 23.hour.ago.to_s(:db) %>
+  last_ping_at: <%= 1.minute.ago.to_fs(:db) %>
+  first_ping_at: <%= 23.hour.ago.to_fs(:db) %>
   job_uuid: zzzzz-8i9sb-2gx6rz0pjl033w3  # nearly_finished_job
   properties: {}
   info:
@@ -24,8 +24,8 @@ down:
   slot_number: 1
   domain: ""
   ip_address: 172.17.2.173
-  last_ping_at: <%= 1.hour.ago.to_s(:db) %>
-  first_ping_at: <%= 23.hour.ago.to_s(:db) %>
+  last_ping_at: <%= 1.hour.ago.to_fs(:db) %>
+  first_ping_at: <%= 23.hour.ago.to_fs(:db) %>
   job_uuid: ~
   properties: {}
   info:
@@ -38,8 +38,8 @@ idle:
   slot_number: 2
   domain: ""
   ip_address: 172.17.2.174
-  last_ping_at: <%= 2.minute.ago.to_s(:db) %>
-  first_ping_at: <%= 23.hour.ago.to_s(:db) %>
+  last_ping_at: <%= 2.minute.ago.to_fs(:db) %>
+  first_ping_at: <%= 23.hour.ago.to_fs(:db) %>
   job_uuid: ~
   info:
     ping_secret: "69udawxvn3zzj45hs8bumvndricrha4lcpi23pd69e44soanc0"
@@ -54,8 +54,8 @@ was_idle_now_down:
   slot_number: ~
   domain: ""
   ip_address: 172.17.2.174
-  last_ping_at: <%= 1.hour.ago.to_s(:db) %>
-  first_ping_at: <%= 23.hour.ago.to_s(:db) %>
+  last_ping_at: <%= 1.hour.ago.to_fs(:db) %>
+  first_ping_at: <%= 23.hour.ago.to_fs(:db) %>
   job_uuid: ~
   info:
     ping_secret: "1bd1yi0x4lb5q4gzqqtrnq30oyj08r8dtdimmanbqw49z1anz2"
diff --git a/services/api/test/fixtures/pipeline_instances.yml b/services/api/test/fixtures/pipeline_instances.yml
index a504c9fadd..714fc60771 100644
--- a/services/api/test/fixtures/pipeline_instances.yml
+++ b/services/api/test/fixtures/pipeline_instances.yml
@@ -6,19 +6,19 @@ new_pipeline:
   state: New
   uuid: zzzzz-d1hrv-f4gneyn6br1xize
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
-  created_at: <%= 1.minute.ago.to_s(:db) %>
+  created_at: <%= 1.minute.ago.to_fs(:db) %>
 
 new_pipeline_in_subproject:
   state: New
   uuid: zzzzz-d1hrv-subprojpipeline
   owner_uuid: zzzzz-j7d0g-axqo7eu9pwvna1x
-  created_at: <%= 1.minute.ago.to_s(:db) %>
+  created_at: <%= 1.minute.ago.to_fs(:db) %>
 
 has_component_with_no_script_parameters:
   state: Ready
   uuid: zzzzz-d1hrv-1xfj6xkicf2muk2
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
-  created_at: <%= 10.minute.ago.to_s(:db) %>
+  created_at: <%= 10.minute.ago.to_fs(:db) %>
   components:
    foo:
     script: foo
@@ -29,7 +29,7 @@ has_component_with_empty_script_parameters:
   state: Ready
   uuid: zzzzz-d1hrv-jq16l10gcsnyumo
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
-  created_at: <%= 3.minute.ago.to_s(:db) %>
+  created_at: <%= 3.minute.ago.to_fs(:db) %>
   components:
    foo:
     script: foo
@@ -46,9 +46,9 @@ has_component_with_completed_jobs:
   state: Complete
   uuid: zzzzz-d1hrv-i3e77t9z5y8j9cc
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
-  created_at: <%= 11.minute.ago.to_s(:db) %>
-  started_at: <%= 10.minute.ago.to_s(:db) %>
-  finished_at: <%= 9.minute.ago.to_s(:db) %>
+  created_at: <%= 11.minute.ago.to_fs(:db) %>
+  started_at: <%= 10.minute.ago.to_fs(:db) %>
+  finished_at: <%= 9.minute.ago.to_fs(:db) %>
   components:
    foo:
     script: foo
@@ -57,9 +57,9 @@ has_component_with_completed_jobs:
     job:
       uuid: zzzzz-8i9sb-rft1xdewxkwgxnz
       script_version: main
-      created_at: <%= 10.minute.ago.to_s(:db) %>
-      started_at: <%= 10.minute.ago.to_s(:db) %>
-      finished_at: <%= 9.minute.ago.to_s(:db) %>
+      created_at: <%= 10.minute.ago.to_fs(:db) %>
+      started_at: <%= 10.minute.ago.to_fs(:db) %>
+      finished_at: <%= 9.minute.ago.to_fs(:db) %>
       state: Complete
       tasks_summary:
         failed: 0
@@ -73,8 +73,8 @@ has_component_with_completed_jobs:
     job:
       uuid: zzzzz-8i9sb-r2dtbzr6bfread7
       script_version: main
-      created_at: <%= 9.minute.ago.to_s(:db) %>
-      started_at: <%= 9.minute.ago.to_s(:db) %>
+      created_at: <%= 9.minute.ago.to_fs(:db) %>
+      started_at: <%= 9.minute.ago.to_fs(:db) %>
       state: Running
       tasks_summary:
         failed: 0
@@ -88,7 +88,7 @@ has_component_with_completed_jobs:
     job:
       uuid: zzzzz-8i9sb-c7408rni11o7r6s
       script_version: main
-      created_at: <%= 9.minute.ago.to_s(:db) %>
+      created_at: <%= 9.minute.ago.to_fs(:db) %>
       state: Queued
       tasks_summary: {}
 
@@ -97,7 +97,7 @@ has_job:
   state: Ready
   uuid: zzzzz-d1hrv-1yfj6xkidf2muk3
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
-  created_at: <%= 2.9.minute.ago.to_s(:db) %>
+  created_at: <%= 2.9.minute.ago.to_fs(:db) %>
   components:
    foo:
     script: foo
@@ -112,7 +112,7 @@ components_is_jobspec:
   # Helps test that clients cope with funny-shaped components.
   # For an example, see #3321.
   uuid: zzzzz-d1hrv-1yfj61234abcdk4
-  created_at: <%= 4.minute.ago.to_s(:db) %>
+  created_at: <%= 4.minute.ago.to_fs(:db) %>
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
@@ -132,7 +132,7 @@ pipeline_with_tagged_collection_input:
   state: Ready
   uuid: zzzzz-d1hrv-1yfj61234abcdk3
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
-  created_at: <%= 3.2.minute.ago.to_s(:db) %>
+  created_at: <%= 3.2.minute.ago.to_fs(:db) %>
   components:
     part-one:
       script_parameters:
@@ -145,7 +145,7 @@ pipeline_to_merge_params:
   uuid: zzzzz-d1hrv-1yfj6dcba4321k3
   pipeline_template_uuid: zzzzz-p5p6p-aox0k0ofxrystgw
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
-  created_at: <%= 3.3.minute.ago.to_s(:db) %>
+  created_at: <%= 3.3.minute.ago.to_fs(:db) %>
   components:
     part-one:
       script_parameters:
@@ -260,7 +260,7 @@ pipeline_in_publicly_accessible_project:
   name: Pipeline in publicly accessible project
   pipeline_template_uuid: zzzzz-p5p6p-tmpltpublicproj
   state: Complete
-  created_at: <%= 30.minute.ago.to_s(:db) %>
+  created_at: <%= 30.minute.ago.to_fs(:db) %>
   components:
     foo:
       script: foo
@@ -363,8 +363,8 @@ pipeline_in_running_state:
   name: running_with_job
   uuid: zzzzz-d1hrv-runningpipeline
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
-  created_at: <%= 2.8.minute.ago.to_s(:db) %>
-  started_at: <%= 2.8.minute.ago.to_s(:db) %>
+  created_at: <%= 2.8.minute.ago.to_fs(:db) %>
+  started_at: <%= 2.8.minute.ago.to_fs(:db) %>
   state: RunningOnServer
   components:
    foo:
@@ -379,7 +379,7 @@ running_pipeline_with_complete_job:
   uuid: zzzzz-d1hrv-partdonepipelin
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   state: RunningOnServer
-  created_at: <%= 15.minute.ago.to_s(:db) %>
+  created_at: <%= 15.minute.ago.to_fs(:db) %>
   components:
    previous:
     job:
@@ -393,9 +393,9 @@ complete_pipeline_with_two_jobs:
   uuid: zzzzz-d1hrv-twodonepipeline
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   state: Complete
-  created_at: <%= 2.5.minute.ago.to_s(:db) %>
-  started_at: <%= 2.minute.ago.to_s(:db) %>
-  finished_at: <%= 1.minute.ago.to_s(:db) %>
+  created_at: <%= 2.5.minute.ago.to_fs(:db) %>
+  started_at: <%= 2.minute.ago.to_fs(:db) %>
+  finished_at: <%= 1.minute.ago.to_fs(:db) %>
   components:
    ancient:
     job:
@@ -409,7 +409,7 @@ complete_pipeline_with_two_jobs:
 failed_pipeline_with_two_jobs:
   uuid: zzzzz-d1hrv-twofailpipeline
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
-  created_at: <%= 55.minute.ago.to_s(:db) %>
+  created_at: <%= 55.minute.ago.to_fs(:db) %>
   state: Failed
   components:
    ancient:
@@ -426,8 +426,8 @@ job_child_pipeline_with_components_at_level_2:
   state: RunningOnServer
   uuid: zzzzz-d1hrv-picomponentsl02
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
-  created_at: <%= 12.hour.ago.to_s(:db) %>
-  started_at: <%= 12.hour.ago.to_s(:db) %>
+  created_at: <%= 12.hour.ago.to_fs(:db) %>
+  started_at: <%= 12.hour.ago.to_fs(:db) %>
   components:
    foo:
     script: foo
@@ -436,8 +436,8 @@ job_child_pipeline_with_components_at_level_2:
     job:
       uuid: zzzzz-8i9sb-job1atlevel3noc
       script_version: main
-      created_at: <%= 12.hour.ago.to_s(:db) %>
-      started_at: <%= 12.hour.ago.to_s(:db) %>
+      created_at: <%= 12.hour.ago.to_fs(:db) %>
+      started_at: <%= 12.hour.ago.to_fs(:db) %>
       state: Running
       tasks_summary:
         failed: 0
@@ -451,8 +451,8 @@ job_child_pipeline_with_components_at_level_2:
     job:
       uuid: zzzzz-8i9sb-job2atlevel3noc
       script_version: main
-      created_at: <%= 12.hour.ago.to_s(:db) %>
-      started_at: <%= 12.hour.ago.to_s(:db) %>
+      created_at: <%= 12.hour.ago.to_fs(:db) %>
+      started_at: <%= 12.hour.ago.to_fs(:db) %>
       state: Running
       tasks_summary:
         failed: 0
@@ -470,9 +470,9 @@ pipeline_<%=i%>_of_10:
   name: pipeline_<%= i %>
   uuid: zzzzz-d1hrv-10pipelines0<%= i.to_s.rjust(3, '0') %>
   owner_uuid: zzzzz-j7d0g-000010pipelines
-  created_at: <%= (2*(i-1)).hour.ago.to_s(:db) %>
-  started_at: <%= (2*(i-1)).hour.ago.to_s(:db) %>
-  finished_at: <%= (i-1).minute.ago.to_s(:db) %>
+  created_at: <%= (2*(i-1)).hour.ago.to_fs(:db) %>
+  started_at: <%= (2*(i-1)).hour.ago.to_fs(:db) %>
+  finished_at: <%= (i-1).minute.ago.to_fs(:db) %>
   state: Failed
   components:
     foo:
@@ -494,7 +494,7 @@ pipeline_<%=i%>_of_2_pipelines_and_60_crs:
   state: New
   uuid: zzzzz-d1hrv-abcgneyn6brx<%= i.to_s.rjust(3, '0') %>
   owner_uuid: zzzzz-j7d0g-nnncrspipelines
-  created_at: <%= i.minute.ago.to_s(:db) %>
+  created_at: <%= i.minute.ago.to_fs(:db) %>
   components:
     foo:
       script: foo
@@ -513,9 +513,9 @@ pipeline_<%=i%>_of_25:
   state: Failed
   uuid: zzzzz-d1hrv-25pipelines0<%= i.to_s.rjust(3, '0') %>
   owner_uuid: zzzzz-j7d0g-000025pipelines
-  created_at: <%= i.hour.ago.to_s(:db) %>
-  started_at: <%= i.hour.ago.to_s(:db) %>
-  finished_at: <%= i.minute.ago.to_s(:db) %>
+  created_at: <%= i.hour.ago.to_fs(:db) %>
+  started_at: <%= i.hour.ago.to_fs(:db) %>
+  finished_at: <%= i.minute.ago.to_fs(:db) %>
   components:
     foo:
       script: foo
diff --git a/services/api/test/fixtures/workflows.yml b/services/api/test/fixtures/workflows.yml
index 29b76abb45..ad9c7d2676 100644
--- a/services/api/test/fixtures/workflows.yml
+++ b/services/api/test/fixtures/workflows.yml
@@ -28,7 +28,7 @@ workflow_with_input_specifications:
   owner_uuid: zzzzz-j7d0g-zhxawtyetzwc5f0
   name: Workflow with input specifications
   description: this workflow has inputs specified
-  created_at: <%= 1.minute.ago.to_s(:db) %>
+  created_at: <%= 1.minute.ago.to_fs(:db) %>
   definition: |
     cwlVersion: v1.0
     class: CommandLineTool
@@ -54,7 +54,7 @@ workflow_with_input_defaults:
   owner_uuid: zzzzz-j7d0g-zhxawtyetzwc5f0
   name: Workflow with default input specifications
   description: this workflow has inputs specified
-  created_at: <%= 1.minute.ago.to_s(:db) %>
+  created_at: <%= 1.minute.ago.to_fs(:db) %>
   definition: |
     cwlVersion: v1.0
     class: CommandLineTool
@@ -73,7 +73,7 @@ workflow_with_wrr:
   owner_uuid: zzzzz-j7d0g-zhxawtyetzwc5f0
   name: Workflow with WorkflowRunnerResources
   description: this workflow has WorkflowRunnerResources
-  created_at: <%= 1.minute.ago.to_s(:db) %>
+  created_at: <%= 1.minute.ago.to_fs(:db) %>
   definition: |
     cwlVersion: v1.0
     class: CommandLineTool

commit 2d80ba8feac6b59b24e4f536ed29698789c276c6
Author: Tom Clegg <tom at curii.com>
Date:   Thu Sep 21 11:49:52 2023 -0400

    20300: Delete obsolete websocket stub.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/config/initializers/eventbus.rb b/services/api/config/initializers/eventbus.rb
deleted file mode 100644
index eb5561a47f..0000000000
--- a/services/api/config/initializers/eventbus.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
-if ENV['ARVADOS_WEBSOCKETS']
-  Server::Application.configure do
-    Rails.logger.error "Built-in websocket server is disabled. See note (2017-03-23, e8cc0d7) at https://dev.arvados.org/projects/arvados/wiki/Upgrading_to_master"
-
-    class EventBusRemoved
-      def overloaded?
-        false
-      end
-      def on_connect ws
-        ws.on :open do |e|
-          EM::Timer.new 1 do
-            ws.send(SafeJSON.dump({status: 501, message: "Server misconfigured? see http://doc.arvados.org/install/install-ws.html"}))
-          end
-          EM::Timer.new 3 do
-            ws.close
-          end
-        end
-      end
-    end
-
-    config.middleware.insert_after(ArvadosApiToken, RackSocket, {
-                                     handler: EventBusRemoved,
-                                     mount: "/websocket",
-                                     websocket_only: (ENV['ARVADOS_WEBSOCKETS'] == "ws-only")
-                                   })
-  end
-end

commit 3c0213756adf7694a993db1e6dc87568ed89c74d
Author: Tom Clegg <tom at curii.com>
Date:   Thu Sep 21 11:49:27 2023 -0400

    20300: Fix missing imports in initializers.
    
    Autoload no longer works in initializers.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/config/initializers/authorization.rb b/services/api/config/initializers/authorization.rb
index ec80048c8f..71d5557445 100644
--- a/services/api/config/initializers/authorization.rb
+++ b/services/api/config/initializers/authorization.rb
@@ -2,6 +2,8 @@
 #
 # SPDX-License-Identifier: AGPL-3.0
 
+require_relative "../../app/middlewares/arvados_api_token"
+
 Server::Application.configure do
   config.middleware.delete ActionDispatch::RemoteIp
   config.middleware.insert 0, ActionDispatch::RemoteIp
diff --git a/services/api/config/initializers/custom_types.rb b/services/api/config/initializers/custom_types.rb
index aecd4cfd4b..9d909e6cbb 100644
--- a/services/api/config/initializers/custom_types.rb
+++ b/services/api/config/initializers/custom_types.rb
@@ -2,6 +2,8 @@
 #
 # SPDX-License-Identifier: AGPL-3.0
 
+require_relative "../../app/models/jsonb_type"
+
 # JSONB backed Hash & Array types that default to their empty versions when
 # reading NULL from the database, or get nil passed by parameter.
 ActiveRecord::Type.register(:jsonbHash, JsonbType::Hash)
diff --git a/services/api/lib/enable_jobs_api.rb b/services/api/lib/enable_jobs_api.rb
index cef76f08a5..6718d384ee 100644
--- a/services/api/lib/enable_jobs_api.rb
+++ b/services/api/lib/enable_jobs_api.rb
@@ -47,7 +47,7 @@ def check_enable_legacy_jobs_api
 
   if Rails.configuration.Containers.JobsAPI.Enable == "false" ||
      (Rails.configuration.Containers.JobsAPI.Enable == "auto" &&
-      Job.count == 0)
+      ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM jobs LIMIT 1") == 0)
     Rails.configuration.API.DisabledAPIs.merge! Disable_jobs_api_method_list
   end
 end

commit 6ad3e0ec18c16248aad6922a8f9fb594f8eda76c
Author: Tom Clegg <tom at curii.com>
Date:   Wed Sep 20 22:18:33 2023 -0400

    20300: Add sprockets-rails.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/Gemfile b/services/api/Gemfile
index a7ceeb4a18..dafb0d3801 100644
--- a/services/api/Gemfile
+++ b/services/api/Gemfile
@@ -7,6 +7,7 @@ source 'https://rubygems.org'
 gem 'rails', '~> 7.0.0'
 gem 'responders'
 gem 'i18n'
+gem 'sprockets-rails'
 
 group :test, :development do
   gem 'factory_bot_rails'
diff --git a/services/api/Gemfile.lock b/services/api/Gemfile.lock
index 9e09fb64af..d18fc8b07a 100644
--- a/services/api/Gemfile.lock
+++ b/services/api/Gemfile.lock
@@ -257,6 +257,13 @@ GEM
     simplecov-html (0.7.1)
     simplecov-rcov (0.3.1)
       simplecov (>= 0.4.1)
+    sprockets (4.2.1)
+      concurrent-ruby (~> 1.0)
+      rack (>= 2.2.4, < 4)
+    sprockets-rails (3.4.2)
+      actionpack (>= 5.2)
+      activesupport (>= 5.2)
+      sprockets (>= 3.0.0)
     test-unit (3.6.1)
       power_assert
     thor (1.2.2)
@@ -300,6 +307,7 @@ DEPENDENCIES
   ruby-prof (~> 0.15.0)
   simplecov (~> 0.7.1)
   simplecov-rcov
+  sprockets-rails
   test-unit (~> 3.0)
   themes_for_rails!
 
diff --git a/services/api/app/assets/config/manifest.js b/services/api/app/assets/config/manifest.js
new file mode 100644
index 0000000000..ceb233892c
--- /dev/null
+++ b/services/api/app/assets/config/manifest.js
@@ -0,0 +1,7 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+//= link_tree ../images
+//= link_directory ../javascripts .js
+//= link_directory ../stylesheets .css
diff --git a/services/api/config/application.rb b/services/api/config/application.rb
index be91e8673c..ca5eff1a47 100644
--- a/services/api/config/application.rb
+++ b/services/api/config/application.rb
@@ -16,6 +16,7 @@ require "action_mailer/railtie"
 # require "action_text/engine"
 require "action_view/railtie"
 # require "action_cable/engine"
+require "sprockets/railtie"
 require "rails/test_unit/railtie"
 
 # Require the gems listed in Gemfile, including any gems

commit c01fc0b27737f2fc0535f4fe2f1fe614bc308354
Author: Tom Clegg <tom at curii.com>
Date:   Wed Sep 20 22:06:34 2023 -0400

    20300: Rail 7.0 app:update.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/bin/rails b/services/api/bin/rails
index 6fb4e4051c..efc0377492 100755
--- a/services/api/bin/rails
+++ b/services/api/bin/rails
@@ -1,4 +1,4 @@
 #!/usr/bin/env ruby
-APP_PATH = File.expand_path('../config/application', __dir__)
+APP_PATH = File.expand_path("../config/application", __dir__)
 require_relative "../config/boot"
 require "rails/commands"
diff --git a/services/api/bin/setup b/services/api/bin/setup
index 57923026c4..ec47b79b3b 100755
--- a/services/api/bin/setup
+++ b/services/api/bin/setup
@@ -2,7 +2,7 @@
 require "fileutils"
 
 # path to your application root.
-APP_ROOT = File.expand_path('..', __dir__)
+APP_ROOT = File.expand_path("..", __dir__)
 
 def system!(*args)
   system(*args) || abort("\n== Command #{args} failed ==")
@@ -13,21 +13,21 @@ FileUtils.chdir APP_ROOT do
   # This script is idempotent, so that you can run it at any time and get an expectable outcome.
   # Add necessary setup steps to this file.
 
-  puts '== Installing dependencies =='
-  system! 'gem install bundler --conservative'
-  system('bundle check') || system!('bundle install')
+  puts "== Installing dependencies =="
+  system! "gem install bundler --conservative"
+  system("bundle check") || system!("bundle install")
 
   # puts "\n== Copying sample files =="
-  # unless File.exist?('config/database.yml')
-  #   FileUtils.cp 'config/database.yml.sample', 'config/database.yml'
+  # unless File.exist?("config/database.yml")
+  #   FileUtils.cp "config/database.yml.sample", "config/database.yml"
   # end
 
   puts "\n== Preparing database =="
-  system! 'bin/rails db:prepare'
+  system! "bin/rails db:prepare"
 
   puts "\n== Removing old logs and tempfiles =="
-  system! 'bin/rails log:clear tmp:clear'
+  system! "bin/rails log:clear tmp:clear"
 
   puts "\n== Restarting application server =="
-  system! 'bin/rails restart'
+  system! "bin/rails restart"
 end
diff --git a/services/api/config/boot.rb b/services/api/config/boot.rb
index d69bd27dca..282011619d 100644
--- a/services/api/config/boot.rb
+++ b/services/api/config/boot.rb
@@ -1,3 +1,3 @@
-ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
 
 require "bundler/setup" # Set up gems listed in the Gemfile.
diff --git a/services/api/config/initializers/assets.rb b/services/api/config/initializers/assets.rb
index fe48fc34ee..2eeef966fe 100644
--- a/services/api/config/initializers/assets.rb
+++ b/services/api/config/initializers/assets.rb
@@ -1,7 +1,7 @@
 # Be sure to restart your server when you modify this file.
 
 # Version of your assets, change this if you want to expire all your assets.
-Rails.application.config.assets.version = '1.0'
+Rails.application.config.assets.version = "1.0"
 
 # Add additional assets to the asset load path.
 # Rails.application.config.assets.paths << Emoji.images_path
diff --git a/services/api/config/initializers/content_security_policy.rb b/services/api/config/initializers/content_security_policy.rb
index 41c43016f1..54f47cf15f 100644
--- a/services/api/config/initializers/content_security_policy.rb
+++ b/services/api/config/initializers/content_security_policy.rb
@@ -1,28 +1,25 @@
 # Be sure to restart your server when you modify this file.
 
-# Define an application-wide content security policy
-# For further information see the following documentation
-# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
+# Define an application-wide content security policy.
+# See the Securing Rails Applications Guide for more information:
+# https://guides.rubyonrails.org/security.html#content-security-policy-header
 
-# Rails.application.config.content_security_policy do |policy|
-#   policy.default_src :self, :https
-#   policy.font_src    :self, :https, :data
-#   policy.img_src     :self, :https, :data
-#   policy.object_src  :none
-#   policy.script_src  :self, :https
-#   policy.style_src   :self, :https
-
-#   # Specify URI for violation reports
-#   # policy.report_uri "/csp-violation-report-endpoint"
+# Rails.application.configure do
+#   config.content_security_policy do |policy|
+#     policy.default_src :self, :https
+#     policy.font_src    :self, :https, :data
+#     policy.img_src     :self, :https, :data
+#     policy.object_src  :none
+#     policy.script_src  :self, :https
+#     policy.style_src   :self, :https
+#     # Specify URI for violation reports
+#     # policy.report_uri "/csp-violation-report-endpoint"
+#   end
+#
+#   # Generate session nonces for permitted importmap and inline scripts
+#   config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
+#   config.content_security_policy_nonce_directives = %w(script-src)
+#
+#   # Report violations without enforcing the policy.
+#   # config.content_security_policy_report_only = true
 # end
-
-# If you are using UJS then enable automatic nonce generation
-# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
-
-# Set the nonce only to specific directives
-# Rails.application.config.content_security_policy_nonce_directives = %w(script-src)
-
-# Report CSP violations to a specified URI
-# For further information see the following documentation:
-# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
-# Rails.application.config.content_security_policy_report_only = true
diff --git a/services/api/config/initializers/filter_parameter_logging.rb b/services/api/config/initializers/filter_parameter_logging.rb
index 4b34a03668..adc6568ce8 100644
--- a/services/api/config/initializers/filter_parameter_logging.rb
+++ b/services/api/config/initializers/filter_parameter_logging.rb
@@ -1,6 +1,8 @@
 # Be sure to restart your server when you modify this file.
 
-# Configure sensitive parameters which will be filtered from the log file.
+# Configure parameters to be filtered from the log file. Use this to limit dissemination of
+# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
+# notations and behaviors.
 Rails.application.config.filter_parameters += [
   :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
 ]
diff --git a/services/api/config/initializers/inflections.rb b/services/api/config/initializers/inflections.rb
index 8fee195a91..bd92f2fd76 100644
--- a/services/api/config/initializers/inflections.rb
+++ b/services/api/config/initializers/inflections.rb
@@ -8,15 +8,15 @@
 # are locale specific, and you may define rules for as many different
 # locales as you wish. All of these examples are active by default:
 # ActiveSupport::Inflector.inflections(:en) do |inflect|
-#   inflect.plural /^(ox)$/i, '\1en'
-#   inflect.singular /^(ox)en/i, '\1'
-#   inflect.irregular 'person', 'people'
+#   inflect.plural /^(ox)$/i, "\\1en"
+#   inflect.singular /^(ox)en/i, "\\1"
+#   inflect.irregular "person", "people"
 #   inflect.uncountable %w( fish sheep )
 # end
 
 # These inflection rules are supported but not enabled by default:
 # ActiveSupport::Inflector.inflections(:en) do |inflect|
-#   inflect.acronym 'RESTful'
+#   inflect.acronym "RESTful"
 # end
 
 ActiveSupport::Inflector.inflections do |inflect|
diff --git a/services/api/config/initializers/new_framework_defaults_7_0.rb b/services/api/config/initializers/new_framework_defaults_7_0.rb
new file mode 100644
index 0000000000..b13ef5ed16
--- /dev/null
+++ b/services/api/config/initializers/new_framework_defaults_7_0.rb
@@ -0,0 +1,143 @@
+# Be sure to restart your server when you modify this file.
+#
+# This file eases your Rails 7.0 framework defaults upgrade.
+#
+# Uncomment each configuration one by one to switch to the new default.
+# Once your application is ready to run with all new defaults, you can remove
+# this file and set the `config.load_defaults` to `7.0`.
+#
+# Read the Guide for Upgrading Ruby on Rails for more info on each option.
+# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html
+
+# `button_to` view helper will render `<button>` element, regardless of whether
+# or not the content is passed as the first argument or as a block.
+# Rails.application.config.action_view.button_to_generates_button_tag = true
+
+# `stylesheet_link_tag` view helper will not render the media attribute by default.
+# Rails.application.config.action_view.apply_stylesheet_media_default = false
+
+# Change the digest class for the key generators to `OpenSSL::Digest::SHA256`.
+# Changing this default means invalidate all encrypted messages generated by
+# your application and, all the encrypted cookies. Only change this after you
+# rotated all the messages using the key rotator.
+#
+# See upgrading guide for more information on how to build a rotator.
+# https://guides.rubyonrails.org/v7.0/upgrading_ruby_on_rails.html
+# Rails.application.config.active_support.key_generator_hash_digest_class = OpenSSL::Digest::SHA256
+
+# Change the digest class for ActiveSupport::Digest.
+# Changing this default means that for example Etags change and
+# various cache keys leading to cache invalidation.
+# Rails.application.config.active_support.hash_digest_class = OpenSSL::Digest::SHA256
+
+# Don't override ActiveSupport::TimeWithZone.name and use the default Ruby
+# implementation.
+# Rails.application.config.active_support.remove_deprecated_time_with_zone_name = true
+
+# Calls `Rails.application.executor.wrap` around test cases.
+# This makes test cases behave closer to an actual request or job.
+# Several features that are normally disabled in test, such as Active Record query cache
+# and asynchronous queries will then be enabled.
+# Rails.application.config.active_support.executor_around_test_case = true
+
+# Set both the `:open_timeout` and `:read_timeout` values for `:smtp` delivery method.
+# Rails.application.config.action_mailer.smtp_timeout = 5
+
+# The ActiveStorage video previewer will now use scene change detection to generate
+# better preview images (rather than the previous default of using the first frame
+# of the video).
+# Rails.application.config.active_storage.video_preview_arguments =
+#   "-vf 'select=eq(n\\,0)+eq(key\\,1)+gt(scene\\,0.015),loop=loop=-1:size=2,trim=start_frame=1' -frames:v 1 -f image2"
+
+# Automatically infer `inverse_of` for associations with a scope.
+# Rails.application.config.active_record.automatic_scope_inversing = true
+
+# Raise when running tests if fixtures contained foreign key violations
+# Rails.application.config.active_record.verify_foreign_keys_for_fixtures = true
+
+# Disable partial inserts.
+# This default means that all columns will be referenced in INSERT queries
+# regardless of whether they have a default or not.
+# Rails.application.config.active_record.partial_inserts = false
+
+# Protect from open redirect attacks in `redirect_back_or_to` and `redirect_to`.
+# Rails.application.config.action_controller.raise_on_open_redirects = true
+
+# Change the variant processor for Active Storage.
+# Changing this default means updating all places in your code that
+# generate variants to use image processing macros and ruby-vips
+# operations. See the upgrading guide for detail on the changes required.
+# The `:mini_magick` option is not deprecated; it's fine to keep using it.
+# Rails.application.config.active_storage.variant_processor = :vips
+
+# Enable parameter wrapping for JSON.
+# Previously this was set in an initializer. It's fine to keep using that initializer if you've customized it.
+# To disable parameter wrapping entirely, set this config to `false`.
+# Rails.application.config.action_controller.wrap_parameters_by_default = true
+
+# Specifies whether generated namespaced UUIDs follow the RFC 4122 standard for namespace IDs provided as a
+# `String` to `Digest::UUID.uuid_v3` or `Digest::UUID.uuid_v5` method calls.
+#
+# See https://guides.rubyonrails.org/configuring.html#config-active-support-use-rfc4122-namespaced-uuids for
+# more information.
+# Rails.application.config.active_support.use_rfc4122_namespaced_uuids = true
+
+# Change the default headers to disable browsers' flawed legacy XSS protection.
+# Rails.application.config.action_dispatch.default_headers = {
+#   "X-Frame-Options" => "SAMEORIGIN",
+#   "X-XSS-Protection" => "0",
+#   "X-Content-Type-Options" => "nosniff",
+#   "X-Download-Options" => "noopen",
+#   "X-Permitted-Cross-Domain-Policies" => "none",
+#   "Referrer-Policy" => "strict-origin-when-cross-origin"
+# }
+
+
+# ** Please read carefully, this must be configured in config/application.rb **
+# Change the format of the cache entry.
+# Changing this default means that all new cache entries added to the cache
+# will have a different format that is not supported by Rails 6.1 applications.
+# Only change this value after your application is fully deployed to Rails 7.0
+# and you have no plans to rollback.
+# When you're ready to change format, add this to `config/application.rb` (NOT this file):
+#  config.active_support.cache_format_version = 7.0
+
+
+# Cookie serializer: 2 options
+#
+# If you're upgrading and haven't set `cookies_serializer` previously, your cookie serializer
+# is `:marshal`. The default for new apps is `:json`.
+#
+# Rails.application.config.action_dispatch.cookies_serializer = :json
+#
+#
+# To migrate an existing application to the `:json` serializer, use the `:hybrid` option.
+#
+# Rails transparently deserializes existing (Marshal-serialized) cookies on read and
+# re-writes them in the JSON format.
+#
+# It is fine to use `:hybrid` long term; you should do that until you're confident *all* your cookies
+# have been converted to JSON. To keep using `:hybrid` long term, move this config to its own
+# initializer or to `config/application.rb`.
+#
+# Rails.application.config.action_dispatch.cookies_serializer = :hybrid
+#
+#
+# If your cookies can't yet be serialized to JSON, keep using `:marshal` for backward-compatibility.
+#
+# If you have configured the serializer elsewhere, you can remove this section of the file.
+#
+# See https://guides.rubyonrails.org/action_controller_overview.html#cookies for more information.
+
+# Change the return value of `ActionDispatch::Request#content_type` to the Content-Type header without modification.
+# Rails.application.config.action_dispatch.return_only_request_media_type_on_content_type = false
+
+# Active Storage `has_many_attached` relationships will default to replacing the current collection instead of appending to it.
+# Thus, to support submitting an empty collection, the `file_field` helper will render an hidden field `include_hidden` by default when `multiple_file_field_include_hidden` is set to `true`.
+# See https://guides.rubyonrails.org/configuring.html#config-active-storage-multiple-file-field-include-hidden for more information.
+# Rails.application.config.active_storage.multiple_file_field_include_hidden = true
+
+# ** Please read carefully, this must be configured in config/application.rb (NOT this file) **
+# Disables the deprecated #to_s override in some Ruby core classes
+# See https://guides.rubyonrails.org/configuring.html#config-active-support-disable-to-s-conversion for more information.
+# config.active_support.disable_to_s_conversion = true

commit 4419f1fcdbee8eb8b9c8da8825df7d307b124eed
Author: Tom Clegg <tom at curii.com>
Date:   Wed Sep 20 21:19:18 2023 -0400

    20300: Update to Rails 7.0.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/Gemfile b/services/api/Gemfile
index 1d30c6cd05..a7ceeb4a18 100644
--- a/services/api/Gemfile
+++ b/services/api/Gemfile
@@ -4,14 +4,10 @@
 
 source 'https://rubygems.org'
 
-gem 'rails', '~> 6.1.0'
+gem 'rails', '~> 7.0.0'
 gem 'responders'
 gem 'i18n'
 
-# Pin sprockets to < 4.0 to avoid issues when upgrading rails to 5.2
-# See: https://github.com/rails/sprockets-rails/issues/443
-gem 'sprockets', '~> 3.0'
-
 group :test, :development do
   gem 'factory_bot_rails'
 
diff --git a/services/api/Gemfile.lock b/services/api/Gemfile.lock
index 3fdd442a45..9e09fb64af 100644
--- a/services/api/Gemfile.lock
+++ b/services/api/Gemfile.lock
@@ -8,65 +8,71 @@ GIT
 GEM
   remote: https://rubygems.org/
   specs:
-    actioncable (6.1.7.6)
-      actionpack (= 6.1.7.6)
-      activesupport (= 6.1.7.6)
+    actioncable (7.0.8)
+      actionpack (= 7.0.8)
+      activesupport (= 7.0.8)
       nio4r (~> 2.0)
       websocket-driver (>= 0.6.1)
-    actionmailbox (6.1.7.6)
-      actionpack (= 6.1.7.6)
-      activejob (= 6.1.7.6)
-      activerecord (= 6.1.7.6)
-      activestorage (= 6.1.7.6)
-      activesupport (= 6.1.7.6)
+    actionmailbox (7.0.8)
+      actionpack (= 7.0.8)
+      activejob (= 7.0.8)
+      activerecord (= 7.0.8)
+      activestorage (= 7.0.8)
+      activesupport (= 7.0.8)
       mail (>= 2.7.1)
-    actionmailer (6.1.7.6)
-      actionpack (= 6.1.7.6)
-      actionview (= 6.1.7.6)
-      activejob (= 6.1.7.6)
-      activesupport (= 6.1.7.6)
+      net-imap
+      net-pop
+      net-smtp
+    actionmailer (7.0.8)
+      actionpack (= 7.0.8)
+      actionview (= 7.0.8)
+      activejob (= 7.0.8)
+      activesupport (= 7.0.8)
       mail (~> 2.5, >= 2.5.4)
+      net-imap
+      net-pop
+      net-smtp
       rails-dom-testing (~> 2.0)
-    actionpack (6.1.7.6)
-      actionview (= 6.1.7.6)
-      activesupport (= 6.1.7.6)
-      rack (~> 2.0, >= 2.0.9)
+    actionpack (7.0.8)
+      actionview (= 7.0.8)
+      activesupport (= 7.0.8)
+      rack (~> 2.0, >= 2.2.4)
       rack-test (>= 0.6.3)
       rails-dom-testing (~> 2.0)
       rails-html-sanitizer (~> 1.0, >= 1.2.0)
-    actiontext (6.1.7.6)
-      actionpack (= 6.1.7.6)
-      activerecord (= 6.1.7.6)
-      activestorage (= 6.1.7.6)
-      activesupport (= 6.1.7.6)
+    actiontext (7.0.8)
+      actionpack (= 7.0.8)
+      activerecord (= 7.0.8)
+      activestorage (= 7.0.8)
+      activesupport (= 7.0.8)
+      globalid (>= 0.6.0)
       nokogiri (>= 1.8.5)
-    actionview (6.1.7.6)
-      activesupport (= 6.1.7.6)
+    actionview (7.0.8)
+      activesupport (= 7.0.8)
       builder (~> 3.1)
       erubi (~> 1.4)
       rails-dom-testing (~> 2.0)
       rails-html-sanitizer (~> 1.1, >= 1.2.0)
-    activejob (6.1.7.6)
-      activesupport (= 6.1.7.6)
+    activejob (7.0.8)
+      activesupport (= 7.0.8)
       globalid (>= 0.3.6)
-    activemodel (6.1.7.6)
-      activesupport (= 6.1.7.6)
-    activerecord (6.1.7.6)
-      activemodel (= 6.1.7.6)
-      activesupport (= 6.1.7.6)
-    activestorage (6.1.7.6)
-      actionpack (= 6.1.7.6)
-      activejob (= 6.1.7.6)
-      activerecord (= 6.1.7.6)
-      activesupport (= 6.1.7.6)
+    activemodel (7.0.8)
+      activesupport (= 7.0.8)
+    activerecord (7.0.8)
+      activemodel (= 7.0.8)
+      activesupport (= 7.0.8)
+    activestorage (7.0.8)
+      actionpack (= 7.0.8)
+      activejob (= 7.0.8)
+      activerecord (= 7.0.8)
+      activesupport (= 7.0.8)
       marcel (~> 1.0)
       mini_mime (>= 1.1.0)
-    activesupport (6.1.7.6)
+    activesupport (7.0.8)
       concurrent-ruby (~> 1.0, >= 1.0.2)
       i18n (>= 1.6, < 2)
       minitest (>= 5.1)
       tzinfo (~> 2.0)
-      zeitwerk (~> 2.3)
     acts_as_api (1.0.1)
       activemodel (>= 3.0.0)
       activesupport (>= 3.0.0)
@@ -174,7 +180,7 @@ GEM
       net-protocol
     net-protocol (0.2.1)
       timeout
-    net-smtp (0.3.3)
+    net-smtp (0.4.0)
       net-protocol
     nio4r (2.5.9)
     nokogiri (1.15.4)
@@ -193,21 +199,20 @@ GEM
     rack (2.2.8)
     rack-test (2.1.0)
       rack (>= 1.3)
-    rails (6.1.7.6)
-      actioncable (= 6.1.7.6)
-      actionmailbox (= 6.1.7.6)
-      actionmailer (= 6.1.7.6)
-      actionpack (= 6.1.7.6)
-      actiontext (= 6.1.7.6)
-      actionview (= 6.1.7.6)
-      activejob (= 6.1.7.6)
-      activemodel (= 6.1.7.6)
-      activerecord (= 6.1.7.6)
-      activestorage (= 6.1.7.6)
-      activesupport (= 6.1.7.6)
+    rails (7.0.8)
+      actioncable (= 7.0.8)
+      actionmailbox (= 7.0.8)
+      actionmailer (= 7.0.8)
+      actionpack (= 7.0.8)
+      actiontext (= 7.0.8)
+      actionview (= 7.0.8)
+      activejob (= 7.0.8)
+      activemodel (= 7.0.8)
+      activerecord (= 7.0.8)
+      activestorage (= 7.0.8)
+      activesupport (= 7.0.8)
       bundler (>= 1.15.0)
-      railties (= 6.1.7.6)
-      sprockets-rails (>= 2.0.0)
+      railties (= 7.0.8)
     rails-controller-testing (1.0.5)
       actionpack (>= 5.0.1.rc1)
       actionview (>= 5.0.1.rc1)
@@ -222,12 +227,13 @@ GEM
     rails-observers (0.1.5)
       activemodel (>= 4.0)
     rails-perftest (0.0.7)
-    railties (6.1.7.6)
-      actionpack (= 6.1.7.6)
-      activesupport (= 6.1.7.6)
+    railties (7.0.8)
+      actionpack (= 7.0.8)
+      activesupport (= 7.0.8)
       method_source
       rake (>= 12.2)
       thor (~> 1.0)
+      zeitwerk (~> 2.5)
     rake (13.0.6)
     rb-fsevent (0.11.2)
     rb-inotify (0.10.1)
@@ -251,13 +257,6 @@ GEM
     simplecov-html (0.7.1)
     simplecov-rcov (0.3.1)
       simplecov (>= 0.4.1)
-    sprockets (3.7.2)
-      concurrent-ruby (~> 1.0)
-      rack (> 1, < 3)
-    sprockets-rails (3.4.2)
-      actionpack (>= 5.2)
-      activesupport (>= 5.2)
-      sprockets (>= 3.0.0)
     test-unit (3.6.1)
       power_assert
     thor (1.2.2)
@@ -293,7 +292,7 @@ DEPENDENCIES
   optimist
   passenger
   pg (~> 1.0)
-  rails (~> 6.1.0)
+  rails (~> 7.0.0)
   rails-controller-testing
   rails-observers
   rails-perftest
@@ -301,7 +300,6 @@ DEPENDENCIES
   ruby-prof (~> 0.15.0)
   simplecov (~> 0.7.1)
   simplecov-rcov
-  sprockets (~> 3.0)
   test-unit (~> 3.0)
   themes_for_rails!
 
diff --git a/services/api/config/application.rb b/services/api/config/application.rb
index ca5eff1a47..be91e8673c 100644
--- a/services/api/config/application.rb
+++ b/services/api/config/application.rb
@@ -16,7 +16,6 @@ require "action_mailer/railtie"
 # require "action_text/engine"
 require "action_view/railtie"
 # require "action_cable/engine"
-require "sprockets/railtie"
 require "rails/test_unit/railtie"
 
 # Require the gems listed in Gemfile, including any gems

commit c9e5e85ab5665baaf4fde6a87cd5f163dadba010
Author: Tom Clegg <tom at curii.com>
Date:   Wed Sep 20 22:05:55 2023 -0400

    20300: Use Rails 6.1 config defaults.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/config/application.rb b/services/api/config/application.rb
index e5f968b0bf..ca5eff1a47 100644
--- a/services/api/config/application.rb
+++ b/services/api/config/application.rb
@@ -33,7 +33,7 @@ module Server
     require_relative "arvados_config.rb"
 
     # Initialize configuration defaults for specified Rails version.
-    config.load_defaults 6.0
+    config.load_defaults 6.1
 
     # Configuration for the application, engines, and railties goes here.
     #
diff --git a/services/api/config/initializers/new_framework_defaults_6_1.rb b/services/api/config/initializers/new_framework_defaults_6_1.rb
deleted file mode 100644
index 9526b835ab..0000000000
--- a/services/api/config/initializers/new_framework_defaults_6_1.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-# Be sure to restart your server when you modify this file.
-#
-# This file contains migration options to ease your Rails 6.1 upgrade.
-#
-# Once upgraded flip defaults one by one to migrate to the new default.
-#
-# Read the Guide for Upgrading Ruby on Rails for more info on each option.
-
-# Support for inversing belongs_to -> has_many Active Record associations.
-# Rails.application.config.active_record.has_many_inversing = true
-
-# Track Active Storage variants in the database.
-# Rails.application.config.active_storage.track_variants = true
-
-# Apply random variation to the delay when retrying failed jobs.
-# Rails.application.config.active_job.retry_jitter = 0.15
-
-# Stop executing `after_enqueue`/`after_perform` callbacks if
-# `before_enqueue`/`before_perform` respectively halts with `throw :abort`.
-# Rails.application.config.active_job.skip_after_callbacks_if_terminated = true
-
-# Specify cookies SameSite protection level: either :none, :lax, or :strict.
-#
-# This change is not backwards compatible with earlier Rails versions.
-# It's best enabled when your entire app is migrated and stable on 6.1.
-# Rails.application.config.action_dispatch.cookies_same_site_protection = :lax
-
-# Generate CSRF tokens that are encoded in URL-safe Base64.
-#
-# This change is not backwards compatible with earlier Rails versions.
-# It's best enabled when your entire app is migrated and stable on 6.1.
-# Rails.application.config.action_controller.urlsafe_csrf_tokens = true
-
-# Specify whether `ActiveSupport::TimeZone.utc_to_local` returns a time with an
-# UTC offset or a UTC time.
-# ActiveSupport.utc_to_local_returns_utc_offset_times = true
-
-# Change the default HTTP status code to `308` when redirecting non-GET/HEAD
-# requests to HTTPS in `ActionDispatch::SSL` middleware.
-# Rails.application.config.action_dispatch.ssl_default_redirect_status = 308
-
-# Use new connection handling API. For most applications this won't have any
-# effect. For applications using multiple databases, this new API provides
-# support for granular connection swapping.
-# Rails.application.config.active_record.legacy_connection_handling = false
-
-# Make `form_with` generate non-remote forms by default.
-# Rails.application.config.action_view.form_with_generates_remote_forms = false
-
-# Set the default queue name for the analysis job to the queue adapter default.
-# Rails.application.config.active_storage.queues.analysis = nil
-
-# Set the default queue name for the purge job to the queue adapter default.
-# Rails.application.config.active_storage.queues.purge = nil
-
-# Set the default queue name for the incineration job to the queue adapter default.
-# Rails.application.config.action_mailbox.queues.incineration = nil
-
-# Set the default queue name for the routing job to the queue adapter default.
-# Rails.application.config.action_mailbox.queues.routing = nil
-
-# Set the default queue name for the mail deliver job to the queue adapter default.
-# Rails.application.config.action_mailer.deliver_later_queue_name = nil
-
-# Generate a `Link` header that gives a hint to modern browsers about
-# preloading assets when using `javascript_include_tag` and `stylesheet_link_tag`.
-# Rails.application.config.action_view.preload_links_header = true

commit be461c33036baefa10dfa2b424721d37b64f3e76
Author: Tom Clegg <tom at curii.com>
Date:   Tue Sep 19 11:10:56 2023 -0400

    20300: Remove config overrides from Rails 5.0 upgrade.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/config/initializers/new_framework_defaults.rb b/services/api/config/initializers/new_framework_defaults.rb
deleted file mode 100644
index 161dbe10e6..0000000000
--- a/services/api/config/initializers/new_framework_defaults.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
-# Be sure to restart your server when you modify this file.
-#
-# This file contains migration options to ease your Rails 5.0 upgrade.
-#
-# Once upgraded flip defaults one by one to migrate to the new default.
-#
-# Read the Guide for Upgrading Ruby on Rails for more info on each option.
-
-Rails.application.config.action_controller.raise_on_unfiltered_parameters = true
-
-# Enable per-form CSRF tokens. Previous versions had false.
-Rails.application.config.action_controller.per_form_csrf_tokens = false
-
-# Enable origin-checking CSRF mitigation. Previous versions had false.
-Rails.application.config.action_controller.forgery_protection_origin_check = false
-
-# Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`.
-# Previous versions had false.
-ActiveSupport.to_time_preserves_timezone = false

commit c14aed71863a2a485b56bffcc70048f6d4dc5df0
Author: Tom Clegg <tom at curii.com>
Date:   Wed Sep 20 22:05:32 2023 -0400

    20300: Use Rails 6.0 config defaults.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/config/application.rb b/services/api/config/application.rb
index c1caba0fd8..e5f968b0bf 100644
--- a/services/api/config/application.rb
+++ b/services/api/config/application.rb
@@ -33,7 +33,7 @@ module Server
     require_relative "arvados_config.rb"
 
     # Initialize configuration defaults for specified Rails version.
-    config.load_defaults 5.2
+    config.load_defaults 6.0
 
     # Configuration for the application, engines, and railties goes here.
     #
diff --git a/services/api/config/initializers/new_framework_defaults_6_0.rb b/services/api/config/initializers/new_framework_defaults_6_0.rb
deleted file mode 100644
index 92240ef5f5..0000000000
--- a/services/api/config/initializers/new_framework_defaults_6_0.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# Be sure to restart your server when you modify this file.
-#
-# This file contains migration options to ease your Rails 6.0 upgrade.
-#
-# Once upgraded flip defaults one by one to migrate to the new default.
-#
-# Read the Guide for Upgrading Ruby on Rails for more info on each option.
-
-# Don't force requests from old versions of IE to be UTF-8 encoded.
-# Rails.application.config.action_view.default_enforce_utf8 = false
-
-# Embed purpose and expiry metadata inside signed and encrypted
-# cookies for increased security.
-#
-# This option is not backwards compatible with earlier Rails versions.
-# It's best enabled when your entire app is migrated and stable on 6.0.
-# Rails.application.config.action_dispatch.use_cookies_with_metadata = true
-
-# Change the return value of `ActionDispatch::Response#content_type` to Content-Type header without modification.
-# Rails.application.config.action_dispatch.return_only_media_type_on_content_type = false
-
-# Return false instead of self when enqueuing is aborted from a callback.
-# Rails.application.config.active_job.return_false_on_aborted_enqueue = true
-
-# Send Active Storage analysis and purge jobs to dedicated queues.
-# Rails.application.config.active_storage.queues.analysis = :active_storage_analysis
-# Rails.application.config.active_storage.queues.purge    = :active_storage_purge
-
-# When assigning to a collection of attachments declared via `has_many_attached`, replace existing
-# attachments instead of appending. Use #attach to add new attachments without replacing existing ones.
-# Rails.application.config.active_storage.replace_on_assign_to_many = true
-
-# Use ActionMailer::MailDeliveryJob for sending parameterized and normal mail.
-#
-# The default delivery jobs (ActionMailer::Parameterized::DeliveryJob, ActionMailer::DeliveryJob),
-# will be removed in Rails 6.1. This setting is not backwards compatible with earlier Rails versions.
-# If you send mail in the background, job workers need to have a copy of
-# MailDeliveryJob to ensure all delivery jobs are processed properly.
-# Make sure your entire app is migrated and stable on 6.0 before using this setting.
-# Rails.application.config.action_mailer.delivery_job = "ActionMailer::MailDeliveryJob"
-
-# Enable the same cache key to be reused when the object being cached of type
-# `ActiveRecord::Relation` changes by moving the volatile information (max updated at and count)
-# of the relation's cache key into the cache version to support recycling cache key.
-# Rails.application.config.active_record.collection_cache_versioning = true

commit bb8f9b8724cc2baf6b311d8cfcc25f45f4ad2cc1
Author: Tom Clegg <tom at curii.com>
Date:   Tue Sep 19 10:04:16 2023 -0400

    20300: Update symbols to strings in relations.
    
    In Rails 6.1 the column names are converted to strings internally, so
    checking for "foreign_key == :owner_uuid" no longer works.
    
    Using symbols continues to work when declaring relations, but they are
    updated to strings anyway for clarity, and to match
    canonical/documented usage.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/app/models/arvados_model.rb b/services/api/app/models/arvados_model.rb
index c909e47cef..81cdbfcd1c 100644
--- a/services/api/app/models/arvados_model.rb
+++ b/services/api/app/models/arvados_model.rb
@@ -37,9 +37,9 @@ class ArvadosModel < ApplicationRecord
   # user.uuid==object.owner_uuid.
   has_many(:permissions,
            ->{where(link_class: 'permission')},
-           foreign_key: :head_uuid,
+           foreign_key: 'head_uuid',
            class_name: 'Link',
-           primary_key: :uuid)
+           primary_key: 'uuid')
 
   # If async is true at create or update, permission graph
   # update is deferred allowing making multiple calls without the performance
diff --git a/services/api/app/models/authorized_key.rb b/services/api/app/models/authorized_key.rb
index 0da9f7f8c5..cf4a1d55de 100644
--- a/services/api/app/models/authorized_key.rb
+++ b/services/api/app/models/authorized_key.rb
@@ -10,9 +10,9 @@ class AuthorizedKey < ArvadosModel
   before_update :permission_to_set_authorized_user_uuid
 
   belongs_to :authorized_user, {
-               foreign_key: :authorized_user_uuid,
+               foreign_key: 'authorized_user_uuid',
                class_name: 'User',
-               primary_key: :uuid,
+               primary_key: 'uuid',
                optional: true,
              }
 
diff --git a/services/api/app/models/container.rb b/services/api/app/models/container.rb
index 9104a21107..97f8dfa279 100644
--- a/services/api/app/models/container.rb
+++ b/services/api/app/models/container.rb
@@ -51,11 +51,15 @@ class Container < ArvadosModel
   after_save :update_cr_logs
   after_save :handle_completed
 
-  has_many :container_requests, :foreign_key => :container_uuid, :class_name => 'ContainerRequest', :primary_key => :uuid
+  has_many :container_requests, {
+             class_name: 'ContainerRequest',
+             foreign_key: 'container_uuid',
+             primary_key: 'uuid',
+           }
   belongs_to :auth, {
                class_name: 'ApiClientAuthorization',
-               foreign_key: :auth_uuid,
-               primary_key: :uuid,
+               foreign_key: 'auth_uuid',
+               primary_key: 'uuid',
                optional: true,
              }
 
diff --git a/services/api/app/models/container_request.rb b/services/api/app/models/container_request.rb
index b499e37e7d..374859f2c1 100644
--- a/services/api/app/models/container_request.rb
+++ b/services/api/app/models/container_request.rb
@@ -13,14 +13,14 @@ class ContainerRequest < ArvadosModel
   include WhitelistUpdate
 
   belongs_to :container, {
-               foreign_key: :container_uuid,
-               primary_key: :uuid,
+               foreign_key: 'container_uuid',
+               primary_key: 'uuid',
                optional: true,
              }
   belongs_to :requesting_container, {
                class_name: 'Container',
-               foreign_key: :requesting_container_uuid,
-               primary_key: :uuid,
+               foreign_key: 'requesting_container_uuid',
+               primary_key: 'uuid',
                optional: true,
              }
 
diff --git a/services/api/app/models/job.rb b/services/api/app/models/job.rb
index f792c04842..029a313285 100644
--- a/services/api/app/models/job.rb
+++ b/services/api/app/models/job.rb
@@ -50,7 +50,7 @@ class Job < ArvadosModel
   before_create :create_disabled
   before_update :update_disabled
 
-  has_many(:nodes, foreign_key: :job_uuid, primary_key: :uuid)
+  has_many(:nodes, foreign_key: 'job_uuid', primary_key: 'uuid')
 
   class SubmitIdReused < RequestError
   end
diff --git a/services/api/app/models/node.rb b/services/api/app/models/node.rb
index 2770dd5ccc..9e250acfb1 100644
--- a/services/api/app/models/node.rb
+++ b/services/api/app/models/node.rb
@@ -21,8 +21,8 @@ class Node < ArvadosModel
   # have access to the associated Job.  They're expected to set
   # job_readable=true if the Job UUID can be included in the API response.
   belongs_to :job, {
-               foreign_key: :job_uuid,
-               primary_key: :uuid,
+               foreign_key: 'job_uuid',
+               primary_key: 'uuid',
                optional: true,
              }
   attr_accessor :job_readable
diff --git a/services/api/app/models/pipeline_instance.rb b/services/api/app/models/pipeline_instance.rb
index d5ed5e533d..23a08051ca 100644
--- a/services/api/app/models/pipeline_instance.rb
+++ b/services/api/app/models/pipeline_instance.rb
@@ -10,8 +10,8 @@ class PipelineInstance < ArvadosModel
   serialize :properties, Hash
   serialize :components_summary, Hash
   belongs_to :pipeline_template, {
-               foreign_key: :pipeline_template_uuid,
-               primary_key: :uuid,
+               foreign_key: 'pipeline_template_uuid',
+               primary_key: 'uuid',
                optional: true,
              }
 
diff --git a/services/api/app/models/user.rb b/services/api/app/models/user.rb
index 88314c6775..afc2d18b8a 100644
--- a/services/api/app/models/user.rb
+++ b/services/api/app/models/user.rb
@@ -56,8 +56,8 @@ class User < ArvadosModel
   before_destroy :clear_permissions
   after_destroy :remove_self_from_permissions
 
-  has_many :authorized_keys, :foreign_key => :authorized_user_uuid, :primary_key => :uuid
-  has_many :repositories, foreign_key: :owner_uuid, primary_key: :uuid
+  has_many :authorized_keys, foreign_key: 'authorized_user_uuid', primary_key: 'uuid'
+  has_many :repositories, foreign_key: 'owner_uuid', primary_key: 'uuid'
 
   default_scope { where('redirect_to_user_uuid is null') }
 
diff --git a/services/api/app/models/virtual_machine.rb b/services/api/app/models/virtual_machine.rb
index 0b3557eef6..09687385ca 100644
--- a/services/api/app/models/virtual_machine.rb
+++ b/services/api/app/models/virtual_machine.rb
@@ -9,9 +9,9 @@ class VirtualMachine < ArvadosModel
 
   has_many(:login_permissions,
            -> { where("link_class = 'permission' and name = 'can_login'") },
-           foreign_key: :head_uuid,
+           foreign_key: 'head_uuid',
            class_name: 'Link',
-           primary_key: :uuid)
+           primary_key: 'uuid')
 
   api_accessible :user, extend: :common do |t|
     t.add :hostname
diff --git a/services/api/lib/can_be_an_owner.rb b/services/api/lib/can_be_an_owner.rb
index 6f30f5ae33..fc66f84bfc 100644
--- a/services/api/lib/can_be_an_owner.rb
+++ b/services/api/lib/can_be_an_owner.rb
@@ -22,8 +22,8 @@ module CanBeAnOwner
       klass = t.classify.constantize
       next unless klass and 'owner_uuid'.in?(klass.columns.collect(&:name))
       base.has_many(t.to_sym,
-                    foreign_key: :owner_uuid,
-                    primary_key: :uuid,
+                    foreign_key: 'owner_uuid',
+                    primary_key: 'uuid',
                     dependent: :restrict_with_exception)
     end
     # We need custom protection for changing an owner's primary
@@ -75,7 +75,7 @@ module CanBeAnOwner
 
     # Check for objects that have my old uuid listed as their owner.
     self.class.reflect_on_all_associations(:has_many).each do |assoc|
-      next unless assoc.foreign_key == :owner_uuid
+      next unless assoc.foreign_key == 'owner_uuid'
       if assoc.klass.where(owner_uuid: uuid_was).any?
         errors.add(:uuid,
                    "cannot be changed on a #{self.class} that owns objects")
diff --git a/services/api/lib/has_uuid.rb b/services/api/lib/has_uuid.rb
index 2074566941..217113beec 100644
--- a/services/api/lib/has_uuid.rb
+++ b/services/api/lib/has_uuid.rb
@@ -14,14 +14,14 @@ module HasUuid
     base.has_many(:links_via_head,
                   -> { where("not (link_class = 'permission')") },
                   class_name: 'Link',
-                  foreign_key: :head_uuid,
-                  primary_key: :uuid,
+                  foreign_key: 'head_uuid',
+                  primary_key: 'uuid',
                   dependent: :destroy)
     base.has_many(:links_via_tail,
                   -> { where("not (link_class = 'permission')") },
                   class_name: 'Link',
-                  foreign_key: :tail_uuid,
-                  primary_key: :uuid,
+                  foreign_key: 'tail_uuid',
+                  primary_key: 'uuid',
                   dependent: :destroy)
   end
 
diff --git a/services/api/test/functional/arvados/v1/collections_controller_test.rb b/services/api/test/functional/arvados/v1/collections_controller_test.rb
index 8a1d044d6a..574cd366fc 100644
--- a/services/api/test/functional/arvados/v1/collections_controller_test.rb
+++ b/services/api/test/functional/arvados/v1/collections_controller_test.rb
@@ -1222,6 +1222,20 @@ EOS
     assert_nil json_response['trash_at']
   end
 
+  test 'untrash a trashed collection by assigning nil to trash_at' do
+    authorize_with :active
+    post :update, params: {
+           id: collections(:expired_collection).uuid,
+           collection: {
+             trash_at: nil,
+           },
+           include_trash: true,
+    }
+    assert_response 200
+    assert_equal false, json_response['is_trashed']
+    assert_nil json_response['trash_at']
+  end
+
   test 'untrash error on not trashed collection' do
     authorize_with :active
     post :untrash, params: {

commit 78701caa6b3f07425ac32eb95c60107af91f42f7
Author: Tom Clegg <tom at curii.com>
Date:   Tue Sep 19 09:44:52 2023 -0400

    20300: Use Rails 5.2 config defaults.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/app/controllers/application_controller.rb b/services/api/app/controllers/application_controller.rb
index d62a09b0a9..b1e2a4008f 100644
--- a/services/api/app/controllers/application_controller.rb
+++ b/services/api/app/controllers/application_controller.rb
@@ -29,6 +29,9 @@ class ApplicationController < ActionController::Base
   include DbCurrentTime
 
   respond_to :json
+
+  # Although CSRF protection is already enabled by default, this is
+  # still needed to reposition CSRF checks later in callback order.
   protect_from_forgery
 
   ERROR_ACTIONS = [:render_error, :render_not_found]
diff --git a/services/api/config/application.rb b/services/api/config/application.rb
index 74744e2208..c1caba0fd8 100644
--- a/services/api/config/application.rb
+++ b/services/api/config/application.rb
@@ -32,8 +32,8 @@ module Server
 
     require_relative "arvados_config.rb"
 
-    # Initialize configuration defaults for originally generated Rails version.
-    config.load_defaults 5.0
+    # Initialize configuration defaults for specified Rails version.
+    config.load_defaults 5.2
 
     # Configuration for the application, engines, and railties goes here.
     #
diff --git a/services/api/config/initializers/new_framework_defaults_5_2.rb b/services/api/config/initializers/new_framework_defaults_5_2.rb
deleted file mode 100644
index 93a8d52406..0000000000
--- a/services/api/config/initializers/new_framework_defaults_5_2.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
-# Be sure to restart your server when you modify this file.
-#
-# This file contains migration options to ease your Rails 5.2 upgrade.
-#
-# Once upgraded flip defaults one by one to migrate to the new default.
-#
-# Read the Guide for Upgrading Ruby on Rails for more info on each option.
-
-# Make Active Record use stable #cache_key alongside new #cache_version method.
-# This is needed for recyclable cache keys.
-# Rails.application.config.active_record.cache_versioning = true
-
-# Use AES-256-GCM authenticated encryption for encrypted cookies.
-# Also, embed cookie expiry in signed or encrypted cookies for increased security.
-#
-# This option is not backwards compatible with earlier Rails versions.
-# It's best enabled when your entire app is migrated and stable on 5.2.
-#
-# Existing cookies will be converted on read then written with the new scheme.
-# Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true
-
-# Use AES-256-GCM authenticated encryption as default cipher for encrypting messages
-# instead of AES-256-CBC, when use_authenticated_message_encryption is set to true.
-# Rails.application.config.active_support.use_authenticated_message_encryption = true
-
-# Add default protection from forgery to ActionController::Base instead of in
-# ApplicationController.
-# Rails.application.config.action_controller.default_protect_from_forgery = true
-
-# Store boolean values are in sqlite3 databases as 1 and 0 instead of 't' and
-# 'f' after migrating old data.
-# Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true
-
-# Use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header.
-# Rails.application.config.active_support.use_sha1_digests = true
-
-# Make `form_with` generate id attributes for any generated HTML tags.
-# Rails.application.config.action_view.form_with_generates_ids = true

commit 91e3daf696cda4f9c8bee8ef73a2f15ce0476b6e
Author: Tom Clegg <tom at curii.com>
Date:   Tue Sep 19 00:04:45 2023 -0400

    20300: Rail 6.1 app:update.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/bin/rails b/services/api/bin/rails
index 0739660237..6fb4e4051c 100755
--- a/services/api/bin/rails
+++ b/services/api/bin/rails
@@ -1,4 +1,4 @@
 #!/usr/bin/env ruby
 APP_PATH = File.expand_path('../config/application', __dir__)
-require_relative '../config/boot'
-require 'rails/commands'
+require_relative "../config/boot"
+require "rails/commands"
diff --git a/services/api/bin/rake b/services/api/bin/rake
index 17240489f6..4fbf10b960 100755
--- a/services/api/bin/rake
+++ b/services/api/bin/rake
@@ -1,4 +1,4 @@
 #!/usr/bin/env ruby
-require_relative '../config/boot'
-require 'rake'
+require_relative "../config/boot"
+require "rake"
 Rake.application.run
diff --git a/services/api/bin/setup b/services/api/bin/setup
index 0e39e8cb13..57923026c4 100755
--- a/services/api/bin/setup
+++ b/services/api/bin/setup
@@ -1,5 +1,5 @@
 #!/usr/bin/env ruby
-require 'fileutils'
+require "fileutils"
 
 # path to your application root.
 APP_ROOT = File.expand_path('..', __dir__)
@@ -9,8 +9,8 @@ def system!(*args)
 end
 
 FileUtils.chdir APP_ROOT do
-  # This script is a way to setup or update your development environment automatically.
-  # This script is idempotent, so that you can run it at anytime and get an expectable outcome.
+  # This script is a way to set up or update your development environment automatically.
+  # This script is idempotent, so that you can run it at any time and get an expectable outcome.
   # Add necessary setup steps to this file.
 
   puts '== Installing dependencies =='
diff --git a/services/api/config.ru b/services/api/config.ru
index 30e8281843..4a3c09a688 100644
--- a/services/api/config.ru
+++ b/services/api/config.ru
@@ -1,8 +1,6 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
 # This file is used by Rack-based servers to start the application.
 
-require ::File.expand_path('../config/environment',  __FILE__)
-run Server::Application
+require_relative "config/environment"
+
+run Rails.application
+Rails.application.load_server
diff --git a/services/api/config/application.rb b/services/api/config/application.rb
index 587a09d4e0..74744e2208 100644
--- a/services/api/config/application.rb
+++ b/services/api/config/application.rb
@@ -2,7 +2,7 @@
 #
 # SPDX-License-Identifier: AGPL-3.0
 
-require_relative 'boot'
+require_relative "boot"
 
 require "rails"
 # Pick the frameworks you want:
@@ -35,10 +35,13 @@ module Server
     # Initialize configuration defaults for originally generated Rails version.
     config.load_defaults 5.0
 
-    # Settings in config/environments/* take precedence over those specified here.
-    # Application configuration can go into files in config/initializers
-    # -- all .rb files in that directory are automatically loaded after loading
-    # the framework and any gems in your application.
+    # Configuration for the application, engines, and railties goes here.
+    #
+    # These settings can be overridden in specific environments using the files
+    # in config/environments, which are processed later.
+    #
+    # config.time_zone = "Central Time (US & Canada)"
+    # config.eager_load_paths << Rails.root.join("extras")
 
     # We use db/structure.sql instead of db/schema.rb.
     config.active_record.schema_format = :sql
@@ -50,7 +53,7 @@ module Server
     # container_request records can contain arbitrary data structures
     # in mounts.*.content, so rails must not munge them.
     config.action_dispatch.perform_deep_munge = false
-    
+
     # force_ssl's redirect-to-https feature doesn't work when the
     # client supplies a port number, and prevents arvados-controller
     # from connecting to Rails internally via plain http.
diff --git a/services/api/config/boot.rb b/services/api/config/boot.rb
index 30f5120df6..d69bd27dca 100644
--- a/services/api/config/boot.rb
+++ b/services/api/config/boot.rb
@@ -1,3 +1,3 @@
 ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
 
-require 'bundler/setup' # Set up gems listed in the Gemfile.
+require "bundler/setup" # Set up gems listed in the Gemfile.
diff --git a/services/api/config/environment.rb b/services/api/config/environment.rb
index 426333bb46..cac5315775 100644
--- a/services/api/config/environment.rb
+++ b/services/api/config/environment.rb
@@ -1,5 +1,5 @@
 # Load the Rails application.
-require_relative 'application'
+require_relative "application"
 
 # Initialize the Rails application.
 Rails.application.initialize!
diff --git a/services/api/config/initializers/backtrace_silencers.rb b/services/api/config/initializers/backtrace_silencers.rb
index 59385cdf37..33699c3091 100644
--- a/services/api/config/initializers/backtrace_silencers.rb
+++ b/services/api/config/initializers/backtrace_silencers.rb
@@ -1,7 +1,8 @@
 # Be sure to restart your server when you modify this file.
 
 # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
-# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
+# Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) }
 
-# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
-# Rails.backtrace_cleaner.remove_silencers!
+# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code
+# by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'".
+Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"]
diff --git a/services/api/config/initializers/filter_parameter_logging.rb b/services/api/config/initializers/filter_parameter_logging.rb
index 4a994e1e7b..4b34a03668 100644
--- a/services/api/config/initializers/filter_parameter_logging.rb
+++ b/services/api/config/initializers/filter_parameter_logging.rb
@@ -1,4 +1,6 @@
 # Be sure to restart your server when you modify this file.
 
 # Configure sensitive parameters which will be filtered from the log file.
-Rails.application.config.filter_parameters += [:password]
+Rails.application.config.filter_parameters += [
+  :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
+]
diff --git a/services/api/config/initializers/new_framework_defaults_6_1.rb b/services/api/config/initializers/new_framework_defaults_6_1.rb
new file mode 100644
index 0000000000..9526b835ab
--- /dev/null
+++ b/services/api/config/initializers/new_framework_defaults_6_1.rb
@@ -0,0 +1,67 @@
+# Be sure to restart your server when you modify this file.
+#
+# This file contains migration options to ease your Rails 6.1 upgrade.
+#
+# Once upgraded flip defaults one by one to migrate to the new default.
+#
+# Read the Guide for Upgrading Ruby on Rails for more info on each option.
+
+# Support for inversing belongs_to -> has_many Active Record associations.
+# Rails.application.config.active_record.has_many_inversing = true
+
+# Track Active Storage variants in the database.
+# Rails.application.config.active_storage.track_variants = true
+
+# Apply random variation to the delay when retrying failed jobs.
+# Rails.application.config.active_job.retry_jitter = 0.15
+
+# Stop executing `after_enqueue`/`after_perform` callbacks if
+# `before_enqueue`/`before_perform` respectively halts with `throw :abort`.
+# Rails.application.config.active_job.skip_after_callbacks_if_terminated = true
+
+# Specify cookies SameSite protection level: either :none, :lax, or :strict.
+#
+# This change is not backwards compatible with earlier Rails versions.
+# It's best enabled when your entire app is migrated and stable on 6.1.
+# Rails.application.config.action_dispatch.cookies_same_site_protection = :lax
+
+# Generate CSRF tokens that are encoded in URL-safe Base64.
+#
+# This change is not backwards compatible with earlier Rails versions.
+# It's best enabled when your entire app is migrated and stable on 6.1.
+# Rails.application.config.action_controller.urlsafe_csrf_tokens = true
+
+# Specify whether `ActiveSupport::TimeZone.utc_to_local` returns a time with an
+# UTC offset or a UTC time.
+# ActiveSupport.utc_to_local_returns_utc_offset_times = true
+
+# Change the default HTTP status code to `308` when redirecting non-GET/HEAD
+# requests to HTTPS in `ActionDispatch::SSL` middleware.
+# Rails.application.config.action_dispatch.ssl_default_redirect_status = 308
+
+# Use new connection handling API. For most applications this won't have any
+# effect. For applications using multiple databases, this new API provides
+# support for granular connection swapping.
+# Rails.application.config.active_record.legacy_connection_handling = false
+
+# Make `form_with` generate non-remote forms by default.
+# Rails.application.config.action_view.form_with_generates_remote_forms = false
+
+# Set the default queue name for the analysis job to the queue adapter default.
+# Rails.application.config.active_storage.queues.analysis = nil
+
+# Set the default queue name for the purge job to the queue adapter default.
+# Rails.application.config.active_storage.queues.purge = nil
+
+# Set the default queue name for the incineration job to the queue adapter default.
+# Rails.application.config.action_mailbox.queues.incineration = nil
+
+# Set the default queue name for the routing job to the queue adapter default.
+# Rails.application.config.action_mailbox.queues.routing = nil
+
+# Set the default queue name for the mail deliver job to the queue adapter default.
+# Rails.application.config.action_mailer.deliver_later_queue_name = nil
+
+# Generate a `Link` header that gives a hint to modern browsers about
+# preloading assets when using `javascript_include_tag` and `stylesheet_link_tag`.
+# Rails.application.config.action_view.preload_links_header = true
diff --git a/services/api/config/initializers/permissions_policy.rb b/services/api/config/initializers/permissions_policy.rb
new file mode 100644
index 0000000000..00f64d71b0
--- /dev/null
+++ b/services/api/config/initializers/permissions_policy.rb
@@ -0,0 +1,11 @@
+# Define an application-wide HTTP permissions policy. For further
+# information see https://developers.google.com/web/updates/2018/06/feature-policy
+#
+# Rails.application.config.permissions_policy do |f|
+#   f.camera      :none
+#   f.gyroscope   :none
+#   f.microphone  :none
+#   f.usb         :none
+#   f.fullscreen  :self
+#   f.payment     :self, "https://secure.example.com"
+# end

commit cbda91a4e67fc2cd88ddfb54e5faf4605a79c6c7
Author: Tom Clegg <tom at curii.com>
Date:   Mon Sep 18 23:56:57 2023 -0400

    20300: Update bundle to rails 6.1.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/Gemfile b/services/api/Gemfile
index d9ac19dd08..1d30c6cd05 100644
--- a/services/api/Gemfile
+++ b/services/api/Gemfile
@@ -4,7 +4,7 @@
 
 source 'https://rubygems.org'
 
-gem 'rails', '~> 6.0.0'
+gem 'rails', '~> 6.1.0'
 gem 'responders'
 gem 'i18n'
 
diff --git a/services/api/Gemfile.lock b/services/api/Gemfile.lock
index 1aedac9030..3fdd442a45 100644
--- a/services/api/Gemfile.lock
+++ b/services/api/Gemfile.lock
@@ -8,61 +8,65 @@ GIT
 GEM
   remote: https://rubygems.org/
   specs:
-    actioncable (6.0.6.1)
-      actionpack (= 6.0.6.1)
+    actioncable (6.1.7.6)
+      actionpack (= 6.1.7.6)
+      activesupport (= 6.1.7.6)
       nio4r (~> 2.0)
       websocket-driver (>= 0.6.1)
-    actionmailbox (6.0.6.1)
-      actionpack (= 6.0.6.1)
-      activejob (= 6.0.6.1)
-      activerecord (= 6.0.6.1)
-      activestorage (= 6.0.6.1)
-      activesupport (= 6.0.6.1)
+    actionmailbox (6.1.7.6)
+      actionpack (= 6.1.7.6)
+      activejob (= 6.1.7.6)
+      activerecord (= 6.1.7.6)
+      activestorage (= 6.1.7.6)
+      activesupport (= 6.1.7.6)
       mail (>= 2.7.1)
-    actionmailer (6.0.6.1)
-      actionpack (= 6.0.6.1)
-      actionview (= 6.0.6.1)
-      activejob (= 6.0.6.1)
+    actionmailer (6.1.7.6)
+      actionpack (= 6.1.7.6)
+      actionview (= 6.1.7.6)
+      activejob (= 6.1.7.6)
+      activesupport (= 6.1.7.6)
       mail (~> 2.5, >= 2.5.4)
       rails-dom-testing (~> 2.0)
-    actionpack (6.0.6.1)
-      actionview (= 6.0.6.1)
-      activesupport (= 6.0.6.1)
-      rack (~> 2.0, >= 2.0.8)
+    actionpack (6.1.7.6)
+      actionview (= 6.1.7.6)
+      activesupport (= 6.1.7.6)
+      rack (~> 2.0, >= 2.0.9)
       rack-test (>= 0.6.3)
       rails-dom-testing (~> 2.0)
       rails-html-sanitizer (~> 1.0, >= 1.2.0)
-    actiontext (6.0.6.1)
-      actionpack (= 6.0.6.1)
-      activerecord (= 6.0.6.1)
-      activestorage (= 6.0.6.1)
-      activesupport (= 6.0.6.1)
+    actiontext (6.1.7.6)
+      actionpack (= 6.1.7.6)
+      activerecord (= 6.1.7.6)
+      activestorage (= 6.1.7.6)
+      activesupport (= 6.1.7.6)
       nokogiri (>= 1.8.5)
-    actionview (6.0.6.1)
-      activesupport (= 6.0.6.1)
+    actionview (6.1.7.6)
+      activesupport (= 6.1.7.6)
       builder (~> 3.1)
       erubi (~> 1.4)
       rails-dom-testing (~> 2.0)
       rails-html-sanitizer (~> 1.1, >= 1.2.0)
-    activejob (6.0.6.1)
-      activesupport (= 6.0.6.1)
+    activejob (6.1.7.6)
+      activesupport (= 6.1.7.6)
       globalid (>= 0.3.6)
-    activemodel (6.0.6.1)
-      activesupport (= 6.0.6.1)
-    activerecord (6.0.6.1)
-      activemodel (= 6.0.6.1)
-      activesupport (= 6.0.6.1)
-    activestorage (6.0.6.1)
-      actionpack (= 6.0.6.1)
-      activejob (= 6.0.6.1)
-      activerecord (= 6.0.6.1)
+    activemodel (6.1.7.6)
+      activesupport (= 6.1.7.6)
+    activerecord (6.1.7.6)
+      activemodel (= 6.1.7.6)
+      activesupport (= 6.1.7.6)
+    activestorage (6.1.7.6)
+      actionpack (= 6.1.7.6)
+      activejob (= 6.1.7.6)
+      activerecord (= 6.1.7.6)
+      activesupport (= 6.1.7.6)
       marcel (~> 1.0)
-    activesupport (6.0.6.1)
+      mini_mime (>= 1.1.0)
+    activesupport (6.1.7.6)
       concurrent-ruby (~> 1.0, >= 1.0.2)
-      i18n (>= 0.7, < 2)
-      minitest (~> 5.1)
-      tzinfo (~> 1.1)
-      zeitwerk (~> 2.2, >= 2.2.2)
+      i18n (>= 1.6, < 2)
+      minitest (>= 5.1)
+      tzinfo (~> 2.0)
+      zeitwerk (~> 2.3)
     acts_as_api (1.0.1)
       activemodel (>= 3.0.0)
       activesupport (>= 3.0.0)
@@ -116,8 +120,8 @@ GEM
       multipart-post (~> 2)
     faraday-net_http (3.0.2)
     ffi (1.15.5)
-    globalid (1.1.0)
-      activesupport (>= 5.0)
+    globalid (1.2.1)
+      activesupport (>= 6.1)
     googleauth (1.7.0)
       faraday (>= 0.17.3, < 3.a)
       jwt (>= 1.4, < 3.0)
@@ -189,20 +193,20 @@ GEM
     rack (2.2.8)
     rack-test (2.1.0)
       rack (>= 1.3)
-    rails (6.0.6.1)
-      actioncable (= 6.0.6.1)
-      actionmailbox (= 6.0.6.1)
-      actionmailer (= 6.0.6.1)
-      actionpack (= 6.0.6.1)
-      actiontext (= 6.0.6.1)
-      actionview (= 6.0.6.1)
-      activejob (= 6.0.6.1)
-      activemodel (= 6.0.6.1)
-      activerecord (= 6.0.6.1)
-      activestorage (= 6.0.6.1)
-      activesupport (= 6.0.6.1)
-      bundler (>= 1.3.0)
-      railties (= 6.0.6.1)
+    rails (6.1.7.6)
+      actioncable (= 6.1.7.6)
+      actionmailbox (= 6.1.7.6)
+      actionmailer (= 6.1.7.6)
+      actionpack (= 6.1.7.6)
+      actiontext (= 6.1.7.6)
+      actionview (= 6.1.7.6)
+      activejob (= 6.1.7.6)
+      activemodel (= 6.1.7.6)
+      activerecord (= 6.1.7.6)
+      activestorage (= 6.1.7.6)
+      activesupport (= 6.1.7.6)
+      bundler (>= 1.15.0)
+      railties (= 6.1.7.6)
       sprockets-rails (>= 2.0.0)
     rails-controller-testing (1.0.5)
       actionpack (>= 5.0.1.rc1)
@@ -218,12 +222,12 @@ GEM
     rails-observers (0.1.5)
       activemodel (>= 4.0)
     rails-perftest (0.0.7)
-    railties (6.0.6.1)
-      actionpack (= 6.0.6.1)
-      activesupport (= 6.0.6.1)
+    railties (6.1.7.6)
+      actionpack (= 6.1.7.6)
+      activesupport (= 6.1.7.6)
       method_source
-      rake (>= 0.8.7)
-      thor (>= 0.20.3, < 2.0)
+      rake (>= 12.2)
+      thor (~> 1.0)
     rake (13.0.6)
     rb-fsevent (0.11.2)
     rb-inotify (0.10.1)
@@ -257,10 +261,9 @@ GEM
     test-unit (3.6.1)
       power_assert
     thor (1.2.2)
-    thread_safe (0.3.6)
     timeout (0.4.0)
-    tzinfo (1.2.11)
-      thread_safe (~> 0.1)
+    tzinfo (2.0.6)
+      concurrent-ruby (~> 1.0)
     websocket-driver (0.7.6)
       websocket-extensions (>= 0.1.0)
     websocket-extensions (0.1.5)
@@ -290,7 +293,7 @@ DEPENDENCIES
   optimist
   passenger
   pg (~> 1.0)
-  rails (~> 6.0.0)
+  rails (~> 6.1.0)
   rails-controller-testing
   rails-observers
   rails-perftest

commit f05038cfab2cc9bea0c34088e478177fb9b0a439
Author: Tom Clegg <tom at curii.com>
Date:   Mon Sep 18 23:55:46 2023 -0400

    20300: Update test to new Rails API.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/test/functional/arvados/v1/management_controller_test.rb b/services/api/test/functional/arvados/v1/management_controller_test.rb
index ab57c995df..d8d2d52c89 100644
--- a/services/api/test/functional/arvados/v1/management_controller_test.rb
+++ b/services/api/test/functional/arvados/v1/management_controller_test.rb
@@ -39,7 +39,8 @@ class Arvados::V1::ManagementControllerTest < ActionController::TestCase
     @request.headers['Authorization'] = "Bearer configuredmanagementtoken"
     get :metrics
     assert_response :success
-    assert_equal 'text/plain; charset=utf-8', @response.content_type
+    assert_equal 'text/plain', @response.media_type
+    assert_equal 'utf-8', @response.charset
 
     assert_match /\narvados_config_source_timestamp_seconds{sha256="#{hash}"} #{Regexp.escape mtime.utc.to_f.to_s}\n/, @response.body
 

commit 4a1735427d84743f6c2f3576263fdcae397cf9e9
Author: Tom Clegg <tom at curii.com>
Date:   Mon Sep 18 16:06:29 2023 -0400

    20300: Mark belongs_to relations as optional.
    
    Temporary migration switch is no longer effective.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/app/models/api_client_authorization.rb b/services/api/app/models/api_client_authorization.rb
index a6ce6aa54a..756d835eff 100644
--- a/services/api/app/models/api_client_authorization.rb
+++ b/services/api/app/models/api_client_authorization.rb
@@ -10,8 +10,8 @@ class ApiClientAuthorization < ArvadosModel
   extend CurrentApiClient
   extend DbCurrentTime
 
-  belongs_to :api_client
-  belongs_to :user
+  belongs_to :api_client, optional: true
+  belongs_to :user, optional: true
   after_initialize :assign_random_api_token
   serialize :scopes, Array
 
diff --git a/services/api/app/models/authorized_key.rb b/services/api/app/models/authorized_key.rb
index ce348e0f8a..0da9f7f8c5 100644
--- a/services/api/app/models/authorized_key.rb
+++ b/services/api/app/models/authorized_key.rb
@@ -9,7 +9,12 @@ class AuthorizedKey < ArvadosModel
   before_create :permission_to_set_authorized_user_uuid
   before_update :permission_to_set_authorized_user_uuid
 
-  belongs_to :authorized_user, :foreign_key => :authorized_user_uuid, :class_name => 'User', :primary_key => :uuid
+  belongs_to :authorized_user, {
+               foreign_key: :authorized_user_uuid,
+               class_name: 'User',
+               primary_key: :uuid,
+               optional: true,
+             }
 
   validate :public_key_must_be_unique
 
diff --git a/services/api/app/models/container.rb b/services/api/app/models/container.rb
index 44334ba12f..9104a21107 100644
--- a/services/api/app/models/container.rb
+++ b/services/api/app/models/container.rb
@@ -52,7 +52,12 @@ class Container < ArvadosModel
   after_save :handle_completed
 
   has_many :container_requests, :foreign_key => :container_uuid, :class_name => 'ContainerRequest', :primary_key => :uuid
-  belongs_to :auth, :class_name => 'ApiClientAuthorization', :foreign_key => :auth_uuid, :primary_key => :uuid
+  belongs_to :auth, {
+               class_name: 'ApiClientAuthorization',
+               foreign_key: :auth_uuid,
+               primary_key: :uuid,
+               optional: true,
+             }
 
   api_accessible :user, extend: :common do |t|
     t.add :command
diff --git a/services/api/app/models/container_request.rb b/services/api/app/models/container_request.rb
index 9895ceb14e..b499e37e7d 100644
--- a/services/api/app/models/container_request.rb
+++ b/services/api/app/models/container_request.rb
@@ -12,11 +12,16 @@ class ContainerRequest < ArvadosModel
   include CommonApiTemplate
   include WhitelistUpdate
 
-  belongs_to :container, foreign_key: :container_uuid, primary_key: :uuid
+  belongs_to :container, {
+               foreign_key: :container_uuid,
+               primary_key: :uuid,
+               optional: true,
+             }
   belongs_to :requesting_container, {
                class_name: 'Container',
                foreign_key: :requesting_container_uuid,
                primary_key: :uuid,
+               optional: true,
              }
 
   # Posgresql JSONB columns should NOT be declared as serialized, Rails 5
diff --git a/services/api/app/models/node.rb b/services/api/app/models/node.rb
index adc2512475..2770dd5ccc 100644
--- a/services/api/app/models/node.rb
+++ b/services/api/app/models/node.rb
@@ -20,7 +20,11 @@ class Node < ArvadosModel
   # Only a controller can figure out whether or not the current API tokens
   # have access to the associated Job.  They're expected to set
   # job_readable=true if the Job UUID can be included in the API response.
-  belongs_to(:job, foreign_key: :job_uuid, primary_key: :uuid)
+  belongs_to :job, {
+               foreign_key: :job_uuid,
+               primary_key: :uuid,
+               optional: true,
+             }
   attr_accessor :job_readable
 
   UNUSED_NODE_IP = '127.40.4.0'
diff --git a/services/api/app/models/pipeline_instance.rb b/services/api/app/models/pipeline_instance.rb
index 271b155aaf..d5ed5e533d 100644
--- a/services/api/app/models/pipeline_instance.rb
+++ b/services/api/app/models/pipeline_instance.rb
@@ -9,7 +9,11 @@ class PipelineInstance < ArvadosModel
   serialize :components, Hash
   serialize :properties, Hash
   serialize :components_summary, Hash
-  belongs_to :pipeline_template, :foreign_key => :pipeline_template_uuid, :primary_key => :uuid
+  belongs_to :pipeline_template, {
+               foreign_key: :pipeline_template_uuid,
+               primary_key: :uuid,
+               optional: true,
+             }
 
   before_validation :bootstrap_components
   before_validation :update_state
diff --git a/services/api/config/initializers/new_framework_defaults.rb b/services/api/config/initializers/new_framework_defaults.rb
index 2e2f0b1810..161dbe10e6 100644
--- a/services/api/config/initializers/new_framework_defaults.rb
+++ b/services/api/config/initializers/new_framework_defaults.rb
@@ -21,6 +21,3 @@ Rails.application.config.action_controller.forgery_protection_origin_check = fal
 # Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`.
 # Previous versions had false.
 ActiveSupport.to_time_preserves_timezone = false
-
-# Require `belongs_to` associations by default. Previous versions had false.
-Rails.application.config.active_record.belongs_to_required_by_default = false

commit a04b65f896ea8e070bf087dc15ef552e16b48106
Author: Tom Clegg <tom at curii.com>
Date:   Mon Sep 25 14:36:56 2023 -0400

    20300: Rails 6.0 app:update
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/bin/rails b/services/api/bin/rails
index 5f594d1186..0739660237 100755
--- a/services/api/bin/rails
+++ b/services/api/bin/rails
@@ -1,9 +1,4 @@
 #!/usr/bin/env ruby
-
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
 APP_PATH = File.expand_path('../config/application', __dir__)
 require_relative '../config/boot'
 require 'rails/commands'
diff --git a/services/api/bin/rake b/services/api/bin/rake
index 87484df469..17240489f6 100755
--- a/services/api/bin/rake
+++ b/services/api/bin/rake
@@ -1,9 +1,4 @@
 #!/usr/bin/env ruby
-
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
 require_relative '../config/boot'
 require 'rake'
 Rake.application.run
diff --git a/services/api/bin/setup b/services/api/bin/setup
index c9142b942e..0e39e8cb13 100755
--- a/services/api/bin/setup
+++ b/services/api/bin/setup
@@ -1,11 +1,5 @@
 #!/usr/bin/env ruby
-
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
 require 'fileutils'
-include FileUtils
 
 # path to your application root.
 APP_ROOT = File.expand_path('..', __dir__)
@@ -14,8 +8,9 @@ def system!(*args)
   system(*args) || abort("\n== Command #{args} failed ==")
 end
 
-chdir APP_ROOT do
-  # This script is a starting point to setup your application.
+FileUtils.chdir APP_ROOT do
+  # This script is a way to setup or update your development environment automatically.
+  # This script is idempotent, so that you can run it at anytime and get an expectable outcome.
   # Add necessary setup steps to this file.
 
   puts '== Installing dependencies =='
@@ -24,11 +19,11 @@ chdir APP_ROOT do
 
   # puts "\n== Copying sample files =="
   # unless File.exist?('config/database.yml')
-  #   cp 'config/database.yml.sample', 'config/database.yml'
+  #   FileUtils.cp 'config/database.yml.sample', 'config/database.yml'
   # end
 
   puts "\n== Preparing database =="
-  system! 'bin/rails db:setup'
+  system! 'bin/rails db:prepare'
 
   puts "\n== Removing old logs and tempfiles =="
   system! 'bin/rails log:clear tmp:clear'
diff --git a/services/api/config/application.rb b/services/api/config/application.rb
index dcd77d7b6b..587a09d4e0 100644
--- a/services/api/config/application.rb
+++ b/services/api/config/application.rb
@@ -5,41 +5,23 @@
 require_relative 'boot'
 
 require "rails"
-# Pick only the frameworks we need:
+# Pick the frameworks you want:
 require "active_model/railtie"
 require "active_job/railtie"
 require "active_record/railtie"
+# require "active_storage/engine"
 require "action_controller/railtie"
 require "action_mailer/railtie"
+# require "action_mailbox/engine"
+# require "action_text/engine"
 require "action_view/railtie"
+# require "action_cable/engine"
 require "sprockets/railtie"
 require "rails/test_unit/railtie"
-# Skipping the following:
-# * ActionCable (new in Rails 5.0) as it adds '/cable' routes that we're not using
-# * ActiveStorage (new in Rails 5.1)
 
-require 'digest'
-
-module Kernel
-  def suppress_warnings
-    verbose_orig = $VERBOSE
-    begin
-      $VERBOSE = nil
-      yield
-    ensure
-      $VERBOSE = verbose_orig
-    end
-  end
-end
-
-if defined?(Bundler)
-  suppress_warnings do
-    # If you precompile assets before deploying to production, use this line
-    Bundler.require(*Rails.groups(:assets => %w(development test)))
-    # If you want your assets lazily compiled in production, use this line
-    # Bundler.require(:default, :assets, Rails.env)
-  end
-end
+# Require the gems listed in Gemfile, including any gems
+# you've limited to :test, :development, or :production.
+Bundler.require(*Rails.groups)
 
 if ENV["ARVADOS_RAILS_LOG_TO_STDOUT"]
   Rails.logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
@@ -50,42 +32,30 @@ module Server
 
     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.
-
-    # Custom directories with classes and modules you want to be autoloadable.
-    # config.autoload_paths += %W(#{config.root}/extras)
+    # Initialize configuration defaults for originally generated Rails version.
+    config.load_defaults 5.0
 
-    # Only load the plugins named here, in the order given (default is alphabetical).
-    # :all can be used as a placeholder for all plugins not explicitly named.
-    # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
+    # Settings in config/environments/* take precedence over those specified here.
+    # Application configuration can go into files in config/initializers
+    # -- all .rb files in that directory are automatically loaded after loading
+    # the framework and any gems in your application.
 
-    # Activate observers that should always be running.
-    # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
+    # We use db/structure.sql instead of db/schema.rb.
     config.active_record.schema_format = :sql
 
-    # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
-    # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
-    # config.i18n.default_locale = :de
-
-    # Configure sensitive parameters which will be filtered from the log file.
-    config.filter_parameters += [:password]
-
-    # Load entire application at startup.
     config.eager_load = true
 
     config.active_support.test_order = :sorted
 
+    # container_request records can contain arbitrary data structures
+    # in mounts.*.content, so rails must not munge them.
     config.action_dispatch.perform_deep_munge = false
-
+    
     # force_ssl's redirect-to-https feature doesn't work when the
     # client supplies a port number, and prevents arvados-controller
     # from connecting to Rails internally via plain http.
     config.ssl_options = {redirect: false}
 
-    I18n.enforce_available_locales = false
-
     # Before using the filesystem backend for Rails.cache, check
     # whether we own the relevant directory. If we don't, using it is
     # likely to either fail or (if we're root) pollute it and cause
diff --git a/services/api/config/boot.rb b/services/api/config/boot.rb
index 8087911837..30f5120df6 100644
--- a/services/api/config/boot.rb
+++ b/services/api/config/boot.rb
@@ -1,8 +1,3 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
-# Set up gems listed in the Gemfile.
 ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
 
 require 'bundler/setup' # Set up gems listed in the Gemfile.
diff --git a/services/api/config/environment.rb b/services/api/config/environment.rb
index cd706940a3..426333bb46 100644
--- a/services/api/config/environment.rb
+++ b/services/api/config/environment.rb
@@ -1,9 +1,5 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
-# Load the rails application
+# Load the Rails application.
 require_relative 'application'
 
-# Initialize the rails application
+# Initialize the Rails application.
 Rails.application.initialize!
diff --git a/services/api/config/initializers/application_controller_renderer.rb b/services/api/config/initializers/application_controller_renderer.rb
index 525d6adf95..89d2efab2b 100644
--- a/services/api/config/initializers/application_controller_renderer.rb
+++ b/services/api/config/initializers/application_controller_renderer.rb
@@ -1,7 +1,3 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
 # Be sure to restart your server when you modify this file.
 
 # ActiveSupport::Reloader.to_prepare do
diff --git a/services/api/config/initializers/assets.rb b/services/api/config/initializers/assets.rb
index f02c87b731..fe48fc34ee 100644
--- a/services/api/config/initializers/assets.rb
+++ b/services/api/config/initializers/assets.rb
@@ -1,15 +1,12 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
 # Be sure to restart your server when you modify this file.
 
 # Version of your assets, change this if you want to expire all your assets.
 Rails.application.config.assets.version = '1.0'
 
-# Add additional assets to the asset load path
+# Add additional assets to the asset load path.
 # Rails.application.config.assets.paths << Emoji.images_path
 
 # Precompile additional assets.
-# application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
-# Rails.application.config.assets.precompile += %w( search.js )
+# application.js, application.css, and all non-JS/CSS in the app/assets
+# folder are already added.
+# Rails.application.config.assets.precompile += %w( admin.js admin.css )
diff --git a/services/api/config/initializers/backtrace_silencers.rb b/services/api/config/initializers/backtrace_silencers.rb
index b9c6bceef5..59385cdf37 100644
--- a/services/api/config/initializers/backtrace_silencers.rb
+++ b/services/api/config/initializers/backtrace_silencers.rb
@@ -1,7 +1,3 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
 # Be sure to restart your server when you modify this file.
 
 # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
diff --git a/services/api/config/initializers/content_security_policy.rb b/services/api/config/initializers/content_security_policy.rb
index 853ecdeec4..41c43016f1 100644
--- a/services/api/config/initializers/content_security_policy.rb
+++ b/services/api/config/initializers/content_security_policy.rb
@@ -1,7 +1,3 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
 # Be sure to restart your server when you modify this file.
 
 # Define an application-wide content security policy
@@ -23,6 +19,9 @@
 # If you are using UJS then enable automatic nonce generation
 # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
 
+# Set the nonce only to specific directives
+# Rails.application.config.content_security_policy_nonce_directives = %w(script-src)
+
 # Report CSP violations to a specified URI
 # For further information see the following documentation:
 # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
diff --git a/services/api/config/initializers/cookies_serializer.rb b/services/api/config/initializers/cookies_serializer.rb
index 5409f55c0b..5a6a32d371 100644
--- a/services/api/config/initializers/cookies_serializer.rb
+++ b/services/api/config/initializers/cookies_serializer.rb
@@ -1,9 +1,5 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
 # Be sure to restart your server when you modify this file.
 
 # Specify a serializer for the signed and encrypted cookie jars.
 # Valid options are :json, :marshal, and :hybrid.
-Rails.application.config.action_dispatch.cookies_serializer = :marshal
+Rails.application.config.action_dispatch.cookies_serializer = :json
diff --git a/services/api/config/initializers/filter_parameter_logging.rb b/services/api/config/initializers/filter_parameter_logging.rb
index f26d0ad223..4a994e1e7b 100644
--- a/services/api/config/initializers/filter_parameter_logging.rb
+++ b/services/api/config/initializers/filter_parameter_logging.rb
@@ -1,7 +1,3 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
 # Be sure to restart your server when you modify this file.
 
 # Configure sensitive parameters which will be filtered from the log file.
diff --git a/services/api/config/initializers/inflections.rb b/services/api/config/initializers/inflections.rb
index 50bd0d5f55..8fee195a91 100644
--- a/services/api/config/initializers/inflections.rb
+++ b/services/api/config/initializers/inflections.rb
@@ -4,15 +4,21 @@
 
 # Be sure to restart your server when you modify this file.
 
-# Add new inflection rules using the following format
-# (all these examples are active by default):
-# ActiveSupport::Inflector.inflections do |inflect|
+# Add new inflection rules using the following format. Inflections
+# are locale specific, and you may define rules for as many different
+# locales as you wish. All of these examples are active by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
 #   inflect.plural /^(ox)$/i, '\1en'
 #   inflect.singular /^(ox)en/i, '\1'
 #   inflect.irregular 'person', 'people'
 #   inflect.uncountable %w( fish sheep )
 # end
 
+# These inflection rules are supported but not enabled by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+#   inflect.acronym 'RESTful'
+# end
+
 ActiveSupport::Inflector.inflections do |inflect|
   inflect.plural(/^([Ss]pecimen)$/i, '\1s')
   inflect.singular(/^([Ss]pecimen)s?/i, '\1')
diff --git a/services/api/config/initializers/mime_types.rb b/services/api/config/initializers/mime_types.rb
index 36683cc246..dc1899682b 100644
--- a/services/api/config/initializers/mime_types.rb
+++ b/services/api/config/initializers/mime_types.rb
@@ -1,9 +1,4 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
 # Be sure to restart your server when you modify this file.
 
 # Add new mime types for use in respond_to blocks:
 # Mime::Type.register "text/richtext", :rtf
-# Mime::Type.register_alias "text/html", :iphone
diff --git a/services/api/config/initializers/new_framework_defaults_6_0.rb b/services/api/config/initializers/new_framework_defaults_6_0.rb
new file mode 100644
index 0000000000..92240ef5f5
--- /dev/null
+++ b/services/api/config/initializers/new_framework_defaults_6_0.rb
@@ -0,0 +1,45 @@
+# Be sure to restart your server when you modify this file.
+#
+# This file contains migration options to ease your Rails 6.0 upgrade.
+#
+# Once upgraded flip defaults one by one to migrate to the new default.
+#
+# Read the Guide for Upgrading Ruby on Rails for more info on each option.
+
+# Don't force requests from old versions of IE to be UTF-8 encoded.
+# Rails.application.config.action_view.default_enforce_utf8 = false
+
+# Embed purpose and expiry metadata inside signed and encrypted
+# cookies for increased security.
+#
+# This option is not backwards compatible with earlier Rails versions.
+# It's best enabled when your entire app is migrated and stable on 6.0.
+# Rails.application.config.action_dispatch.use_cookies_with_metadata = true
+
+# Change the return value of `ActionDispatch::Response#content_type` to Content-Type header without modification.
+# Rails.application.config.action_dispatch.return_only_media_type_on_content_type = false
+
+# Return false instead of self when enqueuing is aborted from a callback.
+# Rails.application.config.active_job.return_false_on_aborted_enqueue = true
+
+# Send Active Storage analysis and purge jobs to dedicated queues.
+# Rails.application.config.active_storage.queues.analysis = :active_storage_analysis
+# Rails.application.config.active_storage.queues.purge    = :active_storage_purge
+
+# When assigning to a collection of attachments declared via `has_many_attached`, replace existing
+# attachments instead of appending. Use #attach to add new attachments without replacing existing ones.
+# Rails.application.config.active_storage.replace_on_assign_to_many = true
+
+# Use ActionMailer::MailDeliveryJob for sending parameterized and normal mail.
+#
+# The default delivery jobs (ActionMailer::Parameterized::DeliveryJob, ActionMailer::DeliveryJob),
+# will be removed in Rails 6.1. This setting is not backwards compatible with earlier Rails versions.
+# If you send mail in the background, job workers need to have a copy of
+# MailDeliveryJob to ensure all delivery jobs are processed properly.
+# Make sure your entire app is migrated and stable on 6.0 before using this setting.
+# Rails.application.config.action_mailer.delivery_job = "ActionMailer::MailDeliveryJob"
+
+# Enable the same cache key to be reused when the object being cached of type
+# `ActiveRecord::Relation` changes by moving the volatile information (max updated at and count)
+# of the relation's cache key into the cache version to support recycling cache key.
+# Rails.application.config.active_record.collection_cache_versioning = true
diff --git a/services/api/config/initializers/wrap_parameters.rb b/services/api/config/initializers/wrap_parameters.rb
index 6fb9786504..bbfc3961bf 100644
--- a/services/api/config/initializers/wrap_parameters.rb
+++ b/services/api/config/initializers/wrap_parameters.rb
@@ -1,9 +1,5 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
 # Be sure to restart your server when you modify this file.
-#
+
 # This file contains settings for ActionController::ParamsWrapper which
 # is enabled by default.
 
@@ -12,7 +8,7 @@ ActiveSupport.on_load(:action_controller) do
   wrap_parameters format: [:json]
 end
 
-# Disable root element in JSON by default.
-ActiveSupport.on_load(:active_record) do
-  self.include_root_in_json = false
-end
+# To enable root element in JSON for ActiveRecord objects.
+# ActiveSupport.on_load(:active_record) do
+#   self.include_root_in_json = true
+# end
diff --git a/services/api/config/locales/en.yml b/services/api/config/locales/en.yml
index e6a62cb837..cf9b342d0a 100644
--- a/services/api/config/locales/en.yml
+++ b/services/api/config/locales/en.yml
@@ -1,9 +1,33 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
+# Files in the config/locales directory are used for internationalization
+# and are automatically loaded by Rails. If you want to use locales other
+# than English, add the necessary files in this directory.
 #
-# SPDX-License-Identifier: AGPL-3.0
-
-# Sample localization file for English. Add more files in this directory for other locales.
-# See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
+# To use the locales, use `I18n.t`:
+#
+#     I18n.t 'hello'
+#
+# In views, this is aliased to just `t`:
+#
+#     <%= t('hello') %>
+#
+# To use a different locale, set it with `I18n.locale`:
+#
+#     I18n.locale = :es
+#
+# This would use the information in config/locales/es.yml.
+#
+# The following keys must be escaped otherwise they will not be retrieved by
+# the default I18n backend:
+#
+# true, false, on, off, yes, no
+#
+# Instead, surround them with single quotes.
+#
+# en:
+#   'true': 'foo'
+#
+# To learn more, please read the Rails Internationalization guide
+# available at https://guides.rubyonrails.org/i18n.html.
 
 en:
   hello: "Hello world"

commit cdc9e2ceeb7518f2f8bec47531151a521830fb2b
Author: Tom Clegg <tom at curii.com>
Date:   Thu Sep 14 09:37:26 2023 -0400

    20300: Add rails-generated files to .licenseignore.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/.licenseignore b/.licenseignore
index af273b837b..7d595a3349 100644
--- a/.licenseignore
+++ b/.licenseignore
@@ -98,3 +98,23 @@ SECURITY.md
 lib/crunchstat/testdata/*
 lib/controller/localdb/testdata/*.pub
 sdk/ruby-google-api-client/*
+services/api/bin/rails
+services/api/bin/rake
+services/api/bin/setup
+services/api/bin/yarn
+services/api/storage.yml
+services/api/test.rb.example
+services/api/config/boot.rb
+services/api/config/environment.rb
+services/api/config/initializers/application_controller_renderer.rb
+services/api/config/initializers/assets.rb
+services/api/config/initializers/backtrace_silencers.rb
+services/api/config/initializers/content_security_policy.rb
+services/api/config/initializers/cookies_serializer.rb
+services/api/config/initializers/filter_parameter_logging.rb
+services/api/config/initializers/mime_types.rb
+services/api/config/initializers/new_framework_defaults_*.rb
+services/api/config/initializers/permissions_policy.rb
+services/api/config/initializers/wrap_parameters.rb
+services/api/config/locales/en.yml
+services/api/config.ru

commit 5199588a10410b200aa2c01a75fce7963296fd09
Author: Tom Clegg <tom at curii.com>
Date:   Thu Sep 7 14:01:24 2023 -0400

    20300: Fix key order sensitivity in test, part 2.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/sdk/python/arvados-v1-discovery.json b/sdk/python/arvados-v1-discovery.json
index 5d4666bf94..935e953a3f 100644
--- a/sdk/python/arvados-v1-discovery.json
+++ b/sdk/python/arvados-v1-discovery.json
@@ -50,17 +50,17 @@
   },
   "protocol": "rest",
   "resources": {
-    "jobs": {
+    "api_clients": {
       "methods": {
         "get": {
-          "id": "arvados.jobs.get",
-          "path": "jobs/{uuid}",
+          "id": "arvados.api_clients.get",
+          "path": "api_clients/{uuid}",
           "httpMethod": "GET",
-          "description": "Gets a Job's metadata by UUID.",
+          "description": "Gets a ApiClient's metadata by UUID.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Job in question.",
+              "description": "The UUID of the ApiClient in question.",
               "required": true,
               "location": "path"
             }
@@ -69,7 +69,7 @@
             "uuid"
           ],
           "response": {
-            "$ref": "Job"
+            "$ref": "ApiClient"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -77,10 +77,10 @@
           ]
         },
         "index": {
-          "id": "arvados.jobs.list",
-          "path": "jobs",
+          "id": "arvados.api_clients.list",
+          "path": "api_clients",
           "httpMethod": "GET",
-          "description": "List Jobs.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Jobs. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#jobList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List ApiClients.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching ApiClients. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#apiClientList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -148,7 +148,7 @@
             }
           },
           "response": {
-            "$ref": "JobList"
+            "$ref": "ApiClientList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -156,10 +156,10 @@
           ]
         },
         "create": {
-          "id": "arvados.jobs.create",
-          "path": "jobs",
+          "id": "arvados.api_clients.create",
+          "path": "api_clients",
           "httpMethod": "POST",
-          "description": "Create a new Job.",
+          "description": "Create a new ApiClient.",
           "parameters": {
             "select": {
               "type": "array",
@@ -179,57 +179,32 @@
               "description": "Create object on a remote federated cluster instead of the current one.",
               "location": "query",
               "required": false
-            },
-            "find_or_create": {
-              "type": "boolean",
-              "required": false,
-              "default": "false",
-              "description": "",
-              "location": "query"
-            },
-            "filters": {
-              "type": "array",
-              "required": false,
-              "description": "",
-              "location": "query"
-            },
-            "minimum_script_version": {
-              "type": "string",
-              "required": false,
-              "description": "",
-              "location": "query"
-            },
-            "exclude_script_versions": {
-              "type": "array",
-              "required": false,
-              "description": "",
-              "location": "query"
             }
           },
           "request": {
             "required": true,
             "properties": {
-              "job": {
-                "$ref": "Job"
+              "api_client": {
+                "$ref": "ApiClient"
               }
             }
           },
           "response": {
-            "$ref": "Job"
+            "$ref": "ApiClient"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "update": {
-          "id": "arvados.jobs.update",
-          "path": "jobs/{uuid}",
+          "id": "arvados.api_clients.update",
+          "path": "api_clients/{uuid}",
           "httpMethod": "PUT",
-          "description": "Update attributes of an existing Job.",
+          "description": "Update attributes of an existing ApiClient.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Job in question.",
+              "description": "The UUID of the ApiClient in question.",
               "required": true,
               "location": "path"
             },
@@ -243,174 +218,43 @@
           "request": {
             "required": true,
             "properties": {
-              "job": {
-                "$ref": "Job"
+              "api_client": {
+                "$ref": "ApiClient"
               }
             }
           },
           "response": {
-            "$ref": "Job"
+            "$ref": "ApiClient"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "delete": {
-          "id": "arvados.jobs.delete",
-          "path": "jobs/{uuid}",
+          "id": "arvados.api_clients.delete",
+          "path": "api_clients/{uuid}",
           "httpMethod": "DELETE",
-          "description": "Delete an existing Job.",
-          "parameters": {
-            "uuid": {
-              "type": "string",
-              "description": "The UUID of the Job in question.",
-              "required": true,
-              "location": "path"
-            }
-          },
-          "response": {
-            "$ref": "Job"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados"
-          ]
-        },
-        "queue": {
-          "id": "arvados.jobs.queue",
-          "path": "jobs/queue",
-          "httpMethod": "GET",
-          "description": "queue jobs",
-          "parameters": {
-            "filters": {
-              "type": "array",
-              "required": false,
-              "description": "",
-              "location": "query"
-            },
-            "where": {
-              "type": "object",
-              "required": false,
-              "description": "",
-              "location": "query"
-            },
-            "order": {
-              "type": "array",
-              "required": false,
-              "description": "",
-              "location": "query"
-            },
-            "select": {
-              "type": "array",
-              "description": "Attributes of each object to return in the response.",
-              "required": false,
-              "location": "query"
-            },
-            "distinct": {
-              "type": "boolean",
-              "required": false,
-              "default": "false",
-              "description": "",
-              "location": "query"
-            },
-            "limit": {
-              "type": "integer",
-              "required": false,
-              "default": "100",
-              "description": "",
-              "location": "query"
-            },
-            "offset": {
-              "type": "integer",
-              "required": false,
-              "default": "0",
-              "description": "",
-              "location": "query"
-            },
-            "count": {
-              "type": "string",
-              "required": false,
-              "default": "exact",
-              "description": "",
-              "location": "query"
-            },
-            "cluster_id": {
-              "type": "string",
-              "description": "List objects on a remote federated cluster instead of the current one.",
-              "location": "query",
-              "required": false
-            },
-            "bypass_federation": {
-              "type": "boolean",
-              "required": false,
-              "description": "bypass federation behavior, list items from local instance database only",
-              "location": "query"
-            }
-          },
-          "response": {
-            "$ref": "Job"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados"
-          ]
-        },
-        "queue_size": {
-          "id": "arvados.jobs.queue_size",
-          "path": "jobs/queue_size",
-          "httpMethod": "GET",
-          "description": "queue_size jobs",
-          "parameters": {},
-          "response": {
-            "$ref": "Job"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados"
-          ]
-        },
-        "cancel": {
-          "id": "arvados.jobs.cancel",
-          "path": "jobs/{uuid}/cancel",
-          "httpMethod": "POST",
-          "description": "cancel jobs",
-          "parameters": {
-            "uuid": {
-              "type": "string",
-              "description": "",
-              "required": true,
-              "location": "path"
-            }
-          },
-          "response": {
-            "$ref": "Job"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados"
-          ]
-        },
-        "lock": {
-          "id": "arvados.jobs.lock",
-          "path": "jobs/{uuid}/lock",
-          "httpMethod": "POST",
-          "description": "lock jobs",
+          "description": "Delete an existing ApiClient.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "",
+              "description": "The UUID of the ApiClient in question.",
               "required": true,
               "location": "path"
             }
           },
           "response": {
-            "$ref": "Job"
+            "$ref": "ApiClient"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "list": {
-          "id": "arvados.jobs.list",
-          "path": "jobs",
+          "id": "arvados.api_clients.list",
+          "path": "api_clients",
           "httpMethod": "GET",
-          "description": "List Jobs.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Jobs. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#jobList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List ApiClients.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching ApiClients. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#apiClientList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -478,7 +322,7 @@
             }
           },
           "response": {
-            "$ref": "JobList"
+            "$ref": "ApiClientList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -486,10 +330,10 @@
           ]
         },
         "show": {
-          "id": "arvados.jobs.show",
-          "path": "jobs/{uuid}",
+          "id": "arvados.api_clients.show",
+          "path": "api_clients/{uuid}",
           "httpMethod": "GET",
-          "description": "show jobs",
+          "description": "show api_clients",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -505,17 +349,17 @@
             }
           },
           "response": {
-            "$ref": "Job"
+            "$ref": "ApiClient"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "destroy": {
-          "id": "arvados.jobs.destroy",
-          "path": "jobs/{uuid}",
+          "id": "arvados.api_clients.destroy",
+          "path": "api_clients/{uuid}",
           "httpMethod": "DELETE",
-          "description": "destroy jobs",
+          "description": "destroy api_clients",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -525,7 +369,7 @@
             }
           },
           "response": {
-            "$ref": "Job"
+            "$ref": "ApiClient"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
@@ -533,17 +377,17 @@
         }
       }
     },
-    "api_clients": {
+    "api_client_authorizations": {
       "methods": {
         "get": {
-          "id": "arvados.api_clients.get",
-          "path": "api_clients/{uuid}",
+          "id": "arvados.api_client_authorizations.get",
+          "path": "api_client_authorizations/{uuid}",
           "httpMethod": "GET",
-          "description": "Gets a ApiClient's metadata by UUID.",
+          "description": "Gets a ApiClientAuthorization's metadata by UUID.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the ApiClient in question.",
+              "description": "The UUID of the ApiClientAuthorization in question.",
               "required": true,
               "location": "path"
             }
@@ -552,7 +396,7 @@
             "uuid"
           ],
           "response": {
-            "$ref": "ApiClient"
+            "$ref": "ApiClientAuthorization"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -560,10 +404,10 @@
           ]
         },
         "index": {
-          "id": "arvados.api_clients.list",
-          "path": "api_clients",
+          "id": "arvados.api_client_authorizations.list",
+          "path": "api_client_authorizations",
           "httpMethod": "GET",
-          "description": "List ApiClients.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching ApiClients. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#apiClientList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List ApiClientAuthorizations.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching ApiClientAuthorizations. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#apiClientAuthorizationList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -631,7 +475,7 @@
             }
           },
           "response": {
-            "$ref": "ApiClientList"
+            "$ref": "ApiClientAuthorizationList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -639,10 +483,10 @@
           ]
         },
         "create": {
-          "id": "arvados.api_clients.create",
-          "path": "api_clients",
+          "id": "arvados.api_client_authorizations.create",
+          "path": "api_client_authorizations",
           "httpMethod": "POST",
-          "description": "Create a new ApiClient.",
+          "description": "Create a new ApiClientAuthorization.",
           "parameters": {
             "select": {
               "type": "array",
@@ -667,27 +511,27 @@
           "request": {
             "required": true,
             "properties": {
-              "api_client": {
-                "$ref": "ApiClient"
+              "api_client_authorization": {
+                "$ref": "ApiClientAuthorization"
               }
             }
           },
           "response": {
-            "$ref": "ApiClient"
+            "$ref": "ApiClientAuthorization"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "update": {
-          "id": "arvados.api_clients.update",
-          "path": "api_clients/{uuid}",
+          "id": "arvados.api_client_authorizations.update",
+          "path": "api_client_authorizations/{uuid}",
           "httpMethod": "PUT",
-          "description": "Update attributes of an existing ApiClient.",
+          "description": "Update attributes of an existing ApiClientAuthorization.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the ApiClient in question.",
+              "description": "The UUID of the ApiClientAuthorization in question.",
               "required": true,
               "location": "path"
             },
@@ -701,43 +545,82 @@
           "request": {
             "required": true,
             "properties": {
-              "api_client": {
-                "$ref": "ApiClient"
+              "api_client_authorization": {
+                "$ref": "ApiClientAuthorization"
               }
             }
           },
           "response": {
-            "$ref": "ApiClient"
+            "$ref": "ApiClientAuthorization"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "delete": {
-          "id": "arvados.api_clients.delete",
-          "path": "api_clients/{uuid}",
+          "id": "arvados.api_client_authorizations.delete",
+          "path": "api_client_authorizations/{uuid}",
           "httpMethod": "DELETE",
-          "description": "Delete an existing ApiClient.",
+          "description": "Delete an existing ApiClientAuthorization.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the ApiClient in question.",
+              "description": "The UUID of the ApiClientAuthorization in question.",
               "required": true,
               "location": "path"
             }
           },
           "response": {
-            "$ref": "ApiClient"
+            "$ref": "ApiClientAuthorization"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados"
+          ]
+        },
+        "create_system_auth": {
+          "id": "arvados.api_client_authorizations.create_system_auth",
+          "path": "api_client_authorizations/create_system_auth",
+          "httpMethod": "POST",
+          "description": "create_system_auth api_client_authorizations",
+          "parameters": {
+            "api_client_id": {
+              "type": "integer",
+              "required": false,
+              "description": "",
+              "location": "query"
+            },
+            "scopes": {
+              "type": "array",
+              "required": false,
+              "description": "",
+              "location": "query"
+            }
+          },
+          "response": {
+            "$ref": "ApiClientAuthorization"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados"
+          ]
+        },
+        "current": {
+          "id": "arvados.api_client_authorizations.current",
+          "path": "api_client_authorizations/current",
+          "httpMethod": "GET",
+          "description": "current api_client_authorizations",
+          "parameters": {},
+          "response": {
+            "$ref": "ApiClientAuthorization"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "list": {
-          "id": "arvados.api_clients.list",
-          "path": "api_clients",
+          "id": "arvados.api_client_authorizations.list",
+          "path": "api_client_authorizations",
           "httpMethod": "GET",
-          "description": "List ApiClients.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching ApiClients. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#apiClientList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List ApiClientAuthorizations.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching ApiClientAuthorizations. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#apiClientAuthorizationList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -805,7 +688,7 @@
             }
           },
           "response": {
-            "$ref": "ApiClientList"
+            "$ref": "ApiClientAuthorizationList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -813,10 +696,10 @@
           ]
         },
         "show": {
-          "id": "arvados.api_clients.show",
-          "path": "api_clients/{uuid}",
+          "id": "arvados.api_client_authorizations.show",
+          "path": "api_client_authorizations/{uuid}",
           "httpMethod": "GET",
-          "description": "show api_clients",
+          "description": "show api_client_authorizations",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -832,17 +715,17 @@
             }
           },
           "response": {
-            "$ref": "ApiClient"
+            "$ref": "ApiClientAuthorization"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "destroy": {
-          "id": "arvados.api_clients.destroy",
-          "path": "api_clients/{uuid}",
+          "id": "arvados.api_client_authorizations.destroy",
+          "path": "api_client_authorizations/{uuid}",
           "httpMethod": "DELETE",
-          "description": "destroy api_clients",
+          "description": "destroy api_client_authorizations",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -852,7 +735,7 @@
             }
           },
           "response": {
-            "$ref": "ApiClient"
+            "$ref": "ApiClientAuthorization"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
@@ -860,17 +743,17 @@
         }
       }
     },
-    "api_client_authorizations": {
+    "authorized_keys": {
       "methods": {
         "get": {
-          "id": "arvados.api_client_authorizations.get",
-          "path": "api_client_authorizations/{uuid}",
+          "id": "arvados.authorized_keys.get",
+          "path": "authorized_keys/{uuid}",
           "httpMethod": "GET",
-          "description": "Gets a ApiClientAuthorization's metadata by UUID.",
+          "description": "Gets a AuthorizedKey's metadata by UUID.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the ApiClientAuthorization in question.",
+              "description": "The UUID of the AuthorizedKey in question.",
               "required": true,
               "location": "path"
             }
@@ -879,7 +762,7 @@
             "uuid"
           ],
           "response": {
-            "$ref": "ApiClientAuthorization"
+            "$ref": "AuthorizedKey"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -887,10 +770,10 @@
           ]
         },
         "index": {
-          "id": "arvados.api_client_authorizations.list",
-          "path": "api_client_authorizations",
+          "id": "arvados.authorized_keys.list",
+          "path": "authorized_keys",
           "httpMethod": "GET",
-          "description": "List ApiClientAuthorizations.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching ApiClientAuthorizations. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#apiClientAuthorizationList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List AuthorizedKeys.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching AuthorizedKeys. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#authorizedKeyList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -958,7 +841,7 @@
             }
           },
           "response": {
-            "$ref": "ApiClientAuthorizationList"
+            "$ref": "AuthorizedKeyList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -966,10 +849,10 @@
           ]
         },
         "create": {
-          "id": "arvados.api_client_authorizations.create",
-          "path": "api_client_authorizations",
+          "id": "arvados.authorized_keys.create",
+          "path": "authorized_keys",
           "httpMethod": "POST",
-          "description": "Create a new ApiClientAuthorization.",
+          "description": "Create a new AuthorizedKey.",
           "parameters": {
             "select": {
               "type": "array",
@@ -994,27 +877,27 @@
           "request": {
             "required": true,
             "properties": {
-              "api_client_authorization": {
-                "$ref": "ApiClientAuthorization"
+              "authorized_key": {
+                "$ref": "AuthorizedKey"
               }
             }
           },
           "response": {
-            "$ref": "ApiClientAuthorization"
+            "$ref": "AuthorizedKey"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "update": {
-          "id": "arvados.api_client_authorizations.update",
-          "path": "api_client_authorizations/{uuid}",
+          "id": "arvados.authorized_keys.update",
+          "path": "authorized_keys/{uuid}",
           "httpMethod": "PUT",
-          "description": "Update attributes of an existing ApiClientAuthorization.",
+          "description": "Update attributes of an existing AuthorizedKey.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the ApiClientAuthorization in question.",
+              "description": "The UUID of the AuthorizedKey in question.",
               "required": true,
               "location": "path"
             },
@@ -1028,82 +911,43 @@
           "request": {
             "required": true,
             "properties": {
-              "api_client_authorization": {
-                "$ref": "ApiClientAuthorization"
+              "authorized_key": {
+                "$ref": "AuthorizedKey"
               }
             }
           },
           "response": {
-            "$ref": "ApiClientAuthorization"
+            "$ref": "AuthorizedKey"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "delete": {
-          "id": "arvados.api_client_authorizations.delete",
-          "path": "api_client_authorizations/{uuid}",
+          "id": "arvados.authorized_keys.delete",
+          "path": "authorized_keys/{uuid}",
           "httpMethod": "DELETE",
-          "description": "Delete an existing ApiClientAuthorization.",
+          "description": "Delete an existing AuthorizedKey.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the ApiClientAuthorization in question.",
+              "description": "The UUID of the AuthorizedKey in question.",
               "required": true,
               "location": "path"
             }
           },
           "response": {
-            "$ref": "ApiClientAuthorization"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados"
-          ]
-        },
-        "create_system_auth": {
-          "id": "arvados.api_client_authorizations.create_system_auth",
-          "path": "api_client_authorizations/create_system_auth",
-          "httpMethod": "POST",
-          "description": "create_system_auth api_client_authorizations",
-          "parameters": {
-            "api_client_id": {
-              "type": "integer",
-              "required": false,
-              "description": "",
-              "location": "query"
-            },
-            "scopes": {
-              "type": "array",
-              "required": false,
-              "description": "",
-              "location": "query"
-            }
-          },
-          "response": {
-            "$ref": "ApiClientAuthorization"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados"
-          ]
-        },
-        "current": {
-          "id": "arvados.api_client_authorizations.current",
-          "path": "api_client_authorizations/current",
-          "httpMethod": "GET",
-          "description": "current api_client_authorizations",
-          "parameters": {},
-          "response": {
-            "$ref": "ApiClientAuthorization"
+            "$ref": "AuthorizedKey"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "list": {
-          "id": "arvados.api_client_authorizations.list",
-          "path": "api_client_authorizations",
+          "id": "arvados.authorized_keys.list",
+          "path": "authorized_keys",
           "httpMethod": "GET",
-          "description": "List ApiClientAuthorizations.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching ApiClientAuthorizations. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#apiClientAuthorizationList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List AuthorizedKeys.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching AuthorizedKeys. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#authorizedKeyList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -1171,7 +1015,7 @@
             }
           },
           "response": {
-            "$ref": "ApiClientAuthorizationList"
+            "$ref": "AuthorizedKeyList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -1179,10 +1023,10 @@
           ]
         },
         "show": {
-          "id": "arvados.api_client_authorizations.show",
-          "path": "api_client_authorizations/{uuid}",
+          "id": "arvados.authorized_keys.show",
+          "path": "authorized_keys/{uuid}",
           "httpMethod": "GET",
-          "description": "show api_client_authorizations",
+          "description": "show authorized_keys",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -1198,17 +1042,17 @@
             }
           },
           "response": {
-            "$ref": "ApiClientAuthorization"
+            "$ref": "AuthorizedKey"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "destroy": {
-          "id": "arvados.api_client_authorizations.destroy",
-          "path": "api_client_authorizations/{uuid}",
+          "id": "arvados.authorized_keys.destroy",
+          "path": "authorized_keys/{uuid}",
           "httpMethod": "DELETE",
-          "description": "destroy api_client_authorizations",
+          "description": "destroy authorized_keys",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -1218,7 +1062,7 @@
             }
           },
           "response": {
-            "$ref": "ApiClientAuthorization"
+            "$ref": "AuthorizedKey"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
@@ -1226,17 +1070,17 @@
         }
       }
     },
-    "authorized_keys": {
+    "collections": {
       "methods": {
         "get": {
-          "id": "arvados.authorized_keys.get",
-          "path": "authorized_keys/{uuid}",
+          "id": "arvados.collections.get",
+          "path": "collections/{uuid}",
           "httpMethod": "GET",
-          "description": "Gets a AuthorizedKey's metadata by UUID.",
+          "description": "Gets a Collection's metadata by UUID.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the AuthorizedKey in question.",
+              "description": "The UUID of the Collection in question.",
               "required": true,
               "location": "path"
             }
@@ -1245,7 +1089,7 @@
             "uuid"
           ],
           "response": {
-            "$ref": "AuthorizedKey"
+            "$ref": "Collection"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -1253,10 +1097,10 @@
           ]
         },
         "index": {
-          "id": "arvados.authorized_keys.list",
-          "path": "authorized_keys",
+          "id": "arvados.collections.list",
+          "path": "collections",
           "httpMethod": "GET",
-          "description": "List AuthorizedKeys.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching AuthorizedKeys. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#authorizedKeyList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List Collections.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Collections. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#collectionList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -1321,10 +1165,24 @@
               "required": false,
               "description": "bypass federation behavior, list items from local instance database only",
               "location": "query"
+            },
+            "include_trash": {
+              "type": "boolean",
+              "required": false,
+              "default": "false",
+              "description": "Include collections whose is_trashed attribute is true.",
+              "location": "query"
+            },
+            "include_old_versions": {
+              "type": "boolean",
+              "required": false,
+              "default": "false",
+              "description": "Include past collection versions.",
+              "location": "query"
             }
           },
           "response": {
-            "$ref": "AuthorizedKeyList"
+            "$ref": "CollectionList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -1332,10 +1190,10 @@
           ]
         },
         "create": {
-          "id": "arvados.authorized_keys.create",
-          "path": "authorized_keys",
+          "id": "arvados.collections.create",
+          "path": "collections",
           "httpMethod": "POST",
-          "description": "Create a new AuthorizedKey.",
+          "description": "Create a new Collection.",
           "parameters": {
             "select": {
               "type": "array",
@@ -1355,32 +1213,42 @@
               "description": "Create object on a remote federated cluster instead of the current one.",
               "location": "query",
               "required": false
+            },
+            "replace_files": {
+              "type": "object",
+              "description": "Files and directories to initialize/replace with content from other collections.",
+              "required": false,
+              "location": "query",
+              "properties": {},
+              "additionalProperties": {
+                "type": "string"
+              }
             }
           },
           "request": {
             "required": true,
             "properties": {
-              "authorized_key": {
-                "$ref": "AuthorizedKey"
+              "collection": {
+                "$ref": "Collection"
               }
             }
           },
           "response": {
-            "$ref": "AuthorizedKey"
+            "$ref": "Collection"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "update": {
-          "id": "arvados.authorized_keys.update",
-          "path": "authorized_keys/{uuid}",
+          "id": "arvados.collections.update",
+          "path": "collections/{uuid}",
           "httpMethod": "PUT",
-          "description": "Update attributes of an existing AuthorizedKey.",
+          "description": "Update attributes of an existing Collection.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the AuthorizedKey in question.",
+              "description": "The UUID of the Collection in question.",
               "required": true,
               "location": "path"
             },
@@ -1389,48 +1257,138 @@
               "description": "Attributes of the updated object to return in the response.",
               "required": false,
               "location": "query"
+            },
+            "replace_files": {
+              "type": "object",
+              "description": "Files and directories to initialize/replace with content from other collections.",
+              "required": false,
+              "location": "query",
+              "properties": {},
+              "additionalProperties": {
+                "type": "string"
+              }
             }
           },
           "request": {
             "required": true,
             "properties": {
-              "authorized_key": {
-                "$ref": "AuthorizedKey"
+              "collection": {
+                "$ref": "Collection"
               }
             }
           },
           "response": {
-            "$ref": "AuthorizedKey"
+            "$ref": "Collection"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "delete": {
-          "id": "arvados.authorized_keys.delete",
-          "path": "authorized_keys/{uuid}",
+          "id": "arvados.collections.delete",
+          "path": "collections/{uuid}",
           "httpMethod": "DELETE",
-          "description": "Delete an existing AuthorizedKey.",
+          "description": "Delete an existing Collection.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the AuthorizedKey in question.",
+              "description": "The UUID of the Collection in question.",
               "required": true,
               "location": "path"
             }
           },
           "response": {
-            "$ref": "AuthorizedKey"
+            "$ref": "Collection"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados"
+          ]
+        },
+        "provenance": {
+          "id": "arvados.collections.provenance",
+          "path": "collections/{uuid}/provenance",
+          "httpMethod": "GET",
+          "description": "provenance collections",
+          "parameters": {
+            "uuid": {
+              "type": "string",
+              "description": "",
+              "required": true,
+              "location": "path"
+            }
+          },
+          "response": {
+            "$ref": "Collection"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados"
+          ]
+        },
+        "used_by": {
+          "id": "arvados.collections.used_by",
+          "path": "collections/{uuid}/used_by",
+          "httpMethod": "GET",
+          "description": "used_by collections",
+          "parameters": {
+            "uuid": {
+              "type": "string",
+              "description": "",
+              "required": true,
+              "location": "path"
+            }
+          },
+          "response": {
+            "$ref": "Collection"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados"
+          ]
+        },
+        "trash": {
+          "id": "arvados.collections.trash",
+          "path": "collections/{uuid}/trash",
+          "httpMethod": "POST",
+          "description": "trash collections",
+          "parameters": {
+            "uuid": {
+              "type": "string",
+              "description": "",
+              "required": true,
+              "location": "path"
+            }
+          },
+          "response": {
+            "$ref": "Collection"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados"
+          ]
+        },
+        "untrash": {
+          "id": "arvados.collections.untrash",
+          "path": "collections/{uuid}/untrash",
+          "httpMethod": "POST",
+          "description": "untrash collections",
+          "parameters": {
+            "uuid": {
+              "type": "string",
+              "description": "",
+              "required": true,
+              "location": "path"
+            }
+          },
+          "response": {
+            "$ref": "Collection"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "list": {
-          "id": "arvados.authorized_keys.list",
-          "path": "authorized_keys",
+          "id": "arvados.collections.list",
+          "path": "collections",
           "httpMethod": "GET",
-          "description": "List AuthorizedKeys.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching AuthorizedKeys. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#authorizedKeyList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List Collections.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Collections. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#collectionList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -1495,10 +1453,24 @@
               "required": false,
               "description": "bypass federation behavior, list items from local instance database only",
               "location": "query"
+            },
+            "include_trash": {
+              "type": "boolean",
+              "required": false,
+              "default": "false",
+              "description": "Include collections whose is_trashed attribute is true.",
+              "location": "query"
+            },
+            "include_old_versions": {
+              "type": "boolean",
+              "required": false,
+              "default": "false",
+              "description": "Include past collection versions.",
+              "location": "query"
             }
           },
           "response": {
-            "$ref": "AuthorizedKeyList"
+            "$ref": "CollectionList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -1506,10 +1478,10 @@
           ]
         },
         "show": {
-          "id": "arvados.authorized_keys.show",
-          "path": "authorized_keys/{uuid}",
+          "id": "arvados.collections.show",
+          "path": "collections/{uuid}",
           "httpMethod": "GET",
-          "description": "show authorized_keys",
+          "description": "show collections",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -1522,20 +1494,34 @@
               "description": "Attributes of the object to return in the response.",
               "required": false,
               "location": "query"
+            },
+            "include_trash": {
+              "type": "boolean",
+              "required": false,
+              "default": "false",
+              "description": "Show collection even if its is_trashed attribute is true.",
+              "location": "query"
+            },
+            "include_old_versions": {
+              "type": "boolean",
+              "required": false,
+              "default": "true",
+              "description": "Include past collection versions.",
+              "location": "query"
             }
           },
           "response": {
-            "$ref": "AuthorizedKey"
+            "$ref": "Collection"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "destroy": {
-          "id": "arvados.authorized_keys.destroy",
-          "path": "authorized_keys/{uuid}",
+          "id": "arvados.collections.destroy",
+          "path": "collections/{uuid}",
           "httpMethod": "DELETE",
-          "description": "destroy authorized_keys",
+          "description": "destroy collections",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -1545,7 +1531,7 @@
             }
           },
           "response": {
-            "$ref": "AuthorizedKey"
+            "$ref": "Collection"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
@@ -1553,17 +1539,17 @@
         }
       }
     },
-    "collections": {
+    "containers": {
       "methods": {
         "get": {
-          "id": "arvados.collections.get",
-          "path": "collections/{uuid}",
+          "id": "arvados.containers.get",
+          "path": "containers/{uuid}",
           "httpMethod": "GET",
-          "description": "Gets a Collection's metadata by UUID.",
+          "description": "Gets a Container's metadata by UUID.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Collection in question.",
+              "description": "The UUID of the Container in question.",
               "required": true,
               "location": "path"
             }
@@ -1572,7 +1558,7 @@
             "uuid"
           ],
           "response": {
-            "$ref": "Collection"
+            "$ref": "Container"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -1580,10 +1566,10 @@
           ]
         },
         "index": {
-          "id": "arvados.collections.list",
-          "path": "collections",
+          "id": "arvados.containers.list",
+          "path": "containers",
           "httpMethod": "GET",
-          "description": "List Collections.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Collections. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#collectionList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List Containers.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Containers. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#containerList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -1648,24 +1634,10 @@
               "required": false,
               "description": "bypass federation behavior, list items from local instance database only",
               "location": "query"
-            },
-            "include_trash": {
-              "type": "boolean",
-              "required": false,
-              "default": "false",
-              "description": "Include collections whose is_trashed attribute is true.",
-              "location": "query"
-            },
-            "include_old_versions": {
-              "type": "boolean",
-              "required": false,
-              "default": "false",
-              "description": "Include past collection versions.",
-              "location": "query"
             }
           },
           "response": {
-            "$ref": "CollectionList"
+            "$ref": "ContainerList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -1673,10 +1645,10 @@
           ]
         },
         "create": {
-          "id": "arvados.collections.create",
-          "path": "collections",
+          "id": "arvados.containers.create",
+          "path": "containers",
           "httpMethod": "POST",
-          "description": "Create a new Collection.",
+          "description": "Create a new Container.",
           "parameters": {
             "select": {
               "type": "array",
@@ -1696,42 +1668,32 @@
               "description": "Create object on a remote federated cluster instead of the current one.",
               "location": "query",
               "required": false
-            },
-            "replace_files": {
-              "type": "object",
-              "description": "Files and directories to initialize/replace with content from other collections.",
-              "required": false,
-              "location": "query",
-              "properties": {},
-              "additionalProperties": {
-                "type": "string"
-              }
             }
           },
           "request": {
             "required": true,
             "properties": {
-              "collection": {
-                "$ref": "Collection"
+              "container": {
+                "$ref": "Container"
               }
             }
           },
           "response": {
-            "$ref": "Collection"
+            "$ref": "Container"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "update": {
-          "id": "arvados.collections.update",
-          "path": "collections/{uuid}",
+          "id": "arvados.containers.update",
+          "path": "containers/{uuid}",
           "httpMethod": "PUT",
-          "description": "Update attributes of an existing Collection.",
+          "description": "Update attributes of an existing Container.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Collection in question.",
+              "description": "The UUID of the Container in question.",
               "required": true,
               "location": "path"
             },
@@ -1740,58 +1702,48 @@
               "description": "Attributes of the updated object to return in the response.",
               "required": false,
               "location": "query"
-            },
-            "replace_files": {
-              "type": "object",
-              "description": "Files and directories to initialize/replace with content from other collections.",
-              "required": false,
-              "location": "query",
-              "properties": {},
-              "additionalProperties": {
-                "type": "string"
-              }
             }
           },
           "request": {
             "required": true,
             "properties": {
-              "collection": {
-                "$ref": "Collection"
+              "container": {
+                "$ref": "Container"
               }
             }
           },
           "response": {
-            "$ref": "Collection"
+            "$ref": "Container"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "delete": {
-          "id": "arvados.collections.delete",
-          "path": "collections/{uuid}",
+          "id": "arvados.containers.delete",
+          "path": "containers/{uuid}",
           "httpMethod": "DELETE",
-          "description": "Delete an existing Collection.",
+          "description": "Delete an existing Container.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Collection in question.",
+              "description": "The UUID of the Container in question.",
               "required": true,
               "location": "path"
             }
           },
           "response": {
-            "$ref": "Collection"
+            "$ref": "Container"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
-        "provenance": {
-          "id": "arvados.collections.provenance",
-          "path": "collections/{uuid}/provenance",
+        "auth": {
+          "id": "arvados.containers.auth",
+          "path": "containers/{uuid}/auth",
           "httpMethod": "GET",
-          "description": "provenance collections",
+          "description": "auth containers",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -1801,17 +1753,17 @@
             }
           },
           "response": {
-            "$ref": "Collection"
+            "$ref": "Container"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
-        "used_by": {
-          "id": "arvados.collections.used_by",
-          "path": "collections/{uuid}/used_by",
-          "httpMethod": "GET",
-          "description": "used_by collections",
+        "lock": {
+          "id": "arvados.containers.lock",
+          "path": "containers/{uuid}/lock",
+          "httpMethod": "POST",
+          "description": "lock containers",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -1821,17 +1773,17 @@
             }
           },
           "response": {
-            "$ref": "Collection"
+            "$ref": "Container"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
-        "trash": {
-          "id": "arvados.collections.trash",
-          "path": "collections/{uuid}/trash",
+        "unlock": {
+          "id": "arvados.containers.unlock",
+          "path": "containers/{uuid}/unlock",
           "httpMethod": "POST",
-          "description": "trash collections",
+          "description": "unlock containers",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -1841,17 +1793,17 @@
             }
           },
           "response": {
-            "$ref": "Collection"
+            "$ref": "Container"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
-        "untrash": {
-          "id": "arvados.collections.untrash",
-          "path": "collections/{uuid}/untrash",
+        "update_priority": {
+          "id": "arvados.containers.update_priority",
+          "path": "containers/{uuid}/update_priority",
           "httpMethod": "POST",
-          "description": "untrash collections",
+          "description": "update_priority containers",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -1861,17 +1813,50 @@
             }
           },
           "response": {
-            "$ref": "Collection"
+            "$ref": "Container"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados"
+          ]
+        },
+        "secret_mounts": {
+          "id": "arvados.containers.secret_mounts",
+          "path": "containers/{uuid}/secret_mounts",
+          "httpMethod": "GET",
+          "description": "secret_mounts containers",
+          "parameters": {
+            "uuid": {
+              "type": "string",
+              "description": "",
+              "required": true,
+              "location": "path"
+            }
+          },
+          "response": {
+            "$ref": "Container"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados"
+          ]
+        },
+        "current": {
+          "id": "arvados.containers.current",
+          "path": "containers/current",
+          "httpMethod": "GET",
+          "description": "current containers",
+          "parameters": {},
+          "response": {
+            "$ref": "Container"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "list": {
-          "id": "arvados.collections.list",
-          "path": "collections",
+          "id": "arvados.containers.list",
+          "path": "containers",
           "httpMethod": "GET",
-          "description": "List Collections.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Collections. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#collectionList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List Containers.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Containers. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#containerList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -1936,24 +1921,10 @@
               "required": false,
               "description": "bypass federation behavior, list items from local instance database only",
               "location": "query"
-            },
-            "include_trash": {
-              "type": "boolean",
-              "required": false,
-              "default": "false",
-              "description": "Include collections whose is_trashed attribute is true.",
-              "location": "query"
-            },
-            "include_old_versions": {
-              "type": "boolean",
-              "required": false,
-              "default": "false",
-              "description": "Include past collection versions.",
-              "location": "query"
             }
           },
           "response": {
-            "$ref": "CollectionList"
+            "$ref": "ContainerList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -1961,10 +1932,10 @@
           ]
         },
         "show": {
-          "id": "arvados.collections.show",
-          "path": "collections/{uuid}",
+          "id": "arvados.containers.show",
+          "path": "containers/{uuid}",
           "httpMethod": "GET",
-          "description": "show collections",
+          "description": "show containers",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -1977,34 +1948,20 @@
               "description": "Attributes of the object to return in the response.",
               "required": false,
               "location": "query"
-            },
-            "include_trash": {
-              "type": "boolean",
-              "required": false,
-              "default": "false",
-              "description": "Show collection even if its is_trashed attribute is true.",
-              "location": "query"
-            },
-            "include_old_versions": {
-              "type": "boolean",
-              "required": false,
-              "default": "true",
-              "description": "Include past collection versions.",
-              "location": "query"
             }
           },
           "response": {
-            "$ref": "Collection"
+            "$ref": "Container"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "destroy": {
-          "id": "arvados.collections.destroy",
-          "path": "collections/{uuid}",
+          "id": "arvados.containers.destroy",
+          "path": "containers/{uuid}",
           "httpMethod": "DELETE",
-          "description": "destroy collections",
+          "description": "destroy containers",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -2014,7 +1971,7 @@
             }
           },
           "response": {
-            "$ref": "Collection"
+            "$ref": "Container"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
@@ -2022,17 +1979,17 @@
         }
       }
     },
-    "containers": {
+    "container_requests": {
       "methods": {
         "get": {
-          "id": "arvados.containers.get",
-          "path": "containers/{uuid}",
+          "id": "arvados.container_requests.get",
+          "path": "container_requests/{uuid}",
           "httpMethod": "GET",
-          "description": "Gets a Container's metadata by UUID.",
+          "description": "Gets a ContainerRequest's metadata by UUID.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Container in question.",
+              "description": "The UUID of the ContainerRequest in question.",
               "required": true,
               "location": "path"
             }
@@ -2041,7 +1998,7 @@
             "uuid"
           ],
           "response": {
-            "$ref": "Container"
+            "$ref": "ContainerRequest"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -2049,10 +2006,10 @@
           ]
         },
         "index": {
-          "id": "arvados.containers.list",
-          "path": "containers",
+          "id": "arvados.container_requests.list",
+          "path": "container_requests",
           "httpMethod": "GET",
-          "description": "List Containers.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Containers. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#containerList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List ContainerRequests.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching ContainerRequests. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#containerRequestList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -2117,10 +2074,17 @@
               "required": false,
               "description": "bypass federation behavior, list items from local instance database only",
               "location": "query"
+            },
+            "include_trash": {
+              "type": "boolean",
+              "required": false,
+              "default": "false",
+              "description": "Include container requests whose owner project is trashed.",
+              "location": "query"
             }
           },
           "response": {
-            "$ref": "ContainerList"
+            "$ref": "ContainerRequestList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -2128,10 +2092,10 @@
           ]
         },
         "create": {
-          "id": "arvados.containers.create",
-          "path": "containers",
+          "id": "arvados.container_requests.create",
+          "path": "container_requests",
           "httpMethod": "POST",
-          "description": "Create a new Container.",
+          "description": "Create a new ContainerRequest.",
           "parameters": {
             "select": {
               "type": "array",
@@ -2156,27 +2120,27 @@
           "request": {
             "required": true,
             "properties": {
-              "container": {
-                "$ref": "Container"
+              "container_request": {
+                "$ref": "ContainerRequest"
               }
             }
           },
           "response": {
-            "$ref": "Container"
+            "$ref": "ContainerRequest"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "update": {
-          "id": "arvados.containers.update",
-          "path": "containers/{uuid}",
+          "id": "arvados.container_requests.update",
+          "path": "container_requests/{uuid}",
           "httpMethod": "PUT",
-          "description": "Update attributes of an existing Container.",
+          "description": "Update attributes of an existing ContainerRequest.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Container in question.",
+              "description": "The UUID of the ContainerRequest in question.",
               "required": true,
               "location": "path"
             },
@@ -2190,166 +2154,53 @@
           "request": {
             "required": true,
             "properties": {
-              "container": {
-                "$ref": "Container"
+              "container_request": {
+                "$ref": "ContainerRequest"
               }
             }
           },
           "response": {
-            "$ref": "Container"
+            "$ref": "ContainerRequest"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "delete": {
-          "id": "arvados.containers.delete",
-          "path": "containers/{uuid}",
+          "id": "arvados.container_requests.delete",
+          "path": "container_requests/{uuid}",
           "httpMethod": "DELETE",
-          "description": "Delete an existing Container.",
+          "description": "Delete an existing ContainerRequest.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Container in question.",
+              "description": "The UUID of the ContainerRequest in question.",
               "required": true,
               "location": "path"
             }
           },
           "response": {
-            "$ref": "Container"
+            "$ref": "ContainerRequest"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
-        "auth": {
-          "id": "arvados.containers.auth",
-          "path": "containers/{uuid}/auth",
+        "list": {
+          "id": "arvados.container_requests.list",
+          "path": "container_requests",
           "httpMethod": "GET",
-          "description": "auth containers",
+          "description": "List ContainerRequests.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching ContainerRequests. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#containerRequestList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
-            "uuid": {
-              "type": "string",
+            "filters": {
+              "type": "array",
+              "required": false,
               "description": "",
-              "required": true,
-              "location": "path"
-            }
-          },
-          "response": {
-            "$ref": "Container"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados"
-          ]
-        },
-        "lock": {
-          "id": "arvados.containers.lock",
-          "path": "containers/{uuid}/lock",
-          "httpMethod": "POST",
-          "description": "lock containers",
-          "parameters": {
-            "uuid": {
-              "type": "string",
-              "description": "",
-              "required": true,
-              "location": "path"
-            }
-          },
-          "response": {
-            "$ref": "Container"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados"
-          ]
-        },
-        "unlock": {
-          "id": "arvados.containers.unlock",
-          "path": "containers/{uuid}/unlock",
-          "httpMethod": "POST",
-          "description": "unlock containers",
-          "parameters": {
-            "uuid": {
-              "type": "string",
-              "description": "",
-              "required": true,
-              "location": "path"
-            }
-          },
-          "response": {
-            "$ref": "Container"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados"
-          ]
-        },
-        "update_priority": {
-          "id": "arvados.containers.update_priority",
-          "path": "containers/{uuid}/update_priority",
-          "httpMethod": "POST",
-          "description": "update_priority containers",
-          "parameters": {
-            "uuid": {
-              "type": "string",
-              "description": "",
-              "required": true,
-              "location": "path"
-            }
-          },
-          "response": {
-            "$ref": "Container"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados"
-          ]
-        },
-        "secret_mounts": {
-          "id": "arvados.containers.secret_mounts",
-          "path": "containers/{uuid}/secret_mounts",
-          "httpMethod": "GET",
-          "description": "secret_mounts containers",
-          "parameters": {
-            "uuid": {
-              "type": "string",
-              "description": "",
-              "required": true,
-              "location": "path"
-            }
-          },
-          "response": {
-            "$ref": "Container"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados"
-          ]
-        },
-        "current": {
-          "id": "arvados.containers.current",
-          "path": "containers/current",
-          "httpMethod": "GET",
-          "description": "current containers",
-          "parameters": {},
-          "response": {
-            "$ref": "Container"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados"
-          ]
-        },
-        "list": {
-          "id": "arvados.containers.list",
-          "path": "containers",
-          "httpMethod": "GET",
-          "description": "List Containers.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Containers. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#containerList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
-          "parameters": {
-            "filters": {
-              "type": "array",
-              "required": false,
-              "description": "",
-              "location": "query"
-            },
-            "where": {
-              "type": "object",
-              "required": false,
+              "location": "query"
+            },
+            "where": {
+              "type": "object",
+              "required": false,
               "description": "",
               "location": "query"
             },
@@ -2404,10 +2255,17 @@
               "required": false,
               "description": "bypass federation behavior, list items from local instance database only",
               "location": "query"
+            },
+            "include_trash": {
+              "type": "boolean",
+              "required": false,
+              "default": "false",
+              "description": "Include container requests whose owner project is trashed.",
+              "location": "query"
             }
           },
           "response": {
-            "$ref": "ContainerList"
+            "$ref": "ContainerRequestList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -2415,10 +2273,10 @@
           ]
         },
         "show": {
-          "id": "arvados.containers.show",
-          "path": "containers/{uuid}",
+          "id": "arvados.container_requests.show",
+          "path": "container_requests/{uuid}",
           "httpMethod": "GET",
-          "description": "show containers",
+          "description": "show container_requests",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -2431,20 +2289,27 @@
               "description": "Attributes of the object to return in the response.",
               "required": false,
               "location": "query"
+            },
+            "include_trash": {
+              "type": "boolean",
+              "required": false,
+              "default": "false",
+              "description": "Show container request even if its owner project is trashed.",
+              "location": "query"
             }
           },
           "response": {
-            "$ref": "Container"
+            "$ref": "ContainerRequest"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "destroy": {
-          "id": "arvados.containers.destroy",
-          "path": "containers/{uuid}",
+          "id": "arvados.container_requests.destroy",
+          "path": "container_requests/{uuid}",
           "httpMethod": "DELETE",
-          "description": "destroy containers",
+          "description": "destroy container_requests",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -2454,7 +2319,7 @@
             }
           },
           "response": {
-            "$ref": "Container"
+            "$ref": "ContainerRequest"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
@@ -2462,17 +2327,17 @@
         }
       }
     },
-    "container_requests": {
+    "groups": {
       "methods": {
         "get": {
-          "id": "arvados.container_requests.get",
-          "path": "container_requests/{uuid}",
+          "id": "arvados.groups.get",
+          "path": "groups/{uuid}",
           "httpMethod": "GET",
-          "description": "Gets a ContainerRequest's metadata by UUID.",
+          "description": "Gets a Group's metadata by UUID.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the ContainerRequest in question.",
+              "description": "The UUID of the Group in question.",
               "required": true,
               "location": "path"
             }
@@ -2481,7 +2346,7 @@
             "uuid"
           ],
           "response": {
-            "$ref": "ContainerRequest"
+            "$ref": "Group"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -2489,10 +2354,10 @@
           ]
         },
         "index": {
-          "id": "arvados.container_requests.list",
-          "path": "container_requests",
+          "id": "arvados.groups.list",
+          "path": "groups",
           "httpMethod": "GET",
-          "description": "List ContainerRequests.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching ContainerRequests. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#containerRequestList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List Groups.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Groups. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#groupList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -2562,12 +2427,12 @@
               "type": "boolean",
               "required": false,
               "default": "false",
-              "description": "Include container requests whose owner project is trashed.",
+              "description": "Include items whose is_trashed attribute is true.",
               "location": "query"
             }
           },
           "response": {
-            "$ref": "ContainerRequestList"
+            "$ref": "GroupList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -2575,10 +2440,10 @@
           ]
         },
         "create": {
-          "id": "arvados.container_requests.create",
-          "path": "container_requests",
+          "id": "arvados.groups.create",
+          "path": "groups",
           "httpMethod": "POST",
-          "description": "Create a new ContainerRequest.",
+          "description": "Create a new Group.",
           "parameters": {
             "select": {
               "type": "array",
@@ -2598,32 +2463,39 @@
               "description": "Create object on a remote federated cluster instead of the current one.",
               "location": "query",
               "required": false
+            },
+            "async": {
+              "required": false,
+              "type": "boolean",
+              "location": "query",
+              "default": "false",
+              "description": "defer permissions update"
             }
           },
           "request": {
             "required": true,
             "properties": {
-              "container_request": {
-                "$ref": "ContainerRequest"
+              "group": {
+                "$ref": "Group"
               }
             }
           },
           "response": {
-            "$ref": "ContainerRequest"
+            "$ref": "Group"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "update": {
-          "id": "arvados.container_requests.update",
-          "path": "container_requests/{uuid}",
+          "id": "arvados.groups.update",
+          "path": "groups/{uuid}",
           "httpMethod": "PUT",
-          "description": "Update attributes of an existing ContainerRequest.",
+          "description": "Update attributes of an existing Group.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the ContainerRequest in question.",
+              "description": "The UUID of the Group in question.",
               "required": true,
               "location": "path"
             },
@@ -2632,48 +2504,55 @@
               "description": "Attributes of the updated object to return in the response.",
               "required": false,
               "location": "query"
+            },
+            "async": {
+              "required": false,
+              "type": "boolean",
+              "location": "query",
+              "default": "false",
+              "description": "defer permissions update"
             }
           },
           "request": {
             "required": true,
             "properties": {
-              "container_request": {
-                "$ref": "ContainerRequest"
+              "group": {
+                "$ref": "Group"
               }
             }
           },
           "response": {
-            "$ref": "ContainerRequest"
+            "$ref": "Group"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "delete": {
-          "id": "arvados.container_requests.delete",
-          "path": "container_requests/{uuid}",
+          "id": "arvados.groups.delete",
+          "path": "groups/{uuid}",
           "httpMethod": "DELETE",
-          "description": "Delete an existing ContainerRequest.",
+          "description": "Delete an existing Group.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the ContainerRequest in question.",
+              "description": "The UUID of the Group in question.",
               "required": true,
               "location": "path"
             }
           },
           "response": {
-            "$ref": "ContainerRequest"
+            "$ref": "Group"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
-        "list": {
-          "id": "arvados.container_requests.list",
-          "path": "container_requests",
+        "contents": {
+          "id": "arvados.groups.contents",
+          "path": "groups/contents",
           "httpMethod": "GET",
-          "description": "List ContainerRequests.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching ContainerRequests. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#containerRequestList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "contents groups",
           "parameters": {
             "filters": {
               "type": "array",
@@ -2743,104 +2622,49 @@
               "type": "boolean",
               "required": false,
               "default": "false",
-              "description": "Include container requests whose owner project is trashed.",
+              "description": "Include items whose is_trashed attribute is true.",
               "location": "query"
-            }
-          },
-          "response": {
-            "$ref": "ContainerRequestList"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados",
-            "https://api.arvados.org/auth/arvados.readonly"
-          ]
-        },
-        "show": {
-          "id": "arvados.container_requests.show",
-          "path": "container_requests/{uuid}",
-          "httpMethod": "GET",
-          "description": "show container_requests",
-          "parameters": {
+            },
             "uuid": {
               "type": "string",
+              "required": false,
+              "default": "",
               "description": "",
-              "required": true,
-              "location": "path"
+              "location": "query"
             },
-            "select": {
-              "type": "array",
-              "description": "Attributes of the object to return in the response.",
+            "recursive": {
+              "type": "boolean",
+              "required": false,
+              "default": "false",
+              "description": "Include contents from child groups recursively.",
+              "location": "query"
+            },
+            "include": {
+              "type": "string",
               "required": false,
+              "description": "Include objects referred to by listed field in \"included\" (only owner_uuid).",
               "location": "query"
             },
-            "include_trash": {
+            "include_old_versions": {
               "type": "boolean",
               "required": false,
               "default": "false",
-              "description": "Show container request even if its owner project is trashed.",
+              "description": "Include past collection versions.",
               "location": "query"
             }
           },
           "response": {
-            "$ref": "ContainerRequest"
+            "$ref": "Group"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
-        "destroy": {
-          "id": "arvados.container_requests.destroy",
-          "path": "container_requests/{uuid}",
-          "httpMethod": "DELETE",
-          "description": "destroy container_requests",
-          "parameters": {
-            "uuid": {
-              "type": "string",
-              "description": "",
-              "required": true,
-              "location": "path"
-            }
-          },
-          "response": {
-            "$ref": "ContainerRequest"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados"
-          ]
-        }
-      }
-    },
-    "groups": {
-      "methods": {
-        "get": {
-          "id": "arvados.groups.get",
-          "path": "groups/{uuid}",
-          "httpMethod": "GET",
-          "description": "Gets a Group's metadata by UUID.",
-          "parameters": {
-            "uuid": {
-              "type": "string",
-              "description": "The UUID of the Group in question.",
-              "required": true,
-              "location": "path"
-            }
-          },
-          "parameterOrder": [
-            "uuid"
-          ],
-          "response": {
-            "$ref": "Group"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados",
-            "https://api.arvados.org/auth/arvados.readonly"
-          ]
-        },
-        "index": {
-          "id": "arvados.groups.list",
-          "path": "groups",
-          "httpMethod": "GET",
-          "description": "List Groups.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Groups. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#groupList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+        "shared": {
+          "id": "arvados.groups.shared",
+          "path": "groups/shared",
+          "httpMethod": "GET",
+          "description": "shared groups",
           "parameters": {
             "filters": {
               "type": "array",
@@ -2912,55 +2736,12 @@
               "default": "false",
               "description": "Include items whose is_trashed attribute is true.",
               "location": "query"
-            }
-          },
-          "response": {
-            "$ref": "GroupList"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados",
-            "https://api.arvados.org/auth/arvados.readonly"
-          ]
-        },
-        "create": {
-          "id": "arvados.groups.create",
-          "path": "groups",
-          "httpMethod": "POST",
-          "description": "Create a new Group.",
-          "parameters": {
-            "select": {
-              "type": "array",
-              "description": "Attributes of the new object to return in the response.",
-              "required": false,
-              "location": "query"
-            },
-            "ensure_unique_name": {
-              "type": "boolean",
-              "description": "Adjust name to ensure uniqueness instead of returning an error on (owner_uuid, name) collision.",
-              "location": "query",
-              "required": false,
-              "default": "false"
             },
-            "cluster_id": {
+            "include": {
               "type": "string",
-              "description": "Create object on a remote federated cluster instead of the current one.",
-              "location": "query",
-              "required": false
-            },
-            "async": {
               "required": false,
-              "type": "boolean",
-              "location": "query",
-              "default": "false",
-              "description": "defer permissions update"
-            }
-          },
-          "request": {
-            "required": true,
-            "properties": {
-              "group": {
-                "$ref": "Group"
-              }
+              "description": "",
+              "location": "query"
             }
           },
           "response": {
@@ -2970,38 +2751,17 @@
             "https://api.arvados.org/auth/arvados"
           ]
         },
-        "update": {
-          "id": "arvados.groups.update",
-          "path": "groups/{uuid}",
-          "httpMethod": "PUT",
-          "description": "Update attributes of an existing Group.",
+        "trash": {
+          "id": "arvados.groups.trash",
+          "path": "groups/{uuid}/trash",
+          "httpMethod": "POST",
+          "description": "trash groups",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Group in question.",
+              "description": "",
               "required": true,
               "location": "path"
-            },
-            "select": {
-              "type": "array",
-              "description": "Attributes of the updated object to return in the response.",
-              "required": false,
-              "location": "query"
-            },
-            "async": {
-              "required": false,
-              "type": "boolean",
-              "location": "query",
-              "default": "false",
-              "description": "defer permissions update"
-            }
-          },
-          "request": {
-            "required": true,
-            "properties": {
-              "group": {
-                "$ref": "Group"
-              }
             }
           },
           "response": {
@@ -3011,15 +2771,15 @@
             "https://api.arvados.org/auth/arvados"
           ]
         },
-        "delete": {
-          "id": "arvados.groups.delete",
-          "path": "groups/{uuid}",
-          "httpMethod": "DELETE",
-          "description": "Delete an existing Group.",
+        "untrash": {
+          "id": "arvados.groups.untrash",
+          "path": "groups/{uuid}/untrash",
+          "httpMethod": "POST",
+          "description": "untrash groups",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Group in question.",
+              "description": "",
               "required": true,
               "location": "path"
             }
@@ -3031,11 +2791,11 @@
             "https://api.arvados.org/auth/arvados"
           ]
         },
-        "contents": {
-          "id": "arvados.groups.contents",
-          "path": "groups/contents",
+        "list": {
+          "id": "arvados.groups.list",
+          "path": "groups",
           "httpMethod": "GET",
-          "description": "contents groups",
+          "description": "List Groups.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Groups. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#groupList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -3107,32 +2867,39 @@
               "default": "false",
               "description": "Include items whose is_trashed attribute is true.",
               "location": "query"
-            },
+            }
+          },
+          "response": {
+            "$ref": "GroupList"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados",
+            "https://api.arvados.org/auth/arvados.readonly"
+          ]
+        },
+        "show": {
+          "id": "arvados.groups.show",
+          "path": "groups/{uuid}",
+          "httpMethod": "GET",
+          "description": "show groups",
+          "parameters": {
             "uuid": {
               "type": "string",
-              "required": false,
-              "default": "",
               "description": "",
-              "location": "query"
-            },
-            "recursive": {
-              "type": "boolean",
-              "required": false,
-              "default": "false",
-              "description": "Include contents from child groups recursively.",
-              "location": "query"
+              "required": true,
+              "location": "path"
             },
-            "include": {
-              "type": "string",
+            "select": {
+              "type": "array",
+              "description": "Attributes of the object to return in the response.",
               "required": false,
-              "description": "Include objects referred to by listed field in \"included\" (only owner_uuid).",
               "location": "query"
             },
-            "include_old_versions": {
+            "include_trash": {
               "type": "boolean",
               "required": false,
               "default": "false",
-              "description": "Include past collection versions.",
+              "description": "Show group/project even if its is_trashed attribute is true.",
               "location": "query"
             }
           },
@@ -3143,11 +2910,59 @@
             "https://api.arvados.org/auth/arvados"
           ]
         },
-        "shared": {
-          "id": "arvados.groups.shared",
-          "path": "groups/shared",
+        "destroy": {
+          "id": "arvados.groups.destroy",
+          "path": "groups/{uuid}",
+          "httpMethod": "DELETE",
+          "description": "destroy groups",
+          "parameters": {
+            "uuid": {
+              "type": "string",
+              "description": "",
+              "required": true,
+              "location": "path"
+            }
+          },
+          "response": {
+            "$ref": "Group"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados"
+          ]
+        }
+      }
+    },
+    "humans": {
+      "methods": {
+        "get": {
+          "id": "arvados.humans.get",
+          "path": "humans/{uuid}",
           "httpMethod": "GET",
-          "description": "shared groups",
+          "description": "Gets a Human's metadata by UUID.",
+          "parameters": {
+            "uuid": {
+              "type": "string",
+              "description": "The UUID of the Human in question.",
+              "required": true,
+              "location": "path"
+            }
+          },
+          "parameterOrder": [
+            "uuid"
+          ],
+          "response": {
+            "$ref": "Human"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados",
+            "https://api.arvados.org/auth/arvados.readonly"
+          ]
+        },
+        "index": {
+          "id": "arvados.humans.list",
+          "path": "humans",
+          "httpMethod": "GET",
+          "description": "List Humans.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Humans. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#humanList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -3212,73 +3027,116 @@
               "required": false,
               "description": "bypass federation behavior, list items from local instance database only",
               "location": "query"
-            },
-            "include_trash": {
-              "type": "boolean",
-              "required": false,
-              "default": "false",
-              "description": "Include items whose is_trashed attribute is true.",
-              "location": "query"
-            },
-            "include": {
-              "type": "string",
-              "required": false,
-              "description": "",
-              "location": "query"
             }
           },
           "response": {
-            "$ref": "Group"
+            "$ref": "HumanList"
           },
           "scopes": [
-            "https://api.arvados.org/auth/arvados"
+            "https://api.arvados.org/auth/arvados",
+            "https://api.arvados.org/auth/arvados.readonly"
           ]
         },
-        "trash": {
-          "id": "arvados.groups.trash",
-          "path": "groups/{uuid}/trash",
+        "create": {
+          "id": "arvados.humans.create",
+          "path": "humans",
           "httpMethod": "POST",
-          "description": "trash groups",
+          "description": "Create a new Human.",
+          "parameters": {
+            "select": {
+              "type": "array",
+              "description": "Attributes of the new object to return in the response.",
+              "required": false,
+              "location": "query"
+            },
+            "ensure_unique_name": {
+              "type": "boolean",
+              "description": "Adjust name to ensure uniqueness instead of returning an error on (owner_uuid, name) collision.",
+              "location": "query",
+              "required": false,
+              "default": "false"
+            },
+            "cluster_id": {
+              "type": "string",
+              "description": "Create object on a remote federated cluster instead of the current one.",
+              "location": "query",
+              "required": false
+            }
+          },
+          "request": {
+            "required": true,
+            "properties": {
+              "human": {
+                "$ref": "Human"
+              }
+            }
+          },
+          "response": {
+            "$ref": "Human"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados"
+          ]
+        },
+        "update": {
+          "id": "arvados.humans.update",
+          "path": "humans/{uuid}",
+          "httpMethod": "PUT",
+          "description": "Update attributes of an existing Human.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "",
+              "description": "The UUID of the Human in question.",
               "required": true,
               "location": "path"
+            },
+            "select": {
+              "type": "array",
+              "description": "Attributes of the updated object to return in the response.",
+              "required": false,
+              "location": "query"
+            }
+          },
+          "request": {
+            "required": true,
+            "properties": {
+              "human": {
+                "$ref": "Human"
+              }
             }
           },
           "response": {
-            "$ref": "Group"
+            "$ref": "Human"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
-        "untrash": {
-          "id": "arvados.groups.untrash",
-          "path": "groups/{uuid}/untrash",
-          "httpMethod": "POST",
-          "description": "untrash groups",
+        "delete": {
+          "id": "arvados.humans.delete",
+          "path": "humans/{uuid}",
+          "httpMethod": "DELETE",
+          "description": "Delete an existing Human.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "",
+              "description": "The UUID of the Human in question.",
               "required": true,
               "location": "path"
             }
           },
           "response": {
-            "$ref": "Group"
+            "$ref": "Human"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "list": {
-          "id": "arvados.groups.list",
-          "path": "groups",
+          "id": "arvados.humans.list",
+          "path": "humans",
           "httpMethod": "GET",
-          "description": "List Groups.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Groups. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#groupList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List Humans.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Humans. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#humanList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -3343,17 +3201,10 @@
               "required": false,
               "description": "bypass federation behavior, list items from local instance database only",
               "location": "query"
-            },
-            "include_trash": {
-              "type": "boolean",
-              "required": false,
-              "default": "false",
-              "description": "Include items whose is_trashed attribute is true.",
-              "location": "query"
             }
           },
           "response": {
-            "$ref": "GroupList"
+            "$ref": "HumanList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -3361,10 +3212,10 @@
           ]
         },
         "show": {
-          "id": "arvados.groups.show",
-          "path": "groups/{uuid}",
+          "id": "arvados.humans.show",
+          "path": "humans/{uuid}",
           "httpMethod": "GET",
-          "description": "show groups",
+          "description": "show humans",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -3377,27 +3228,20 @@
               "description": "Attributes of the object to return in the response.",
               "required": false,
               "location": "query"
-            },
-            "include_trash": {
-              "type": "boolean",
-              "required": false,
-              "default": "false",
-              "description": "Show group/project even if its is_trashed attribute is true.",
-              "location": "query"
             }
           },
           "response": {
-            "$ref": "Group"
+            "$ref": "Human"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "destroy": {
-          "id": "arvados.groups.destroy",
-          "path": "groups/{uuid}",
+          "id": "arvados.humans.destroy",
+          "path": "humans/{uuid}",
           "httpMethod": "DELETE",
-          "description": "destroy groups",
+          "description": "destroy humans",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -3407,7 +3251,7 @@
             }
           },
           "response": {
-            "$ref": "Group"
+            "$ref": "Human"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
@@ -3415,17 +3259,17 @@
         }
       }
     },
-    "humans": {
+    "jobs": {
       "methods": {
         "get": {
-          "id": "arvados.humans.get",
-          "path": "humans/{uuid}",
+          "id": "arvados.jobs.get",
+          "path": "jobs/{uuid}",
           "httpMethod": "GET",
-          "description": "Gets a Human's metadata by UUID.",
+          "description": "Gets a Job's metadata by UUID.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Human in question.",
+              "description": "The UUID of the Job in question.",
               "required": true,
               "location": "path"
             }
@@ -3434,7 +3278,7 @@
             "uuid"
           ],
           "response": {
-            "$ref": "Human"
+            "$ref": "Job"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -3442,10 +3286,10 @@
           ]
         },
         "index": {
-          "id": "arvados.humans.list",
-          "path": "humans",
+          "id": "arvados.jobs.list",
+          "path": "jobs",
           "httpMethod": "GET",
-          "description": "List Humans.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Humans. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#humanList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List Jobs.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Jobs. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#jobList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -3513,7 +3357,7 @@
             }
           },
           "response": {
-            "$ref": "HumanList"
+            "$ref": "JobList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -3521,10 +3365,10 @@
           ]
         },
         "create": {
-          "id": "arvados.humans.create",
-          "path": "humans",
+          "id": "arvados.jobs.create",
+          "path": "jobs",
           "httpMethod": "POST",
-          "description": "Create a new Human.",
+          "description": "Create a new Job.",
           "parameters": {
             "select": {
               "type": "array",
@@ -3544,32 +3388,57 @@
               "description": "Create object on a remote federated cluster instead of the current one.",
               "location": "query",
               "required": false
+            },
+            "find_or_create": {
+              "type": "boolean",
+              "required": false,
+              "default": "false",
+              "description": "",
+              "location": "query"
+            },
+            "filters": {
+              "type": "array",
+              "required": false,
+              "description": "",
+              "location": "query"
+            },
+            "minimum_script_version": {
+              "type": "string",
+              "required": false,
+              "description": "",
+              "location": "query"
+            },
+            "exclude_script_versions": {
+              "type": "array",
+              "required": false,
+              "description": "",
+              "location": "query"
             }
           },
           "request": {
             "required": true,
             "properties": {
-              "human": {
-                "$ref": "Human"
+              "job": {
+                "$ref": "Job"
               }
             }
           },
           "response": {
-            "$ref": "Human"
+            "$ref": "Job"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "update": {
-          "id": "arvados.humans.update",
-          "path": "humans/{uuid}",
+          "id": "arvados.jobs.update",
+          "path": "jobs/{uuid}",
           "httpMethod": "PUT",
-          "description": "Update attributes of an existing Human.",
+          "description": "Update attributes of an existing Job.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Human in question.",
+              "description": "The UUID of the Job in question.",
               "required": true,
               "location": "path"
             },
@@ -3583,43 +3452,43 @@
           "request": {
             "required": true,
             "properties": {
-              "human": {
-                "$ref": "Human"
+              "job": {
+                "$ref": "Job"
               }
             }
           },
           "response": {
-            "$ref": "Human"
+            "$ref": "Job"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "delete": {
-          "id": "arvados.humans.delete",
-          "path": "humans/{uuid}",
+          "id": "arvados.jobs.delete",
+          "path": "jobs/{uuid}",
           "httpMethod": "DELETE",
-          "description": "Delete an existing Human.",
+          "description": "Delete an existing Job.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Human in question.",
+              "description": "The UUID of the Job in question.",
               "required": true,
               "location": "path"
             }
           },
           "response": {
-            "$ref": "Human"
+            "$ref": "Job"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
-        "list": {
-          "id": "arvados.humans.list",
-          "path": "humans",
+        "queue": {
+          "id": "arvados.jobs.queue",
+          "path": "jobs/queue",
           "httpMethod": "GET",
-          "description": "List Humans.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Humans. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#humanList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "queue jobs",
           "parameters": {
             "filters": {
               "type": "array",
@@ -3687,44 +3556,30 @@
             }
           },
           "response": {
-            "$ref": "HumanList"
+            "$ref": "Job"
           },
           "scopes": [
-            "https://api.arvados.org/auth/arvados",
-            "https://api.arvados.org/auth/arvados.readonly"
+            "https://api.arvados.org/auth/arvados"
           ]
         },
-        "show": {
-          "id": "arvados.humans.show",
-          "path": "humans/{uuid}",
+        "queue_size": {
+          "id": "arvados.jobs.queue_size",
+          "path": "jobs/queue_size",
           "httpMethod": "GET",
-          "description": "show humans",
-          "parameters": {
-            "uuid": {
-              "type": "string",
-              "description": "",
-              "required": true,
-              "location": "path"
-            },
-            "select": {
-              "type": "array",
-              "description": "Attributes of the object to return in the response.",
-              "required": false,
-              "location": "query"
-            }
-          },
+          "description": "queue_size jobs",
+          "parameters": {},
           "response": {
-            "$ref": "Human"
+            "$ref": "Job"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
-        "destroy": {
-          "id": "arvados.humans.destroy",
-          "path": "humans/{uuid}",
-          "httpMethod": "DELETE",
-          "description": "destroy humans",
+        "cancel": {
+          "id": "arvados.jobs.cancel",
+          "path": "jobs/{uuid}/cancel",
+          "httpMethod": "POST",
+          "description": "cancel jobs",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -3734,12 +3589,157 @@
             }
           },
           "response": {
-            "$ref": "Human"
+            "$ref": "Job"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
-        }
+        },
+        "lock": {
+          "id": "arvados.jobs.lock",
+          "path": "jobs/{uuid}/lock",
+          "httpMethod": "POST",
+          "description": "lock jobs",
+          "parameters": {
+            "uuid": {
+              "type": "string",
+              "description": "",
+              "required": true,
+              "location": "path"
+            }
+          },
+          "response": {
+            "$ref": "Job"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados"
+          ]
+        },
+        "list": {
+          "id": "arvados.jobs.list",
+          "path": "jobs",
+          "httpMethod": "GET",
+          "description": "List Jobs.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Jobs. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#jobList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "parameters": {
+            "filters": {
+              "type": "array",
+              "required": false,
+              "description": "",
+              "location": "query"
+            },
+            "where": {
+              "type": "object",
+              "required": false,
+              "description": "",
+              "location": "query"
+            },
+            "order": {
+              "type": "array",
+              "required": false,
+              "description": "",
+              "location": "query"
+            },
+            "select": {
+              "type": "array",
+              "description": "Attributes of each object to return in the response.",
+              "required": false,
+              "location": "query"
+            },
+            "distinct": {
+              "type": "boolean",
+              "required": false,
+              "default": "false",
+              "description": "",
+              "location": "query"
+            },
+            "limit": {
+              "type": "integer",
+              "required": false,
+              "default": "100",
+              "description": "",
+              "location": "query"
+            },
+            "offset": {
+              "type": "integer",
+              "required": false,
+              "default": "0",
+              "description": "",
+              "location": "query"
+            },
+            "count": {
+              "type": "string",
+              "required": false,
+              "default": "exact",
+              "description": "",
+              "location": "query"
+            },
+            "cluster_id": {
+              "type": "string",
+              "description": "List objects on a remote federated cluster instead of the current one.",
+              "location": "query",
+              "required": false
+            },
+            "bypass_federation": {
+              "type": "boolean",
+              "required": false,
+              "description": "bypass federation behavior, list items from local instance database only",
+              "location": "query"
+            }
+          },
+          "response": {
+            "$ref": "JobList"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados",
+            "https://api.arvados.org/auth/arvados.readonly"
+          ]
+        },
+        "show": {
+          "id": "arvados.jobs.show",
+          "path": "jobs/{uuid}",
+          "httpMethod": "GET",
+          "description": "show jobs",
+          "parameters": {
+            "uuid": {
+              "type": "string",
+              "description": "",
+              "required": true,
+              "location": "path"
+            },
+            "select": {
+              "type": "array",
+              "description": "Attributes of the object to return in the response.",
+              "required": false,
+              "location": "query"
+            }
+          },
+          "response": {
+            "$ref": "Job"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados"
+          ]
+        },
+        "destroy": {
+          "id": "arvados.jobs.destroy",
+          "path": "jobs/{uuid}",
+          "httpMethod": "DELETE",
+          "description": "destroy jobs",
+          "parameters": {
+            "uuid": {
+              "type": "string",
+              "description": "",
+              "required": true,
+              "location": "path"
+            }
+          },
+          "response": {
+            "$ref": "Job"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados"
+          ]
+        }
       }
     },
     "job_tasks": {
@@ -5831,17 +5831,17 @@
         }
       }
     },
-    "users": {
+    "pipeline_instances": {
       "methods": {
         "get": {
-          "id": "arvados.users.get",
-          "path": "users/{uuid}",
+          "id": "arvados.pipeline_instances.get",
+          "path": "pipeline_instances/{uuid}",
           "httpMethod": "GET",
-          "description": "Gets a User's metadata by UUID.",
+          "description": "Gets a PipelineInstance's metadata by UUID.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the User in question.",
+              "description": "The UUID of the PipelineInstance in question.",
               "required": true,
               "location": "path"
             }
@@ -5850,7 +5850,7 @@
             "uuid"
           ],
           "response": {
-            "$ref": "User"
+            "$ref": "PipelineInstance"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -5858,10 +5858,10 @@
           ]
         },
         "index": {
-          "id": "arvados.users.list",
-          "path": "users",
+          "id": "arvados.pipeline_instances.list",
+          "path": "pipeline_instances",
           "httpMethod": "GET",
-          "description": "List Users.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Users. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#userList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List PipelineInstances.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching PipelineInstances. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#pipelineInstanceList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -5929,7 +5929,7 @@
             }
           },
           "response": {
-            "$ref": "UserList"
+            "$ref": "PipelineInstanceList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -5937,10 +5937,10 @@
           ]
         },
         "create": {
-          "id": "arvados.users.create",
-          "path": "users",
+          "id": "arvados.pipeline_instances.create",
+          "path": "pipeline_instances",
           "httpMethod": "POST",
-          "description": "Create a new User.",
+          "description": "Create a new PipelineInstance.",
           "parameters": {
             "select": {
               "type": "array",
@@ -5965,27 +5965,27 @@
           "request": {
             "required": true,
             "properties": {
-              "user": {
-                "$ref": "User"
+              "pipeline_instance": {
+                "$ref": "PipelineInstance"
               }
             }
           },
           "response": {
-            "$ref": "User"
+            "$ref": "PipelineInstance"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "update": {
-          "id": "arvados.users.update",
-          "path": "users/{uuid}",
+          "id": "arvados.pipeline_instances.update",
+          "path": "pipeline_instances/{uuid}",
           "httpMethod": "PUT",
-          "description": "Update attributes of an existing User.",
+          "description": "Update attributes of an existing PipelineInstance.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the User in question.",
+              "description": "The UUID of the PipelineInstance in question.",
               "required": true,
               "location": "path"
             },
@@ -5994,81 +5994,48 @@
               "description": "Attributes of the updated object to return in the response.",
               "required": false,
               "location": "query"
-            },
-            "bypass_federation": {
-              "type": "boolean",
-              "required": false,
-              "default": "false",
-              "description": "",
-              "location": "query"
             }
           },
           "request": {
             "required": true,
             "properties": {
-              "user": {
-                "$ref": "User"
+              "pipeline_instance": {
+                "$ref": "PipelineInstance"
               }
             }
           },
           "response": {
-            "$ref": "User"
+            "$ref": "PipelineInstance"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "delete": {
-          "id": "arvados.users.delete",
-          "path": "users/{uuid}",
+          "id": "arvados.pipeline_instances.delete",
+          "path": "pipeline_instances/{uuid}",
           "httpMethod": "DELETE",
-          "description": "Delete an existing User.",
+          "description": "Delete an existing PipelineInstance.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the User in question.",
+              "description": "The UUID of the PipelineInstance in question.",
               "required": true,
               "location": "path"
             }
           },
           "response": {
-            "$ref": "User"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados"
-          ]
-        },
-        "current": {
-          "id": "arvados.users.current",
-          "path": "users/current",
-          "httpMethod": "GET",
-          "description": "current users",
-          "parameters": {},
-          "response": {
-            "$ref": "User"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados"
-          ]
-        },
-        "system": {
-          "id": "arvados.users.system",
-          "path": "users/system",
-          "httpMethod": "GET",
-          "description": "system users",
-          "parameters": {},
-          "response": {
-            "$ref": "User"
+            "$ref": "PipelineInstance"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
-        "activate": {
-          "id": "arvados.users.activate",
-          "path": "users/{uuid}/activate",
+        "cancel": {
+          "id": "arvados.pipeline_instances.cancel",
+          "path": "pipeline_instances/{uuid}/cancel",
           "httpMethod": "POST",
-          "description": "activate users",
+          "description": "cancel pipeline_instances",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -6078,127 +6045,17 @@
             }
           },
           "response": {
-            "$ref": "User"
+            "$ref": "PipelineInstance"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
-        "setup": {
-          "id": "arvados.users.setup",
-          "path": "users/setup",
-          "httpMethod": "POST",
-          "description": "setup users",
-          "parameters": {
-            "uuid": {
-              "type": "string",
-              "required": false,
-              "description": "",
-              "location": "query"
-            },
-            "user": {
-              "type": "object",
-              "required": false,
-              "description": "",
-              "location": "query"
-            },
-            "repo_name": {
-              "type": "string",
-              "required": false,
-              "description": "",
-              "location": "query"
-            },
-            "vm_uuid": {
-              "type": "string",
-              "required": false,
-              "description": "",
-              "location": "query"
-            },
-            "send_notification_email": {
-              "type": "boolean",
-              "required": false,
-              "default": "false",
-              "description": "",
-              "location": "query"
-            }
-          },
-          "response": {
-            "$ref": "User"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados"
-          ]
-        },
-        "unsetup": {
-          "id": "arvados.users.unsetup",
-          "path": "users/{uuid}/unsetup",
-          "httpMethod": "POST",
-          "description": "unsetup users",
-          "parameters": {
-            "uuid": {
-              "type": "string",
-              "description": "",
-              "required": true,
-              "location": "path"
-            }
-          },
-          "response": {
-            "$ref": "User"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados"
-          ]
-        },
-        "merge": {
-          "id": "arvados.users.merge",
-          "path": "users/merge",
-          "httpMethod": "POST",
-          "description": "merge users",
-          "parameters": {
-            "new_owner_uuid": {
-              "type": "string",
-              "required": true,
-              "description": "",
-              "location": "query"
-            },
-            "new_user_token": {
-              "type": "string",
-              "required": false,
-              "description": "",
-              "location": "query"
-            },
-            "redirect_to_new_user": {
-              "type": "boolean",
-              "required": false,
-              "default": "false",
-              "description": "",
-              "location": "query"
-            },
-            "old_user_uuid": {
-              "type": "string",
-              "required": false,
-              "description": "",
-              "location": "query"
-            },
-            "new_user_uuid": {
-              "type": "string",
-              "required": false,
-              "description": "",
-              "location": "query"
-            }
-          },
-          "response": {
-            "$ref": "User"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados"
-          ]
-        },
-        "list": {
-          "id": "arvados.users.list",
-          "path": "users",
-          "httpMethod": "GET",
-          "description": "List Users.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Users. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#userList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+        "list": {
+          "id": "arvados.pipeline_instances.list",
+          "path": "pipeline_instances",
+          "httpMethod": "GET",
+          "description": "List PipelineInstances.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching PipelineInstances. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#pipelineInstanceList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -6266,7 +6123,7 @@
             }
           },
           "response": {
-            "$ref": "UserList"
+            "$ref": "PipelineInstanceList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -6274,10 +6131,10 @@
           ]
         },
         "show": {
-          "id": "arvados.users.show",
-          "path": "users/{uuid}",
+          "id": "arvados.pipeline_instances.show",
+          "path": "pipeline_instances/{uuid}",
           "httpMethod": "GET",
-          "description": "show users",
+          "description": "show pipeline_instances",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -6293,17 +6150,17 @@
             }
           },
           "response": {
-            "$ref": "User"
+            "$ref": "PipelineInstance"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "destroy": {
-          "id": "arvados.users.destroy",
-          "path": "users/{uuid}",
+          "id": "arvados.pipeline_instances.destroy",
+          "path": "pipeline_instances/{uuid}",
           "httpMethod": "DELETE",
-          "description": "destroy users",
+          "description": "destroy pipeline_instances",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -6313,7 +6170,7 @@
             }
           },
           "response": {
-            "$ref": "User"
+            "$ref": "PipelineInstance"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
@@ -6321,17 +6178,17 @@
         }
       }
     },
-    "pipeline_instances": {
+    "pipeline_templates": {
       "methods": {
         "get": {
-          "id": "arvados.pipeline_instances.get",
-          "path": "pipeline_instances/{uuid}",
+          "id": "arvados.pipeline_templates.get",
+          "path": "pipeline_templates/{uuid}",
           "httpMethod": "GET",
-          "description": "Gets a PipelineInstance's metadata by UUID.",
+          "description": "Gets a PipelineTemplate's metadata by UUID.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the PipelineInstance in question.",
+              "description": "The UUID of the PipelineTemplate in question.",
               "required": true,
               "location": "path"
             }
@@ -6340,7 +6197,7 @@
             "uuid"
           ],
           "response": {
-            "$ref": "PipelineInstance"
+            "$ref": "PipelineTemplate"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -6348,10 +6205,10 @@
           ]
         },
         "index": {
-          "id": "arvados.pipeline_instances.list",
-          "path": "pipeline_instances",
+          "id": "arvados.pipeline_templates.list",
+          "path": "pipeline_templates",
           "httpMethod": "GET",
-          "description": "List PipelineInstances.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching PipelineInstances. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#pipelineInstanceList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List PipelineTemplates.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching PipelineTemplates. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#pipelineTemplateList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -6419,7 +6276,7 @@
             }
           },
           "response": {
-            "$ref": "PipelineInstanceList"
+            "$ref": "PipelineTemplateList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -6427,10 +6284,10 @@
           ]
         },
         "create": {
-          "id": "arvados.pipeline_instances.create",
-          "path": "pipeline_instances",
+          "id": "arvados.pipeline_templates.create",
+          "path": "pipeline_templates",
           "httpMethod": "POST",
-          "description": "Create a new PipelineInstance.",
+          "description": "Create a new PipelineTemplate.",
           "parameters": {
             "select": {
               "type": "array",
@@ -6455,27 +6312,27 @@
           "request": {
             "required": true,
             "properties": {
-              "pipeline_instance": {
-                "$ref": "PipelineInstance"
+              "pipeline_template": {
+                "$ref": "PipelineTemplate"
               }
             }
           },
           "response": {
-            "$ref": "PipelineInstance"
+            "$ref": "PipelineTemplate"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "update": {
-          "id": "arvados.pipeline_instances.update",
-          "path": "pipeline_instances/{uuid}",
+          "id": "arvados.pipeline_templates.update",
+          "path": "pipeline_templates/{uuid}",
           "httpMethod": "PUT",
-          "description": "Update attributes of an existing PipelineInstance.",
+          "description": "Update attributes of an existing PipelineTemplate.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the PipelineInstance in question.",
+              "description": "The UUID of the PipelineTemplate in question.",
               "required": true,
               "location": "path"
             },
@@ -6489,63 +6346,43 @@
           "request": {
             "required": true,
             "properties": {
-              "pipeline_instance": {
-                "$ref": "PipelineInstance"
+              "pipeline_template": {
+                "$ref": "PipelineTemplate"
               }
             }
           },
           "response": {
-            "$ref": "PipelineInstance"
+            "$ref": "PipelineTemplate"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "delete": {
-          "id": "arvados.pipeline_instances.delete",
-          "path": "pipeline_instances/{uuid}",
+          "id": "arvados.pipeline_templates.delete",
+          "path": "pipeline_templates/{uuid}",
           "httpMethod": "DELETE",
-          "description": "Delete an existing PipelineInstance.",
-          "parameters": {
-            "uuid": {
-              "type": "string",
-              "description": "The UUID of the PipelineInstance in question.",
-              "required": true,
-              "location": "path"
-            }
-          },
-          "response": {
-            "$ref": "PipelineInstance"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados"
-          ]
-        },
-        "cancel": {
-          "id": "arvados.pipeline_instances.cancel",
-          "path": "pipeline_instances/{uuid}/cancel",
-          "httpMethod": "POST",
-          "description": "cancel pipeline_instances",
+          "description": "Delete an existing PipelineTemplate.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "",
+              "description": "The UUID of the PipelineTemplate in question.",
               "required": true,
               "location": "path"
             }
           },
           "response": {
-            "$ref": "PipelineInstance"
+            "$ref": "PipelineTemplate"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "list": {
-          "id": "arvados.pipeline_instances.list",
-          "path": "pipeline_instances",
+          "id": "arvados.pipeline_templates.list",
+          "path": "pipeline_templates",
           "httpMethod": "GET",
-          "description": "List PipelineInstances.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching PipelineInstances. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#pipelineInstanceList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List PipelineTemplates.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching PipelineTemplates. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#pipelineTemplateList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -6613,7 +6450,7 @@
             }
           },
           "response": {
-            "$ref": "PipelineInstanceList"
+            "$ref": "PipelineTemplateList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -6621,10 +6458,10 @@
           ]
         },
         "show": {
-          "id": "arvados.pipeline_instances.show",
-          "path": "pipeline_instances/{uuid}",
+          "id": "arvados.pipeline_templates.show",
+          "path": "pipeline_templates/{uuid}",
           "httpMethod": "GET",
-          "description": "show pipeline_instances",
+          "description": "show pipeline_templates",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -6640,17 +6477,17 @@
             }
           },
           "response": {
-            "$ref": "PipelineInstance"
+            "$ref": "PipelineTemplate"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "destroy": {
-          "id": "arvados.pipeline_instances.destroy",
-          "path": "pipeline_instances/{uuid}",
+          "id": "arvados.pipeline_templates.destroy",
+          "path": "pipeline_templates/{uuid}",
           "httpMethod": "DELETE",
-          "description": "destroy pipeline_instances",
+          "description": "destroy pipeline_templates",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -6660,7 +6497,7 @@
             }
           },
           "response": {
-            "$ref": "PipelineInstance"
+            "$ref": "PipelineTemplate"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
@@ -6668,17 +6505,17 @@
         }
       }
     },
-    "pipeline_templates": {
+    "repositories": {
       "methods": {
         "get": {
-          "id": "arvados.pipeline_templates.get",
-          "path": "pipeline_templates/{uuid}",
+          "id": "arvados.repositories.get",
+          "path": "repositories/{uuid}",
           "httpMethod": "GET",
-          "description": "Gets a PipelineTemplate's metadata by UUID.",
+          "description": "Gets a Repository's metadata by UUID.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the PipelineTemplate in question.",
+              "description": "The UUID of the Repository in question.",
               "required": true,
               "location": "path"
             }
@@ -6687,7 +6524,7 @@
             "uuid"
           ],
           "response": {
-            "$ref": "PipelineTemplate"
+            "$ref": "Repository"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -6695,10 +6532,10 @@
           ]
         },
         "index": {
-          "id": "arvados.pipeline_templates.list",
-          "path": "pipeline_templates",
+          "id": "arvados.repositories.list",
+          "path": "repositories",
           "httpMethod": "GET",
-          "description": "List PipelineTemplates.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching PipelineTemplates. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#pipelineTemplateList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List Repositories.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Repositories. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#repositoryList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -6766,7 +6603,7 @@
             }
           },
           "response": {
-            "$ref": "PipelineTemplateList"
+            "$ref": "RepositoryList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -6774,10 +6611,10 @@
           ]
         },
         "create": {
-          "id": "arvados.pipeline_templates.create",
-          "path": "pipeline_templates",
+          "id": "arvados.repositories.create",
+          "path": "repositories",
           "httpMethod": "POST",
-          "description": "Create a new PipelineTemplate.",
+          "description": "Create a new Repository.",
           "parameters": {
             "select": {
               "type": "array",
@@ -6802,27 +6639,27 @@
           "request": {
             "required": true,
             "properties": {
-              "pipeline_template": {
-                "$ref": "PipelineTemplate"
-              }
+              "repository": {
+                "$ref": "Repository"
+              }
             }
           },
           "response": {
-            "$ref": "PipelineTemplate"
+            "$ref": "Repository"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "update": {
-          "id": "arvados.pipeline_templates.update",
-          "path": "pipeline_templates/{uuid}",
+          "id": "arvados.repositories.update",
+          "path": "repositories/{uuid}",
           "httpMethod": "PUT",
-          "description": "Update attributes of an existing PipelineTemplate.",
+          "description": "Update attributes of an existing Repository.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the PipelineTemplate in question.",
+              "description": "The UUID of the Repository in question.",
               "required": true,
               "location": "path"
             },
@@ -6836,43 +6673,56 @@
           "request": {
             "required": true,
             "properties": {
-              "pipeline_template": {
-                "$ref": "PipelineTemplate"
+              "repository": {
+                "$ref": "Repository"
               }
             }
           },
           "response": {
-            "$ref": "PipelineTemplate"
+            "$ref": "Repository"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "delete": {
-          "id": "arvados.pipeline_templates.delete",
-          "path": "pipeline_templates/{uuid}",
+          "id": "arvados.repositories.delete",
+          "path": "repositories/{uuid}",
           "httpMethod": "DELETE",
-          "description": "Delete an existing PipelineTemplate.",
+          "description": "Delete an existing Repository.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the PipelineTemplate in question.",
+              "description": "The UUID of the Repository in question.",
               "required": true,
               "location": "path"
             }
           },
           "response": {
-            "$ref": "PipelineTemplate"
+            "$ref": "Repository"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados"
+          ]
+        },
+        "get_all_permissions": {
+          "id": "arvados.repositories.get_all_permissions",
+          "path": "repositories/get_all_permissions",
+          "httpMethod": "GET",
+          "description": "get_all_permissions repositories",
+          "parameters": {},
+          "response": {
+            "$ref": "Repository"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "list": {
-          "id": "arvados.pipeline_templates.list",
-          "path": "pipeline_templates",
+          "id": "arvados.repositories.list",
+          "path": "repositories",
           "httpMethod": "GET",
-          "description": "List PipelineTemplates.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching PipelineTemplates. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#pipelineTemplateList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List Repositories.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Repositories. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#repositoryList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -6940,7 +6790,7 @@
             }
           },
           "response": {
-            "$ref": "PipelineTemplateList"
+            "$ref": "RepositoryList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -6948,10 +6798,10 @@
           ]
         },
         "show": {
-          "id": "arvados.pipeline_templates.show",
-          "path": "pipeline_templates/{uuid}",
+          "id": "arvados.repositories.show",
+          "path": "repositories/{uuid}",
           "httpMethod": "GET",
-          "description": "show pipeline_templates",
+          "description": "show repositories",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -6967,17 +6817,17 @@
             }
           },
           "response": {
-            "$ref": "PipelineTemplate"
+            "$ref": "Repository"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "destroy": {
-          "id": "arvados.pipeline_templates.destroy",
-          "path": "pipeline_templates/{uuid}",
+          "id": "arvados.repositories.destroy",
+          "path": "repositories/{uuid}",
           "httpMethod": "DELETE",
-          "description": "destroy pipeline_templates",
+          "description": "destroy repositories",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -6987,7 +6837,7 @@
             }
           },
           "response": {
-            "$ref": "PipelineTemplate"
+            "$ref": "Repository"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
@@ -6995,17 +6845,17 @@
         }
       }
     },
-    "repositories": {
+    "specimens": {
       "methods": {
         "get": {
-          "id": "arvados.repositories.get",
-          "path": "repositories/{uuid}",
+          "id": "arvados.specimens.get",
+          "path": "specimens/{uuid}",
           "httpMethod": "GET",
-          "description": "Gets a Repository's metadata by UUID.",
+          "description": "Gets a Specimen's metadata by UUID.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Repository in question.",
+              "description": "The UUID of the Specimen in question.",
               "required": true,
               "location": "path"
             }
@@ -7014,7 +6864,7 @@
             "uuid"
           ],
           "response": {
-            "$ref": "Repository"
+            "$ref": "Specimen"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -7022,10 +6872,10 @@
           ]
         },
         "index": {
-          "id": "arvados.repositories.list",
-          "path": "repositories",
+          "id": "arvados.specimens.list",
+          "path": "specimens",
           "httpMethod": "GET",
-          "description": "List Repositories.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Repositories. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#repositoryList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List Specimens.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Specimens. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#specimenList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -7093,7 +6943,7 @@
             }
           },
           "response": {
-            "$ref": "RepositoryList"
+            "$ref": "SpecimenList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -7101,10 +6951,10 @@
           ]
         },
         "create": {
-          "id": "arvados.repositories.create",
-          "path": "repositories",
+          "id": "arvados.specimens.create",
+          "path": "specimens",
           "httpMethod": "POST",
-          "description": "Create a new Repository.",
+          "description": "Create a new Specimen.",
           "parameters": {
             "select": {
               "type": "array",
@@ -7129,27 +6979,27 @@
           "request": {
             "required": true,
             "properties": {
-              "repository": {
-                "$ref": "Repository"
+              "specimen": {
+                "$ref": "Specimen"
               }
             }
           },
           "response": {
-            "$ref": "Repository"
+            "$ref": "Specimen"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "update": {
-          "id": "arvados.repositories.update",
-          "path": "repositories/{uuid}",
+          "id": "arvados.specimens.update",
+          "path": "specimens/{uuid}",
           "httpMethod": "PUT",
-          "description": "Update attributes of an existing Repository.",
+          "description": "Update attributes of an existing Specimen.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Repository in question.",
+              "description": "The UUID of the Specimen in question.",
               "required": true,
               "location": "path"
             },
@@ -7163,56 +7013,43 @@
           "request": {
             "required": true,
             "properties": {
-              "repository": {
-                "$ref": "Repository"
+              "specimen": {
+                "$ref": "Specimen"
               }
             }
           },
           "response": {
-            "$ref": "Repository"
+            "$ref": "Specimen"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "delete": {
-          "id": "arvados.repositories.delete",
-          "path": "repositories/{uuid}",
+          "id": "arvados.specimens.delete",
+          "path": "specimens/{uuid}",
           "httpMethod": "DELETE",
-          "description": "Delete an existing Repository.",
+          "description": "Delete an existing Specimen.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Repository in question.",
+              "description": "The UUID of the Specimen in question.",
               "required": true,
               "location": "path"
             }
           },
           "response": {
-            "$ref": "Repository"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados"
-          ]
-        },
-        "get_all_permissions": {
-          "id": "arvados.repositories.get_all_permissions",
-          "path": "repositories/get_all_permissions",
-          "httpMethod": "GET",
-          "description": "get_all_permissions repositories",
-          "parameters": {},
-          "response": {
-            "$ref": "Repository"
+            "$ref": "Specimen"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "list": {
-          "id": "arvados.repositories.list",
-          "path": "repositories",
+          "id": "arvados.specimens.list",
+          "path": "specimens",
           "httpMethod": "GET",
-          "description": "List Repositories.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Repositories. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#repositoryList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List Specimens.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Specimens. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#specimenList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -7280,7 +7117,7 @@
             }
           },
           "response": {
-            "$ref": "RepositoryList"
+            "$ref": "SpecimenList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -7288,10 +7125,10 @@
           ]
         },
         "show": {
-          "id": "arvados.repositories.show",
-          "path": "repositories/{uuid}",
+          "id": "arvados.specimens.show",
+          "path": "specimens/{uuid}",
           "httpMethod": "GET",
-          "description": "show repositories",
+          "description": "show specimens",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -7307,17 +7144,17 @@
             }
           },
           "response": {
-            "$ref": "Repository"
+            "$ref": "Specimen"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "destroy": {
-          "id": "arvados.repositories.destroy",
-          "path": "repositories/{uuid}",
+          "id": "arvados.specimens.destroy",
+          "path": "specimens/{uuid}",
           "httpMethod": "DELETE",
-          "description": "destroy repositories",
+          "description": "destroy specimens",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -7327,7 +7164,7 @@
             }
           },
           "response": {
-            "$ref": "Repository"
+            "$ref": "Specimen"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
@@ -7335,17 +7172,17 @@
         }
       }
     },
-    "specimens": {
+    "traits": {
       "methods": {
         "get": {
-          "id": "arvados.specimens.get",
-          "path": "specimens/{uuid}",
+          "id": "arvados.traits.get",
+          "path": "traits/{uuid}",
           "httpMethod": "GET",
-          "description": "Gets a Specimen's metadata by UUID.",
+          "description": "Gets a Trait's metadata by UUID.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Specimen in question.",
+              "description": "The UUID of the Trait in question.",
               "required": true,
               "location": "path"
             }
@@ -7354,7 +7191,7 @@
             "uuid"
           ],
           "response": {
-            "$ref": "Specimen"
+            "$ref": "Trait"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -7362,10 +7199,10 @@
           ]
         },
         "index": {
-          "id": "arvados.specimens.list",
-          "path": "specimens",
+          "id": "arvados.traits.list",
+          "path": "traits",
           "httpMethod": "GET",
-          "description": "List Specimens.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Specimens. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#specimenList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List Traits.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Traits. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#traitList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -7433,7 +7270,7 @@
             }
           },
           "response": {
-            "$ref": "SpecimenList"
+            "$ref": "TraitList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -7441,10 +7278,10 @@
           ]
         },
         "create": {
-          "id": "arvados.specimens.create",
-          "path": "specimens",
+          "id": "arvados.traits.create",
+          "path": "traits",
           "httpMethod": "POST",
-          "description": "Create a new Specimen.",
+          "description": "Create a new Trait.",
           "parameters": {
             "select": {
               "type": "array",
@@ -7469,27 +7306,27 @@
           "request": {
             "required": true,
             "properties": {
-              "specimen": {
-                "$ref": "Specimen"
+              "trait": {
+                "$ref": "Trait"
               }
             }
           },
           "response": {
-            "$ref": "Specimen"
+            "$ref": "Trait"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "update": {
-          "id": "arvados.specimens.update",
-          "path": "specimens/{uuid}",
+          "id": "arvados.traits.update",
+          "path": "traits/{uuid}",
           "httpMethod": "PUT",
-          "description": "Update attributes of an existing Specimen.",
+          "description": "Update attributes of an existing Trait.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Specimen in question.",
+              "description": "The UUID of the Trait in question.",
               "required": true,
               "location": "path"
             },
@@ -7503,43 +7340,43 @@
           "request": {
             "required": true,
             "properties": {
-              "specimen": {
-                "$ref": "Specimen"
+              "trait": {
+                "$ref": "Trait"
               }
             }
           },
           "response": {
-            "$ref": "Specimen"
+            "$ref": "Trait"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "delete": {
-          "id": "arvados.specimens.delete",
-          "path": "specimens/{uuid}",
+          "id": "arvados.traits.delete",
+          "path": "traits/{uuid}",
           "httpMethod": "DELETE",
-          "description": "Delete an existing Specimen.",
+          "description": "Delete an existing Trait.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Specimen in question.",
+              "description": "The UUID of the Trait in question.",
               "required": true,
               "location": "path"
             }
           },
           "response": {
-            "$ref": "Specimen"
+            "$ref": "Trait"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "list": {
-          "id": "arvados.specimens.list",
-          "path": "specimens",
+          "id": "arvados.traits.list",
+          "path": "traits",
           "httpMethod": "GET",
-          "description": "List Specimens.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Specimens. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#specimenList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List Traits.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Traits. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#traitList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -7607,7 +7444,7 @@
             }
           },
           "response": {
-            "$ref": "SpecimenList"
+            "$ref": "TraitList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -7615,10 +7452,10 @@
           ]
         },
         "show": {
-          "id": "arvados.specimens.show",
-          "path": "specimens/{uuid}",
+          "id": "arvados.traits.show",
+          "path": "traits/{uuid}",
           "httpMethod": "GET",
-          "description": "show specimens",
+          "description": "show traits",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -7634,17 +7471,17 @@
             }
           },
           "response": {
-            "$ref": "Specimen"
+            "$ref": "Trait"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "destroy": {
-          "id": "arvados.specimens.destroy",
-          "path": "specimens/{uuid}",
+          "id": "arvados.traits.destroy",
+          "path": "traits/{uuid}",
           "httpMethod": "DELETE",
-          "description": "destroy specimens",
+          "description": "destroy traits",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -7654,7 +7491,7 @@
             }
           },
           "response": {
-            "$ref": "Specimen"
+            "$ref": "Trait"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
@@ -7662,17 +7499,17 @@
         }
       }
     },
-    "traits": {
+    "users": {
       "methods": {
         "get": {
-          "id": "arvados.traits.get",
-          "path": "traits/{uuid}",
+          "id": "arvados.users.get",
+          "path": "users/{uuid}",
           "httpMethod": "GET",
-          "description": "Gets a Trait's metadata by UUID.",
+          "description": "Gets a User's metadata by UUID.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Trait in question.",
+              "description": "The UUID of the User in question.",
               "required": true,
               "location": "path"
             }
@@ -7681,7 +7518,7 @@
             "uuid"
           ],
           "response": {
-            "$ref": "Trait"
+            "$ref": "User"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -7689,10 +7526,10 @@
           ]
         },
         "index": {
-          "id": "arvados.traits.list",
-          "path": "traits",
+          "id": "arvados.users.list",
+          "path": "users",
           "httpMethod": "GET",
-          "description": "List Traits.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Traits. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#traitList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List Users.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Users. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#userList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -7760,7 +7597,7 @@
             }
           },
           "response": {
-            "$ref": "TraitList"
+            "$ref": "UserList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -7768,10 +7605,10 @@
           ]
         },
         "create": {
-          "id": "arvados.traits.create",
-          "path": "traits",
+          "id": "arvados.users.create",
+          "path": "users",
           "httpMethod": "POST",
-          "description": "Create a new Trait.",
+          "description": "Create a new User.",
           "parameters": {
             "select": {
               "type": "array",
@@ -7796,27 +7633,27 @@
           "request": {
             "required": true,
             "properties": {
-              "trait": {
-                "$ref": "Trait"
+              "user": {
+                "$ref": "User"
               }
             }
           },
           "response": {
-            "$ref": "Trait"
+            "$ref": "User"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "update": {
-          "id": "arvados.traits.update",
-          "path": "traits/{uuid}",
+          "id": "arvados.users.update",
+          "path": "users/{uuid}",
           "httpMethod": "PUT",
-          "description": "Update attributes of an existing Trait.",
+          "description": "Update attributes of an existing User.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Trait in question.",
+              "description": "The UUID of the User in question.",
               "required": true,
               "location": "path"
             },
@@ -7825,48 +7662,211 @@
               "description": "Attributes of the updated object to return in the response.",
               "required": false,
               "location": "query"
+            },
+            "bypass_federation": {
+              "type": "boolean",
+              "required": false,
+              "default": "false",
+              "description": "",
+              "location": "query"
             }
           },
           "request": {
             "required": true,
             "properties": {
-              "trait": {
-                "$ref": "Trait"
+              "user": {
+                "$ref": "User"
               }
             }
           },
           "response": {
-            "$ref": "Trait"
+            "$ref": "User"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "delete": {
-          "id": "arvados.traits.delete",
-          "path": "traits/{uuid}",
+          "id": "arvados.users.delete",
+          "path": "users/{uuid}",
           "httpMethod": "DELETE",
-          "description": "Delete an existing Trait.",
+          "description": "Delete an existing User.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Trait in question.",
+              "description": "The UUID of the User in question.",
               "required": true,
               "location": "path"
             }
           },
           "response": {
-            "$ref": "Trait"
+            "$ref": "User"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados"
+          ]
+        },
+        "current": {
+          "id": "arvados.users.current",
+          "path": "users/current",
+          "httpMethod": "GET",
+          "description": "current users",
+          "parameters": {},
+          "response": {
+            "$ref": "User"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados"
+          ]
+        },
+        "system": {
+          "id": "arvados.users.system",
+          "path": "users/system",
+          "httpMethod": "GET",
+          "description": "system users",
+          "parameters": {},
+          "response": {
+            "$ref": "User"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados"
+          ]
+        },
+        "activate": {
+          "id": "arvados.users.activate",
+          "path": "users/{uuid}/activate",
+          "httpMethod": "POST",
+          "description": "activate users",
+          "parameters": {
+            "uuid": {
+              "type": "string",
+              "description": "",
+              "required": true,
+              "location": "path"
+            }
+          },
+          "response": {
+            "$ref": "User"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados"
+          ]
+        },
+        "setup": {
+          "id": "arvados.users.setup",
+          "path": "users/setup",
+          "httpMethod": "POST",
+          "description": "setup users",
+          "parameters": {
+            "uuid": {
+              "type": "string",
+              "required": false,
+              "description": "",
+              "location": "query"
+            },
+            "user": {
+              "type": "object",
+              "required": false,
+              "description": "",
+              "location": "query"
+            },
+            "repo_name": {
+              "type": "string",
+              "required": false,
+              "description": "",
+              "location": "query"
+            },
+            "vm_uuid": {
+              "type": "string",
+              "required": false,
+              "description": "",
+              "location": "query"
+            },
+            "send_notification_email": {
+              "type": "boolean",
+              "required": false,
+              "default": "false",
+              "description": "",
+              "location": "query"
+            }
+          },
+          "response": {
+            "$ref": "User"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados"
+          ]
+        },
+        "unsetup": {
+          "id": "arvados.users.unsetup",
+          "path": "users/{uuid}/unsetup",
+          "httpMethod": "POST",
+          "description": "unsetup users",
+          "parameters": {
+            "uuid": {
+              "type": "string",
+              "description": "",
+              "required": true,
+              "location": "path"
+            }
+          },
+          "response": {
+            "$ref": "User"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados"
+          ]
+        },
+        "merge": {
+          "id": "arvados.users.merge",
+          "path": "users/merge",
+          "httpMethod": "POST",
+          "description": "merge users",
+          "parameters": {
+            "new_owner_uuid": {
+              "type": "string",
+              "required": true,
+              "description": "",
+              "location": "query"
+            },
+            "new_user_token": {
+              "type": "string",
+              "required": false,
+              "description": "",
+              "location": "query"
+            },
+            "redirect_to_new_user": {
+              "type": "boolean",
+              "required": false,
+              "default": "false",
+              "description": "",
+              "location": "query"
+            },
+            "old_user_uuid": {
+              "type": "string",
+              "required": false,
+              "description": "",
+              "location": "query"
+            },
+            "new_user_uuid": {
+              "type": "string",
+              "required": false,
+              "description": "",
+              "location": "query"
+            }
+          },
+          "response": {
+            "$ref": "User"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "list": {
-          "id": "arvados.traits.list",
-          "path": "traits",
+          "id": "arvados.users.list",
+          "path": "users",
           "httpMethod": "GET",
-          "description": "List Traits.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Traits. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#traitList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List Users.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Users. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#userList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -7934,7 +7934,7 @@
             }
           },
           "response": {
-            "$ref": "TraitList"
+            "$ref": "UserList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -7942,10 +7942,10 @@
           ]
         },
         "show": {
-          "id": "arvados.traits.show",
-          "path": "traits/{uuid}",
+          "id": "arvados.users.show",
+          "path": "users/{uuid}",
           "httpMethod": "GET",
-          "description": "show traits",
+          "description": "show users",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -7961,17 +7961,17 @@
             }
           },
           "response": {
-            "$ref": "Trait"
+            "$ref": "User"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "destroy": {
-          "id": "arvados.traits.destroy",
-          "path": "traits/{uuid}",
+          "id": "arvados.users.destroy",
+          "path": "users/{uuid}",
           "httpMethod": "DELETE",
-          "description": "destroy traits",
+          "description": "destroy users",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -7981,7 +7981,7 @@
             }
           },
           "response": {
-            "$ref": "Trait"
+            "$ref": "User"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
@@ -7989,17 +7989,17 @@
         }
       }
     },
-    "virtual_machines": {
+    "user_agreements": {
       "methods": {
         "get": {
-          "id": "arvados.virtual_machines.get",
-          "path": "virtual_machines/{uuid}",
+          "id": "arvados.user_agreements.get",
+          "path": "user_agreements/{uuid}",
           "httpMethod": "GET",
-          "description": "Gets a VirtualMachine's metadata by UUID.",
+          "description": "Gets a UserAgreement's metadata by UUID.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the VirtualMachine in question.",
+              "description": "The UUID of the UserAgreement in question.",
               "required": true,
               "location": "path"
             }
@@ -8008,7 +8008,7 @@
             "uuid"
           ],
           "response": {
-            "$ref": "VirtualMachine"
+            "$ref": "UserAgreement"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -8016,10 +8016,10 @@
           ]
         },
         "index": {
-          "id": "arvados.virtual_machines.list",
-          "path": "virtual_machines",
+          "id": "arvados.user_agreements.list",
+          "path": "user_agreements",
           "httpMethod": "GET",
-          "description": "List VirtualMachines.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching VirtualMachines. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#virtualMachineList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List UserAgreements.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching UserAgreements. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#userAgreementList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -8087,7 +8087,7 @@
             }
           },
           "response": {
-            "$ref": "VirtualMachineList"
+            "$ref": "UserAgreementList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -8095,10 +8095,10 @@
           ]
         },
         "create": {
-          "id": "arvados.virtual_machines.create",
-          "path": "virtual_machines",
+          "id": "arvados.user_agreements.create",
+          "path": "user_agreements",
           "httpMethod": "POST",
-          "description": "Create a new VirtualMachine.",
+          "description": "Create a new UserAgreement.",
           "parameters": {
             "select": {
               "type": "array",
@@ -8123,27 +8123,27 @@
           "request": {
             "required": true,
             "properties": {
-              "virtual_machine": {
-                "$ref": "VirtualMachine"
+              "user_agreement": {
+                "$ref": "UserAgreement"
               }
             }
           },
           "response": {
-            "$ref": "VirtualMachine"
+            "$ref": "UserAgreement"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "update": {
-          "id": "arvados.virtual_machines.update",
-          "path": "virtual_machines/{uuid}",
+          "id": "arvados.user_agreements.update",
+          "path": "user_agreements/{uuid}",
           "httpMethod": "PUT",
-          "description": "Update attributes of an existing VirtualMachine.",
+          "description": "Update attributes of an existing UserAgreement.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the VirtualMachine in question.",
+              "description": "The UUID of the UserAgreement in question.",
               "required": true,
               "location": "path"
             },
@@ -8157,76 +8157,69 @@
           "request": {
             "required": true,
             "properties": {
-              "virtual_machine": {
-                "$ref": "VirtualMachine"
+              "user_agreement": {
+                "$ref": "UserAgreement"
               }
             }
           },
           "response": {
-            "$ref": "VirtualMachine"
+            "$ref": "UserAgreement"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "delete": {
-          "id": "arvados.virtual_machines.delete",
-          "path": "virtual_machines/{uuid}",
+          "id": "arvados.user_agreements.delete",
+          "path": "user_agreements/{uuid}",
           "httpMethod": "DELETE",
-          "description": "Delete an existing VirtualMachine.",
+          "description": "Delete an existing UserAgreement.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the VirtualMachine in question.",
+              "description": "The UUID of the UserAgreement in question.",
               "required": true,
               "location": "path"
             }
           },
           "response": {
-            "$ref": "VirtualMachine"
+            "$ref": "UserAgreement"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
-        "logins": {
-          "id": "arvados.virtual_machines.logins",
-          "path": "virtual_machines/{uuid}/logins",
+        "signatures": {
+          "id": "arvados.user_agreements.signatures",
+          "path": "user_agreements/signatures",
           "httpMethod": "GET",
-          "description": "logins virtual_machines",
-          "parameters": {
-            "uuid": {
-              "type": "string",
-              "description": "",
-              "required": true,
-              "location": "path"
-            }
-          },
+          "description": "signatures user_agreements",
+          "parameters": {},
           "response": {
-            "$ref": "VirtualMachine"
+            "$ref": "UserAgreement"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
-        "get_all_logins": {
-          "id": "arvados.virtual_machines.get_all_logins",
-          "path": "virtual_machines/get_all_logins",
-          "httpMethod": "GET",
-          "description": "get_all_logins virtual_machines",
+        "sign": {
+          "id": "arvados.user_agreements.sign",
+          "path": "user_agreements/sign",
+          "httpMethod": "POST",
+          "description": "sign user_agreements",
           "parameters": {},
           "response": {
-            "$ref": "VirtualMachine"
+            "$ref": "UserAgreement"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "list": {
-          "id": "arvados.virtual_machines.list",
-          "path": "virtual_machines",
+          "id": "arvados.user_agreements.list",
+          "path": "user_agreements",
           "httpMethod": "GET",
-          "description": "List VirtualMachines.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching VirtualMachines. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#virtualMachineList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List UserAgreements.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching UserAgreements. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#userAgreementList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -8294,18 +8287,31 @@
             }
           },
           "response": {
-            "$ref": "VirtualMachineList"
+            "$ref": "UserAgreementList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
             "https://api.arvados.org/auth/arvados.readonly"
           ]
         },
+        "new": {
+          "id": "arvados.user_agreements.new",
+          "path": "user_agreements/new",
+          "httpMethod": "GET",
+          "description": "new user_agreements",
+          "parameters": {},
+          "response": {
+            "$ref": "UserAgreement"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados"
+          ]
+        },
         "show": {
-          "id": "arvados.virtual_machines.show",
-          "path": "virtual_machines/{uuid}",
+          "id": "arvados.user_agreements.show",
+          "path": "user_agreements/{uuid}",
           "httpMethod": "GET",
-          "description": "show virtual_machines",
+          "description": "show user_agreements",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -8321,17 +8327,17 @@
             }
           },
           "response": {
-            "$ref": "VirtualMachine"
+            "$ref": "UserAgreement"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "destroy": {
-          "id": "arvados.virtual_machines.destroy",
-          "path": "virtual_machines/{uuid}",
+          "id": "arvados.user_agreements.destroy",
+          "path": "user_agreements/{uuid}",
           "httpMethod": "DELETE",
-          "description": "destroy virtual_machines",
+          "description": "destroy user_agreements",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -8341,7 +8347,7 @@
             }
           },
           "response": {
-            "$ref": "VirtualMachine"
+            "$ref": "UserAgreement"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
@@ -8349,17 +8355,17 @@
         }
       }
     },
-    "workflows": {
+    "virtual_machines": {
       "methods": {
         "get": {
-          "id": "arvados.workflows.get",
-          "path": "workflows/{uuid}",
+          "id": "arvados.virtual_machines.get",
+          "path": "virtual_machines/{uuid}",
           "httpMethod": "GET",
-          "description": "Gets a Workflow's metadata by UUID.",
+          "description": "Gets a VirtualMachine's metadata by UUID.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Workflow in question.",
+              "description": "The UUID of the VirtualMachine in question.",
               "required": true,
               "location": "path"
             }
@@ -8368,7 +8374,7 @@
             "uuid"
           ],
           "response": {
-            "$ref": "Workflow"
+            "$ref": "VirtualMachine"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -8376,10 +8382,10 @@
           ]
         },
         "index": {
-          "id": "arvados.workflows.list",
-          "path": "workflows",
+          "id": "arvados.virtual_machines.list",
+          "path": "virtual_machines",
           "httpMethod": "GET",
-          "description": "List Workflows.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Workflows. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#workflowList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List VirtualMachines.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching VirtualMachines. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#virtualMachineList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -8447,7 +8453,7 @@
             }
           },
           "response": {
-            "$ref": "WorkflowList"
+            "$ref": "VirtualMachineList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -8455,10 +8461,10 @@
           ]
         },
         "create": {
-          "id": "arvados.workflows.create",
-          "path": "workflows",
+          "id": "arvados.virtual_machines.create",
+          "path": "virtual_machines",
           "httpMethod": "POST",
-          "description": "Create a new Workflow.",
+          "description": "Create a new VirtualMachine.",
           "parameters": {
             "select": {
               "type": "array",
@@ -8483,27 +8489,27 @@
           "request": {
             "required": true,
             "properties": {
-              "workflow": {
-                "$ref": "Workflow"
+              "virtual_machine": {
+                "$ref": "VirtualMachine"
               }
             }
           },
           "response": {
-            "$ref": "Workflow"
+            "$ref": "VirtualMachine"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "update": {
-          "id": "arvados.workflows.update",
-          "path": "workflows/{uuid}",
+          "id": "arvados.virtual_machines.update",
+          "path": "virtual_machines/{uuid}",
           "httpMethod": "PUT",
-          "description": "Update attributes of an existing Workflow.",
+          "description": "Update attributes of an existing VirtualMachine.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Workflow in question.",
+              "description": "The UUID of the VirtualMachine in question.",
               "required": true,
               "location": "path"
             },
@@ -8517,43 +8523,76 @@
           "request": {
             "required": true,
             "properties": {
-              "workflow": {
-                "$ref": "Workflow"
+              "virtual_machine": {
+                "$ref": "VirtualMachine"
               }
             }
           },
           "response": {
-            "$ref": "Workflow"
+            "$ref": "VirtualMachine"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "delete": {
-          "id": "arvados.workflows.delete",
-          "path": "workflows/{uuid}",
+          "id": "arvados.virtual_machines.delete",
+          "path": "virtual_machines/{uuid}",
           "httpMethod": "DELETE",
-          "description": "Delete an existing Workflow.",
+          "description": "Delete an existing VirtualMachine.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the Workflow in question.",
+              "description": "The UUID of the VirtualMachine in question.",
               "required": true,
               "location": "path"
             }
           },
           "response": {
-            "$ref": "Workflow"
+            "$ref": "VirtualMachine"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados"
+          ]
+        },
+        "logins": {
+          "id": "arvados.virtual_machines.logins",
+          "path": "virtual_machines/{uuid}/logins",
+          "httpMethod": "GET",
+          "description": "logins virtual_machines",
+          "parameters": {
+            "uuid": {
+              "type": "string",
+              "description": "",
+              "required": true,
+              "location": "path"
+            }
+          },
+          "response": {
+            "$ref": "VirtualMachine"
+          },
+          "scopes": [
+            "https://api.arvados.org/auth/arvados"
+          ]
+        },
+        "get_all_logins": {
+          "id": "arvados.virtual_machines.get_all_logins",
+          "path": "virtual_machines/get_all_logins",
+          "httpMethod": "GET",
+          "description": "get_all_logins virtual_machines",
+          "parameters": {},
+          "response": {
+            "$ref": "VirtualMachine"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "list": {
-          "id": "arvados.workflows.list",
-          "path": "workflows",
+          "id": "arvados.virtual_machines.list",
+          "path": "virtual_machines",
           "httpMethod": "GET",
-          "description": "List Workflows.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Workflows. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#workflowList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List VirtualMachines.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching VirtualMachines. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#virtualMachineList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -8621,7 +8660,7 @@
             }
           },
           "response": {
-            "$ref": "WorkflowList"
+            "$ref": "VirtualMachineList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -8629,10 +8668,10 @@
           ]
         },
         "show": {
-          "id": "arvados.workflows.show",
-          "path": "workflows/{uuid}",
+          "id": "arvados.virtual_machines.show",
+          "path": "virtual_machines/{uuid}",
           "httpMethod": "GET",
-          "description": "show workflows",
+          "description": "show virtual_machines",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -8648,17 +8687,17 @@
             }
           },
           "response": {
-            "$ref": "Workflow"
+            "$ref": "VirtualMachine"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "destroy": {
-          "id": "arvados.workflows.destroy",
-          "path": "workflows/{uuid}",
+          "id": "arvados.virtual_machines.destroy",
+          "path": "virtual_machines/{uuid}",
           "httpMethod": "DELETE",
-          "description": "destroy workflows",
+          "description": "destroy virtual_machines",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -8668,7 +8707,7 @@
             }
           },
           "response": {
-            "$ref": "Workflow"
+            "$ref": "VirtualMachine"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
@@ -8676,17 +8715,17 @@
         }
       }
     },
-    "user_agreements": {
+    "workflows": {
       "methods": {
         "get": {
-          "id": "arvados.user_agreements.get",
-          "path": "user_agreements/{uuid}",
+          "id": "arvados.workflows.get",
+          "path": "workflows/{uuid}",
           "httpMethod": "GET",
-          "description": "Gets a UserAgreement's metadata by UUID.",
+          "description": "Gets a Workflow's metadata by UUID.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the UserAgreement in question.",
+              "description": "The UUID of the Workflow in question.",
               "required": true,
               "location": "path"
             }
@@ -8695,7 +8734,7 @@
             "uuid"
           ],
           "response": {
-            "$ref": "UserAgreement"
+            "$ref": "Workflow"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -8703,10 +8742,10 @@
           ]
         },
         "index": {
-          "id": "arvados.user_agreements.list",
-          "path": "user_agreements",
+          "id": "arvados.workflows.list",
+          "path": "workflows",
           "httpMethod": "GET",
-          "description": "List UserAgreements.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching UserAgreements. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#userAgreementList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List Workflows.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Workflows. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#workflowList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -8774,7 +8813,7 @@
             }
           },
           "response": {
-            "$ref": "UserAgreementList"
+            "$ref": "WorkflowList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
@@ -8782,10 +8821,10 @@
           ]
         },
         "create": {
-          "id": "arvados.user_agreements.create",
-          "path": "user_agreements",
+          "id": "arvados.workflows.create",
+          "path": "workflows",
           "httpMethod": "POST",
-          "description": "Create a new UserAgreement.",
+          "description": "Create a new Workflow.",
           "parameters": {
             "select": {
               "type": "array",
@@ -8810,27 +8849,27 @@
           "request": {
             "required": true,
             "properties": {
-              "user_agreement": {
-                "$ref": "UserAgreement"
+              "workflow": {
+                "$ref": "Workflow"
               }
             }
           },
           "response": {
-            "$ref": "UserAgreement"
+            "$ref": "Workflow"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "update": {
-          "id": "arvados.user_agreements.update",
-          "path": "user_agreements/{uuid}",
+          "id": "arvados.workflows.update",
+          "path": "workflows/{uuid}",
           "httpMethod": "PUT",
-          "description": "Update attributes of an existing UserAgreement.",
+          "description": "Update attributes of an existing Workflow.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the UserAgreement in question.",
+              "description": "The UUID of the Workflow in question.",
               "required": true,
               "location": "path"
             },
@@ -8844,69 +8883,43 @@
           "request": {
             "required": true,
             "properties": {
-              "user_agreement": {
-                "$ref": "UserAgreement"
+              "workflow": {
+                "$ref": "Workflow"
               }
             }
           },
           "response": {
-            "$ref": "UserAgreement"
+            "$ref": "Workflow"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "delete": {
-          "id": "arvados.user_agreements.delete",
-          "path": "user_agreements/{uuid}",
+          "id": "arvados.workflows.delete",
+          "path": "workflows/{uuid}",
           "httpMethod": "DELETE",
-          "description": "Delete an existing UserAgreement.",
+          "description": "Delete an existing Workflow.",
           "parameters": {
             "uuid": {
               "type": "string",
-              "description": "The UUID of the UserAgreement in question.",
+              "description": "The UUID of the Workflow in question.",
               "required": true,
               "location": "path"
             }
           },
           "response": {
-            "$ref": "UserAgreement"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados"
-          ]
-        },
-        "signatures": {
-          "id": "arvados.user_agreements.signatures",
-          "path": "user_agreements/signatures",
-          "httpMethod": "GET",
-          "description": "signatures user_agreements",
-          "parameters": {},
-          "response": {
-            "$ref": "UserAgreement"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados"
-          ]
-        },
-        "sign": {
-          "id": "arvados.user_agreements.sign",
-          "path": "user_agreements/sign",
-          "httpMethod": "POST",
-          "description": "sign user_agreements",
-          "parameters": {},
-          "response": {
-            "$ref": "UserAgreement"
+            "$ref": "Workflow"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "list": {
-          "id": "arvados.user_agreements.list",
-          "path": "user_agreements",
+          "id": "arvados.workflows.list",
+          "path": "workflows",
           "httpMethod": "GET",
-          "description": "List UserAgreements.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching UserAgreements. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#userAgreementList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
+          "description": "List Workflows.\n\n                   The <code>list</code> method returns a\n                   <a href=\"/api/resources.html\">resource list</a> of\n                   matching Workflows. For example:\n\n                   <pre>\n                   {\n                    \"kind\":\"arvados#workflowList\",\n                    \"etag\":\"\",\n                    \"self_link\":\"\",\n                    \"next_page_token\":\"\",\n                    \"next_link\":\"\",\n                    \"items\":[\n                       ...\n                    ],\n                    \"items_available\":745,\n                    \"_profile\":{\n                     \"request_time\":0.157236317\n                    }\n                    </pre>",
           "parameters": {
             "filters": {
               "type": "array",
@@ -8974,31 +8987,18 @@
             }
           },
           "response": {
-            "$ref": "UserAgreementList"
+            "$ref": "WorkflowList"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados",
             "https://api.arvados.org/auth/arvados.readonly"
           ]
         },
-        "new": {
-          "id": "arvados.user_agreements.new",
-          "path": "user_agreements/new",
-          "httpMethod": "GET",
-          "description": "new user_agreements",
-          "parameters": {},
-          "response": {
-            "$ref": "UserAgreement"
-          },
-          "scopes": [
-            "https://api.arvados.org/auth/arvados"
-          ]
-        },
         "show": {
-          "id": "arvados.user_agreements.show",
-          "path": "user_agreements/{uuid}",
+          "id": "arvados.workflows.show",
+          "path": "workflows/{uuid}",
           "httpMethod": "GET",
-          "description": "show user_agreements",
+          "description": "show workflows",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -9014,17 +9014,17 @@
             }
           },
           "response": {
-            "$ref": "UserAgreement"
+            "$ref": "Workflow"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
           ]
         },
         "destroy": {
-          "id": "arvados.user_agreements.destroy",
-          "path": "user_agreements/{uuid}",
+          "id": "arvados.workflows.destroy",
+          "path": "workflows/{uuid}",
           "httpMethod": "DELETE",
-          "description": "destroy user_agreements",
+          "description": "destroy workflows",
           "parameters": {
             "uuid": {
               "type": "string",
@@ -9034,7 +9034,7 @@
             }
           },
           "response": {
-            "$ref": "UserAgreement"
+            "$ref": "Workflow"
           },
           "scopes": [
             "https://api.arvados.org/auth/arvados"
@@ -9063,179 +9063,39 @@
       "methods": {
         "get": {
           "id": "arvados.vocabularies.get",
-          "path": "vocabulary",
-          "httpMethod": "GET",
-          "description": "Get vocabulary definition",
-          "parameters": {},
-          "parameterOrder": [],
-          "response": {},
-          "scopes": [
-            "https://api.arvados.org/auth/arvados",
-            "https://api.arvados.org/auth/arvados.readonly"
-          ]
-        }
-      }
-    },
-    "sys": {
-      "methods": {
-        "get": {
-          "id": "arvados.sys.trash_sweep",
-          "path": "sys/trash_sweep",
-          "httpMethod": "POST",
-          "description": "apply scheduled trash and delete operations",
-          "parameters": {},
-          "parameterOrder": [],
-          "response": {},
-          "scopes": [
-            "https://api.arvados.org/auth/arvados",
-            "https://api.arvados.org/auth/arvados.readonly"
-          ]
-        }
-      }
-    }
-  },
-  "revision": "20220510",
-  "schemas": {
-    "JobList": {
-      "id": "JobList",
-      "description": "Job list",
-      "type": "object",
-      "properties": {
-        "kind": {
-          "type": "string",
-          "description": "Object type. Always arvados#jobList.",
-          "default": "arvados#jobList"
-        },
-        "etag": {
-          "type": "string",
-          "description": "List version."
-        },
-        "items": {
-          "type": "array",
-          "description": "The list of Jobs.",
-          "items": {
-            "$ref": "Job"
-          }
-        },
-        "next_link": {
-          "type": "string",
-          "description": "A link to the next page of Jobs."
-        },
-        "next_page_token": {
-          "type": "string",
-          "description": "The page token for the next page of Jobs."
-        },
-        "selfLink": {
-          "type": "string",
-          "description": "A link back to this list."
-        }
-      }
-    },
-    "Job": {
-      "id": "Job",
-      "description": "Job",
-      "type": "object",
-      "uuidPrefix": "8i9sb",
-      "properties": {
-        "uuid": {
-          "type": "string"
-        },
-        "etag": {
-          "type": "string",
-          "description": "Object version."
-        },
-        "owner_uuid": {
-          "type": "string"
-        },
-        "modified_by_client_uuid": {
-          "type": "string"
-        },
-        "modified_by_user_uuid": {
-          "type": "string"
-        },
-        "modified_at": {
-          "type": "datetime"
-        },
-        "submit_id": {
-          "type": "string"
-        },
-        "script": {
-          "type": "string"
-        },
-        "script_version": {
-          "type": "string"
-        },
-        "script_parameters": {
-          "type": "Hash"
-        },
-        "cancelled_by_client_uuid": {
-          "type": "string"
-        },
-        "cancelled_by_user_uuid": {
-          "type": "string"
-        },
-        "cancelled_at": {
-          "type": "datetime"
-        },
-        "started_at": {
-          "type": "datetime"
-        },
-        "finished_at": {
-          "type": "datetime"
-        },
-        "running": {
-          "type": "boolean"
-        },
-        "success": {
-          "type": "boolean"
-        },
-        "output": {
-          "type": "string"
-        },
-        "created_at": {
-          "type": "datetime"
-        },
-        "is_locked_by_uuid": {
-          "type": "string"
-        },
-        "log": {
-          "type": "string"
-        },
-        "tasks_summary": {
-          "type": "Hash"
-        },
-        "runtime_constraints": {
-          "type": "Hash"
-        },
-        "nondeterministic": {
-          "type": "boolean"
-        },
-        "repository": {
-          "type": "string"
-        },
-        "supplied_script_version": {
-          "type": "string"
-        },
-        "docker_image_locator": {
-          "type": "string"
-        },
-        "priority": {
-          "type": "integer"
-        },
-        "description": {
-          "type": "string"
-        },
-        "state": {
-          "type": "string"
-        },
-        "arvados_sdk_version": {
-          "type": "string"
-        },
-        "components": {
-          "type": "Hash"
+          "path": "vocabulary",
+          "httpMethod": "GET",
+          "description": "Get vocabulary definition",
+          "parameters": {},
+          "parameterOrder": [],
+          "response": {},
+          "scopes": [
+            "https://api.arvados.org/auth/arvados",
+            "https://api.arvados.org/auth/arvados.readonly"
+          ]
         }
       }
     },
+    "sys": {
+      "methods": {
+        "get": {
+          "id": "arvados.sys.trash_sweep",
+          "path": "sys/trash_sweep",
+          "httpMethod": "POST",
+          "description": "apply scheduled trash and delete operations",
+          "parameters": {},
+          "parameterOrder": [],
+          "response": {},
+          "scopes": [
+            "https://api.arvados.org/auth/arvados",
+            "https://api.arvados.org/auth/arvados.readonly"
+          ]
+        }
+      }
+    }
+  },
+  "revision": "20220510",
+  "schemas": {
     "ApiClientList": {
       "id": "ApiClientList",
       "description": "ApiClient list",
@@ -10044,6 +9904,146 @@
         }
       }
     },
+    "JobList": {
+      "id": "JobList",
+      "description": "Job list",
+      "type": "object",
+      "properties": {
+        "kind": {
+          "type": "string",
+          "description": "Object type. Always arvados#jobList.",
+          "default": "arvados#jobList"
+        },
+        "etag": {
+          "type": "string",
+          "description": "List version."
+        },
+        "items": {
+          "type": "array",
+          "description": "The list of Jobs.",
+          "items": {
+            "$ref": "Job"
+          }
+        },
+        "next_link": {
+          "type": "string",
+          "description": "A link to the next page of Jobs."
+        },
+        "next_page_token": {
+          "type": "string",
+          "description": "The page token for the next page of Jobs."
+        },
+        "selfLink": {
+          "type": "string",
+          "description": "A link back to this list."
+        }
+      }
+    },
+    "Job": {
+      "id": "Job",
+      "description": "Job",
+      "type": "object",
+      "uuidPrefix": "8i9sb",
+      "properties": {
+        "uuid": {
+          "type": "string"
+        },
+        "etag": {
+          "type": "string",
+          "description": "Object version."
+        },
+        "owner_uuid": {
+          "type": "string"
+        },
+        "modified_by_client_uuid": {
+          "type": "string"
+        },
+        "modified_by_user_uuid": {
+          "type": "string"
+        },
+        "modified_at": {
+          "type": "datetime"
+        },
+        "submit_id": {
+          "type": "string"
+        },
+        "script": {
+          "type": "string"
+        },
+        "script_version": {
+          "type": "string"
+        },
+        "script_parameters": {
+          "type": "Hash"
+        },
+        "cancelled_by_client_uuid": {
+          "type": "string"
+        },
+        "cancelled_by_user_uuid": {
+          "type": "string"
+        },
+        "cancelled_at": {
+          "type": "datetime"
+        },
+        "started_at": {
+          "type": "datetime"
+        },
+        "finished_at": {
+          "type": "datetime"
+        },
+        "running": {
+          "type": "boolean"
+        },
+        "success": {
+          "type": "boolean"
+        },
+        "output": {
+          "type": "string"
+        },
+        "created_at": {
+          "type": "datetime"
+        },
+        "is_locked_by_uuid": {
+          "type": "string"
+        },
+        "log": {
+          "type": "string"
+        },
+        "tasks_summary": {
+          "type": "Hash"
+        },
+        "runtime_constraints": {
+          "type": "Hash"
+        },
+        "nondeterministic": {
+          "type": "boolean"
+        },
+        "repository": {
+          "type": "string"
+        },
+        "supplied_script_version": {
+          "type": "string"
+        },
+        "docker_image_locator": {
+          "type": "string"
+        },
+        "priority": {
+          "type": "integer"
+        },
+        "description": {
+          "type": "string"
+        },
+        "state": {
+          "type": "string"
+        },
+        "arvados_sdk_version": {
+          "type": "string"
+        },
+        "components": {
+          "type": "Hash"
+        }
+      }
+    },
     "JobTaskList": {
       "id": "JobTaskList",
       "description": "JobTask list",
@@ -10561,96 +10561,7 @@
         "properties": {
           "type": "Hash"
         },
-        "job_uuid": {
-          "type": "string"
-        }
-      }
-    },
-    "UserList": {
-      "id": "UserList",
-      "description": "User list",
-      "type": "object",
-      "properties": {
-        "kind": {
-          "type": "string",
-          "description": "Object type. Always arvados#userList.",
-          "default": "arvados#userList"
-        },
-        "etag": {
-          "type": "string",
-          "description": "List version."
-        },
-        "items": {
-          "type": "array",
-          "description": "The list of Users.",
-          "items": {
-            "$ref": "User"
-          }
-        },
-        "next_link": {
-          "type": "string",
-          "description": "A link to the next page of Users."
-        },
-        "next_page_token": {
-          "type": "string",
-          "description": "The page token for the next page of Users."
-        },
-        "selfLink": {
-          "type": "string",
-          "description": "A link back to this list."
-        }
-      }
-    },
-    "User": {
-      "id": "User",
-      "description": "User",
-      "type": "object",
-      "uuidPrefix": "tpzed",
-      "properties": {
-        "uuid": {
-          "type": "string"
-        },
-        "etag": {
-          "type": "string",
-          "description": "Object version."
-        },
-        "owner_uuid": {
-          "type": "string"
-        },
-        "created_at": {
-          "type": "datetime"
-        },
-        "modified_by_client_uuid": {
-          "type": "string"
-        },
-        "modified_by_user_uuid": {
-          "type": "string"
-        },
-        "modified_at": {
-          "type": "datetime"
-        },
-        "email": {
-          "type": "string"
-        },
-        "first_name": {
-          "type": "string"
-        },
-        "last_name": {
-          "type": "string"
-        },
-        "identity_url": {
-          "type": "string"
-        },
-        "is_admin": {
-          "type": "boolean"
-        },
-        "prefs": {
-          "type": "Hash"
-        },
-        "is_active": {
-          "type": "boolean"
-        },
-        "username": {
+        "job_uuid": {
           "type": "string"
         }
       }
@@ -11031,15 +10942,15 @@
         }
       }
     },
-    "VirtualMachineList": {
-      "id": "VirtualMachineList",
-      "description": "VirtualMachine list",
+    "UserList": {
+      "id": "UserList",
+      "description": "User list",
       "type": "object",
       "properties": {
         "kind": {
           "type": "string",
-          "description": "Object type. Always arvados#virtualMachineList.",
-          "default": "arvados#virtualMachineList"
+          "description": "Object type. Always arvados#userList.",
+          "default": "arvados#userList"
         },
         "etag": {
           "type": "string",
@@ -11047,18 +10958,18 @@
         },
         "items": {
           "type": "array",
-          "description": "The list of VirtualMachines.",
+          "description": "The list of Users.",
           "items": {
-            "$ref": "VirtualMachine"
+            "$ref": "User"
           }
         },
         "next_link": {
           "type": "string",
-          "description": "A link to the next page of VirtualMachines."
+          "description": "A link to the next page of Users."
         },
         "next_page_token": {
           "type": "string",
-          "description": "The page token for the next page of VirtualMachines."
+          "description": "The page token for the next page of Users."
         },
         "selfLink": {
           "type": "string",
@@ -11066,11 +10977,11 @@
         }
       }
     },
-    "VirtualMachine": {
-      "id": "VirtualMachine",
-      "description": "VirtualMachine",
+    "User": {
+      "id": "User",
+      "description": "User",
       "type": "object",
-      "uuidPrefix": "2x53u",
+      "uuidPrefix": "tpzed",
       "properties": {
         "uuid": {
           "type": "string"
@@ -11082,6 +10993,9 @@
         "owner_uuid": {
           "type": "string"
         },
+        "created_at": {
+          "type": "datetime"
+        },
         "modified_by_client_uuid": {
           "type": "string"
         },
@@ -11091,85 +11005,29 @@
         "modified_at": {
           "type": "datetime"
         },
-        "hostname": {
+        "email": {
           "type": "string"
         },
-        "created_at": {
-          "type": "datetime"
-        }
-      }
-    },
-    "WorkflowList": {
-      "id": "WorkflowList",
-      "description": "Workflow list",
-      "type": "object",
-      "properties": {
-        "kind": {
-          "type": "string",
-          "description": "Object type. Always arvados#workflowList.",
-          "default": "arvados#workflowList"
-        },
-        "etag": {
-          "type": "string",
-          "description": "List version."
-        },
-        "items": {
-          "type": "array",
-          "description": "The list of Workflows.",
-          "items": {
-            "$ref": "Workflow"
-          }
-        },
-        "next_link": {
-          "type": "string",
-          "description": "A link to the next page of Workflows."
-        },
-        "next_page_token": {
-          "type": "string",
-          "description": "The page token for the next page of Workflows."
-        },
-        "selfLink": {
-          "type": "string",
-          "description": "A link back to this list."
-        }
-      }
-    },
-    "Workflow": {
-      "id": "Workflow",
-      "description": "Workflow",
-      "type": "object",
-      "uuidPrefix": "7fd4e",
-      "properties": {
-        "uuid": {
+        "first_name": {
           "type": "string"
         },
-        "etag": {
-          "type": "string",
-          "description": "Object version."
-        },
-        "owner_uuid": {
+        "last_name": {
           "type": "string"
         },
-        "created_at": {
-          "type": "datetime"
-        },
-        "modified_at": {
-          "type": "datetime"
-        },
-        "modified_by_client_uuid": {
+        "identity_url": {
           "type": "string"
         },
-        "modified_by_user_uuid": {
-          "type": "string"
+        "is_admin": {
+          "type": "boolean"
         },
-        "name": {
-          "type": "string"
+        "prefs": {
+          "type": "Hash"
         },
-        "description": {
-          "type": "text"
+        "is_active": {
+          "type": "boolean"
         },
-        "definition": {
-          "type": "text"
+        "username": {
+          "type": "string"
         }
       }
     },
@@ -11294,6 +11152,148 @@
           "type": "integer"
         }
       }
+    },
+    "VirtualMachineList": {
+      "id": "VirtualMachineList",
+      "description": "VirtualMachine list",
+      "type": "object",
+      "properties": {
+        "kind": {
+          "type": "string",
+          "description": "Object type. Always arvados#virtualMachineList.",
+          "default": "arvados#virtualMachineList"
+        },
+        "etag": {
+          "type": "string",
+          "description": "List version."
+        },
+        "items": {
+          "type": "array",
+          "description": "The list of VirtualMachines.",
+          "items": {
+            "$ref": "VirtualMachine"
+          }
+        },
+        "next_link": {
+          "type": "string",
+          "description": "A link to the next page of VirtualMachines."
+        },
+        "next_page_token": {
+          "type": "string",
+          "description": "The page token for the next page of VirtualMachines."
+        },
+        "selfLink": {
+          "type": "string",
+          "description": "A link back to this list."
+        }
+      }
+    },
+    "VirtualMachine": {
+      "id": "VirtualMachine",
+      "description": "VirtualMachine",
+      "type": "object",
+      "uuidPrefix": "2x53u",
+      "properties": {
+        "uuid": {
+          "type": "string"
+        },
+        "etag": {
+          "type": "string",
+          "description": "Object version."
+        },
+        "owner_uuid": {
+          "type": "string"
+        },
+        "modified_by_client_uuid": {
+          "type": "string"
+        },
+        "modified_by_user_uuid": {
+          "type": "string"
+        },
+        "modified_at": {
+          "type": "datetime"
+        },
+        "hostname": {
+          "type": "string"
+        },
+        "created_at": {
+          "type": "datetime"
+        }
+      }
+    },
+    "WorkflowList": {
+      "id": "WorkflowList",
+      "description": "Workflow list",
+      "type": "object",
+      "properties": {
+        "kind": {
+          "type": "string",
+          "description": "Object type. Always arvados#workflowList.",
+          "default": "arvados#workflowList"
+        },
+        "etag": {
+          "type": "string",
+          "description": "List version."
+        },
+        "items": {
+          "type": "array",
+          "description": "The list of Workflows.",
+          "items": {
+            "$ref": "Workflow"
+          }
+        },
+        "next_link": {
+          "type": "string",
+          "description": "A link to the next page of Workflows."
+        },
+        "next_page_token": {
+          "type": "string",
+          "description": "The page token for the next page of Workflows."
+        },
+        "selfLink": {
+          "type": "string",
+          "description": "A link back to this list."
+        }
+      }
+    },
+    "Workflow": {
+      "id": "Workflow",
+      "description": "Workflow",
+      "type": "object",
+      "uuidPrefix": "7fd4e",
+      "properties": {
+        "uuid": {
+          "type": "string"
+        },
+        "etag": {
+          "type": "string",
+          "description": "Object version."
+        },
+        "owner_uuid": {
+          "type": "string"
+        },
+        "created_at": {
+          "type": "datetime"
+        },
+        "modified_at": {
+          "type": "datetime"
+        },
+        "modified_by_client_uuid": {
+          "type": "string"
+        },
+        "modified_by_user_uuid": {
+          "type": "string"
+        },
+        "name": {
+          "type": "string"
+        },
+        "description": {
+          "type": "text"
+        },
+        "definition": {
+          "type": "text"
+        }
+      }
     }
   },
   "servicePath": "arvados/v1/",
diff --git a/services/api/app/controllers/arvados/v1/schema_controller.rb b/services/api/app/controllers/arvados/v1/schema_controller.rb
index 4d15cb1215..e200870da5 100644
--- a/services/api/app/controllers/arvados/v1/schema_controller.rb
+++ b/services/api/app/controllers/arvados/v1/schema_controller.rb
@@ -118,7 +118,7 @@ class Arvados::V1::SchemaController < ApplicationController
       resources: {}
     }
 
-    ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
+    ActiveRecord::Base.descendants.reject(&:abstract_class?).sort_by(&:to_s).each do |k|
       begin
         ctl_class = "Arvados::V1::#{k.to_s.pluralize}Controller".constantize
       rescue
diff --git a/services/api/test/integration/discovery_document_test.rb b/services/api/test/integration/discovery_document_test.rb
index 65d93a07eb..37e7750297 100644
--- a/services/api/test/integration/discovery_document_test.rb
+++ b/services/api/test/integration/discovery_document_test.rb
@@ -31,10 +31,6 @@ class DiscoveryDocumentTest < ActionDispatch::IntegrationTest
     canonical = Hash[CANONICAL_FIELDS.map { |key| [key, json_response[key]] }]
     missing = canonical.select { |key| canonical[key].nil? }
     assert(missing.empty?, "discovery document missing required fields")
-
-    # (Temporary) sort the hash keys so we can diff JSON below.
-    canonical["resources"] = Hash[canonical["resources"].to_a.sort]
-    canonical["schemas"] = Hash[canonical["schemas"].to_a.sort]
     actual_json = JSON.pretty_generate(canonical)
 
     # Currently the Python SDK is the only component using this copy of the
@@ -45,11 +41,6 @@ class DiscoveryDocumentTest < ActionDispatch::IntegrationTest
     src_path = Rails.root.join("../../sdk/python/arvados-v1-discovery.json")
     begin
       expected_json = File.open(src_path) { |f| f.read }
-      # (Temporary) sort the hash keys so we can diff JSON below.
-      j = JSON.parse(expected_json)
-      j["resources"] = Hash[j["resources"].to_a.sort]
-      j["schemas"] = Hash[j["schemas"].to_a.sort]
-      expected_json = JSON.pretty_generate(j)
     rescue Errno::ENOENT
       expected_json = "(#{src_path} not found)"
     end

commit 974cabe78a811766a812206e287a6c488bcc6c52
Author: Tom Clegg <tom at curii.com>
Date:   Thu Sep 7 13:45:13 2023 -0400

    20300: Fix key order sensitivity in test, part 1.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/test/integration/discovery_document_test.rb b/services/api/test/integration/discovery_document_test.rb
index b592d61639..65d93a07eb 100644
--- a/services/api/test/integration/discovery_document_test.rb
+++ b/services/api/test/integration/discovery_document_test.rb
@@ -32,7 +32,11 @@ class DiscoveryDocumentTest < ActionDispatch::IntegrationTest
     missing = canonical.select { |key| canonical[key].nil? }
     assert(missing.empty?, "discovery document missing required fields")
 
-    expected = JSON.pretty_generate(canonical)
+    # (Temporary) sort the hash keys so we can diff JSON below.
+    canonical["resources"] = Hash[canonical["resources"].to_a.sort]
+    canonical["schemas"] = Hash[canonical["schemas"].to_a.sort]
+    actual_json = JSON.pretty_generate(canonical)
+
     # Currently the Python SDK is the only component using this copy of the
     # discovery document, and storing it with the source simplifies the build
     # process, so it lives there. If another component wants to use it later,
@@ -40,16 +44,21 @@ class DiscoveryDocumentTest < ActionDispatch::IntegrationTest
     # Python build process will need to be extended to accommodate that.
     src_path = Rails.root.join("../../sdk/python/arvados-v1-discovery.json")
     begin
-      actual = File.open(src_path) { |f| f.read }
+      expected_json = File.open(src_path) { |f| f.read }
+      # (Temporary) sort the hash keys so we can diff JSON below.
+      j = JSON.parse(expected_json)
+      j["resources"] = Hash[j["resources"].to_a.sort]
+      j["schemas"] = Hash[j["schemas"].to_a.sort]
+      expected_json = JSON.pretty_generate(j)
     rescue Errno::ENOENT
-      actual = "(#{src_path} not found)"
+      expected_json = "(#{src_path} not found)"
     end
 
     out_path = Rails.root.join("tmp", "test-arvados-v1-discovery.json")
-    if expected != actual
-      File.open(out_path, "w") { |f| f.write(expected) }
+    if expected_json != actual_json
+      File.open(out_path, "w") { |f| f.write(actual_json) }
     end
-    assert_equal(expected, actual, [
+    assert_equal(expected_json, actual_json, [
                    "#{src_path} did not match the live discovery document",
                    "Current live version saved to #{out_path}",
                    "Commit that to #{src_path} to regenerate documentation",

commit 48231f0daea46cc2022bb0f153c2419fe7283911
Author: Tom Clegg <tom at curii.com>
Date:   Thu Sep 7 10:13:53 2023 -0400

    20300: Update expected content-type in test.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/test/functional/arvados/v1/management_controller_test.rb b/services/api/test/functional/arvados/v1/management_controller_test.rb
index 6d27bccfc4..ab57c995df 100644
--- a/services/api/test/functional/arvados/v1/management_controller_test.rb
+++ b/services/api/test/functional/arvados/v1/management_controller_test.rb
@@ -39,7 +39,7 @@ class Arvados::V1::ManagementControllerTest < ActionController::TestCase
     @request.headers['Authorization'] = "Bearer configuredmanagementtoken"
     get :metrics
     assert_response :success
-    assert_equal 'text/plain', @response.content_type
+    assert_equal 'text/plain; charset=utf-8', @response.content_type
 
     assert_match /\narvados_config_source_timestamp_seconds{sha256="#{hash}"} #{Regexp.escape mtime.utc.to_f.to_s}\n/, @response.body
 

commit 0127a60904f8f392d60111c07ac80f461fe26c22
Author: Tom Clegg <tom at curii.com>
Date:   Thu Sep 7 09:39:34 2023 -0400

    20300: Fix where(nil, ...) case.
    
    Rails 5 accepted where(nil, ...) as a no-op, but in Rails 6 that's an
    error.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/app/models/arvados_model.rb b/services/api/app/models/arvados_model.rb
index cf9e2c277f..c909e47cef 100644
--- a/services/api/app/models/arvados_model.rb
+++ b/services/api/app/models/arvados_model.rb
@@ -464,6 +464,7 @@ class ArvadosModel < ApplicationRecord
       end
     end
 
+    return self if sql_conds == nil
     self.where(sql_conds,
                user_uuids: all_user_uuids.collect{|c| c["target_uuid"]},
                permission_link_classes: ['permission'])

commit d3a9348beaefdd355a0a88ac9f43ad243ae2603d
Author: Tom Clegg <tom at curii.com>
Date:   Thu Sep 7 09:37:56 2023 -0400

    20300: Fix YAML usage.
    
    YAML.safe_load_file does not exist until Ruby 3.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/apps/workbench/app/controllers/work_units_controller.rb b/apps/workbench/app/controllers/work_units_controller.rb
index 86e3cdd91d..42dd12a4c2 100644
--- a/apps/workbench/app/controllers/work_units_controller.rb
+++ b/apps/workbench/app/controllers/work_units_controller.rb
@@ -70,7 +70,7 @@ class WorkUnitsController < ApplicationController
       workflow = Workflow.find? template_uuid
       if workflow.definition
         begin
-          wf_json = ActiveSupport::HashWithIndifferentAccess.new YAML::load(workflow.definition)
+          wf_json = ActiveSupport::HashWithIndifferentAccess.new YAML.load(workflow.definition)
         rescue => e
           logger.error "Error converting definition yaml to json: #{e.message}"
           raise ArgumentError, "Error converting definition yaml to json: #{e.message}"
diff --git a/apps/workbench/app/views/workflows/_show_definition.html.erb b/apps/workbench/app/views/workflows/_show_definition.html.erb
index f0e01a12ad..9aa75ef067 100644
--- a/apps/workbench/app/views/workflows/_show_definition.html.erb
+++ b/apps/workbench/app/views/workflows/_show_definition.html.erb
@@ -3,7 +3,7 @@
 SPDX-License-Identifier: AGPL-3.0 %>
 
 <%
-  wf_def = ActiveSupport::HashWithIndifferentAccess.new YAML::load(@object.definition) if @object.definition
+  wf_def = ActiveSupport::HashWithIndifferentAccess.new YAML.load(@object.definition) if @object.definition
   wf_def = wf_def[:"$graph"].andand[0] || wf_def if wf_def
 
   items = {}
diff --git a/services/api/app/controllers/database_controller.rb b/services/api/app/controllers/database_controller.rb
index 38d406fe33..8e61d16fa8 100644
--- a/services/api/app/controllers/database_controller.rb
+++ b/services/api/app/controllers/database_controller.rb
@@ -18,10 +18,10 @@ class DatabaseController < ApplicationController
     user_uuids = User.
       where('email is null or (email not like ? and email not like ?)', '%@example.com', '%.example.com').
       collect(&:uuid)
-    fixture_uuids =
-      YAML::safe_load_file(File.expand_path('../../../test/fixtures/users.yml',
-                                            __FILE__)).
-      values.collect { |u| u['uuid'] }
+    fnm = File.expand_path('../../../test/fixtures/users.yml', __FILE__)
+    fixture_uuids = File.open(fnm) do |f|
+      YAML.safe_load(f, filename: fnm, permitted_classes: [Time]).values.collect { |u| u['uuid'] }
+    end
     unexpected_uuids = user_uuids - fixture_uuids
     if unexpected_uuids.any?
       logger.error("Running in test environment, but non-fixture users exist: " +
diff --git a/services/api/script/arvados-git-sync.rb b/services/api/script/arvados-git-sync.rb
index ceebc3518a..9f8f050c10 100755
--- a/services/api/script/arvados-git-sync.rb
+++ b/services/api/script/arvados-git-sync.rb
@@ -26,7 +26,9 @@ DEBUG = 1
 # if present, overriding base config parameters as specified
 path = File.absolute_path('../../config/arvados-clients.yml', __FILE__)
 if File.exist?(path) then
-  cp_config = YAML.safe_load_file(path)[ENV['RAILS_ENV']]
+  cp_config = File.open(path) do |f|
+    YAML.safe_load(f, filename: path)[ENV['RAILS_ENV']]
+  end
 else
   puts "Please create a\n #{path}\n file"
   exit 1
diff --git a/services/api/script/migrate-gitolite-to-uuid-storage.rb b/services/api/script/migrate-gitolite-to-uuid-storage.rb
index d2b9a0418b..98f25ca537 100755
--- a/services/api/script/migrate-gitolite-to-uuid-storage.rb
+++ b/services/api/script/migrate-gitolite-to-uuid-storage.rb
@@ -40,7 +40,9 @@ DEBUG = 1
 # if present, overriding base config parameters as specified
 path = File.dirname(__FILE__) + '/config/arvados-clients.yml'
 if File.exist?(path) then
-  cp_config = YAML.safe_load_file(path)[ENV['RAILS_ENV']]
+  cp_config = File.open(path) do |f|
+    YAML.safe_load(f, filename: path)[ENV['RAILS_ENV']]
+  end
 else
   puts "Please create a\n " + File.dirname(__FILE__) + "/config/arvados-clients.yml\n file"
   exit 1

commit a542b89c232184ccbb7435784f32d76f7a11a892
Author: Tom Clegg <tom at curii.com>
Date:   Thu Sep 7 13:53:41 2023 -0400

    20300: Remove obsolete uses of set_attribute_was.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/app/models/arvados_model.rb b/services/api/app/models/arvados_model.rb
index 3d4d6ef31f..cf9e2c277f 100644
--- a/services/api/app/models/arvados_model.rb
+++ b/services/api/app/models/arvados_model.rb
@@ -938,8 +938,6 @@ class ArvadosModel < ApplicationRecord
   # hook.
   def fill_container_defaults_after_find
     fill_container_defaults
-    set_attribute_was('runtime_constraints', runtime_constraints)
-    set_attribute_was('scheduling_parameters', scheduling_parameters)
     clear_changes_information
   end
 

commit 3b40453701265dc66f8efb5865d29cf508f3ca43
Author: Tom Clegg <tom at curii.com>
Date:   Wed Sep 6 17:21:50 2023 -0400

    20300: Change deprecated update_attributes to update.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/apps/workbench/app/controllers/application_controller.rb b/apps/workbench/app/controllers/application_controller.rb
index c2636bf5d7..3e4502545b 100644
--- a/apps/workbench/app/controllers/application_controller.rb
+++ b/apps/workbench/app/controllers/application_controller.rb
@@ -374,7 +374,7 @@ class ApplicationController < ActionController::Base
         end
       end
     end
-    if @object.update_attributes @updates
+    if @object.update @updates
       show
     else
       self.render_error status: 422
@@ -400,7 +400,7 @@ class ApplicationController < ActionController::Base
     @new_resource_attrs ||= params[model_class.to_s.underscore.singularize]
     @new_resource_attrs ||= {}
     @object = @object.dup
-    @object.update_attributes @new_resource_attrs
+    @object.update @new_resource_attrs
     if not @new_resource_attrs[:name] and @object.respond_to? :name
       if @object.name and @object.name != ''
         @object.name = "Copy of #{@object.name}"
diff --git a/apps/workbench/app/controllers/collections_controller.rb b/apps/workbench/app/controllers/collections_controller.rb
index 680c324f5c..812b80b8ce 100644
--- a/apps/workbench/app/controllers/collections_controller.rb
+++ b/apps/workbench/app/controllers/collections_controller.rb
@@ -258,7 +258,7 @@ class CollectionsController < ApplicationController
       arv_coll.rm "."+p
     end
 
-    if @object.update_attributes manifest_text: arv_coll.manifest_text
+    if @object.update manifest_text: arv_coll.manifest_text
       show
     else
       self.render_error status: 422
@@ -289,7 +289,7 @@ class CollectionsController < ApplicationController
       else
         arv_coll.rename "./"+file_path, new_file_path
 
-        if @object.update_attributes manifest_text: arv_coll.manifest_text
+        if @object.update manifest_text: arv_coll.manifest_text
           show
         else
           self.render_error status: 422
diff --git a/apps/workbench/app/controllers/container_requests_controller.rb b/apps/workbench/app/controllers/container_requests_controller.rb
index be463b022c..9fb534ec24 100644
--- a/apps/workbench/app/controllers/container_requests_controller.rb
+++ b/apps/workbench/app/controllers/container_requests_controller.rb
@@ -83,7 +83,7 @@ class ContainerRequestsController < ApplicationController
         @object.state = 'Final'
       end
     end
-    @object.update_attributes! priority: 0
+    @object.update! priority: 0
     if params[:return_to]
       redirect_to params[:return_to]
     else
diff --git a/apps/workbench/app/controllers/projects_controller.rb b/apps/workbench/app/controllers/projects_controller.rb
index e448e1b453..53a6d80446 100644
--- a/apps/workbench/app/controllers/projects_controller.rb
+++ b/apps/workbench/app/controllers/projects_controller.rb
@@ -141,7 +141,7 @@ class ProjectsController < ApplicationController
         # Object is owned by this project. Remove it from the project by
         # changing owner to the current user.
         begin
-          item.update_attributes owner_uuid: current_user.uuid
+          item.update owner_uuid: current_user.uuid
           @removed_uuids << item.uuid
         rescue ArvadosApiClient::ApiErrorResponseException => e
           if e.message.include? '_owner_uuid_'
@@ -151,7 +151,7 @@ class ProjectsController < ApplicationController
             updates = {}
             updates[:name] = rename_to
             updates[:owner_uuid] = current_user.uuid
-            item.update_attributes updates
+            item.update updates
             @removed_uuids << item.uuid
           else
             raise
@@ -170,7 +170,7 @@ class ProjectsController < ApplicationController
     end
     while (objects = @object.contents).any?
       objects.each do |object|
-        object.update_attributes! owner_uuid: current_user.uuid
+        object.update! owner_uuid: current_user.uuid
       end
     end
     if ArvadosBase::resource_class_for_uuid(@object.owner_uuid) == Group
diff --git a/apps/workbench/app/models/arvados_base.rb b/apps/workbench/app/models/arvados_base.rb
index c5e1a4ed22..cb5eb803bc 100644
--- a/apps/workbench/app/models/arvados_base.rb
+++ b/apps/workbench/app/models/arvados_base.rb
@@ -300,12 +300,12 @@ class ArvadosBase
     self.name.underscore.pluralize.downcase
   end
 
-  def update_attributes raw_params={}
+  def update raw_params={}
     assign_attributes(self.class.permit_attribute_params(raw_params))
     save
   end
 
-  def update_attributes! raw_params={}
+  def update! raw_params={}
     assign_attributes(self.class.permit_attribute_params(raw_params))
     save!
   end
diff --git a/apps/workbench/app/views/layouts/body.html.erb b/apps/workbench/app/views/layouts/body.html.erb
index 8b114478ba..7cb00a15c0 100644
--- a/apps/workbench/app/views/layouts/body.html.erb
+++ b/apps/workbench/app/views/layouts/body.html.erb
@@ -286,6 +286,6 @@ SPDX-License-Identifier: AGPL-3.0 %>
   <%
     prefs = current_user.prefs
     prefs[:getting_started_shown] = Time.now
-    current_user.update_attributes prefs: prefs.to_json
+    current_user.update prefs: prefs.to_json
   %>
 <% end %>
diff --git a/apps/workbench/test/integration/ajax_errors_test.rb b/apps/workbench/test/integration/ajax_errors_test.rb
index b3b1f1f57a..40e7f9ea8f 100644
--- a/apps/workbench/test/integration/ajax_errors_test.rb
+++ b/apps/workbench/test/integration/ajax_errors_test.rb
@@ -38,7 +38,7 @@ class AjaxErrorsTest < ActionDispatch::IntegrationTest
       # Go behind Workbench's back to expire the "active" token.
       token = api_fixture('api_client_authorizations')['active']['api_token']
       auth = ApiClientAuthorization.find(token)
-      auth.update_attributes(expires_at: '1999-12-31T23:59:59Z')
+      auth.update(expires_at: '1999-12-31T23:59:59Z')
     end
     click_link "Subprojects"
     wait_for_ajax
diff --git a/apps/workbench/test/integration/collection_upload_test.rb b/apps/workbench/test/integration/collection_upload_test.rb
index 608cd521de..43e7a22dc2 100644
--- a/apps/workbench/test/integration/collection_upload_test.rb
+++ b/apps/workbench/test/integration/collection_upload_test.rb
@@ -21,7 +21,7 @@ class CollectionUploadTest < ActionDispatch::IntegrationTest
   teardown do
     use_token :admin do
       @keep_services.each do |ks|
-        KeepService.find(ks.uuid).update_attributes(ks.attributes)
+        KeepService.find(ks.uuid).update(ks.attributes)
       end
     end
     testfiles.each do |filename, _|
@@ -80,7 +80,7 @@ class CollectionUploadTest < ActionDispatch::IntegrationTest
     need_selenium "to make file uploads work"
     use_token :admin do
       KeepService.where(service_type: 'proxy').first.
-        update_attributes(service_ssl_flag: false)
+        update(service_ssl_flag: false)
     end
     visit page_with_token 'active', sandbox_path
     find('.nav-tabs a', text: 'Upload').click
@@ -99,7 +99,7 @@ class CollectionUploadTest < ActionDispatch::IntegrationTest
       # Even if port 0 is a thing, surely nx.example.net won't
       # respond
       KeepService.where(service_type: 'proxy').first.
-        update_attributes(service_host: 'nx.example.net',
+        update(service_host: 'nx.example.net',
                           service_port: 0)
     end
     visit page_with_token 'active', sandbox_path
diff --git a/apps/workbench/test/integration_performance/collection_unit_test.rb b/apps/workbench/test/integration_performance/collection_unit_test.rb
index 3feef945d1..bf5dd5b478 100644
--- a/apps/workbench/test/integration_performance/collection_unit_test.rb
+++ b/apps/workbench/test/integration_performance/collection_unit_test.rb
@@ -58,7 +58,7 @@ class BigCollectionTest < ActiveSupport::TestCase
     end
     time_block 'update(name-only)' do
       manifest_text_length = c.manifest_text.length
-      c.update_attributes name: 'renamed during test case'
+      c.update name: 'renamed during test case'
       assert_equal c.manifest_text.length, manifest_text_length
     end
     time_block 'update' do
diff --git a/doc/install/install-workbench2-app.html.textile.liquid b/doc/install/install-workbench2-app.html.textile.liquid
index 6315961182..bbcbd7ef1d 100644
--- a/doc/install/install-workbench2-app.html.textile.liquid
+++ b/doc/install/install-workbench2-app.html.textile.liquid
@@ -99,7 +99,7 @@ At the console, enter the following commands to locate the ApiClient record for
 => ["https://workbench.example.com/", Sat, 19 Apr 2014 03:35:12 UTC +00:00]
 irb(main):002:0> <span class="userinput">include CurrentApiClient</span>
 => true
-irb(main):003:0> <span class="userinput">act_as_system_user do wb.update_attributes!(is_trusted: true) end</span>
+irb(main):003:0> <span class="userinput">act_as_system_user do wb.update!(is_trusted: true) end</span>
 => true
 </code></pre>
 </notextile>
diff --git a/services/api/app/controllers/application_controller.rb b/services/api/app/controllers/application_controller.rb
index b191550240..d62a09b0a9 100644
--- a/services/api/app/controllers/application_controller.rb
+++ b/services/api/app/controllers/application_controller.rb
@@ -120,7 +120,7 @@ class ApplicationController < ActionController::Base
     attrs_to_update = resource_attrs.reject { |k,v|
       [:kind, :etag, :href].index k
     }
-    @object.update_attributes! attrs_to_update
+    @object.update! attrs_to_update
     show
   end
 
diff --git a/services/api/app/controllers/arvados/v1/groups_controller.rb b/services/api/app/controllers/arvados/v1/groups_controller.rb
index efcc43db26..c362cf32d7 100644
--- a/services/api/app/controllers/arvados/v1/groups_controller.rb
+++ b/services/api/app/controllers/arvados/v1/groups_controller.rb
@@ -92,7 +92,7 @@ class Arvados::V1::GroupsController < ApplicationController
       attrs_to_update = resource_attrs.reject { |k, v|
         [:kind, :etag, :href].index k
       }.merge({async_permissions_update: true})
-      @object.update_attributes!(attrs_to_update)
+      @object.update!(attrs_to_update)
       @object.save!
       render_accepted
     else
diff --git a/services/api/app/controllers/arvados/v1/nodes_controller.rb b/services/api/app/controllers/arvados/v1/nodes_controller.rb
index eb72b7096d..2510fd49fa 100644
--- a/services/api/app/controllers/arvados/v1/nodes_controller.rb
+++ b/services/api/app/controllers/arvados/v1/nodes_controller.rb
@@ -37,7 +37,7 @@ class Arvados::V1::NodesController < ApplicationController
     attrs_to_update = resource_attrs.reject { |k,v|
       [:kind, :etag, :href].index k
     }
-    @object.update_attributes!(attrs_to_update)
+    @object.update!(attrs_to_update)
     @object.assign_slot if params[:assign_slot]
     @object.save!
     show
diff --git a/services/api/app/controllers/arvados/v1/users_controller.rb b/services/api/app/controllers/arvados/v1/users_controller.rb
index ded86aa66d..b872c0bbab 100644
--- a/services/api/app/controllers/arvados/v1/users_controller.rb
+++ b/services/api/app/controllers/arvados/v1/users_controller.rb
@@ -30,7 +30,7 @@ class Arvados::V1::UsersController < ApplicationController
       end
       if needupdate.length > 0
         begin
-          u.update_attributes!(needupdate)
+          u.update!(needupdate)
         rescue ActiveRecord::RecordInvalid
           loginCluster = Rails.configuration.Login.LoginCluster
           if u.uuid[0..4] == loginCluster && !needupdate[:username].nil?
@@ -40,7 +40,7 @@ class Arvados::V1::UsersController < ApplicationController
             if local_user.andand.uuid[0..4] == loginCluster && local_user.uuid != u.uuid
               new_username = "#{needupdate[:username]}conflict#{rand(99999999)}"
               Rails.logger.warn("cached username '#{needupdate[:username]}' collision with user '#{local_user.uuid}' - renaming to '#{new_username}' before retrying")
-              local_user.update_attributes!({username: new_username})
+              local_user.update!({username: new_username})
               retry
             end
           end
@@ -103,7 +103,7 @@ class Arvados::V1::UsersController < ApplicationController
           collect(&:head_uuid)
         todo_uuids = required_uuids - signed_uuids
         if todo_uuids.empty?
-          @object.update_attributes is_active: true
+          @object.update is_active: true
           logger.info "User #{@object.uuid} activated"
         else
           logger.warn "User #{@object.uuid} called users.activate " +
diff --git a/services/api/app/models/api_client_authorization.rb b/services/api/app/models/api_client_authorization.rb
index c149ffc329..a6ce6aa54a 100644
--- a/services/api/app/models/api_client_authorization.rb
+++ b/services/api/app/models/api_client_authorization.rb
@@ -413,9 +413,9 @@ class ApiClientAuthorization < ArvadosModel
             (remote_user_prefix == Rails.configuration.Login.LoginCluster or
              Rails.configuration.Users.NewUsersAreActive or
              Rails.configuration.RemoteClusters[remote_user_prefix].andand["ActivateUsers"])
-            user.update_attributes!(is_active: true)
+            user.update!(is_active: true)
           elsif user.is_active && !remote_user['is_active']
-            user.update_attributes!(is_active: false)
+            user.update!(is_active: false)
           end
 
           if remote_user_prefix == Rails.configuration.Login.LoginCluster and
@@ -423,7 +423,7 @@ class ApiClientAuthorization < ArvadosModel
             user.is_admin != remote_user['is_admin']
             # Remote cluster controls our user database, including the
             # admin flag.
-            user.update_attributes!(is_admin: remote_user['is_admin'])
+            user.update!(is_admin: remote_user['is_admin'])
           end
         end
       end
@@ -459,7 +459,7 @@ class ApiClientAuthorization < ArvadosModel
           return nil
         end
       end
-      auth.update_attributes!(user: user,
+      auth.update!(user: user,
                               api_token: stored_secret,
                               api_client_id: 0,
                               scopes: scopes,
diff --git a/services/api/app/models/arvados_model.rb b/services/api/app/models/arvados_model.rb
index d910320ec0..3d4d6ef31f 100644
--- a/services/api/app/models/arvados_model.rb
+++ b/services/api/app/models/arvados_model.rb
@@ -145,7 +145,7 @@ class ArvadosModel < ApplicationRecord
     super(permit_attribute_params(raw_params), *args)
   end
 
-  def update_attributes raw_params={}, *args
+  def update raw_params={}, *args
     super(self.class.permit_attribute_params(raw_params), *args)
   end
 
diff --git a/services/api/app/models/container.rb b/services/api/app/models/container.rb
index d2e76f74e3..44334ba12f 100644
--- a/services/api/app/models/container.rb
+++ b/services/api/app/models/container.rb
@@ -379,7 +379,7 @@ class Container < ArvadosModel
       if self.state != Queued
         raise LockFailedError.new("cannot lock when #{self.state}")
       end
-      self.update_attributes!(state: Locked)
+      self.update!(state: Locked)
     end
   end
 
@@ -397,7 +397,7 @@ class Container < ArvadosModel
       if self.state != Locked
         raise InvalidStateTransitionError.new("cannot unlock when #{self.state}")
       end
-      self.update_attributes!(state: Queued)
+      self.update!(state: Queued)
     end
   end
 
@@ -642,7 +642,7 @@ class Container < ArvadosModel
       # ensure the token doesn't validate later in the same
       # transaction (e.g., in a test case) by satisfying expires_at >
       # transaction timestamp.
-      self.auth.andand.update_attributes(expires_at: db_transaction_time)
+      self.auth.andand.update(expires_at: db_transaction_time)
       self.auth = nil
       return
     elsif self.auth
@@ -835,7 +835,7 @@ class Container < ArvadosModel
                 # Queued with priority 0.  (OTOH, if the child is already
                 # running, leave it alone so it can get cancelled the
                 # usual way, get a copy of the log collection, etc.)
-                cr.update_attributes!(state: ContainerRequest::Final)
+                cr.update!(state: ContainerRequest::Final)
               end
             end
           end
diff --git a/services/api/app/models/container_request.rb b/services/api/app/models/container_request.rb
index d72f00edc8..9895ceb14e 100644
--- a/services/api/app/models/container_request.rb
+++ b/services/api/app/models/container_request.rb
@@ -164,7 +164,7 @@ class ContainerRequest < ArvadosModel
         end
       elsif state == Committed
         # Behave as if the container is cancelled
-        update_attributes!(state: Final)
+        update!(state: Final)
       end
       return true
     end
@@ -228,7 +228,7 @@ class ContainerRequest < ArvadosModel
         end
       end
     end
-    update_attributes!(state: Final)
+    update!(state: Final)
   end
 
   def update_collections(container:, collections: ['log', 'output'])
@@ -308,7 +308,7 @@ class ContainerRequest < ArvadosModel
   end
 
   def set_priority_zero
-    self.update_attributes!(priority: 0) if self.priority > 0 && self.state != Final
+    self.update!(priority: 0) if self.priority > 0 && self.state != Final
   end
 
   protected
diff --git a/services/api/app/models/job.rb b/services/api/app/models/job.rb
index 37e5f455df..f792c04842 100644
--- a/services/api/app/models/job.rb
+++ b/services/api/app/models/job.rb
@@ -107,7 +107,7 @@ class Job < ArvadosModel
   end
 
   def assert_finished
-    update_attributes(finished_at: finished_at || db_current_time,
+    update(finished_at: finished_at || db_current_time,
                       success: success.nil? ? false : success,
                       running: false)
   end
diff --git a/services/api/app/models/keep_disk.rb b/services/api/app/models/keep_disk.rb
index 5751c135d8..589936f845 100644
--- a/services/api/app/models/keep_disk.rb
+++ b/services/api/app/models/keep_disk.rb
@@ -40,7 +40,7 @@ class KeepDisk < ArvadosModel
     end
 
     @bypass_arvados_authorization = true
-    self.update_attributes!(o.select { |k,v|
+    self.update!(o.select { |k,v|
                              [:bytes_total,
                               :bytes_free,
                               :is_readable,
diff --git a/services/api/app/models/node.rb b/services/api/app/models/node.rb
index c8a606e2b8..adc2512475 100644
--- a/services/api/app/models/node.rb
+++ b/services/api/app/models/node.rb
@@ -176,7 +176,7 @@ class Node < ArvadosModel
         # as the new node. Clear the ip_address field on the stale
         # nodes. Otherwise, we (via SLURM) might inadvertently connect
         # to the new node using the old node's hostname.
-        stale_node.update_attributes!(ip_address: nil)
+        stale_node.update!(ip_address: nil)
       end
     end
     if hostname_before_last_save && saved_change_to_hostname?
diff --git a/services/api/app/models/user.rb b/services/api/app/models/user.rb
index bbdd9c2843..88314c6775 100644
--- a/services/api/app/models/user.rb
+++ b/services/api/app/models/user.rb
@@ -497,7 +497,7 @@ SELECT target_uuid, perm_level
       end
 
       if redirect_to_new_user
-        update_attributes!(redirect_to_user_uuid: new_user.uuid, username: nil)
+        update!(redirect_to_user_uuid: new_user.uuid, username: nil)
       end
       skip_check_permissions_against_full_refresh do
         update_permissions self.uuid, self.uuid, CAN_MANAGE_PERM
diff --git a/services/api/db/migrate/20130118002239_rename_metadata_attributes.rb b/services/api/db/migrate/20130118002239_rename_metadata_attributes.rb
index 049b5e2d63..2c1b406000 100644
--- a/services/api/db/migrate/20130118002239_rename_metadata_attributes.rb
+++ b/services/api/db/migrate/20130118002239_rename_metadata_attributes.rb
@@ -17,7 +17,7 @@ class RenameMetadataAttributes < ActiveRecord::Migration[4.2]
       Metadatum.where('head like ?', 'orvos#%').each do |m|
         kind_uuid = m.head.match /^(orvos\#.*)\#([-0-9a-z]+)$/
         if kind_uuid
-          m.update_attributes(head_kind: kind_uuid[1],
+          m.update(head_kind: kind_uuid[1],
                               head: kind_uuid[2])
         end
       end
@@ -28,7 +28,7 @@ class RenameMetadataAttributes < ActiveRecord::Migration[4.2]
   def down
     begin
       Metadatum.where('head_kind is not null and head_kind <> ? and head is not null', '').each do |m|
-        m.update_attributes(head: m.head_kind + '#' + m.head)
+        m.update(head: m.head_kind + '#' + m.head)
       end
     rescue
     end
diff --git a/services/api/db/migrate/20150203180223_set_group_class_on_anonymous_group.rb b/services/api/db/migrate/20150203180223_set_group_class_on_anonymous_group.rb
index 71f769c157..0a05718fdd 100644
--- a/services/api/db/migrate/20150203180223_set_group_class_on_anonymous_group.rb
+++ b/services/api/db/migrate/20150203180223_set_group_class_on_anonymous_group.rb
@@ -6,13 +6,13 @@ class SetGroupClassOnAnonymousGroup < ActiveRecord::Migration[4.2]
   include CurrentApiClient
   def up
     act_as_system_user do
-      anonymous_group.update_attributes group_class: 'role', name: 'Anonymous users', description: 'Anonymous users'
+      anonymous_group.update group_class: 'role', name: 'Anonymous users', description: 'Anonymous users'
     end
   end
 
   def down
     act_as_system_user do
-      anonymous_group.update_attributes group_class: nil, name: 'Anonymous group', description: 'Anonymous group'
+      anonymous_group.update group_class: nil, name: 'Anonymous group', description: 'Anonymous group'
     end
   end
 end
diff --git a/services/api/db/migrate/20150303210106_fix_collection_portable_data_hash_with_hinted_manifest.rb b/services/api/db/migrate/20150303210106_fix_collection_portable_data_hash_with_hinted_manifest.rb
index 8814fc87d3..1d3a6ed1b4 100644
--- a/services/api/db/migrate/20150303210106_fix_collection_portable_data_hash_with_hinted_manifest.rb
+++ b/services/api/db/migrate/20150303210106_fix_collection_portable_data_hash_with_hinted_manifest.rb
@@ -107,7 +107,7 @@ class FixCollectionPortableDataHashWithHintedManifest < ActiveRecord::Migration[
       attributes[:properties]["migrated_from"] ||= coll.uuid
       coll_copy = Collection.create!(attributes)
       Log.log_create(coll_copy)
-      coll.update_attributes(portable_data_hash: stripped_pdh)
+      coll.update(portable_data_hash: stripped_pdh)
       Log.log_update(coll, start_log)
     end
   end
diff --git a/services/api/db/migrate/20221219165512_dedup_permission_links.rb b/services/api/db/migrate/20221219165512_dedup_permission_links.rb
index 6ae04cc954..6aef343f1c 100644
--- a/services/api/db/migrate/20221219165512_dedup_permission_links.rb
+++ b/services/api/db/migrate/20221219165512_dedup_permission_links.rb
@@ -22,7 +22,7 @@ class DedupPermissionLinks < ActiveRecord::Migration[5.2]
           # This no-op update has the side effect that the update hooks
           # will merge the highest available permission into this one
           # and then delete the others.
-          link.update_attributes!(properties: link.properties.dup)
+          link.update!(properties: link.properties.dup)
         end
 
         rows = ActiveRecord::Base.connection.select_all("SELECT MIN(uuid) AS uuid, COUNT(uuid) AS n FROM links
@@ -35,7 +35,7 @@ class DedupPermissionLinks < ActiveRecord::Migration[5.2]
         rows.each do |row|
           Rails.logger.debug "DedupPermissionLinks: consolidating #{row['n']} links into #{row['uuid']}"
           link = Link.find_by_uuid(row['uuid'])
-          link.update_attributes!(properties: link.properties.dup)
+          link.update!(properties: link.properties.dup)
         end
       end
     end
diff --git a/services/api/lib/tasks/manage_long_lived_tokens.rake b/services/api/lib/tasks/manage_long_lived_tokens.rake
index 7a665ff7e7..70a0f24284 100644
--- a/services/api/lib/tasks/manage_long_lived_tokens.rake
+++ b/services/api/lib/tasks/manage_long_lived_tokens.rake
@@ -31,7 +31,7 @@ namespace :db do
       end
       if (auth.user.uuid =~ /-tpzed-000000000000000/).nil? and (auth.user.uuid =~ /-tpzed-anonymouspublic/).nil?
         CurrentApiClientHelper.act_as_system_user do
-          auth.update_attributes!(expires_at: exp_date)
+          auth.update!(expires_at: exp_date)
         end
         token_count += 1
       end
diff --git a/services/api/lib/trashable.rb b/services/api/lib/trashable.rb
index c99b08513b..50611c305d 100644
--- a/services/api/lib/trashable.rb
+++ b/services/api/lib/trashable.rb
@@ -93,19 +93,19 @@ end
 module TrashableController
   def destroy
     if !@object.is_trashed
-      @object.update_attributes!(trash_at: db_current_time)
+      @object.update!(trash_at: db_current_time)
     end
     earliest_delete = (@object.trash_at +
                        Rails.configuration.Collections.BlobSigningTTL)
     if @object.delete_at > earliest_delete
-      @object.update_attributes!(delete_at: earliest_delete)
+      @object.update!(delete_at: earliest_delete)
     end
     show
   end
 
   def trash
     if !@object.is_trashed
-      @object.update_attributes!(trash_at: db_current_time)
+      @object.update!(trash_at: db_current_time)
     end
     show
   end
diff --git a/services/api/test/functional/arvados/v1/container_requests_controller_test.rb b/services/api/test/functional/arvados/v1/container_requests_controller_test.rb
index f287a11faf..87eb37cde7 100644
--- a/services/api/test/functional/arvados/v1/container_requests_controller_test.rb
+++ b/services/api/test/functional/arvados/v1/container_requests_controller_test.rb
@@ -103,7 +103,7 @@ class Arvados::V1::ContainerRequestsControllerTest < ActionController::TestCase
   test "update without deleting secret_mounts" do
     authorize_with :active
     req = container_requests(:uncommitted)
-    req.update_attributes!(secret_mounts: {'/foo' => {'kind' => 'json', 'content' => 'bar'}})
+    req.update!(secret_mounts: {'/foo' => {'kind' => 'json', 'content' => 'bar'}})
 
     patch :update, params: {
             id: req.uuid,
@@ -169,7 +169,7 @@ class Arvados::V1::ContainerRequestsControllerTest < ActionController::TestCase
   test "filter on container subproperty runtime_status[foo] = bar" do
     ctr = containers(:running)
     act_as_system_user do
-      ctr.update_attributes!(runtime_status: {foo: 'bar'})
+      ctr.update!(runtime_status: {foo: 'bar'})
     end
     authorize_with :active
     get :index, params: {
diff --git a/services/api/test/functional/arvados/v1/groups_controller_test.rb b/services/api/test/functional/arvados/v1/groups_controller_test.rb
index a64ea76692..d8daa4bdd7 100644
--- a/services/api/test/functional/arvados/v1/groups_controller_test.rb
+++ b/services/api/test/functional/arvados/v1/groups_controller_test.rb
@@ -984,7 +984,7 @@ class Arvados::V1::GroupsControllerTest < ActionController::TestCase
     innertrash = Collection.create!(name: 'inner-trashed', owner_uuid: innerproj.uuid, trash_at: trashtime)
     innertrashproj = Group.create!(group_class: 'project', name: 'inner-trashed-proj', owner_uuid: innerproj.uuid, trash_at: trashtime)
     outertrash = Collection.create!(name: 'outer-trashed', owner_uuid: outerproj.uuid, trash_at: trashtime)
-    innerproj.update_attributes!(frozen_by_uuid: users(:active).uuid)
+    innerproj.update!(frozen_by_uuid: users(:active).uuid)
     get :contents, params: {id: outerproj.uuid, include_trash: true, recursive: true}
     assert_response :success
     uuids = json_response['items'].collect { |item| item['uuid'] }
diff --git a/services/api/test/functional/arvados/v1/users_controller_test.rb b/services/api/test/functional/arvados/v1/users_controller_test.rb
index b7d683df29..8bffac8dd1 100644
--- a/services/api/test/functional/arvados/v1/users_controller_test.rb
+++ b/services/api/test/functional/arvados/v1/users_controller_test.rb
@@ -889,7 +889,7 @@ The Arvados team.
    ['dst', :project_viewer_trustedclient]].each do |which_scoped, auth|
     test "refuse to merge with scoped #{which_scoped} token" do
       act_as_system_user do
-        api_client_authorizations(auth).update_attributes(scopes: ["GET /", "POST /", "PUT /"])
+        api_client_authorizations(auth).update(scopes: ["GET /", "POST /", "PUT /"])
       end
       authorize_with(:active_trustedclient)
       post(:merge, params: {
diff --git a/services/api/test/unit/arvados_model_test.rb b/services/api/test/unit/arvados_model_test.rb
index 1e2e08059e..69a2710bb9 100644
--- a/services/api/test/unit/arvados_model_test.rb
+++ b/services/api/test/unit/arvados_model_test.rb
@@ -217,13 +217,13 @@ class ArvadosModelTest < ActiveSupport::TestCase
     assert group.valid?, "group is not valid"
 
     # update 1
-    group.update_attributes!(name: "test create and update name 1")
+    group.update!(name: "test create and update name 1")
     results = Group.where(uuid: group.uuid)
     assert_equal "test create and update name 1", results.first.name, "Expected name to be updated to 1"
     updated_at_1 = results.first.updated_at.to_f
 
     # update 2
-    group.update_attributes!(name: "test create and update name 2")
+    group.update!(name: "test create and update name 2")
     results = Group.where(uuid: group.uuid)
     assert_equal "test create and update name 2", results.first.name, "Expected name to be updated to 2"
     updated_at_2 = results.first.updated_at.to_f
@@ -237,15 +237,15 @@ class ArvadosModelTest < ActiveSupport::TestCase
     c = Collection.create!(properties: {})
     assert_equal({}, c.properties)
 
-    c.update_attributes(properties: {'foo' => 'foo'})
+    c.update(properties: {'foo' => 'foo'})
     c.reload
     assert_equal({'foo' => 'foo'}, c.properties)
 
-    c.update_attributes(properties: nil)
+    c.update(properties: nil)
     c.reload
     assert_equal({}, c.properties)
 
-    c.update_attributes(properties: {foo: 'bar'})
+    c.update(properties: {foo: 'bar'})
     assert_equal({'foo' => 'bar'}, c.properties)
     c.reload
     assert_equal({'foo' => 'bar'}, c.properties)
diff --git a/services/api/test/unit/collection_test.rb b/services/api/test/unit/collection_test.rb
index e7134a5be5..f3b48dbf70 100644
--- a/services/api/test/unit/collection_test.rb
+++ b/services/api/test/unit/collection_test.rb
@@ -91,19 +91,19 @@ class CollectionTest < ActiveSupport::TestCase
       assert_equal 34, c.file_size_total
 
       # Updating the manifest should change file stats
-      c.update_attributes(manifest_text: ". d41d8cd98f00b204e9800998ecf8427e 0:34:foo.txt 0:34:foo2.txt\n")
+      c.update(manifest_text: ". d41d8cd98f00b204e9800998ecf8427e 0:34:foo.txt 0:34:foo2.txt\n")
       assert c.valid?
       assert_equal 2, c.file_count
       assert_equal 68, c.file_size_total
 
       # Updating file stats and the manifest should use manifest values
-      c.update_attributes(manifest_text: ". d41d8cd98f00b204e9800998ecf8427e 0:34:foo.txt\n", file_count:10, file_size_total: 10)
+      c.update(manifest_text: ". d41d8cd98f00b204e9800998ecf8427e 0:34:foo.txt\n", file_count:10, file_size_total: 10)
       assert c.valid?
       assert_equal 1, c.file_count
       assert_equal 34, c.file_size_total
 
       # Updating just the file stats should be ignored
-      c.update_attributes(file_count: 10, file_size_total: 10)
+      c.update(file_count: 10, file_size_total: 10)
       assert c.valid?
       assert_equal 1, c.file_count
       assert_equal 34, c.file_size_total
@@ -166,7 +166,7 @@ class CollectionTest < ActiveSupport::TestCase
       assert_equal 1, c.version
       assert_equal false, c.preserve_version
       # Make a versionable update, it shouldn't create a new version yet
-      c.update_attributes!({'name' => 'bar'})
+      c.update!({'name' => 'bar'})
       c.reload
       assert_equal 'bar', c.name
       assert_equal 1, c.version
@@ -175,12 +175,12 @@ class CollectionTest < ActiveSupport::TestCase
       c.update_column('modified_at', fifteen_min_ago) # Update without validations/callbacks
       c.reload
       assert_equal fifteen_min_ago.to_i, c.modified_at.to_i
-      c.update_attributes!({'name' => 'baz'})
+      c.update!({'name' => 'baz'})
       c.reload
       assert_equal 'baz', c.name
       assert_equal 2, c.version
       # Make another update, no new version should be created
-      c.update_attributes!({'name' => 'foobar'})
+      c.update!({'name' => 'foobar'})
       c.reload
       assert_equal 'foobar', c.name
       assert_equal 2, c.version
@@ -197,7 +197,7 @@ class CollectionTest < ActiveSupport::TestCase
       assert_not_nil c.replication_confirmed_at
       assert_not_nil c.replication_confirmed
       # Make the versionable update
-      c.update_attributes!({'name' => 'foobarbaz'})
+      c.update!({'name' => 'foobarbaz'})
       c.reload
       assert_equal 'foobarbaz', c.name
       assert_equal 3, c.version
@@ -214,7 +214,7 @@ class CollectionTest < ActiveSupport::TestCase
       assert_equal 1, c.version
       assert_equal false, c.preserve_version
       # This update shouldn't produce a new version, as the idle time is not up
-      c.update_attributes!({
+      c.update!({
         'name' => 'bar'
       })
       c.reload
@@ -223,7 +223,7 @@ class CollectionTest < ActiveSupport::TestCase
       assert_equal false, c.preserve_version
       # This update should produce a new version, even if the idle time is not up
       # and also keep the preserve_version=true flag to persist it.
-      c.update_attributes!({
+      c.update!({
         'name' => 'baz',
         'preserve_version' => true
       })
@@ -234,7 +234,7 @@ class CollectionTest < ActiveSupport::TestCase
       # Make sure preserve_version is not disabled after being enabled, unless
       # a new version is created.
       # This is a non-versionable update
-      c.update_attributes!({
+      c.update!({
         'preserve_version' => false,
         'replication_desired' => 2
       })
@@ -243,7 +243,7 @@ class CollectionTest < ActiveSupport::TestCase
       assert_equal 2, c.replication_desired
       assert_equal true, c.preserve_version
       # This is a versionable update
-      c.update_attributes!({
+      c.update!({
         'preserve_version' => false,
         'name' => 'foobar'
       })
@@ -252,7 +252,7 @@ class CollectionTest < ActiveSupport::TestCase
       assert_equal false, c.preserve_version
       assert_equal 'foobar', c.name
       # Flipping only 'preserve_version' to true doesn't create a new version
-      c.update_attributes!({'preserve_version' => true})
+      c.update!({'preserve_version' => true})
       c.reload
       assert_equal 3, c.version
       assert_equal true, c.preserve_version
@@ -265,7 +265,7 @@ class CollectionTest < ActiveSupport::TestCase
       assert c.valid?
       assert_equal false, c.preserve_version
       modified_at = c.modified_at.to_f
-      c.update_attributes!({'preserve_version' => true})
+      c.update!({'preserve_version' => true})
       c.reload
       assert_equal true, c.preserve_version
       assert_equal modified_at, c.modified_at.to_f,
@@ -285,7 +285,7 @@ class CollectionTest < ActiveSupport::TestCase
         assert_equal 1, c.version
 
         assert_raises(ActiveRecord::RecordInvalid) do
-          c.update_attributes!({
+          c.update!({
             name => new_value
           })
         end
@@ -302,14 +302,14 @@ class CollectionTest < ActiveSupport::TestCase
       assert c.valid?
       assert_equal 1, c.version
       # Make changes so that a new version is created
-      c.update_attributes!({'name' => 'bar'})
+      c.update!({'name' => 'bar'})
       c.reload
       assert_equal 2, c.version
       assert_equal 2, Collection.where(current_version_uuid: c.uuid).count
       new_uuid = 'zzzzz-4zz18-somefakeuuidnow'
       assert_empty Collection.where(uuid: new_uuid)
       # Update UUID on current version, check that both collections point to it
-      c.update_attributes!({'uuid' => new_uuid})
+      c.update!({'uuid' => new_uuid})
       c.reload
       assert_equal new_uuid, c.uuid
       assert_equal 2, Collection.where(current_version_uuid: new_uuid).count
@@ -364,7 +364,7 @@ class CollectionTest < ActiveSupport::TestCase
         # Set up initial collection
         c = create_collection 'foo', Encoding::US_ASCII
         assert c.valid?
-        c.update_attributes!({'properties' => value_1})
+        c.update!({'properties' => value_1})
         c.reload
         assert c.changes.keys.empty?
         c.properties = value_2
@@ -386,7 +386,7 @@ class CollectionTest < ActiveSupport::TestCase
       assert c.valid?
       original_version_modified_at = c.modified_at.to_f
       # Make changes so that a new version is created
-      c.update_attributes!({'name' => 'bar'})
+      c.update!({'name' => 'bar'})
       c.reload
       assert_equal 2, c.version
       # Get the old version
@@ -400,7 +400,7 @@ class CollectionTest < ActiveSupport::TestCase
       # Make update on current version so old version get the attribute synced;
       # its modified_at should not change.
       new_replication = 3
-      c.update_attributes!({'replication_desired' => new_replication})
+      c.update!({'replication_desired' => new_replication})
       c.reload
       assert_equal new_replication, c.replication_desired
       c_old.reload
@@ -441,7 +441,7 @@ class CollectionTest < ActiveSupport::TestCase
       c = create_collection 'foo', Encoding::US_ASCII
       assert c.valid?
       # Make changes so that a new version is created
-      c.update_attributes!({'name' => 'bar'})
+      c.update!({'name' => 'bar'})
       c.reload
       assert_equal 2, c.version
       # Get the old version
@@ -479,7 +479,7 @@ class CollectionTest < ActiveSupport::TestCase
         assert_not_equal first_val, c.attributes[attr]
         # Make changes so that a new version is created and a synced field is
         # updated on both
-        c.update_attributes!({'name' => 'bar', attr => first_val})
+        c.update!({'name' => 'bar', attr => first_val})
         c.reload
         assert_equal 2, c.version
         assert_equal first_val, c.attributes[attr]
@@ -487,7 +487,7 @@ class CollectionTest < ActiveSupport::TestCase
         assert_equal first_val, Collection.where(current_version_uuid: c.uuid, version: 1).first.attributes[attr]
         # Only make an update on the same synced field & check that the previously
         # created version also gets it.
-        c.update_attributes!({attr => second_val})
+        c.update!({attr => second_val})
         c.reload
         assert_equal 2, c.version
         assert_equal second_val, c.attributes[attr]
@@ -525,7 +525,7 @@ class CollectionTest < ActiveSupport::TestCase
 
         # Update attribute and check if version number should be incremented
         old_value = c.attributes[attr]
-        c.update_attributes!({attr => val})
+        c.update!({attr => val})
         assert_equal new_version_expected, c.version == 2
         assert_equal val, c.attributes[attr]
 
@@ -559,11 +559,11 @@ class CollectionTest < ActiveSupport::TestCase
       col2 = create_collection 'bar', Encoding::US_ASCII
       assert col2.valid?
       assert_equal 1, col2.version
-      col2.update_attributes({name: 'baz'})
+      col2.update({name: 'baz'})
       assert_equal 2, col2.version
 
       # Try to make col2 a past version of col1. It shouldn't be possible
-      col2.update_attributes({current_version_uuid: col1.uuid})
+      col2.update({current_version_uuid: col1.uuid})
       assert col2.invalid?
       col2.reload
       assert_not_equal col1.uuid, col2.current_version_uuid
@@ -725,10 +725,10 @@ class CollectionTest < ActiveSupport::TestCase
   test "storage_classes_desired cannot be empty" do
     act_as_user users(:active) do
       c = collections(:collection_owned_by_active)
-      c.update_attributes storage_classes_desired: ["hot"]
+      c.update storage_classes_desired: ["hot"]
       assert_equal ["hot"], c.storage_classes_desired
       assert_raise ArvadosModel::InvalidStateTransitionError do
-        c.update_attributes storage_classes_desired: []
+        c.update storage_classes_desired: []
       end
     end
   end
@@ -736,7 +736,7 @@ class CollectionTest < ActiveSupport::TestCase
   test "storage classes lists should only contain non-empty strings" do
     c = collections(:storage_classes_desired_default_unconfirmed)
     act_as_user users(:admin) do
-      assert c.update_attributes(storage_classes_desired: ["default", "a_string"],
+      assert c.update(storage_classes_desired: ["default", "a_string"],
                                  storage_classes_confirmed: ["another_string"])
       [
         ["storage_classes_desired", ["default", 42]],
@@ -745,7 +745,7 @@ class CollectionTest < ActiveSupport::TestCase
         ["storage_classes_confirmed", [""]],
       ].each do |attr, val|
         assert_raise ArvadosModel::InvalidStateTransitionError do
-          assert c.update_attributes({attr => val})
+          assert c.update({attr => val})
         end
       end
     end
@@ -754,7 +754,7 @@ class CollectionTest < ActiveSupport::TestCase
   test "storage_classes_confirmed* can be set by admin user" do
     c = collections(:storage_classes_desired_default_unconfirmed)
     act_as_user users(:admin) do
-      assert c.update_attributes(storage_classes_confirmed: ["default"],
+      assert c.update(storage_classes_confirmed: ["default"],
                                  storage_classes_confirmed_at: Time.now)
     end
   end
@@ -764,16 +764,16 @@ class CollectionTest < ActiveSupport::TestCase
       c = collections(:storage_classes_desired_default_unconfirmed)
       # Cannot set just one at a time.
       assert_raise ArvadosModel::PermissionDeniedError do
-        c.update_attributes storage_classes_confirmed: ["default"]
+        c.update storage_classes_confirmed: ["default"]
       end
       c.reload
       assert_raise ArvadosModel::PermissionDeniedError do
-        c.update_attributes storage_classes_confirmed_at: Time.now
+        c.update storage_classes_confirmed_at: Time.now
       end
       # Cannot set bot at once, either.
       c.reload
       assert_raise ArvadosModel::PermissionDeniedError do
-        assert c.update_attributes(storage_classes_confirmed: ["default"],
+        assert c.update(storage_classes_confirmed: ["default"],
                                    storage_classes_confirmed_at: Time.now)
       end
     end
@@ -784,15 +784,15 @@ class CollectionTest < ActiveSupport::TestCase
       c = collections(:storage_classes_desired_default_confirmed_default)
       # Cannot clear just one at a time.
       assert_raise ArvadosModel::PermissionDeniedError do
-        c.update_attributes storage_classes_confirmed: []
+        c.update storage_classes_confirmed: []
       end
       c.reload
       assert_raise ArvadosModel::PermissionDeniedError do
-        c.update_attributes storage_classes_confirmed_at: nil
+        c.update storage_classes_confirmed_at: nil
       end
       # Can clear both at once.
       c.reload
-      assert c.update_attributes(storage_classes_confirmed: [],
+      assert c.update(storage_classes_confirmed: [],
                                  storage_classes_confirmed_at: nil)
     end
   end
@@ -802,7 +802,7 @@ class CollectionTest < ActiveSupport::TestCase
       Rails.configuration.Collections.DefaultReplication = 2
       act_as_user users(:active) do
         c = collections(:replication_undesired_unconfirmed)
-        c.update_attributes replication_desired: ask
+        c.update replication_desired: ask
         assert_equal ask, c.replication_desired
       end
     end
@@ -811,7 +811,7 @@ class CollectionTest < ActiveSupport::TestCase
   test "replication_confirmed* can be set by admin user" do
     c = collections(:replication_desired_2_unconfirmed)
     act_as_user users(:admin) do
-      assert c.update_attributes(replication_confirmed: 2,
+      assert c.update(replication_confirmed: 2,
                                  replication_confirmed_at: Time.now)
     end
   end
@@ -821,14 +821,14 @@ class CollectionTest < ActiveSupport::TestCase
       c = collections(:replication_desired_2_unconfirmed)
       # Cannot set just one at a time.
       assert_raise ArvadosModel::PermissionDeniedError do
-        c.update_attributes replication_confirmed: 1
+        c.update replication_confirmed: 1
       end
       assert_raise ArvadosModel::PermissionDeniedError do
-        c.update_attributes replication_confirmed_at: Time.now
+        c.update replication_confirmed_at: Time.now
       end
       # Cannot set both at once, either.
       assert_raise ArvadosModel::PermissionDeniedError do
-        c.update_attributes(replication_confirmed: 1,
+        c.update(replication_confirmed: 1,
                             replication_confirmed_at: Time.now)
       end
     end
@@ -839,15 +839,15 @@ class CollectionTest < ActiveSupport::TestCase
       c = collections(:replication_desired_2_confirmed_2)
       # Cannot clear just one at a time.
       assert_raise ArvadosModel::PermissionDeniedError do
-        c.update_attributes replication_confirmed: nil
+        c.update replication_confirmed: nil
       end
       c.reload
       assert_raise ArvadosModel::PermissionDeniedError do
-        c.update_attributes replication_confirmed_at: nil
+        c.update replication_confirmed_at: nil
       end
       # Can clear both at once.
       c.reload
-      assert c.update_attributes(replication_confirmed: nil,
+      assert c.update(replication_confirmed: nil,
                                  replication_confirmed_at: nil)
     end
   end
@@ -855,7 +855,7 @@ class CollectionTest < ActiveSupport::TestCase
   test "clear replication_confirmed* when introducing a new block in manifest" do
     c = collections(:replication_desired_2_confirmed_2)
     act_as_user users(:active) do
-      assert c.update_attributes(manifest_text: collections(:user_agreement).signed_manifest_text_only_for_tests)
+      assert c.update(manifest_text: collections(:user_agreement).signed_manifest_text_only_for_tests)
       assert_nil c.replication_confirmed
       assert_nil c.replication_confirmed_at
     end
@@ -865,7 +865,7 @@ class CollectionTest < ActiveSupport::TestCase
     c = collections(:replication_desired_2_confirmed_2)
     act_as_user users(:active) do
       new_manifest = c.signed_manifest_text_only_for_tests.sub(':bar', ':foo')
-      assert c.update_attributes(manifest_text: new_manifest)
+      assert c.update(manifest_text: new_manifest)
       assert_equal 2, c.replication_confirmed
       assert_not_nil c.replication_confirmed_at
     end
@@ -882,7 +882,7 @@ class CollectionTest < ActiveSupport::TestCase
       # not, this test would pass without testing the relevant case):
       assert_operator new_manifest.length+40, :<, c.signed_manifest_text_only_for_tests.length
 
-      assert c.update_attributes(manifest_text: new_manifest)
+      assert c.update(manifest_text: new_manifest)
       assert_equal 2, c.replication_confirmed
       assert_not_nil c.replication_confirmed_at
     end
@@ -892,7 +892,7 @@ class CollectionTest < ActiveSupport::TestCase
     act_as_user users(:active) do
       t0 = db_current_time
       c = Collection.create!(manifest_text: ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:x\n", name: 'foo')
-      c.update_attributes! trash_at: (t0 + 1.hours)
+      c.update! trash_at: (t0 + 1.hours)
       c.reload
       sig_exp = /\+A[0-9a-f]{40}\@([0-9]+)/.match(c.signed_manifest_text_only_for_tests)[1].to_i
       assert_operator sig_exp.to_i, :<=, (t0 + 1.hours).to_i
@@ -932,7 +932,7 @@ class CollectionTest < ActiveSupport::TestCase
       assert_not_empty c, 'Should be able to find live collection'
 
       # mark collection as expired
-      c.first.update_attributes!(trash_at: Time.new.strftime("%Y-%m-%d"))
+      c.first.update!(trash_at: Time.new.strftime("%Y-%m-%d"))
       c = Collection.readable_by(current_user).where(uuid: uuid)
       assert_empty c, 'Should not be able to find expired collection'
 
@@ -947,7 +947,7 @@ class CollectionTest < ActiveSupport::TestCase
     act_as_user users(:active) do
       t0 = db_current_time
       c = Collection.create!(manifest_text: '', name: 'foo')
-      c.update_attributes! trash_at: (t0 - 2.weeks)
+      c.update! trash_at: (t0 - 2.weeks)
       c.reload
       assert_operator c.trash_at, :>, t0
     end
@@ -1002,7 +1002,7 @@ class CollectionTest < ActiveSupport::TestCase
         else
           c = collections(fixture_name)
         end
-        updates_ok = c.update_attributes(updates)
+        updates_ok = c.update(updates)
         expect_valid = expect[:state] != :invalid
         assert_equal expect_valid, updates_ok, c.errors.full_messages.to_s
         case expect[:state]
@@ -1039,13 +1039,13 @@ class CollectionTest < ActiveSupport::TestCase
     start = db_current_time
     act_as_user users(:active) do
       c = Collection.create!(manifest_text: '', name: 'foo')
-      c.update_attributes!(trash_at: start + 86400.seconds)
+      c.update!(trash_at: start + 86400.seconds)
       assert_operator c.delete_at, :>=, start + (86400*22).seconds
       assert_operator c.delete_at, :<, start + (86400*22 + 30).seconds
       c.destroy
 
       c = Collection.create!(manifest_text: '', name: 'foo')
-      c.update_attributes!(is_trashed: true)
+      c.update!(is_trashed: true)
       assert_operator c.delete_at, :>=, start + (86400*21).seconds
     end
   end
diff --git a/services/api/test/unit/container_request_test.rb b/services/api/test/unit/container_request_test.rb
index a64adba6ff..98136aa53b 100644
--- a/services/api/test/unit/container_request_test.rb
+++ b/services/api/test/unit/container_request_test.rb
@@ -34,8 +34,8 @@ class ContainerRequestTest < ActiveSupport::TestCase
 
   def lock_and_run(ctr)
       act_as_system_user do
-        ctr.update_attributes!(state: Container::Locked)
-        ctr.update_attributes!(state: Container::Running)
+        ctr.update!(state: Container::Locked)
+        ctr.update!(state: Container::Running)
       end
   end
 
@@ -129,7 +129,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
       cr.save!
       assert_raises(ActiveRecord::RecordInvalid) do
         cr = ContainerRequest.find_by_uuid cr.uuid
-        cr.update_attributes!({state: "Committed",
+        cr.update!({state: "Committed",
                                priority: 1}.merge(value))
       end
     end
@@ -138,7 +138,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
   test "Update from fixture" do
     set_user_from_auth :active
     cr = ContainerRequest.find_by_uuid(container_requests(:running).uuid)
-    cr.update_attributes!(description: "New description")
+    cr.update!(description: "New description")
     assert_equal "New description", cr.description
   end
 
@@ -147,7 +147,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
       cr = create_minimal_req!(state: "Uncommitted", priority: 1)
       cr.save!
       cr = ContainerRequest.find_by_uuid cr.uuid
-      cr.update_attributes!(state: "Committed",
+      cr.update!(state: "Committed",
                             runtime_constraints: {"vcpus" => 1, "ram" => 23})
       assert_not_nil cr.container_uuid
   end
@@ -217,7 +217,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
     assert_operator c1.priority, :<, c2.priority
     c2priority_was = c2.priority
 
-    cr1.update_attributes!(priority: 0)
+    cr1.update!(priority: 0)
 
     c1.reload
     assert_equal 0, c1.priority
@@ -233,7 +233,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
 
     act_as_system_user do
       Container.find_by_uuid(cr.container_uuid).
-        update_attributes!(state: Container::Cancelled, cost: 1.25)
+        update!(state: Container::Cancelled, cost: 1.25)
     end
 
     cr.reload
@@ -252,8 +252,8 @@ class ContainerRequestTest < ActiveSupport::TestCase
 
     c = act_as_system_user do
       c = Container.find_by_uuid(cr.container_uuid)
-      c.update_attributes!(state: Container::Locked)
-      c.update_attributes!(state: Container::Running)
+      c.update!(state: Container::Locked)
+      c.update!(state: Container::Running)
       c
     end
 
@@ -263,7 +263,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
     output_pdh = '1f4b0bc7583c2a7f9102c395f4ffc5e3+45'
     log_pdh = 'fa7aeb5140e2848d39b416daeef4ffc5+45'
     act_as_system_user do
-      c.update_attributes!(state: Container::Complete,
+      c.update!(state: Container::Complete,
                            cost: 1.25,
                            output: output_pdh,
                            log: log_pdh)
@@ -302,8 +302,8 @@ class ContainerRequestTest < ActiveSupport::TestCase
 
     c = act_as_system_user do
       c = Container.find_by_uuid(cr.container_uuid)
-      c.update_attributes!(state: Container::Locked)
-      c.update_attributes!(state: Container::Running,
+      c.update!(state: Container::Locked)
+      c.update!(state: Container::Running,
                            output: output_pdh,
                            log: log_pdh)
       c
@@ -315,7 +315,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
     act_as_system_user do
       Collection.where(portable_data_hash: output_pdh).delete_all
       Collection.where(portable_data_hash: log_pdh).delete_all
-      c.update_attributes!(state: Container::Complete)
+      c.update!(state: Container::Complete)
     end
 
     cr.reload
@@ -333,8 +333,8 @@ class ContainerRequestTest < ActiveSupport::TestCase
 
     c = act_as_system_user do
       c = Container.find_by_uuid(cr.container_uuid)
-      c.update_attributes!(state: Container::Locked)
-      c.update_attributes!(state: Container::Running)
+      c.update!(state: Container::Locked)
+      c.update!(state: Container::Running)
       c
     end
 
@@ -454,7 +454,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
     # increasing priority of the most recent toplevel container should
     # reprioritize all of its descendants (including the shared
     # grandchild) above everything else.
-    toplevel_crs[2].update_attributes!(priority: 72)
+    toplevel_crs[2].update!(priority: 72)
     (parents + children + grandchildren + [shared_grandchild]).map(&:reload)
     assert_operator shared_grandchild.priority, :>, grandchildren[0].priority
     assert_operator shared_grandchild.priority, :>, children[0].priority
@@ -471,7 +471,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
     # cancelling the most recent toplevel container should
     # reprioritize all of its descendants (except the shared
     # grandchild) to zero
-    toplevel_crs[2].update_attributes!(priority: 0)
+    toplevel_crs[2].update!(priority: 0)
     (parents + children + grandchildren + [shared_grandchild]).map(&:reload)
     assert_operator 0, :==, parents[2].priority
     assert_operator 0, :==, children[2].priority
@@ -480,7 +480,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
 
     # cancel a child request, the parent should be > 0 but
     # the child and grandchild go to 0.
-    children_crs[1].update_attributes!(priority: 0)
+    children_crs[1].update!(priority: 0)
     (parents + children + grandchildren + [shared_grandchild]).map(&:reload)
     assert_operator 0, :<, parents[1].priority
     assert_operator parents[0].priority, :>, parents[1].priority
@@ -490,7 +490,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
 
     # update the parent, it should get a higher priority but the children and
     # grandchildren should remain at 0
-    toplevel_crs[1].update_attributes!(priority: 6)
+    toplevel_crs[1].update!(priority: 6)
     (parents + children + grandchildren + [shared_grandchild]).map(&:reload)
     assert_operator 0, :<, parents[1].priority
     assert_operator parents[0].priority, :<, parents[1].priority
@@ -805,7 +805,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
       #   should be assigned.
       # * When use_existing is false, a different container should be assigned.
       # * When env1 and env2 are different, a different container should be assigned.
-      cr2.update_attributes!({state: ContainerRequest::Committed})
+      cr2.update!({state: ContainerRequest::Committed})
       assert_equal (cr2.use_existing == true and (env1 == env2)),
                    (cr1.container_uuid == cr2.container_uuid)
     end
@@ -826,8 +826,8 @@ class ContainerRequestTest < ActiveSupport::TestCase
 
     c = act_as_system_user do
       c = Container.find_by_uuid(cr.container_uuid)
-      c.update_attributes!(state: Container::Locked)
-      c.update_attributes!(state: Container::Running)
+      c.update!(state: Container::Locked)
+      c.update!(state: Container::Running)
       c
     end
 
@@ -839,8 +839,8 @@ class ContainerRequestTest < ActiveSupport::TestCase
     prev_container_uuid = cr.container_uuid
 
     act_as_system_user do
-      c.update_attributes!(cost: 0.5, subrequests_cost: 1.25)
-      c.update_attributes!(state: Container::Cancelled)
+      c.update!(cost: 0.5, subrequests_cost: 1.25)
+      c.update!(state: Container::Cancelled)
     end
 
     cr.reload
@@ -852,10 +852,10 @@ class ContainerRequestTest < ActiveSupport::TestCase
 
     c = act_as_system_user do
       c = Container.find_by_uuid(cr.container_uuid)
-      c.update_attributes!(state: Container::Locked)
-      c.update_attributes!(state: Container::Running)
-      c.update_attributes!(cost: 0.125)
-      c.update_attributes!(state: Container::Cancelled)
+      c.update!(state: Container::Locked)
+      c.update!(state: Container::Running)
+      c.update!(cost: 0.125)
+      c.update!(state: Container::Cancelled)
       c
     end
 
@@ -878,8 +878,8 @@ class ContainerRequestTest < ActiveSupport::TestCase
     c = act_as_system_user do
       c = Container.find_by_uuid(cr.container_uuid)
       assert_equal spec.token, c.runtime_token
-      c.update_attributes!(state: Container::Locked)
-      c.update_attributes!(state: Container::Running)
+      c.update!(state: Container::Locked)
+      c.update!(state: Container::Running)
       c
     end
 
@@ -889,7 +889,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
     prev_container_uuid = cr.container_uuid
 
     act_as_system_user do
-      c.update_attributes!(state: Container::Cancelled)
+      c.update!(state: Container::Cancelled)
     end
 
     cr.reload
@@ -900,7 +900,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
     c = act_as_system_user do
       c = Container.find_by_uuid(cr.container_uuid)
       assert_equal spec.token, c.runtime_token
-      c.update_attributes!(state: Container::Cancelled)
+      c.update!(state: Container::Cancelled)
       c
     end
 
@@ -916,8 +916,8 @@ class ContainerRequestTest < ActiveSupport::TestCase
 
     c = act_as_system_user do
       c = Container.find_by_uuid(cr.container_uuid)
-      c.update_attributes!(state: Container::Locked)
-      c.update_attributes!(state: Container::Running)
+      c.update!(state: Container::Locked)
+      c.update!(state: Container::Running)
       c
     end
 
@@ -932,7 +932,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
         logc = Collection.new(manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n")
         logc.save!
         c = Container.find_by_uuid(cr.container_uuid)
-        c.update_attributes!(state: Container::Cancelled, log: logc.portable_data_hash)
+        c.update!(state: Container::Cancelled, log: logc.portable_data_hash)
         c
       end
     end
@@ -955,8 +955,8 @@ class ContainerRequestTest < ActiveSupport::TestCase
     cr1 = create_minimal_req!(priority: 1, state: "Committed", container_count_max: 2, command: ["echo", "foo1"])
     c1 = Container.find_by_uuid(cr1.container_uuid)
     act_as_system_user do
-      c1.update_attributes!(state: Container::Locked)
-      c1.update_attributes!(state: Container::Running)
+      c1.update!(state: Container::Locked)
+      c1.update!(state: Container::Running)
     end
 
     cr2 = with_container_auth(c1) do
@@ -964,8 +964,8 @@ class ContainerRequestTest < ActiveSupport::TestCase
     end
     c2 = Container.find_by_uuid(cr2.container_uuid)
     act_as_system_user do
-      c2.update_attributes!(state: Container::Locked)
-      c2.update_attributes!(state: Container::Running)
+      c2.update!(state: Container::Locked)
+      c2.update!(state: Container::Running)
     end
 
     cr3 = with_container_auth(c2) do
@@ -974,8 +974,8 @@ class ContainerRequestTest < ActiveSupport::TestCase
     c3 = Container.find_by_uuid(cr3.container_uuid)
 
     act_as_system_user do
-      c3.update_attributes!(state: Container::Locked)
-      c3.update_attributes!(state: Container::Running)
+      c3.update!(state: Container::Locked)
+      c3.update!(state: Container::Running)
     end
 
     # All the containers are in running state
@@ -1007,8 +1007,8 @@ class ContainerRequestTest < ActiveSupport::TestCase
     cr1 = create_minimal_req!(priority: 1, state: "Committed", container_count_max: 2, command: ["echo", "foo1"])
     c1 = Container.find_by_uuid(cr1.container_uuid)
     act_as_system_user do
-      c1.update_attributes!(state: Container::Locked)
-      c1.update_attributes!(state: Container::Running)
+      c1.update!(state: Container::Locked)
+      c1.update!(state: Container::Running)
     end
 
     cr2 = with_container_auth(c1) do
@@ -1016,8 +1016,8 @@ class ContainerRequestTest < ActiveSupport::TestCase
     end
     c2 = Container.find_by_uuid(cr2.container_uuid)
     act_as_system_user do
-      c2.update_attributes!(state: Container::Locked)
-      c2.update_attributes!(state: Container::Running)
+      c2.update!(state: Container::Locked)
+      c2.update!(state: Container::Running)
     end
 
     cr3 = with_container_auth(c2) do
@@ -1026,8 +1026,8 @@ class ContainerRequestTest < ActiveSupport::TestCase
     c3 = Container.find_by_uuid(cr3.container_uuid)
 
     act_as_system_user do
-      c3.update_attributes!(state: Container::Locked)
-      c3.update_attributes!(state: Container::Running)
+      c3.update!(state: Container::Locked)
+      c3.update!(state: Container::Running)
     end
 
     # All the containers are in running state
@@ -1066,8 +1066,8 @@ class ContainerRequestTest < ActiveSupport::TestCase
     cr1 = create_minimal_req!(priority: 1, state: "Committed", container_count_max: 2, command: ["echo", "foo1"])
     c1 = Container.find_by_uuid(cr1.container_uuid)
     act_as_system_user do
-      c1.update_attributes!(state: Container::Locked)
-      c1.update_attributes!(state: Container::Running)
+      c1.update!(state: Container::Locked)
+      c1.update!(state: Container::Running)
     end
 
     cr2 = with_container_auth(c1) do
@@ -1075,8 +1075,8 @@ class ContainerRequestTest < ActiveSupport::TestCase
     end
     c2 = Container.find_by_uuid(cr2.container_uuid)
     act_as_system_user do
-      c2.update_attributes!(state: Container::Locked)
-      c2.update_attributes!(state: Container::Running)
+      c2.update!(state: Container::Locked)
+      c2.update!(state: Container::Running)
     end
 
     cr3 = with_container_auth(c2) do
@@ -1085,8 +1085,8 @@ class ContainerRequestTest < ActiveSupport::TestCase
     c3 = Container.find_by_uuid(cr3.container_uuid)
 
     act_as_system_user do
-      c3.update_attributes!(state: Container::Locked)
-      c3.update_attributes!(state: Container::Running)
+      c3.update!(state: Container::Locked)
+      c3.update!(state: Container::Running)
     end
 
     # All the containers are in running state
@@ -1185,9 +1185,9 @@ class ContainerRequestTest < ActiveSupport::TestCase
       logc.save!
 
       c = Container.find_by_uuid(cr.container_uuid)
-      c.update_attributes!(state: Container::Locked)
-      c.update_attributes!(state: Container::Running)
-      c.update_attributes!(state: final_state,
+      c.update!(state: Container::Locked)
+      c.update!(state: Container::Running)
+      c.update!(state: final_state,
                            exit_code: exit_code,
                            output: '1f4b0bc7583c2a7f9102c395f4ffc5e3+45',
                            log: logc.portable_data_hash)
@@ -1211,7 +1211,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
 
     cr3 = create_minimal_req!(priority: 1, state: ContainerRequest::Uncommitted)
     assert_equal ContainerRequest::Uncommitted, cr3.state
-    cr3.update_attributes!(state: ContainerRequest::Committed)
+    cr3.update!(state: ContainerRequest::Committed)
     assert_equal cr.container_uuid, cr3.container_uuid
     assert_equal ContainerRequest::Final, cr3.state
   end
@@ -1307,7 +1307,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
       # Even though preemptible is not allowed, we should be able to
       # commit a CR that was created earlier when preemptible was the
       # default.
-      commit_later.update_attributes!(priority: 1, state: "Committed")
+      commit_later.update!(priority: 1, state: "Committed")
       expect[false].push commit_later
     end
 
@@ -1323,7 +1323,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
       # Cancelling the parent used to fail while updating the child
       # containers' priority, because the child containers' unchanged
       # preemptible fields caused validation to fail.
-      parent.update_attributes!(state: 'Cancelled')
+      parent.update!(state: 'Cancelled')
 
       [false, true].each do |pflag|
         expect[pflag].each do |cr|
@@ -1450,7 +1450,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
         when 'Final'
           act_as_system_user do
             Container.find_by_uuid(cr.container_uuid).
-              update_attributes!(state: Container::Cancelled)
+              update!(state: Container::Cancelled)
           end
           cr.reload
         else
@@ -1458,10 +1458,10 @@ class ContainerRequestTest < ActiveSupport::TestCase
         end
         assert_equal state, cr.state
         if permitted
-          assert cr.update_attributes!(updates)
+          assert cr.update!(updates)
         else
           assert_raises(ActiveRecord::RecordInvalid) do
-            cr.update_attributes!(updates)
+            cr.update!(updates)
           end
         end
       end
@@ -1493,7 +1493,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
       assert_equal 1, c.priority
 
       prj = Group.find_by_uuid cr.owner_uuid
-      prj.update_attributes!(trash_at: db_current_time)
+      prj.update!(trash_at: db_current_time)
 
       # the cr's container now has priority of 0
       c.reload
@@ -1506,7 +1506,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
       # container request to go to final state and run the finalize
       # function
       act_as_system_user do
-        c.update_attributes!(state: 'Cancelled', log: 'fa7aeb5140e2848d39b416daeef4ffc5+45')
+        c.update!(state: 'Cancelled', log: 'fa7aeb5140e2848d39b416daeef4ffc5+45')
       end
       c.reload
       cr.reload
@@ -1615,7 +1615,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
     sm = {'/secret/foo' => {'kind' => 'text', 'content' => secret_string}}
     set_user_from_auth :active
     cr = create_minimal_req!
-    assert_equal false, cr.update_attributes(state: "Committed",
+    assert_equal false, cr.update(state: "Committed",
                                              priority: 1,
                                              mounts: cr.mounts.merge(sm),
                                              secret_mounts: sm)
@@ -1635,7 +1635,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
     assert_not_nil ApiClientAuthorization.find_by_uuid(spec.uuid)
 
     act_as_system_user do
-      c.update_attributes!(state: Container::Complete,
+      c.update!(state: Container::Complete,
                            exit_code: 0,
                            output: '1f4b0bc7583c2a7f9102c395f4ffc5e3+45',
                            log: 'fa7aeb5140e2848d39b416daeef4ffc5+45')
@@ -1714,7 +1714,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
     assert_nil cr2.container_uuid
 
     # Update cr2 to commited state, check for reuse, then run it
-    cr2.update_attributes!({state: ContainerRequest::Committed})
+    cr2.update!({state: ContainerRequest::Committed})
     assert_equal cr1.container_uuid, cr2.container_uuid
 
     cr2.reload
@@ -1748,12 +1748,12 @@ class ContainerRequestTest < ActiveSupport::TestCase
           logc.save!
 
           c = Container.find_by_uuid(cr.container_uuid)
-          c.update_attributes!(state: Container::Locked)
-          c.update_attributes!(state: Container::Running)
+          c.update!(state: Container::Locked)
+          c.update!(state: Container::Running)
 
-          c.update_attributes!(output_properties: container_prop)
+          c.update!(output_properties: container_prop)
 
-          c.update_attributes!(state: Container::Complete,
+          c.update!(state: Container::Complete,
                                exit_code: 0,
                                output: '1f4b0bc7583c2a7f9102c395f4ffc5e3+45',
                                log: logc.portable_data_hash)
@@ -1773,9 +1773,9 @@ class ContainerRequestTest < ActiveSupport::TestCase
     cr = create_minimal_req!(priority: 5, state: "Committed", container_count_max: 3)
     c = Container.find_by_uuid cr.container_uuid
     act_as_system_user do
-      c.update_attributes!(state: Container::Locked)
-      c.update_attributes!(state: Container::Running)
-      c.update_attributes!(state: Container::Cancelled, cost: 3)
+      c.update!(state: Container::Locked)
+      c.update!(state: Container::Running)
+      c.update!(state: Container::Cancelled, cost: 3)
     end
     cr.reload
     assert_equal 3, cr.cumulative_cost
@@ -1792,12 +1792,12 @@ class ContainerRequestTest < ActiveSupport::TestCase
     assert_equal c.uuid, cr2.requesting_container_uuid
     c2 = Container.find_by_uuid cr2.container_uuid
     act_as_system_user do
-      c2.update_attributes!(state: Container::Locked)
-      c2.update_attributes!(state: Container::Running)
+      c2.update!(state: Container::Locked)
+      c2.update!(state: Container::Running)
       logc = Collection.new(owner_uuid: system_user_uuid,
                             manifest_text: ". ef772b2f28e2c8ca84de45466ed19ee9+7815 0:0:arv-mount.txt\n")
       logc.save!
-      c2.update_attributes!(state: Container::Complete,
+      c2.update!(state: Container::Complete,
                             exit_code: 0,
                             output: '1f4b0bc7583c2a7f9102c395f4ffc5e3+45',
                             log: logc.portable_data_hash,
@@ -1818,7 +1818,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
     assert_equal 7, c.subrequests_cost
 
     act_as_system_user do
-      c.update_attributes!(state: Container::Complete, exit_code: 0, cost: 9)
+      c.update!(state: Container::Complete, exit_code: 0, cost: 9)
     end
 
     c.reload
diff --git a/services/api/test/unit/container_test.rb b/services/api/test/unit/container_test.rb
index cb9cd7503b..09b885b391 100644
--- a/services/api/test/unit/container_test.rb
+++ b/services/api/test/unit/container_test.rb
@@ -76,7 +76,7 @@ class ContainerTest < ActiveSupport::TestCase
 
   def check_illegal_updates c, bad_updates
     bad_updates.each do |u|
-      refute c.update_attributes(u), u.inspect
+      refute c.update(u), u.inspect
       refute c.valid?, u.inspect
       c.reload
     end
@@ -173,15 +173,15 @@ class ContainerTest < ActiveSupport::TestCase
     assert_equal Container::Queued, c.state
 
     set_user_from_auth :dispatch1
-    c.update_attributes! state: Container::Locked
-    c.update_attributes! state: Container::Running
+    c.update! state: Container::Locked
+    c.update! state: Container::Running
 
     [
       'error', 'errorDetail', 'warning', 'warningDetail', 'activity'
     ].each do |k|
       # String type is allowed
       string_val = 'A string is accepted'
-      c.update_attributes! runtime_status: {k => string_val}
+      c.update! runtime_status: {k => string_val}
       assert_equal string_val, c.runtime_status[k]
 
       # Other types aren't allowed
@@ -189,7 +189,7 @@ class ContainerTest < ActiveSupport::TestCase
         42, false, [], {}, nil
       ].each do |unallowed_val|
         assert_raises ActiveRecord::RecordInvalid do
-          c.update_attributes! runtime_status: {k => unallowed_val}
+          c.update! runtime_status: {k => unallowed_val}
         end
       end
     end
@@ -209,41 +209,41 @@ class ContainerTest < ActiveSupport::TestCase
 
     assert_equal Container::Queued, c1.state
     assert_raises ArvadosModel::PermissionDeniedError do
-      c1.update_attributes! runtime_status: {'error' => 'Oops!'}
+      c1.update! runtime_status: {'error' => 'Oops!'}
     end
 
     set_user_from_auth :dispatch1
 
     # Allow updates when state = Locked
-    c1.update_attributes! state: Container::Locked
-    c1.update_attributes! runtime_status: {'error' => 'Oops!'}
+    c1.update! state: Container::Locked
+    c1.update! runtime_status: {'error' => 'Oops!'}
     assert c1.runtime_status.key? 'error'
 
     # Reset when transitioning from Locked to Queued
-    c1.update_attributes! state: Container::Queued
+    c1.update! state: Container::Queued
     assert_equal c1.runtime_status, {}
 
     # Allow updates when state = Running
-    c1.update_attributes! state: Container::Locked
-    c1.update_attributes! state: Container::Running
-    c1.update_attributes! runtime_status: {'error' => 'Oops!'}
+    c1.update! state: Container::Locked
+    c1.update! state: Container::Running
+    c1.update! runtime_status: {'error' => 'Oops!'}
     assert c1.runtime_status.key? 'error'
 
     # Don't allow updates on other states
-    c1.update_attributes! state: Container::Complete
+    c1.update! state: Container::Complete
     assert_raises ActiveRecord::RecordInvalid do
-      c1.update_attributes! runtime_status: {'error' => 'Some other error'}
+      c1.update! runtime_status: {'error' => 'Some other error'}
     end
 
     set_user_from_auth :active
     c2, _ = minimal_new(attrs)
     assert_equal c2.runtime_status, {}
     set_user_from_auth :dispatch1
-    c2.update_attributes! state: Container::Locked
-    c2.update_attributes! state: Container::Running
-    c2.update_attributes! state: Container::Cancelled
+    c2.update! state: Container::Locked
+    c2.update! state: Container::Running
+    c2.update! state: Container::Cancelled
     assert_raises ActiveRecord::RecordInvalid do
-      c2.update_attributes! runtime_status: {'error' => 'Oops!'}
+      c2.update! runtime_status: {'error' => 'Oops!'}
     end
   end
 
@@ -294,13 +294,13 @@ class ContainerTest < ActiveSupport::TestCase
     assert_not_equal c_older.uuid, c_recent.uuid
 
     set_user_from_auth :dispatch1
-    c_older.update_attributes!({state: Container::Locked})
-    c_older.update_attributes!({state: Container::Running})
-    c_older.update_attributes!(completed_attrs)
+    c_older.update!({state: Container::Locked})
+    c_older.update!({state: Container::Running})
+    c_older.update!(completed_attrs)
 
-    c_recent.update_attributes!({state: Container::Locked})
-    c_recent.update_attributes!({state: Container::Running})
-    c_recent.update_attributes!(completed_attrs)
+    c_recent.update!({state: Container::Locked})
+    c_recent.update!({state: Container::Running})
+    c_recent.update!(completed_attrs)
 
     reused = Container.find_reusable(common_attrs)
     assert_not_nil reused
@@ -334,14 +334,14 @@ class ContainerTest < ActiveSupport::TestCase
 
     out1 = '1f4b0bc7583c2a7f9102c395f4ffc5e3+45'
     log1 = collections(:real_log_collection).portable_data_hash
-    c_output1.update_attributes!({state: Container::Locked})
-    c_output1.update_attributes!({state: Container::Running})
-    c_output1.update_attributes!(completed_attrs.merge({log: log1, output: out1}))
+    c_output1.update!({state: Container::Locked})
+    c_output1.update!({state: Container::Running})
+    c_output1.update!(completed_attrs.merge({log: log1, output: out1}))
 
     out2 = 'fa7aeb5140e2848d39b416daeef4ffc5+45'
-    c_output2.update_attributes!({state: Container::Locked})
-    c_output2.update_attributes!({state: Container::Running})
-    c_output2.update_attributes!(completed_attrs.merge({log: log1, output: out2}))
+    c_output2.update!({state: Container::Locked})
+    c_output2.update!({state: Container::Running})
+    c_output2.update!(completed_attrs.merge({log: log1, output: out2}))
 
     set_user_from_auth :active
     reused = Container.resolve(ContainerRequest.new(request_only(common_attrs)))
@@ -357,14 +357,14 @@ class ContainerTest < ActiveSupport::TestCase
     # Confirm the 3 container UUIDs are different.
     assert_equal 3, [c_slower.uuid, c_faster_started_first.uuid, c_faster_started_second.uuid].uniq.length
     set_user_from_auth :dispatch1
-    c_slower.update_attributes!({state: Container::Locked})
-    c_slower.update_attributes!({state: Container::Running,
+    c_slower.update!({state: Container::Locked})
+    c_slower.update!({state: Container::Running,
                                  progress: 0.1})
-    c_faster_started_first.update_attributes!({state: Container::Locked})
-    c_faster_started_first.update_attributes!({state: Container::Running,
+    c_faster_started_first.update!({state: Container::Locked})
+    c_faster_started_first.update!({state: Container::Running,
                                                progress: 0.15})
-    c_faster_started_second.update_attributes!({state: Container::Locked})
-    c_faster_started_second.update_attributes!({state: Container::Running,
+    c_faster_started_second.update!({state: Container::Locked})
+    c_faster_started_second.update!({state: Container::Running,
                                                 progress: 0.15})
     reused = Container.find_reusable(common_attrs)
     assert_not_nil reused
@@ -381,14 +381,14 @@ class ContainerTest < ActiveSupport::TestCase
     # Confirm the 3 container UUIDs are different.
     assert_equal 3, [c_slower.uuid, c_faster_started_first.uuid, c_faster_started_second.uuid].uniq.length
     set_user_from_auth :dispatch1
-    c_slower.update_attributes!({state: Container::Locked})
-    c_slower.update_attributes!({state: Container::Running,
+    c_slower.update!({state: Container::Locked})
+    c_slower.update!({state: Container::Running,
                                  progress: 0.1})
-    c_faster_started_first.update_attributes!({state: Container::Locked})
-    c_faster_started_first.update_attributes!({state: Container::Running,
+    c_faster_started_first.update!({state: Container::Locked})
+    c_faster_started_first.update!({state: Container::Running,
                                                progress: 0.15})
-    c_faster_started_second.update_attributes!({state: Container::Locked})
-    c_faster_started_second.update_attributes!({state: Container::Running,
+    c_faster_started_second.update!({state: Container::Locked})
+    c_faster_started_second.update!({state: Container::Running,
                                                 progress: 0.2})
     reused = Container.find_reusable(common_attrs)
     assert_not_nil reused
@@ -405,16 +405,16 @@ class ContainerTest < ActiveSupport::TestCase
     # Confirm the 3 container UUIDs are different.
     assert_equal 3, [c_slower.uuid, c_faster_started_first.uuid, c_faster_started_second.uuid].uniq.length
     set_user_from_auth :dispatch1
-    c_slower.update_attributes!({state: Container::Locked})
-    c_slower.update_attributes!({state: Container::Running,
+    c_slower.update!({state: Container::Locked})
+    c_slower.update!({state: Container::Running,
                                  progress: 0.1})
-    c_faster_started_first.update_attributes!({state: Container::Locked})
-    c_faster_started_first.update_attributes!({state: Container::Running,
+    c_faster_started_first.update!({state: Container::Locked})
+    c_faster_started_first.update!({state: Container::Running,
                                                runtime_status: {'warning' => 'This is not an error'},
                                                progress: 0.15})
-    c_faster_started_second.update_attributes!({state: Container::Locked})
+    c_faster_started_second.update!({state: Container::Locked})
     assert_equal 0, Container.where("runtime_status->'error' is not null").count
-    c_faster_started_second.update_attributes!({state: Container::Running,
+    c_faster_started_second.update!({state: Container::Running,
                                                 runtime_status: {'error' => 'Something bad happened'},
                                                 progress: 0.2})
     assert_equal 1, Container.where("runtime_status->'error' is not null").count
@@ -433,11 +433,11 @@ class ContainerTest < ActiveSupport::TestCase
     # Confirm the 3 container UUIDs are different.
     assert_equal 3, [c_low_priority.uuid, c_high_priority_older.uuid, c_high_priority_newer.uuid].uniq.length
     set_user_from_auth :dispatch1
-    c_low_priority.update_attributes!({state: Container::Locked,
+    c_low_priority.update!({state: Container::Locked,
                                        priority: 1})
-    c_high_priority_older.update_attributes!({state: Container::Locked,
+    c_high_priority_older.update!({state: Container::Locked,
                                               priority: 2})
-    c_high_priority_newer.update_attributes!({state: Container::Locked,
+    c_high_priority_newer.update!({state: Container::Locked,
                                               priority: 2})
     reused = Container.find_reusable(common_attrs)
     assert_not_nil reused
@@ -451,14 +451,14 @@ class ContainerTest < ActiveSupport::TestCase
     c_running, _ = minimal_new(common_attrs.merge({use_existing: false}))
     assert_not_equal c_failed.uuid, c_running.uuid
     set_user_from_auth :dispatch1
-    c_failed.update_attributes!({state: Container::Locked})
-    c_failed.update_attributes!({state: Container::Running})
-    c_failed.update_attributes!({state: Container::Complete,
+    c_failed.update!({state: Container::Locked})
+    c_failed.update!({state: Container::Running})
+    c_failed.update!({state: Container::Complete,
                                  exit_code: 42,
                                  log: 'ea10d51bcf88862dbcc36eb292017dfd+45',
                                  output: 'ea10d51bcf88862dbcc36eb292017dfd+45'})
-    c_running.update_attributes!({state: Container::Locked})
-    c_running.update_attributes!({state: Container::Running,
+    c_running.update!({state: Container::Locked})
+    c_running.update!({state: Container::Running,
                                   progress: 0.15})
     reused = Container.find_reusable(common_attrs)
     assert_not_nil reused
@@ -472,14 +472,14 @@ class ContainerTest < ActiveSupport::TestCase
     c_running, _ = minimal_new(common_attrs.merge({use_existing: false}))
     assert_not_equal c_completed.uuid, c_running.uuid
     set_user_from_auth :dispatch1
-    c_completed.update_attributes!({state: Container::Locked})
-    c_completed.update_attributes!({state: Container::Running})
-    c_completed.update_attributes!({state: Container::Complete,
+    c_completed.update!({state: Container::Locked})
+    c_completed.update!({state: Container::Running})
+    c_completed.update!({state: Container::Complete,
                                     exit_code: 0,
                                     log: 'ea10d51bcf88862dbcc36eb292017dfd+45',
                                     output: '1f4b0bc7583c2a7f9102c395f4ffc5e3+45'})
-    c_running.update_attributes!({state: Container::Locked})
-    c_running.update_attributes!({state: Container::Running,
+    c_running.update!({state: Container::Locked})
+    c_running.update!({state: Container::Running,
                                   progress: 0.15})
     reused = Container.find_reusable(common_attrs)
     assert_not_nil reused
@@ -493,9 +493,9 @@ class ContainerTest < ActiveSupport::TestCase
     c_running, _ = minimal_new(common_attrs.merge({use_existing: false}))
     assert_not_equal c_running.uuid, c_locked.uuid
     set_user_from_auth :dispatch1
-    c_locked.update_attributes!({state: Container::Locked})
-    c_running.update_attributes!({state: Container::Locked})
-    c_running.update_attributes!({state: Container::Running,
+    c_locked.update!({state: Container::Locked})
+    c_running.update!({state: Container::Locked})
+    c_running.update!({state: Container::Running,
                                   progress: 0.15})
     reused = Container.find_reusable(common_attrs)
     assert_not_nil reused
@@ -509,7 +509,7 @@ class ContainerTest < ActiveSupport::TestCase
     c_queued, _ = minimal_new(common_attrs.merge({use_existing: false}))
     assert_not_equal c_queued.uuid, c_locked.uuid
     set_user_from_auth :dispatch1
-    c_locked.update_attributes!({state: Container::Locked})
+    c_locked.update!({state: Container::Locked})
     reused = Container.find_reusable(common_attrs)
     assert_not_nil reused
     assert_equal reused.uuid, c_locked.uuid
@@ -520,9 +520,9 @@ class ContainerTest < ActiveSupport::TestCase
     attrs = REUSABLE_COMMON_ATTRS.merge({environment: {"var" => "failed"}})
     c, _ = minimal_new(attrs)
     set_user_from_auth :dispatch1
-    c.update_attributes!({state: Container::Locked})
-    c.update_attributes!({state: Container::Running})
-    c.update_attributes!({state: Container::Complete,
+    c.update!({state: Container::Locked})
+    c.update!({state: Container::Running})
+    c.update!({state: Container::Complete,
                           exit_code: 33})
     reused = Container.find_reusable(attrs)
     assert_nil reused
@@ -543,8 +543,8 @@ class ContainerTest < ActiveSupport::TestCase
         c1_attrs = REUSABLE_COMMON_ATTRS.merge({environment: {"test" => name, "state" => c1_state}, scheduling_parameters: {"preemptible" => c1_preemptible}})
         c1, _ = minimal_new(c1_attrs)
         set_user_from_auth :dispatch1
-        c1.update_attributes!({state: Container::Locked}) if c1_state != Container::Queued
-        c1.update_attributes!({state: Container::Running, priority: c1_priority}) if c1_state == Container::Running
+        c1.update!({state: Container::Locked}) if c1_state != Container::Queued
+        c1.update!({state: Container::Running, priority: c1_priority}) if c1_state == Container::Running
         c2_attrs = c1_attrs.merge({scheduling_parameters: {"preemptible" => c2_preemptible}})
         reused = Container.find_reusable(c2_attrs)
         if should_reuse && c1_priority > 0
@@ -676,7 +676,7 @@ class ContainerTest < ActiveSupport::TestCase
                               {state: Container::Complete}]
 
     c.lock
-    c.update_attributes! state: Container::Running
+    c.update! state: Container::Running
 
     check_illegal_modify c
     check_bogus_states c
@@ -684,7 +684,7 @@ class ContainerTest < ActiveSupport::TestCase
     check_illegal_updates c, [{state: Container::Queued}]
     c.reload
 
-    c.update_attributes! priority: 3
+    c.update! priority: 3
   end
 
   test "Lock and unlock" do
@@ -699,11 +699,11 @@ class ContainerTest < ActiveSupport::TestCase
       c.lock
     end
     c.reload
-    assert cr.update_attributes priority: 1
+    assert cr.update priority: 1
 
-    refute c.update_attributes(state: Container::Running), "not locked"
+    refute c.update(state: Container::Running), "not locked"
     c.reload
-    refute c.update_attributes(state: Container::Complete), "not locked"
+    refute c.update(state: Container::Complete), "not locked"
     c.reload
 
     assert c.lock, show_errors(c)
@@ -717,13 +717,13 @@ class ContainerTest < ActiveSupport::TestCase
     refute c.locked_by_uuid
     refute c.auth_uuid
 
-    refute c.update_attributes(state: Container::Running), "not locked"
+    refute c.update(state: Container::Running), "not locked"
     c.reload
     refute c.locked_by_uuid
     refute c.auth_uuid
 
     assert c.lock, show_errors(c)
-    assert c.update_attributes(state: Container::Running), show_errors(c)
+    assert c.update(state: Container::Running), show_errors(c)
     assert c.locked_by_uuid
     assert c.auth_uuid
 
@@ -740,7 +740,7 @@ class ContainerTest < ActiveSupport::TestCase
     end
     c.reload
 
-    assert c.update_attributes(state: Container::Complete), show_errors(c)
+    assert c.update(state: Container::Complete), show_errors(c)
     refute c.locked_by_uuid
     refute c.auth_uuid
 
@@ -800,7 +800,7 @@ class ContainerTest < ActiveSupport::TestCase
     set_user_from_auth :active
     c, cr = minimal_new({container_count_max: 1})
     set_user_from_auth :dispatch1
-    assert c.update_attributes(state: Container::Cancelled), show_errors(c)
+    assert c.update(state: Container::Cancelled), show_errors(c)
     check_no_change_from_cancelled c
     cr.reload
     assert_equal ContainerRequest::Final, cr.state
@@ -823,7 +823,7 @@ class ContainerTest < ActiveSupport::TestCase
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     assert c.lock, show_errors(c)
-    assert c.update_attributes(state: Container::Cancelled), show_errors(c)
+    assert c.update(state: Container::Cancelled), show_errors(c)
     check_no_change_from_cancelled c
   end
 
@@ -843,7 +843,7 @@ class ContainerTest < ActiveSupport::TestCase
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     assert c.lock, show_errors(c)
-    assert c.update_attributes(
+    assert c.update(
              state: Container::Cancelled,
              log: collections(:real_log_collection).portable_data_hash,
            ), show_errors(c)
@@ -855,8 +855,8 @@ class ContainerTest < ActiveSupport::TestCase
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     c.lock
-    c.update_attributes! state: Container::Running
-    c.update_attributes! state: Container::Cancelled
+    c.update! state: Container::Running
+    c.update! state: Container::Cancelled
     check_no_change_from_cancelled c
   end
 
@@ -906,16 +906,16 @@ class ContainerTest < ActiveSupport::TestCase
         set_user_from_auth :dispatch1
         c.lock
         if start_state != Container::Locked
-          c.update_attributes! state: Container::Running
+          c.update! state: Container::Running
           if start_state != Container::Running
-            c.update_attributes! state: start_state
+            c.update! state: start_state
           end
         end
       end
       assert_equal c.state, start_state
       set_user_from_auth :active
       assert_raises(ArvadosModel::PermissionDeniedError) do
-        c.update_attributes! updates
+        c.update! updates
       end
     end
   end
@@ -926,9 +926,9 @@ class ContainerTest < ActiveSupport::TestCase
     set_user_from_auth :dispatch1
     c.lock
     check_illegal_updates c, [{exit_code: 1}]
-    c.update_attributes! state: Container::Running
-    assert c.update_attributes(exit_code: 1)
-    assert c.update_attributes(exit_code: 1, state: Container::Complete)
+    c.update! state: Container::Running
+    assert c.update(exit_code: 1)
+    assert c.update(exit_code: 1, state: Container::Complete)
   end
 
   test "locked_by_uuid can update log when locked/running, and output when running" do
@@ -947,8 +947,8 @@ class ContainerTest < ActiveSupport::TestCase
     set_user_from_auth :dispatch1
     c.lock
     assert_equal c.locked_by_uuid, Thread.current[:api_client_authorization].uuid
-    c.update_attributes!(log: logpdh_time1)
-    c.update_attributes!(state: Container::Running)
+    c.update!(log: logpdh_time1)
+    c.update!(state: Container::Running)
     cr1.reload
     cr2.reload
     cr1log_uuid = cr1.log_uuid
@@ -959,17 +959,17 @@ class ContainerTest < ActiveSupport::TestCase
     assert_not_equal logcoll.uuid, cr2log_uuid
     assert_not_equal cr1log_uuid, cr2log_uuid
 
-    logcoll.update_attributes!(manifest_text: logcoll.manifest_text + ". acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:foo.txt\n")
+    logcoll.update!(manifest_text: logcoll.manifest_text + ". acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:foo.txt\n")
     logpdh_time2 = logcoll.portable_data_hash
 
-    assert c.update_attributes(output: collections(:collection_owned_by_active).portable_data_hash)
-    assert c.update_attributes(log: logpdh_time2)
-    assert c.update_attributes(state: Container::Complete, log: logcoll.portable_data_hash)
+    assert c.update(output: collections(:collection_owned_by_active).portable_data_hash)
+    assert c.update(log: logpdh_time2)
+    assert c.update(state: Container::Complete, log: logcoll.portable_data_hash)
     c.reload
     assert_equal collections(:collection_owned_by_active).portable_data_hash, c.output
     assert_equal logpdh_time2, c.log
-    refute c.update_attributes(output: nil)
-    refute c.update_attributes(log: nil)
+    refute c.update(output: nil)
+    refute c.update(log: nil)
     cr1.reload
     cr2.reload
     assert_equal cr1log_uuid, cr1.log_uuid
@@ -992,7 +992,7 @@ class ContainerTest < ActiveSupport::TestCase
       end
       set_user_from_auth :dispatch1
       c.lock
-      c.update_attributes! state: Container::Running
+      c.update! state: Container::Running
 
       if tok == "runtime_token"
         auth = ApiClientAuthorization.validate(token: c.runtime_token)
@@ -1008,14 +1008,14 @@ class ContainerTest < ActiveSupport::TestCase
         Thread.current[:user] = auth.user
       end
 
-      assert c.update_attributes(gateway_address: "127.0.0.1:9")
-      assert c.update_attributes(output: collections(:collection_owned_by_active).portable_data_hash)
-      assert c.update_attributes(runtime_status: {'warning' => 'something happened'})
-      assert c.update_attributes(progress: 0.5)
-      assert c.update_attributes(exit_code: 0)
-      refute c.update_attributes(log: collections(:real_log_collection).portable_data_hash)
+      assert c.update(gateway_address: "127.0.0.1:9")
+      assert c.update(output: collections(:collection_owned_by_active).portable_data_hash)
+      assert c.update(runtime_status: {'warning' => 'something happened'})
+      assert c.update(progress: 0.5)
+      assert c.update(exit_code: 0)
+      refute c.update(log: collections(:real_log_collection).portable_data_hash)
       c.reload
-      assert c.update_attributes(state: Container::Complete, exit_code: 0)
+      assert c.update(state: Container::Complete, exit_code: 0)
     end
   end
 
@@ -1024,13 +1024,13 @@ class ContainerTest < ActiveSupport::TestCase
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     c.lock
-    c.update_attributes! state: Container::Running
+    c.update! state: Container::Running
 
     Thread.current[:api_client_authorization] = ApiClientAuthorization.find_by_uuid(c.auth_uuid)
     Thread.current[:user] = User.find_by_id(Thread.current[:api_client_authorization].user_id)
 
     assert_raises ActiveRecord::RecordInvalid do
-      c.update_attributes! output: collections(:collection_not_readable_by_active).portable_data_hash
+      c.update! output: collections(:collection_not_readable_by_active).portable_data_hash
     end
   end
 
@@ -1039,11 +1039,11 @@ class ContainerTest < ActiveSupport::TestCase
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     c.lock
-    c.update_attributes! state: Container::Running
+    c.update! state: Container::Running
 
     set_user_from_auth :running_to_be_deleted_container_auth
     assert_raises(ArvadosModel::PermissionDeniedError) do
-      c.update_attributes(output: collections(:foo_file).portable_data_hash)
+      c.update(output: collections(:foo_file).portable_data_hash)
     end
   end
 
@@ -1052,13 +1052,13 @@ class ContainerTest < ActiveSupport::TestCase
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     c.lock
-    c.update_attributes! state: Container::Running
+    c.update! state: Container::Running
 
     output = Collection.find_by_uuid('zzzzz-4zz18-mto52zx1s7sn3jk')
 
     assert output.is_trashed
-    assert c.update_attributes output: output.portable_data_hash
-    assert c.update_attributes! state: Container::Complete
+    assert c.update output: output.portable_data_hash
+    assert c.update! state: Container::Complete
   end
 
   test "not allowed to set trashed output that is not readable by current user" do
@@ -1066,7 +1066,7 @@ class ContainerTest < ActiveSupport::TestCase
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     c.lock
-    c.update_attributes! state: Container::Running
+    c.update! state: Container::Running
 
     output = Collection.find_by_uuid('zzzzz-4zz18-mto52zx1s7sn3jr')
 
@@ -1074,7 +1074,7 @@ class ContainerTest < ActiveSupport::TestCase
     Thread.current[:user] = User.find_by_id(Thread.current[:api_client_authorization].user_id)
 
     assert_raises ActiveRecord::RecordInvalid do
-      c.update_attributes! output: output.portable_data_hash
+      c.update! output: output.portable_data_hash
     end
   end
 
@@ -1097,12 +1097,12 @@ class ContainerTest < ActiveSupport::TestCase
                           container_count_max: 1, runtime_token: api_client_authorizations(:active).token)
       set_user_from_auth :dispatch1
       c.lock
-      c.update_attributes!(state: Container::Running)
+      c.update!(state: Container::Running)
       c.reload
       assert c.secret_mounts.has_key?('/secret')
       assert_equal api_client_authorizations(:active).token, c.runtime_token
 
-      c.update_attributes!(final_attrs)
+      c.update!(final_attrs)
       c.reload
       assert_equal({}, c.secret_mounts)
       assert_nil c.runtime_token
@@ -1150,7 +1150,7 @@ class ContainerTest < ActiveSupport::TestCase
     assert_equal(1, containers.length)
     _, container1 = containers.shift
     container1.lock
-    container1.update_attributes!(state: Container::Cancelled)
+    container1.update!(state: Container::Cancelled)
     container1.reload
     request1 = requests.shift
     request1.reload
@@ -1277,8 +1277,8 @@ class ContainerTest < ActiveSupport::TestCase
     end
     container, request = minimal_new(request_params)
     container.lock
-    container.update_attributes!(state: Container::Running)
-    container.update_attributes!(final_attrs)
+    container.update!(state: Container::Running)
+    container.update!(final_attrs)
     return container, request
   end
 
diff --git a/services/api/test/unit/create_superuser_token_test.rb b/services/api/test/unit/create_superuser_token_test.rb
index 3c6dcbdbbc..86ba78cb99 100644
--- a/services/api/test/unit/create_superuser_token_test.rb
+++ b/services/api/test/unit/create_superuser_token_test.rb
@@ -54,7 +54,7 @@ class CreateSuperUserTokenTest < ActiveSupport::TestCase
     apiClientAuth = ApiClientAuthorization.where(api_token: 'atesttoken').first
     refute_nil apiClientAuth
     Thread.current[:user] = users(:admin)
-    apiClientAuth.update_attributes expires_at: '2000-10-10'
+    apiClientAuth.update expires_at: '2000-10-10'
 
     token2 = create_superuser_token
     assert_not_nil token2
diff --git a/services/api/test/unit/group_test.rb b/services/api/test/unit/group_test.rb
index a0c375a6f9..36f42006ff 100644
--- a/services/api/test/unit/group_test.rb
+++ b/services/api/test/unit/group_test.rb
@@ -82,7 +82,7 @@ class GroupTest < ActiveSupport::TestCase
     set_user_from_auth :active_trustedclient
     g = Group.create!(name: "foo", group_class: "role")
     assert_raises(ActiveRecord::RecordInvalid) do
-      g.update_attributes!(group_class: "project")
+      g.update!(group_class: "project")
     end
   end
 
@@ -95,7 +95,7 @@ class GroupTest < ActiveSupport::TestCase
 
     c = Collection.create!(name: "bzzz124")
     assert_raises(ArvadosModel::PermissionDeniedError) do
-      c.update_attributes!(owner_uuid: role.uuid)
+      c.update!(owner_uuid: role.uuid)
     end
   end
 
@@ -336,7 +336,7 @@ update links set tail_uuid='#{g5}' where uuid='#{l1.uuid}'
 
       # Cannot set frozen_by_uuid to a different user
       assert_raises do
-        proj.update_attributes!(frozen_by_uuid: users(:spectator).uuid)
+        proj.update!(frozen_by_uuid: users(:spectator).uuid)
       end
       proj.reload
 
@@ -348,7 +348,7 @@ update links set tail_uuid='#{g5}' where uuid='#{l1.uuid}'
         # First confirm we have write permission
         assert Collection.create(name: 'bar', owner_uuid: proj.uuid)
         assert_raises(ArvadosModel::PermissionDeniedError) do
-          proj.update_attributes!(frozen_by_uuid: users(:spectator).uuid)
+          proj.update!(frozen_by_uuid: users(:spectator).uuid)
         end
       end
       proj.reload
@@ -356,12 +356,12 @@ update links set tail_uuid='#{g5}' where uuid='#{l1.uuid}'
       # Cannot set frozen_by_uuid without description (if so configured)
       Rails.configuration.API.FreezeProjectRequiresDescription = true
       err = assert_raises do
-        proj.update_attributes!(frozen_by_uuid: users(:active).uuid)
+        proj.update!(frozen_by_uuid: users(:active).uuid)
       end
       assert_match /can only be set if description is non-empty/, err.inspect
       proj.reload
       err = assert_raises do
-        proj.update_attributes!(frozen_by_uuid: users(:active).uuid, description: '')
+        proj.update!(frozen_by_uuid: users(:active).uuid, description: '')
       end
       assert_match /can only be set if description is non-empty/, err.inspect
       proj.reload
@@ -369,7 +369,7 @@ update links set tail_uuid='#{g5}' where uuid='#{l1.uuid}'
       # Cannot set frozen_by_uuid without properties (if so configured)
       Rails.configuration.API.FreezeProjectRequiresProperties['frobity'] = true
       err = assert_raises do
-        proj.update_attributes!(
+        proj.update!(
           frozen_by_uuid: users(:active).uuid,
           description: 'ready to freeze')
       end
@@ -379,20 +379,20 @@ update links set tail_uuid='#{g5}' where uuid='#{l1.uuid}'
       # Cannot set frozen_by_uuid while project or its parent is
       # trashed
       [parent, proj].each do |trashed|
-        trashed.update_attributes!(trash_at: db_current_time)
+        trashed.update!(trash_at: db_current_time)
         err = assert_raises do
-          proj.update_attributes!(
+          proj.update!(
             frozen_by_uuid: users(:active).uuid,
             description: 'ready to freeze',
             properties: {'frobity' => 'bar baz'})
         end
         assert_match /cannot be set on a trashed project/, err.inspect
         proj.reload
-        trashed.update_attributes!(trash_at: nil)
+        trashed.update!(trash_at: nil)
       end
 
       # Can set frozen_by_uuid if all conditions are met
-      ok = proj.update_attributes(
+      ok = proj.update(
         frozen_by_uuid: users(:active).uuid,
         description: 'ready to freeze',
         properties: {'frobity' => 'bar baz'})
@@ -404,7 +404,7 @@ update links set tail_uuid='#{g5}' where uuid='#{l1.uuid}'
           # its descendants
           [proj, proj_inner].each do |frozen|
             assert_raises do
-              collections(:collection_owned_by_active).update_attributes!(owner_uuid: frozen.uuid)
+              collections(:collection_owned_by_active).update!(owner_uuid: frozen.uuid)
             end
             assert_raises do
               Collection.create!(owner_uuid: frozen.uuid, name: 'inside-frozen-project')
@@ -427,31 +427,31 @@ update links set tail_uuid='#{g5}' where uuid='#{l1.uuid}'
           # trash, or delete the project or anything beneath it
           [proj, proj_inner, coll].each do |frozen|
             assert_raises(StandardError, "should reject rename of #{frozen.uuid} (#{frozen.name}) with parent #{frozen.owner_uuid}") do
-              frozen.update_attributes!(name: 'foo2')
+              frozen.update!(name: 'foo2')
             end
             frozen.reload
 
             if frozen.is_a?(Collection)
               assert_raises(StandardError, "should reject manifest change of #{frozen.uuid}") do
-                frozen.update_attributes!(manifest_text: ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\n")
+                frozen.update!(manifest_text: ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\n")
               end
             else
               assert_raises(StandardError, "should reject moving a project into #{frozen.uuid}") do
-                groups(:private).update_attributes!(owner_uuid: frozen.uuid)
+                groups(:private).update!(owner_uuid: frozen.uuid)
               end
             end
             frozen.reload
 
             assert_raises(StandardError, "should reject moving #{frozen.uuid} to a different parent project") do
-              frozen.update_attributes!(owner_uuid: groups(:private).uuid)
+              frozen.update!(owner_uuid: groups(:private).uuid)
             end
             frozen.reload
             assert_raises(StandardError, "should reject setting trash_at of #{frozen.uuid}") do
-              frozen.update_attributes!(trash_at: db_current_time)
+              frozen.update!(trash_at: db_current_time)
             end
             frozen.reload
             assert_raises(StandardError, "should reject setting delete_at of #{frozen.uuid}") do
-              frozen.update_attributes!(delete_at: db_current_time)
+              frozen.update!(delete_at: db_current_time)
             end
             frozen.reload
             assert_raises(StandardError, "should reject delete of #{frozen.uuid}") do
@@ -470,35 +470,35 @@ update links set tail_uuid='#{g5}' where uuid='#{l1.uuid}'
         # First confirm we have write permission on the parent project
         assert Collection.create(name: 'bar', owner_uuid: parent.uuid)
         assert_raises(ArvadosModel::PermissionDeniedError) do
-          proj.update_attributes!(frozen_by_uuid: nil)
+          proj.update!(frozen_by_uuid: nil)
         end
       end
       proj.reload
 
       # User with manage permission can unfreeze, then create items
       # inside it and its children
-      assert proj.update_attributes(frozen_by_uuid: nil)
+      assert proj.update(frozen_by_uuid: nil)
       assert Collection.create!(owner_uuid: proj.uuid, name: 'inside-unfrozen-project')
       assert Collection.create!(owner_uuid: proj_inner.uuid, name: 'inside-inner-unfrozen-project')
 
       # Re-freeze, and reconfigure so only admins can unfreeze.
-      assert proj.update_attributes(frozen_by_uuid: users(:active).uuid)
+      assert proj.update(frozen_by_uuid: users(:active).uuid)
       Rails.configuration.API.UnfreezeProjectRequiresAdmin = true
 
       # Owner cannot unfreeze, because not admin.
       err = assert_raises do
-        proj.update_attributes!(frozen_by_uuid: nil)
+        proj.update!(frozen_by_uuid: nil)
       end
       assert_match /can only be changed by an admin user, once set/, err.inspect
       proj.reload
 
       # Cannot trash or delete a frozen project's ancestor
       assert_raises(StandardError, "should not be able to set trash_at on parent of frozen project") do
-        parent.update_attributes!(trash_at: db_current_time)
+        parent.update!(trash_at: db_current_time)
       end
       parent.reload
       assert_raises(StandardError, "should not be able to set delete_at on parent of frozen project") do
-        parent.update_attributes!(delete_at: db_current_time)
+        parent.update!(delete_at: db_current_time)
       end
       parent.reload
       assert_nil parent.frozen_by_uuid
@@ -506,13 +506,13 @@ update links set tail_uuid='#{g5}' where uuid='#{l1.uuid}'
       act_as_user users(:admin) do
         # Even admin cannot change frozen_by_uuid to someone else's UUID.
         err = assert_raises do
-          proj.update_attributes!(frozen_by_uuid: users(:project_viewer).uuid)
+          proj.update!(frozen_by_uuid: users(:project_viewer).uuid)
         end
         assert_match /can only be set to the current user's UUID/, err.inspect
         proj.reload
 
         # Admin can unfreeze.
-        assert proj.update_attributes(frozen_by_uuid: nil), proj.errors.messages
+        assert proj.update(frozen_by_uuid: nil), proj.errors.messages
       end
 
       # Cannot freeze a project if it contains container requests in
@@ -521,15 +521,15 @@ update links set tail_uuid='#{g5}' where uuid='#{l1.uuid}'
       creq_uncommitted = ContainerRequest.create!(test_cr_attrs.merge(owner_uuid: proj_inner.uuid))
       creq_committed = ContainerRequest.create!(test_cr_attrs.merge(owner_uuid: proj_inner.uuid, state: 'Committed'))
       err = assert_raises do
-        proj.update_attributes!(frozen_by_uuid: users(:active).uuid)
+        proj.update!(frozen_by_uuid: users(:active).uuid)
       end
       assert_match /container request zzzzz-xvhdp-.* with state = Committed/, err.inspect
       proj.reload
 
       # Can freeze once all container requests are in Uncommitted or
       # Final state
-      creq_committed.update_attributes!(state: ContainerRequest::Final)
-      assert proj.update_attributes(frozen_by_uuid: users(:active).uuid)
+      creq_committed.update!(state: ContainerRequest::Final)
+      assert proj.update(frozen_by_uuid: users(:active).uuid)
     end
   end
 
diff --git a/services/api/test/unit/link_test.rb b/services/api/test/unit/link_test.rb
index 5d36653a56..b9806486ad 100644
--- a/services/api/test/unit/link_test.rb
+++ b/services/api/test/unit/link_test.rb
@@ -109,7 +109,7 @@ class LinkTest < ActiveSupport::TestCase
 
   test "updating permission causes any conflicting links to be deleted" do
     link1, link2 = create_overlapping_permissions(['can_read', 'can_manage'])
-    Link.find_by_uuid(link2).update_attributes!(name: 'can_write')
+    Link.find_by_uuid(link2).update!(name: 'can_write')
     assert_empty Link.where(uuid: link1)
   end
 
@@ -121,8 +121,8 @@ class LinkTest < ActiveSupport::TestCase
 
   test "updating login permission causes any conflicting links to be deleted" do
     link1, link2 = create_overlapping_permissions(['can_login', 'can_login'], {properties: {username: 'foo1'}})
-    Link.find_by_uuid(link1).update_attributes!(properties: {'username' => 'foo2'})
-    Link.find_by_uuid(link2).update_attributes!(properties: {'username' => 'foo2'})
+    Link.find_by_uuid(link1).update!(properties: {'username' => 'foo2'})
+    Link.find_by_uuid(link2).update!(properties: {'username' => 'foo2'})
     assert_empty Link.where(uuid: link1)
   end
 
diff --git a/services/api/test/unit/log_test.rb b/services/api/test/unit/log_test.rb
index 66c8c8d923..d3a1b618d5 100644
--- a/services/api/test/unit/log_test.rb
+++ b/services/api/test/unit/log_test.rb
@@ -319,7 +319,7 @@ class LogTest < ActiveSupport::TestCase
       assert_logged(coll, :create) do |props|
         assert_equal(txt, props['new_attributes']['manifest_text'])
       end
-      coll.update_attributes!(name: "testing")
+      coll.update!(name: "testing")
       assert_logged(coll, :update) do |props|
         assert_equal(txt, props['old_attributes']['manifest_text'])
         assert_equal(txt, props['new_attributes']['manifest_text'])
diff --git a/services/api/test/unit/owner_test.rb b/services/api/test/unit/owner_test.rb
index aa0ac5f361..1c1bd93b81 100644
--- a/services/api/test/unit/owner_test.rb
+++ b/services/api/test/unit/owner_test.rb
@@ -63,7 +63,7 @@ class OwnerTest < ActiveSupport::TestCase
 
         assert(Specimen.where(uuid: i.uuid).any?,
                "new item should really be in DB")
-        assert(i.update_attributes(owner_uuid: new_o.uuid),
+        assert(i.update(owner_uuid: new_o.uuid),
                "should change owner_uuid from #{o.uuid} to #{new_o.uuid}")
       end
     end
@@ -92,7 +92,7 @@ class OwnerTest < ActiveSupport::TestCase
              "new #{o_class} should really be in DB")
       old_uuid = o.uuid
       new_uuid = o.uuid.sub(/..........$/, rand(2**256).to_s(36)[0..9])
-      assert(o.update_attributes(uuid: new_uuid),
+      assert(o.update(uuid: new_uuid),
               "should change #{o_class} uuid from #{old_uuid} to #{new_uuid}")
       assert_equal(false, o_class.where(uuid: old_uuid).any?,
                    "#{old_uuid} should disappear when renamed to #{new_uuid}")
@@ -118,7 +118,7 @@ class OwnerTest < ActiveSupport::TestCase
       assert_equal(true, Specimen.where(owner_uuid: o.uuid).any?,
                    "need something to be owned by #{o.uuid} for this test")
       new_uuid = o.uuid.sub(/..........$/, rand(2**256).to_s(36)[0..9])
-      assert(!o.update_attributes(uuid: new_uuid),
+      assert(!o.update(uuid: new_uuid),
              "should not change uuid of #{ofixt} that owns objects")
     end
   end
@@ -126,7 +126,7 @@ class OwnerTest < ActiveSupport::TestCase
   test "delete User that owns self" do
     o = User.create!
     assert User.where(uuid: o.uuid).any?, "new User should really be in DB"
-    assert_equal(true, o.update_attributes(owner_uuid: o.uuid),
+    assert_equal(true, o.update(owner_uuid: o.uuid),
                  "setting owner to self should work")
 
     skip_check_permissions_against_full_refresh do
diff --git a/services/api/test/unit/permission_test.rb b/services/api/test/unit/permission_test.rb
index db60b4e6e1..14c810d81a 100644
--- a/services/api/test/unit/permission_test.rb
+++ b/services/api/test/unit/permission_test.rb
@@ -84,7 +84,7 @@ class PermissionTest < ActiveSupport::TestCase
     assert users(:active).can?(write: ob)
     assert users(:active).can?(read: ob)
 
-    l1.update_attributes!(name: 'can_read')
+    l1.update!(name: 'can_read')
 
     assert !users(:active).can?(write: ob)
     assert users(:active).can?(read: ob)
@@ -293,7 +293,7 @@ class PermissionTest < ActiveSupport::TestCase
                    "manager saw the minion's private stuff")
       assert_raises(ArvadosModel::PermissionDeniedError,
                    "manager could update minion's private stuff") do
-        minions_specimen.update_attributes(properties: {'x' => 'y'})
+        minions_specimen.update(properties: {'x' => 'y'})
       end
     end
 
@@ -310,7 +310,7 @@ class PermissionTest < ActiveSupport::TestCase
                          .where(uuid: minions_specimen.uuid),
                        "manager could not find minion's specimen by uuid")
       assert_equal(true,
-                   minions_specimen.update_attributes(properties: {'x' => 'y'}),
+                   minions_specimen.update(properties: {'x' => 'y'}),
                    "manager could not update minion's specimen object")
     end
   end
@@ -355,17 +355,17 @@ class PermissionTest < ActiveSupport::TestCase
                    "OTHER can see #{u.first_name} in the user list")
       act_as_user u do
         assert_raises ArvadosModel::PermissionDeniedError, "wrote without perm" do
-          other.update_attributes!(prefs: {'pwned' => true})
+          other.update!(prefs: {'pwned' => true})
         end
-        assert_equal(true, u.update_attributes!(prefs: {'thisisme' => true}),
+        assert_equal(true, u.update!(prefs: {'thisisme' => true}),
                      "#{u.first_name} can't update its own prefs")
       end
       act_as_user other do
         assert_raises(ArvadosModel::PermissionDeniedError,
                         "OTHER wrote #{u.first_name} without perm") do
-          u.update_attributes!(prefs: {'pwned' => true})
+          u.update!(prefs: {'pwned' => true})
         end
-        assert_equal(true, other.update_attributes!(prefs: {'thisisme' => true}),
+        assert_equal(true, other.update!(prefs: {'thisisme' => true}),
                      "OTHER can't update its own prefs")
       end
     end
@@ -382,7 +382,7 @@ class PermissionTest < ActiveSupport::TestCase
     set_user_from_auth :rominiadmin
     ob = Collection.create!
     assert_raises ArvadosModel::PermissionDeniedError, "changed owner to unwritable user" do
-      ob.update_attributes!(owner_uuid: users(:active).uuid)
+      ob.update!(owner_uuid: users(:active).uuid)
     end
   end
 
@@ -397,7 +397,7 @@ class PermissionTest < ActiveSupport::TestCase
     set_user_from_auth :rominiadmin
     ob = Collection.create!
     assert_raises ArvadosModel::PermissionDeniedError, "changed owner to unwritable group" do
-      ob.update_attributes!(owner_uuid: groups(:aproject).uuid)
+      ob.update!(owner_uuid: groups(:aproject).uuid)
     end
   end
 
diff --git a/services/api/test/unit/repository_test.rb b/services/api/test/unit/repository_test.rb
index cb562ef977..674a34ffd8 100644
--- a/services/api/test/unit/repository_test.rb
+++ b/services/api/test/unit/repository_test.rb
@@ -263,7 +263,7 @@ class RepositoryTest < ActiveSupport::TestCase
 
   test "non-admin can rename own repo" do
     act_as_user users(:active) do
-      assert repositories(:foo).update_attributes(name: 'active/foo12345')
+      assert repositories(:foo).update(name: 'active/foo12345')
     end
   end
 
diff --git a/services/api/test/unit/user_test.rb b/services/api/test/unit/user_test.rb
index 7e19ad5821..35e6ec9e07 100644
--- a/services/api/test/unit/user_test.rb
+++ b/services/api/test/unit/user_test.rb
@@ -166,7 +166,7 @@ class UserTest < ActiveSupport::TestCase
       if auto_admin_first_user_config
         # This test requires no admin users exist (except for the system user)
         act_as_system_user do
-          users(:admin).update_attributes!(is_admin: false)
+          users(:admin).update!(is_admin: false)
         end
         @all_users = User.where("uuid not like '%-000000000000000'").where(:is_admin => true)
         assert_equal 0, @all_users.count, "No admin users should exist (except for the system user)"
@@ -800,7 +800,7 @@ class UserTest < ActiveSupport::TestCase
   test "empty identity_url saves as null" do
     set_user_from_auth :admin
     user = users(:active)
-    assert user.update_attributes(identity_url: '')
+    assert user.update(identity_url: '')
     user.reload
     assert_nil user.identity_url
   end
diff --git a/services/api/test/unit/workflow_test.rb b/services/api/test/unit/workflow_test.rb
index 26cd7f215e..4b3e6095d9 100644
--- a/services/api/test/unit/workflow_test.rb
+++ b/services/api/test/unit/workflow_test.rb
@@ -60,7 +60,7 @@ class WorkflowTest < ActiveSupport::TestCase
     definition = "k1:\n v1: x\n  v2: y"
 
     assert_raises(ActiveRecord::RecordInvalid) do
-      w.update_attributes!(definition: definition)
+      w.update!(definition: definition)
     end
   end
 
@@ -71,7 +71,7 @@ class WorkflowTest < ActiveSupport::TestCase
     # when it does not already have custom values for these fields
     w = Workflow.find_by_uuid(workflows(:workflow_with_no_name_and_desc).uuid)
     definition = "name: test name 1\ndescription: test desc 1\nother: some more"
-    w.update_attributes!(definition: definition)
+    w.update!(definition: definition)
     w.reload
     assert_equal "test name 1", w.name
     assert_equal "test desc 1", w.description
@@ -79,7 +79,7 @@ class WorkflowTest < ActiveSupport::TestCase
     # Workflow name and desc should be set with values from definition yaml
     # when it does not already have custom values for these fields
     definition = "name: test name 2\ndescription: test desc 2\nother: some more"
-    w.update_attributes!(definition: definition)
+    w.update!(definition: definition)
     w.reload
     assert_equal "test name 2", w.name
     assert_equal "test desc 2", w.description
@@ -87,7 +87,7 @@ class WorkflowTest < ActiveSupport::TestCase
     # Workflow name and desc should be set with values from definition yaml
     # even if it means emptying them out
     definition = "more: etc"
-    w.update_attributes!(definition: definition)
+    w.update!(definition: definition)
     w.reload
     assert_nil w.name
     assert_nil w.description
@@ -95,17 +95,17 @@ class WorkflowTest < ActiveSupport::TestCase
     # Workflow name and desc set using definition yaml should be cleared
     # if definition yaml is cleared
     definition = "name: test name 2\ndescription: test desc 2\nother: some more"
-    w.update_attributes!(definition: definition)
+    w.update!(definition: definition)
     w.reload
     definition = nil
-    w.update_attributes!(definition: definition)
+    w.update!(definition: definition)
     w.reload
     assert_nil w.name
     assert_nil w.description
 
     # Workflow name and desc should be set to provided custom values
     definition = "name: test name 3\ndescription: test desc 3\nother: some more"
-    w.update_attributes!(name: "remains", description: "remains", definition: definition)
+    w.update!(name: "remains", description: "remains", definition: definition)
     w.reload
     assert_equal "remains", w.name
     assert_equal "remains", w.description
@@ -113,7 +113,7 @@ class WorkflowTest < ActiveSupport::TestCase
     # Workflow name and desc should retain provided custom values
     # and should not be overwritten by values from yaml
     definition = "name: test name 4\ndescription: test desc 4\nother: some more"
-    w.update_attributes!(definition: definition)
+    w.update!(definition: definition)
     w.reload
     assert_equal "remains", w.name
     assert_equal "remains", w.description
@@ -121,7 +121,7 @@ class WorkflowTest < ActiveSupport::TestCase
     # Workflow name and desc should retain provided custom values
     # and not be affected by the clearing of the definition yaml
     definition = nil
-    w.update_attributes!(definition: definition)
+    w.update!(definition: definition)
     w.reload
     assert_equal "remains", w.name
     assert_equal "remains", w.description

commit 8cc6fad164fa9d3bdf0fb5965967031a8e1ce9e2
Author: Tom Clegg <tom at curii.com>
Date:   Wed Sep 6 17:20:11 2023 -0400

    20300: Call safe_load explicitly instead of using safe_yaml gem.
    
    safe_yaml breaks new i18n.
    
    ArgumentError: wrong number of arguments (given 2, expected 1)
    /home/tom/.gem/ruby/2.7.0/gems/safe_yaml-1.0.5/lib/safe_yaml.rb:37:in `unsafe_load_file'
    /home/tom/.gem/ruby/2.7.0/gems/i18n-1.14.1/lib/i18n/backend/base.rb:254:in `load_yml'
    ...
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/Gemfile b/services/api/Gemfile
index b2b6d1fbc4..d9ac19dd08 100644
--- a/services/api/Gemfile
+++ b/services/api/Gemfile
@@ -53,7 +53,6 @@ gem 'themes_for_rails', git: 'https://github.com/arvados/themes_for_rails'
 gem 'arvados', '~> 2.7.0.rc1'
 gem 'httpclient'
 
-gem 'safe_yaml'
 gem 'lograge'
 gem 'logstash-event'
 
diff --git a/services/api/Gemfile.lock b/services/api/Gemfile.lock
index d7f1938372..1aedac9030 100644
--- a/services/api/Gemfile.lock
+++ b/services/api/Gemfile.lock
@@ -176,13 +176,13 @@ GEM
     nokogiri (1.15.4)
       mini_portile2 (~> 2.8.2)
       racc (~> 1.4)
-    oj (3.16.0)
+    oj (3.16.1)
     optimist (3.1.0)
     os (1.1.4)
     passenger (6.0.18)
       rack
       rake (>= 0.8.1)
-    pg (1.5.3)
+    pg (1.5.4)
     power_assert (2.0.3)
     public_suffix (5.0.3)
     racc (1.7.1)
@@ -236,7 +236,6 @@ GEM
     retriable (1.4.1)
     ruby-prof (0.15.9)
     ruby2_keywords (0.0.5)
-    safe_yaml (1.0.5)
     signet (0.16.1)
       addressable (~> 2.8)
       faraday (>= 0.17.5, < 3.0)
@@ -297,7 +296,6 @@ DEPENDENCIES
   rails-perftest
   responders
   ruby-prof (~> 0.15.0)
-  safe_yaml
   simplecov (~> 0.7.1)
   simplecov-rcov
   sprockets (~> 3.0)
diff --git a/services/api/app/controllers/database_controller.rb b/services/api/app/controllers/database_controller.rb
index 6bcbd52798..38d406fe33 100644
--- a/services/api/app/controllers/database_controller.rb
+++ b/services/api/app/controllers/database_controller.rb
@@ -19,8 +19,8 @@ class DatabaseController < ApplicationController
       where('email is null or (email not like ? and email not like ?)', '%@example.com', '%.example.com').
       collect(&:uuid)
     fixture_uuids =
-      YAML::load_file(File.expand_path('../../../test/fixtures/users.yml',
-                                       __FILE__)).
+      YAML::safe_load_file(File.expand_path('../../../test/fixtures/users.yml',
+                                            __FILE__)).
       values.collect { |u| u['uuid'] }
     unexpected_uuids = user_uuids - fixture_uuids
     if unexpected_uuids.any?
diff --git a/services/api/app/models/workflow.rb b/services/api/app/models/workflow.rb
index 94890c6632..0268c4e979 100644
--- a/services/api/app/models/workflow.rb
+++ b/services/api/app/models/workflow.rb
@@ -18,7 +18,7 @@ class Workflow < ArvadosModel
 
   def validate_definition
     begin
-      @definition_yaml = YAML.load self.definition if !definition.nil?
+      @definition_yaml = YAML.safe_load self.definition if !definition.nil?
     rescue => e
       errors.add :definition, "is not valid yaml: #{e.message}"
     end
@@ -27,7 +27,7 @@ class Workflow < ArvadosModel
   def set_name_and_description
     old_wf = {}
     begin
-      old_wf = YAML.load self.definition_was if !self.definition_was.nil?
+      old_wf = YAML.safe_load self.definition_was if !self.definition_was.nil?
     rescue => e
       logger.warn "set_name_and_description error: #{e.message}"
       return
diff --git a/services/api/config/application.rb b/services/api/config/application.rb
index b28ae0e071..dcd77d7b6b 100644
--- a/services/api/config/application.rb
+++ b/services/api/config/application.rb
@@ -47,8 +47,6 @@ end
 
 module Server
   class Application < Rails::Application
-    # The following is to avoid SafeYAML's warning message
-    SafeYAML::OPTIONS[:default_mode] = :safe
 
     require_relative "arvados_config.rb"
 
diff --git a/services/api/config/arvados_config.rb b/services/api/config/arvados_config.rb
index d928d592c9..f8b9ff8ecd 100644
--- a/services/api/config/arvados_config.rb
+++ b/services/api/config/arvados_config.rb
@@ -36,7 +36,7 @@ if !status.success?
   puts stderr
   raise "error loading config: #{status}"
 end
-confs = YAML.load(defaultYAML, deserialize_symbols: false)
+confs = YAML.safe_load(defaultYAML)
 clusterID, clusterConfig = confs["Clusters"].first
 $arvados_config_defaults = clusterConfig
 $arvados_config_defaults["ClusterID"] = clusterID
@@ -50,7 +50,7 @@ if ENV["ARVADOS_CONFIG"] == "none"
 else
   # Load the global config file
   Open3.popen2("arvados-server", "config-dump", "-skip-legacy") do |stdin, stdout, status_thread|
-    confs = YAML.load(stdout, deserialize_symbols: false)
+    confs = YAML.safe_load(stdout)
     if confs && !confs.empty?
       # config-dump merges defaults with user configuration, so every
       # key should be set.
@@ -198,7 +198,7 @@ application_config = {}
   path = "#{::Rails.root.to_s}/config/#{cfgfile}.yml"
   confs = ConfigLoader.load(path, erb: true)
   # Ignore empty YAML file:
-  next if confs == false
+  next if confs == nil
   application_config.deep_merge!(confs['common'] || {})
   application_config.deep_merge!(confs[::Rails.env.to_s] || {})
 end
diff --git a/services/api/config/initializers/reload_config.rb b/services/api/config/initializers/reload_config.rb
index b54e3bcf87..22eee1601b 100644
--- a/services/api/config/initializers/reload_config.rb
+++ b/services/api/config/initializers/reload_config.rb
@@ -29,7 +29,7 @@ def start_reload_thread
         # precision cannot represent multiple updates per second.
         if t.to_f != t_lastload.to_f || Time.now.to_f - t.to_f < 5
           Open3.popen2("arvados-server", "config-dump", "-skip-legacy") do |stdin, stdout, status_thread|
-            confs = YAML.load(stdout, deserialize_symbols: false)
+            confs = YAML.safe_load(stdout)
             hash = confs["SourceSHA256"]
           rescue => e
             Rails.logger.info("reload_config: config file updated but could not be loaded: #{e}")
diff --git a/services/api/lib/config_loader.rb b/services/api/lib/config_loader.rb
index f421fb5b2a..1d897b39bf 100644
--- a/services/api/lib/config_loader.rb
+++ b/services/api/lib/config_loader.rb
@@ -2,6 +2,16 @@
 #
 # SPDX-License-Identifier: AGPL-3.0
 
+# When loading YAML, deserialize :foo as ":foo", rather than raising
+# "Psych::DisallowedClass: Tried to load unspecified class: Symbol"
+class Psych::ScalarScanner
+  alias :orig_tokenize :tokenize
+  def tokenize string
+    return string if string =~ /^:[a-zA-Z]/
+    orig_tokenize(string)
+  end
+end
+
 module Psych
   module Visitors
     class YAMLTree < Psych::Visitors::Visitor
@@ -226,7 +236,7 @@ class ConfigLoader
       if erb
         yaml = ERB.new(yaml).result(binding)
       end
-      YAML.load(yaml, deserialize_symbols: false)
+      YAML.safe_load(yaml)
     else
       {}
     end
diff --git a/services/api/lib/migrate_yaml_to_json.rb b/services/api/lib/migrate_yaml_to_json.rb
index 1db7ed0113..aa2af60b25 100644
--- a/services/api/lib/migrate_yaml_to_json.rb
+++ b/services/api/lib/migrate_yaml_to_json.rb
@@ -19,7 +19,7 @@ module MigrateYAMLToJSON
         [[nil, '---%']],
       ).rows.map do |id, yaml|
         n += 1
-        json = SafeJSON.dump(YAML.load(yaml))
+        json = SafeJSON.dump(YAML.safe_load(yaml))
         conn.exec_query(
           "UPDATE #{table} SET #{column}=$1 WHERE id=$2 AND #{column}=$3",
           "#{table}.#{column} convert YAML to JSON",
diff --git a/services/api/lib/serializers.rb b/services/api/lib/serializers.rb
index 37734e0bb4..c25b9060b4 100644
--- a/services/api/lib/serializers.rb
+++ b/services/api/lib/serializers.rb
@@ -16,7 +16,7 @@ class Serializer
   end
 
   def self.legacy_load(s)
-    val = Psych.safe_load(s)
+    val = Psych.safe_load(s, permitted_classes: [Time])
     if val.is_a? String
       # If apiserver was downgraded to a YAML-only version after
       # storing JSON in the database, the old code would have loaded
diff --git a/services/api/script/arvados-git-sync.rb b/services/api/script/arvados-git-sync.rb
index ad6aaf9eb5..ceebc3518a 100755
--- a/services/api/script/arvados-git-sync.rb
+++ b/services/api/script/arvados-git-sync.rb
@@ -26,7 +26,7 @@ DEBUG = 1
 # if present, overriding base config parameters as specified
 path = File.absolute_path('../../config/arvados-clients.yml', __FILE__)
 if File.exist?(path) then
-  cp_config = YAML.load_file(path)[ENV['RAILS_ENV']]
+  cp_config = YAML.safe_load_file(path)[ENV['RAILS_ENV']]
 else
   puts "Please create a\n #{path}\n file"
   exit 1
diff --git a/services/api/script/migrate-gitolite-to-uuid-storage.rb b/services/api/script/migrate-gitolite-to-uuid-storage.rb
index 91acf3e256..d2b9a0418b 100755
--- a/services/api/script/migrate-gitolite-to-uuid-storage.rb
+++ b/services/api/script/migrate-gitolite-to-uuid-storage.rb
@@ -40,7 +40,7 @@ DEBUG = 1
 # if present, overriding base config parameters as specified
 path = File.dirname(__FILE__) + '/config/arvados-clients.yml'
 if File.exist?(path) then
-  cp_config = YAML.load_file(path)[ENV['RAILS_ENV']]
+  cp_config = YAML.safe_load_file(path)[ENV['RAILS_ENV']]
 else
   puts "Please create a\n " + File.dirname(__FILE__) + "/config/arvados-clients.yml\n file"
   exit 1

commit 544f1dceeb4e5fcc63b683d3edac48e62bca5958
Author: Tom Clegg <tom at curii.com>
Date:   Tue Sep 5 09:49:33 2023 -0400

    20300: Remove extra Time.parse(), which Rails 6 does implicitly.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/lib/db_current_time.rb b/services/api/lib/db_current_time.rb
index 5e1634ecb9..2d58e3c389 100644
--- a/services/api/lib/db_current_time.rb
+++ b/services/api/lib/db_current_time.rb
@@ -6,10 +6,10 @@ module DbCurrentTime
   CURRENT_TIME_SQL = "SELECT clock_timestamp() AT TIME ZONE 'UTC'"
 
   def db_current_time
-    Time.parse(ActiveRecord::Base.connection.select_value(CURRENT_TIME_SQL) + " +0000")
+    ActiveRecord::Base.connection.select_value(CURRENT_TIME_SQL)
   end
 
   def db_transaction_time
-    Time.parse(ActiveRecord::Base.connection.select_value("SELECT current_timestamp AT TIME ZONE 'UTC'") + " +0000")
+    ActiveRecord::Base.connection.select_value("SELECT current_timestamp AT TIME ZONE 'UTC'")
   end
 end

commit 17ecd8b1d6bf57e0f3e1f1e70dcc589188cad670
Author: Tom Clegg <tom at curii.com>
Date:   Tue Sep 5 09:40:44 2023 -0400

    20300: Remove obsolete i18n version pin.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/apps/workbench/Gemfile b/apps/workbench/Gemfile
index 5d3cd5c099..03f5e02774 100644
--- a/apps/workbench/Gemfile
+++ b/apps/workbench/Gemfile
@@ -7,9 +7,7 @@ source 'https://rubygems.org'
 gem 'rails', '~> 5.2.0'
 gem 'arvados', '~> 2.7.0.rc1'
 
-# i18n 1.14.1 seems to be incompatible with rails 5
-gem 'i18n', '~> 0.9.5'
-
+gem 'i18n'
 gem 'activerecord-nulldb-adapter', git: 'https://github.com/arvados/nulldb'
 gem 'multi_json'
 gem 'oj'
diff --git a/apps/workbench/Gemfile.lock b/apps/workbench/Gemfile.lock
index a10c05f8bb..e83d1a3f78 100644
--- a/apps/workbench/Gemfile.lock
+++ b/apps/workbench/Gemfile.lock
@@ -330,7 +330,7 @@ DEPENDENCIES
   flamegraph
   headless (~> 1.0.2)
   httpclient (~> 2.5)
-  i18n (~> 0.9.5)
+  i18n
   jquery-rails
   launchy (~> 2.4.0)
   lograge

commit e9b8af6b59b16f9a9595bae87fac13c4e0969f23
Author: Tom Clegg <tom at curii.com>
Date:   Mon Sep 18 14:36:54 2023 -0400

    20300: Update gemfiles to rails 6.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/api/Gemfile b/services/api/Gemfile
index adfaa2ecb5..b2b6d1fbc4 100644
--- a/services/api/Gemfile
+++ b/services/api/Gemfile
@@ -4,11 +4,9 @@
 
 source 'https://rubygems.org'
 
-gem 'rails', '~> 5.2.0'
-gem 'responders', '~> 2.0'
-
-# i18n 1.14.1 seems to be incompatible with rails 5
-gem 'i18n', '~> 0.9.5'
+gem 'rails', '~> 6.0.0'
+gem 'responders'
+gem 'i18n'
 
 # Pin sprockets to < 4.0 to avoid issues when upgrading rails to 5.2
 # See: https://github.com/rails/sprockets-rails/issues/443
diff --git a/services/api/Gemfile.lock b/services/api/Gemfile.lock
index a7d46a14a8..d7f1938372 100644
--- a/services/api/Gemfile.lock
+++ b/services/api/Gemfile.lock
@@ -8,47 +8,61 @@ GIT
 GEM
   remote: https://rubygems.org/
   specs:
-    actioncable (5.2.8.1)
-      actionpack (= 5.2.8.1)
+    actioncable (6.0.6.1)
+      actionpack (= 6.0.6.1)
       nio4r (~> 2.0)
       websocket-driver (>= 0.6.1)
-    actionmailer (5.2.8.1)
-      actionpack (= 5.2.8.1)
-      actionview (= 5.2.8.1)
-      activejob (= 5.2.8.1)
+    actionmailbox (6.0.6.1)
+      actionpack (= 6.0.6.1)
+      activejob (= 6.0.6.1)
+      activerecord (= 6.0.6.1)
+      activestorage (= 6.0.6.1)
+      activesupport (= 6.0.6.1)
+      mail (>= 2.7.1)
+    actionmailer (6.0.6.1)
+      actionpack (= 6.0.6.1)
+      actionview (= 6.0.6.1)
+      activejob (= 6.0.6.1)
       mail (~> 2.5, >= 2.5.4)
       rails-dom-testing (~> 2.0)
-    actionpack (5.2.8.1)
-      actionview (= 5.2.8.1)
-      activesupport (= 5.2.8.1)
+    actionpack (6.0.6.1)
+      actionview (= 6.0.6.1)
+      activesupport (= 6.0.6.1)
       rack (~> 2.0, >= 2.0.8)
       rack-test (>= 0.6.3)
       rails-dom-testing (~> 2.0)
-      rails-html-sanitizer (~> 1.0, >= 1.0.2)
-    actionview (5.2.8.1)
-      activesupport (= 5.2.8.1)
+      rails-html-sanitizer (~> 1.0, >= 1.2.0)
+    actiontext (6.0.6.1)
+      actionpack (= 6.0.6.1)
+      activerecord (= 6.0.6.1)
+      activestorage (= 6.0.6.1)
+      activesupport (= 6.0.6.1)
+      nokogiri (>= 1.8.5)
+    actionview (6.0.6.1)
+      activesupport (= 6.0.6.1)
       builder (~> 3.1)
       erubi (~> 1.4)
       rails-dom-testing (~> 2.0)
-      rails-html-sanitizer (~> 1.0, >= 1.0.3)
-    activejob (5.2.8.1)
-      activesupport (= 5.2.8.1)
+      rails-html-sanitizer (~> 1.1, >= 1.2.0)
+    activejob (6.0.6.1)
+      activesupport (= 6.0.6.1)
       globalid (>= 0.3.6)
-    activemodel (5.2.8.1)
-      activesupport (= 5.2.8.1)
-    activerecord (5.2.8.1)
-      activemodel (= 5.2.8.1)
-      activesupport (= 5.2.8.1)
-      arel (>= 9.0)
-    activestorage (5.2.8.1)
-      actionpack (= 5.2.8.1)
-      activerecord (= 5.2.8.1)
-      marcel (~> 1.0.0)
-    activesupport (5.2.8.1)
+    activemodel (6.0.6.1)
+      activesupport (= 6.0.6.1)
+    activerecord (6.0.6.1)
+      activemodel (= 6.0.6.1)
+      activesupport (= 6.0.6.1)
+    activestorage (6.0.6.1)
+      actionpack (= 6.0.6.1)
+      activejob (= 6.0.6.1)
+      activerecord (= 6.0.6.1)
+      marcel (~> 1.0)
+    activesupport (6.0.6.1)
       concurrent-ruby (~> 1.0, >= 1.0.2)
       i18n (>= 0.7, < 2)
       minitest (~> 5.1)
       tzinfo (~> 1.1)
+      zeitwerk (~> 2.2, >= 2.2.2)
     acts_as_api (1.0.1)
       activemodel (>= 3.0.0)
       activesupport (>= 3.0.0)
@@ -56,7 +70,6 @@ GEM
     addressable (2.8.5)
       public_suffix (>= 2.0.2, < 6.0)
     andand (1.3.3)
-    arel (9.0.0)
     arvados (2.7.0.rc2)
       activesupport (>= 3)
       andand (~> 1.3, >= 1.3.3)
@@ -82,16 +95,17 @@ GEM
       extlib (>= 0.9.15)
       multi_json (>= 1.0.0)
     builder (3.2.4)
-    byebug (11.0.1)
+    byebug (11.1.3)
     concurrent-ruby (1.2.2)
     crass (1.0.6)
+    date (3.3.3)
     erubi (1.12.0)
     extlib (0.9.16)
-    factory_bot (5.0.2)
-      activesupport (>= 4.2.0)
-    factory_bot_rails (5.0.1)
-      factory_bot (~> 5.0.0)
-      railties (>= 4.2.0)
+    factory_bot (6.2.1)
+      activesupport (>= 5.0.0)
+    factory_bot_rails (6.2.0)
+      factory_bot (~> 6.2.0)
+      railties (>= 5.0.0)
     faraday (2.7.10)
       faraday-net_http (>= 2.0, < 3.1)
       ruby2_keywords (>= 0.0.4)
@@ -101,7 +115,7 @@ GEM
     faraday-multipart (1.0.4)
       multipart-post (~> 2)
     faraday-net_http (3.0.2)
-    ffi (1.9.25)
+    ffi (1.15.5)
     globalid (1.1.0)
       activesupport (>= 5.0)
     googleauth (1.7.0)
@@ -112,9 +126,9 @@ GEM
       os (>= 0.9, < 2.0)
       signet (>= 0.16, < 2.a)
     httpclient (2.8.3)
-    i18n (0.9.5)
+    i18n (1.14.1)
       concurrent-ruby (~> 1.0)
-    jquery-rails (4.5.1)
+    jquery-rails (4.6.0)
       rails-dom-testing (>= 1, < 3)
       railties (>= 4.2.0)
       thor (>= 0.14, < 2.0)
@@ -122,88 +136,103 @@ GEM
     jwt (1.5.6)
     launchy (2.5.2)
       addressable (~> 2.8)
-    listen (3.2.1)
+    listen (3.8.0)
       rb-fsevent (~> 0.10, >= 0.10.3)
       rb-inotify (~> 0.9, >= 0.9.10)
-    lograge (0.10.0)
+    lograge (0.13.0)
       actionpack (>= 4)
       activesupport (>= 4)
       railties (>= 4)
       request_store (~> 1.0)
     logstash-event (1.2.02)
-    loofah (2.20.0)
+    loofah (2.21.3)
       crass (~> 1.0.2)
-      nokogiri (>= 1.5.9)
-    mail (2.7.1)
+      nokogiri (>= 1.12.0)
+    mail (2.8.1)
       mini_mime (>= 0.1.1)
+      net-imap
+      net-pop
+      net-smtp
     marcel (1.0.2)
     memoist (0.16.2)
-    metaclass (0.0.4)
     method_source (1.0.0)
-    mini_mime (1.1.2)
-    mini_portile2 (2.8.2)
+    mini_mime (1.1.5)
+    mini_portile2 (2.8.4)
     minitest (5.10.3)
-    mocha (1.8.0)
-      metaclass (~> 0.0.1)
+    mocha (2.1.0)
+      ruby2_keywords (>= 0.0.5)
     multi_json (1.15.0)
     multipart-post (2.3.0)
-    nio4r (2.5.8)
-    nokogiri (1.14.3)
-      mini_portile2 (~> 2.8.0)
+    net-imap (0.3.7)
+      date
+      net-protocol
+    net-pop (0.1.2)
+      net-protocol
+    net-protocol (0.2.1)
+      timeout
+    net-smtp (0.3.3)
+      net-protocol
+    nio4r (2.5.9)
+    nokogiri (1.15.4)
+      mini_portile2 (~> 2.8.2)
       racc (~> 1.4)
-    oj (3.9.2)
-    optimist (3.0.0)
+    oj (3.16.0)
+    optimist (3.1.0)
     os (1.1.4)
-    passenger (6.0.15)
+    passenger (6.0.18)
       rack
       rake (>= 0.8.1)
-    pg (1.1.4)
-    power_assert (1.1.4)
+    pg (1.5.3)
+    power_assert (2.0.3)
     public_suffix (5.0.3)
-    racc (1.6.2)
-    rack (2.2.7)
+    racc (1.7.1)
+    rack (2.2.8)
     rack-test (2.1.0)
       rack (>= 1.3)
-    rails (5.2.8.1)
-      actioncable (= 5.2.8.1)
-      actionmailer (= 5.2.8.1)
-      actionpack (= 5.2.8.1)
-      actionview (= 5.2.8.1)
-      activejob (= 5.2.8.1)
-      activemodel (= 5.2.8.1)
-      activerecord (= 5.2.8.1)
-      activestorage (= 5.2.8.1)
-      activesupport (= 5.2.8.1)
+    rails (6.0.6.1)
+      actioncable (= 6.0.6.1)
+      actionmailbox (= 6.0.6.1)
+      actionmailer (= 6.0.6.1)
+      actionpack (= 6.0.6.1)
+      actiontext (= 6.0.6.1)
+      actionview (= 6.0.6.1)
+      activejob (= 6.0.6.1)
+      activemodel (= 6.0.6.1)
+      activerecord (= 6.0.6.1)
+      activestorage (= 6.0.6.1)
+      activesupport (= 6.0.6.1)
       bundler (>= 1.3.0)
-      railties (= 5.2.8.1)
+      railties (= 6.0.6.1)
       sprockets-rails (>= 2.0.0)
-    rails-controller-testing (1.0.4)
-      actionpack (>= 5.0.1.x)
-      actionview (>= 5.0.1.x)
-      activesupport (>= 5.0.1.x)
-    rails-dom-testing (2.0.3)
-      activesupport (>= 4.2.0)
+    rails-controller-testing (1.0.5)
+      actionpack (>= 5.0.1.rc1)
+      actionview (>= 5.0.1.rc1)
+      activesupport (>= 5.0.1.rc1)
+    rails-dom-testing (2.2.0)
+      activesupport (>= 5.0.0)
+      minitest
       nokogiri (>= 1.6)
-    rails-html-sanitizer (1.5.0)
-      loofah (~> 2.19, >= 2.19.1)
+    rails-html-sanitizer (1.6.0)
+      loofah (~> 2.21)
+      nokogiri (~> 1.14)
     rails-observers (0.1.5)
       activemodel (>= 4.0)
     rails-perftest (0.0.7)
-    railties (5.2.8.1)
-      actionpack (= 5.2.8.1)
-      activesupport (= 5.2.8.1)
+    railties (6.0.6.1)
+      actionpack (= 6.0.6.1)
+      activesupport (= 6.0.6.1)
       method_source
       rake (>= 0.8.7)
-      thor (>= 0.19.0, < 2.0)
+      thor (>= 0.20.3, < 2.0)
     rake (13.0.6)
-    rb-fsevent (0.10.3)
-    rb-inotify (0.9.10)
-      ffi (>= 0.5.0, < 2)
-    request_store (1.4.1)
+    rb-fsevent (0.11.2)
+    rb-inotify (0.10.1)
+      ffi (~> 1.0)
+    request_store (1.5.1)
       rack (>= 1.4)
-    responders (2.4.1)
-      actionpack (>= 4.2.0, < 6.0)
-      railties (>= 4.2.0, < 6.0)
+    responders (3.1.0)
+      actionpack (>= 5.2)
+      railties (>= 5.2)
     retriable (1.4.1)
     ruby-prof (0.15.9)
     ruby2_keywords (0.0.5)
@@ -217,7 +246,7 @@ GEM
       multi_json (~> 1.0)
       simplecov-html (~> 0.7.1)
     simplecov-html (0.7.1)
-    simplecov-rcov (0.2.3)
+    simplecov-rcov (0.3.1)
       simplecov (>= 0.4.1)
     sprockets (3.7.2)
       concurrent-ruby (~> 1.0)
@@ -226,15 +255,17 @@ GEM
       actionpack (>= 5.2)
       activesupport (>= 5.2)
       sprockets (>= 3.0.0)
-    test-unit (3.3.1)
+    test-unit (3.6.1)
       power_assert
-    thor (1.2.1)
+    thor (1.2.2)
     thread_safe (0.3.6)
+    timeout (0.4.0)
     tzinfo (1.2.11)
       thread_safe (~> 0.1)
-    websocket-driver (0.7.5)
+    websocket-driver (0.7.6)
       websocket-extensions (>= 0.1.0)
     websocket-extensions (0.1.5)
+    zeitwerk (2.6.11)
     zlib (3.0.0)
 
 PLATFORMS
@@ -247,7 +278,7 @@ DEPENDENCIES
   byebug
   factory_bot_rails
   httpclient
-  i18n (~> 0.9.5)
+  i18n
   jquery-rails
   listen
   lograge
@@ -260,11 +291,11 @@ DEPENDENCIES
   optimist
   passenger
   pg (~> 1.0)
-  rails (~> 5.2.0)
+  rails (~> 6.0.0)
   rails-controller-testing
   rails-observers
   rails-perftest
-  responders (~> 2.0)
+  responders
   ruby-prof (~> 0.15.0)
   safe_yaml
   simplecov (~> 0.7.1)
@@ -274,4 +305,4 @@ DEPENDENCIES
   themes_for_rails!
 
 BUNDLED WITH
-   2.2.19
+   2.4.19

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list