14812: Config migration WIP
authorPeter Amstutz <pamstutz@veritasgenetics.com>
Thu, 27 Jun 2019 17:41:13 +0000 (13:41 -0400)
committerPeter Amstutz <pamstutz@veritasgenetics.com>
Mon, 1 Jul 2019 16:20:58 +0000 (12:20 -0400)
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <pamstutz@veritasgenetics.com>

15 files changed:
apps/workbench/app/controllers/actions_controller.rb
apps/workbench/app/controllers/collections_controller.rb
apps/workbench/app/controllers/jobs_controller.rb
apps/workbench/app/controllers/virtual_machines_controller.rb
apps/workbench/app/controllers/work_units_controller.rb
apps/workbench/app/helpers/application_helper.rb
apps/workbench/app/helpers/collections_helper.rb
apps/workbench/config/application.rb
apps/workbench/config/arvados_config.rb [new file with mode: 0644]
apps/workbench/lib/config_loader.rb [new file with mode: 0644]
apps/workbench/lib/tasks/config.rake [new file with mode: 0644]
apps/workbench/lib/tasks/config_check.rake [deleted file]
apps/workbench/lib/tasks/config_dump.rake [deleted file]
lib/config/config.default.yml
services/login-sync/Gemfile.lock

index b1bbb122670dcb6b11aac21d915f26174851aed9..e6f20be37064ea7f396206ca0b7d68ae3482ef09 100644 (file)
@@ -10,7 +10,7 @@ class ActionsController < ApplicationController
   # Skip require_thread_api_token if this is a show action
   # for an object uuid that supports anonymous access.
   skip_around_action :require_thread_api_token, if: proc { |ctrl|
-    Rails.configuration.anonymous_user_token and
+    !Rails.configuration.Users.AnonymousUserToken.empty? and
     'show' == ctrl.action_name and
     params['uuid'] and
     model_class.in?([Collection, Group, Job, PipelineInstance, PipelineTemplate])
index 8d7e6ee332af5e3cf53dac674e6185626f43d413..2c8cf7e443a650f645571675a55d81c6dcd2be1e 100644 (file)
@@ -10,7 +10,7 @@ class CollectionsController < ApplicationController
   include ActionController::Live
 
   skip_around_action :require_thread_api_token, if: proc { |ctrl|
-    Rails.configuration.anonymous_user_token and
+    !Rails.configuration.Users.AnonymousUserToken.empty? and
     'show' == ctrl.action_name
   }
   skip_around_action(:require_thread_api_token,
@@ -124,7 +124,8 @@ class CollectionsController < ApplicationController
     # Otherwise, it's impossible to know whether any other request succeeded
     # because of the reader token.
     coll = nil
-    tokens = [(Rails.configuration.anonymous_user_token || nil),
+    tokens = [(if !Rails.configuration.Users.AnonymousUserToken.empty? then
+                Rails.configuration.Users.AnonymousUserToken else nil end),
               params[:reader_token],
               Thread.current[:arvados_api_token]].compact
     usable_token = find_usable_token(tokens) do
@@ -138,7 +139,7 @@ class CollectionsController < ApplicationController
     opts = {}
     if usable_token == params[:reader_token]
       opts[:path_token] = usable_token
-    elsif usable_token == Rails.configuration.anonymous_user_token
+    elsif usable_token == Rails.configuration.Users.AnonymousUserToken
       # Don't pass a token at all
     else
       # We pass the current user's real token only if it's necessary
index e38d3ba87b3e40e6df08e4d6150a2a3c392220a7..2caa8ff43a0d094e2714a6805ab64d2efa51f54e 100644 (file)
@@ -4,7 +4,7 @@
 
 class JobsController < ApplicationController
   skip_around_action :require_thread_api_token, if: proc { |ctrl|
-    Rails.configuration.anonymous_user_token and
+    !Rails.configuration.Users.AnonymousUserToken.empty? and
     'show' == ctrl.action_name
   }
 
index 19763b926c78077b97de261d49714d37c0c6b7bb..6809e141fddbfaaa976f8e9b6e429c9e95126f7c 100644 (file)
@@ -25,8 +25,8 @@ class VirtualMachinesController < ApplicationController
   end
 
   def webshell
-    return render_not_found if not Rails.configuration.shell_in_a_box_url
-    @webshell_url = Rails.configuration.shell_in_a_box_url % {
+    return render_not_found if Rails.configuration.Workbench.ShellInABoxURL.empty?
+    @webshell_url = Rails.configuration.Workbench.ShellInABoxURL % {
       uuid: @object.uuid,
       hostname: @object.hostname,
     }
index 0f0033ce4965663ef76a7f2e479d8e38d7642dfb..1ecea99babce40e6755839889d0f6dad6ef26b18 100644 (file)
@@ -4,7 +4,7 @@
 
 class WorkUnitsController < ApplicationController
   skip_around_action :require_thread_api_token, if: proc { |ctrl|
-    Rails.configuration.anonymous_user_token and
+    !Rails.configuration.Users.AnonymousUserToken.empty? and
     'show_child_component' == ctrl.action_name
   }
 
index 6352916b00b14560af0683aa9594b6daf7ce20ca..0a872446d594d42f8485316663b437cbfab0c3c8 100644 (file)
@@ -12,11 +12,11 @@ module ApplicationHelper
   end
 
   def current_api_host
-    Rails.configuration.arvados_v1_base.gsub(/https?:\/\/|\/arvados\/v1/, '')
+    "#{Rails.configuration.Services.Controller.ExternalURL.hostname}:#{Rails.configuration.Services.Controller.ExternalURL.port}"
   end
 
   def current_uuid_prefix
-    current_api_host[0..4]
+    Rails.configuration.ClusterID
   end
 
   def render_markup(markup)
index 5eb1e8c768d927dfdc14e814143d2a945ed0c011..3f62a5601c53e443d8a3b8ddcd49db6671a3c752 100644 (file)
@@ -72,7 +72,7 @@ module CollectionsHelper
     elsif (file_type.raw_media_type == "text") || (file_type.raw_media_type == "image")
       true
     elsif (file_type.raw_media_type == "application") &&
-          (Rails.configuration.application_mimetypes_with_view_icon.include? (file_type.sub_type))
+          Rails.configuration.Workbench.Workbench.ApplicationMimetypesWithViewIcon[file_type.sub_type]
       true
     else
       false
index 1c7a9d0dac8866511f795e1fea048d7f4989a300..da33c2f97b3f815bdf6dc484a658325d5d0adadf 100644 (file)
@@ -21,6 +21,9 @@ Bundler.require(:default, Rails.env)
 
 module ArvadosWorkbench
   class Application < Rails::Application
+
+    require_relative "arvados_config.rb"
+
     # Settings in config/environments/* take precedence over those specified here.
     # Application configuration should go into files in config/initializers
     # -- all .rb files in that directory are automatically loaded.
diff --git a/apps/workbench/config/arvados_config.rb b/apps/workbench/config/arvados_config.rb
new file mode 100644 (file)
index 0000000..5778afc
--- /dev/null
@@ -0,0 +1,142 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+#
+# Load Arvados configuration from /etc/arvados/config.yml, using defaults
+# from config.default.yml
+#
+# Existing application.yml is migrated into the new config structure.
+# Keys in the legacy application.yml take precedence.
+#
+# Use "bundle exec config:dump" to get the complete active configuration
+#
+# Use "bundle exec config:migrate" to migrate application.yml to
+# config.yml.  After adding the output of config:migrate to
+# /etc/arvados/config.yml, you will be able to delete application.yml.
+
+require 'config_loader'
+
+begin
+  # If secret_token.rb exists here, we need to load it first.
+  require_relative 'secret_token.rb'
+rescue LoadError
+  # Normally secret_token.rb is missing and the secret token is
+  # configured by application.yml (i.e., here!) instead.
+end
+
+# Load the defaults
+$arvados_config_defaults = ConfigLoader.load "#{::Rails.root.to_s}/config/config.default.yml"
+if $arvados_config_defaults.empty?
+  raise "Missing #{::Rails.root.to_s}/config/config.default.yml"
+end
+
+def remove_sample_entries(h)
+  return unless h.is_a? Hash
+  h.delete("SAMPLE")
+  h.each { |k, v| remove_sample_entries(v) }
+end
+remove_sample_entries($arvados_config_defaults)
+
+clusterID, clusterConfig = $arvados_config_defaults["Clusters"].first
+$arvados_config_defaults = clusterConfig
+$arvados_config_defaults["ClusterID"] = clusterID
+
+# Initialize the global config with the defaults
+$arvados_config_global = $arvados_config_defaults.deep_dup
+
+# Load the global config file
+confs = ConfigLoader.load "/etc/arvados/config.yml"
+if !confs.empty?
+  clusterID, clusterConfig = confs["Clusters"].first
+  $arvados_config_global["ClusterID"] = clusterID
+
+  # Copy the cluster config over the defaults
+  $arvados_config_global.deep_merge!(clusterConfig)
+end
+
+# Now make a copy
+$arvados_config = $arvados_config_global.deep_dup
+
+# Declare all our configuration items.
+arvcfg = ConfigLoader.new
+
+arvcfg.declare_config "ManagementToken", String, :ManagementToken
+arvcfg.declare_config "TLS.Insecure", Boolean, :arvados_insecure_https
+
+arvcfg.declare_config "Services.Controller.ExternalURL", URI, :arvados_v1_base, ->(cfg, k, v) {
+  u = URI(v)
+  u.path = ""
+  ConfigLoader.set_cfg cfg, "Services.Controller.ExternalURL", u
+}
+
+arvcfg.declare_config "Services.WebShell.ExternalURL", URI, :shell_in_a_box_url, ->(cfg, k, v) {
+  v ||= ""
+  u = URI(v.sub("%{hostname}", "*"))
+  u.path = ""
+  ConfigLoader.set_cfg cfg, "Services.WebShell.ExternalURL", u
+}
+
+arvcfg.declare_config "Services.WebDAV.ExternalURL", URI, :keep_web_service_url, ->(cfg, k, v) {
+  v ||= ""
+  u = URI(v.sub("%{uuid_or_pdh}", "*"))
+  u.path = ""
+  ConfigLoader.set_cfg cfg, "Services.WebDAV.ExternalURL", u
+}
+
+arvcfg.declare_config "Services.WebDAVDownload.ExternalURL", URI, :keep_web_download_url, ->(cfg, k, v) {
+  v ||= ""
+  u = URI(v.sub("%{uuid_or_pdh}", "*"))
+  u.path = ""
+  ConfigLoader.set_cfg cfg, "Services.WebDAVDownload.ExternalURL", u
+}
+
+arvcfg.declare_config "Services.Composer.ExternalURL", URI, :composer_url
+arvcfg.declare_config "Services.Workbench2.ExternalURL", URI, :workbench2_url
+
+arvcfg.declare_config "Workbench.ApplicationMimetypesWithViewIcon", Hash, :application_mimetypes_with_view_icon, ->(cfg, k, v) {
+  mimetypes = {}
+  v.each do |m|
+    mimetypes[m] = {}
+  end
+  ConfigLoader.set_cfg cfg, "Workbench.ApplicationMimetypesWithViewIcon", mimetypes
+}
+
+arvcfg.declare_config "Users.AnonymousUserToken", String, :anonymous_user_token
+
+
+application_config = {}
+%w(application.default application).each do |cfgfile|
+  path = "#{::Rails.root.to_s}/config/#{cfgfile}.yml"
+  confs = ConfigLoader.load(path, erb: true)
+  # Ignore empty YAML file:
+  next if confs == false
+  application_config.deep_merge!(confs['common'] || {})
+  application_config.deep_merge!(confs[::Rails.env.to_s] || {})
+end
+
+$remaining_config = arvcfg.migrate_config(application_config, $arvados_config)
+
+# Checks for wrongly typed configuration items, coerces properties
+# into correct types (such as Duration), and optionally raise error
+# for essential configuration that can't be empty.
+arvcfg.coercion_and_check $arvados_config_defaults, check_nonempty: false
+arvcfg.coercion_and_check $arvados_config_global, check_nonempty: false
+arvcfg.coercion_and_check $arvados_config, check_nonempty: true
+
+# * $arvados_config_defaults is the defaults
+# * $arvados_config_global is $arvados_config_defaults merged with the contents of /etc/arvados/config.yml
+# These are used by the rake config: tasks
+#
+# * $arvados_config is $arvados_config_global merged with the migrated contents of application.yml
+# This is what actually gets copied into the Rails configuration object.
+
+ArvadosWorkbench::Application.configure do
+  # Copy into the Rails config object.  This also turns Hash into
+  # OrderedOptions so that application code can use
+  # Rails.configuration.API.Blah instead of
+  # Rails.configuration.API["Blah"]
+  ConfigLoader.copy_into_config $arvados_config, config
+  ConfigLoader.copy_into_config $remaining_config, config
+  secrets.secret_key_base = $arvados_config["Workbench"]["SecretToken"]
+end
diff --git a/apps/workbench/lib/config_loader.rb b/apps/workbench/lib/config_loader.rb
new file mode 100644 (file)
index 0000000..c95b618
--- /dev/null
@@ -0,0 +1,239 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+module Psych
+  module Visitors
+    class YAMLTree < Psych::Visitors::Visitor
+      def visit_ActiveSupport_Duration o
+        seconds = o.to_i
+        outstr = ""
+        if seconds / 3600 > 0
+          outstr += "#{seconds / 3600}h"
+          seconds = seconds % 3600
+        end
+        if seconds / 60 > 0
+          outstr += "#{seconds / 60}m"
+          seconds = seconds % 60
+        end
+        if seconds > 0
+          outstr += "#{seconds}s"
+        end
+        if outstr == ""
+          outstr = "0s"
+        end
+        @emitter.scalar outstr, nil, nil, true, false, Nodes::Scalar::ANY
+      end
+
+      def visit_URI_Generic o
+        @emitter.scalar o.to_s, nil, nil, true, false, Nodes::Scalar::ANY
+      end
+
+      def visit_URI_HTTP o
+        @emitter.scalar o.to_s, nil, nil, true, false, Nodes::Scalar::ANY
+      end
+
+      def visit_Pathname o
+        @emitter.scalar o.to_s, nil, nil, true, false, Nodes::Scalar::ANY
+      end
+    end
+  end
+end
+
+
+module Boolean; end
+class TrueClass; include Boolean; end
+class FalseClass; include Boolean; end
+
+class NonemptyString < String
+end
+
+class ConfigLoader
+  def initialize
+    @config_migrate_map = {}
+    @config_types = {}
+  end
+
+  def declare_config(assign_to, configtype, migrate_from=nil, migrate_fn=nil)
+    if migrate_from
+      @config_migrate_map[migrate_from] = migrate_fn || ->(cfg, k, v) {
+        ConfigLoader.set_cfg cfg, assign_to, v
+      }
+    end
+    @config_types[assign_to] = configtype
+  end
+
+
+  def migrate_config from_config, to_config
+    remainders = {}
+    from_config.each do |k, v|
+      if @config_migrate_map[k.to_sym]
+        @config_migrate_map[k.to_sym].call to_config, k, v
+      else
+        remainders[k] = v
+      end
+    end
+    remainders
+  end
+
+  def coercion_and_check check_cfg, check_nonempty: true
+    @config_types.each do |cfgkey, cfgtype|
+      begin
+      cfg = check_cfg
+      k = cfgkey
+      ks = k.split '.'
+      k = ks.pop
+      ks.each do |kk|
+        cfg = cfg[kk]
+        if cfg.nil?
+          break
+        end
+      end
+
+      if cfg.nil?
+        raise "missing #{cfgkey}"
+      end
+
+      if cfgtype == String and !cfg[k]
+        cfg[k] = ""
+      end
+
+      if cfgtype == String and cfg[k].is_a? Symbol
+        cfg[k] = cfg[k].to_s
+      end
+
+      if cfgtype == Pathname and cfg[k].is_a? String
+
+        if cfg[k] == ""
+          cfg[k] = Pathname.new("")
+        else
+          cfg[k] = Pathname.new(cfg[k])
+          if !cfg[k].exist?
+            raise "#{cfgkey} path #{cfg[k]} does not exist"
+          end
+        end
+      end
+
+      if cfgtype == NonemptyString
+        if (!cfg[k] || cfg[k] == "") && check_nonempty
+          raise "#{cfgkey} cannot be empty"
+        end
+        if cfg[k].is_a? String
+          next
+        end
+      end
+
+      if cfgtype == ActiveSupport::Duration
+        if cfg[k].is_a? Integer
+          cfg[k] = cfg[k].seconds
+        elsif cfg[k].is_a? String
+          cfg[k] = ConfigLoader.parse_duration(cfg[k], cfgkey: cfgkey)
+        end
+      end
+
+      if cfgtype == URI
+        if cfg[k]
+          cfg[k] = URI(cfg[k])
+        else
+          cfg[k] = URI("")
+        end
+      end
+
+      if cfgtype == Integer && cfg[k].is_a?(String)
+        v = cfg[k].sub(/B\s*$/, '')
+        if mt = /(-?\d*\.?\d+)\s*([KMGTPE]i?)$/.match(v)
+          if mt[1].index('.')
+            v = mt[1].to_f
+          else
+            v = mt[1].to_i
+          end
+          cfg[k] = v * {
+            'K' => 1000,
+            'Ki' => 1 << 10,
+            'M' => 1000000,
+            'Mi' => 1 << 20,
+           "G" =>  1000000000,
+           "Gi" => 1 << 30,
+           "T" =>  1000000000000,
+           "Ti" => 1 << 40,
+           "P" =>  1000000000000000,
+           "Pi" => 1 << 50,
+           "E" =>  1000000000000000000,
+           "Ei" => 1 << 60,
+          }[mt[2]]
+        end
+      end
+
+      rescue => e
+        raise "#{cfgkey} expected #{cfgtype} but '#{cfg[k]}' got error #{e}"
+      end
+
+      if !cfg[k].is_a? cfgtype
+        raise "#{cfgkey} expected #{cfgtype} but was #{cfg[k].class}"
+      end
+    end
+  end
+
+  def self.set_cfg cfg, k, v
+    # "foo.bar = baz" --> { cfg["foo"]["bar"] = baz }
+    ks = k.split '.'
+    k = ks.pop
+    ks.each do |kk|
+      cfg = cfg[kk]
+      if cfg.nil?
+        break
+      end
+    end
+    if !cfg.nil?
+      cfg[k] = v
+    end
+  end
+
+  def self.parse_duration durstr, cfgkey:
+    duration_re = /-?(\d+(\.\d+)?)(s|m|h)/
+    dursec = 0
+    while durstr != ""
+      mt = duration_re.match durstr
+      if !mt
+        raise "#{cfgkey} not a valid duration: '#{durstr}', accepted suffixes are s, m, h"
+      end
+      multiplier = {s: 1, m: 60, h: 3600}
+      dursec += (Float(mt[1]) * multiplier[mt[3].to_sym])
+      durstr = durstr[mt[0].length..-1]
+    end
+    return dursec.seconds
+  end
+
+  def self.copy_into_config src, dst
+    src.each do |k, v|
+      dst.send "#{k}=", self.to_OrderedOptions(v)
+    end
+  end
+
+  def self.to_OrderedOptions confs
+    if confs.is_a? Hash
+      opts = ActiveSupport::OrderedOptions.new
+      confs.each do |k,v|
+        opts[k] = self.to_OrderedOptions(v)
+      end
+      opts
+    elsif confs.is_a? Array
+      confs.map { |v| self.to_OrderedOptions v }
+    else
+      confs
+    end
+  end
+
+  def self.load path, erb: false
+    if File.exist? path
+      yaml = IO.read path
+      if erb
+        yaml = ERB.new(yaml).result(binding)
+      end
+      YAML.load(yaml, deserialize_symbols: false)
+    else
+      {}
+    end
+  end
+
+end
diff --git a/apps/workbench/lib/tasks/config.rake b/apps/workbench/lib/tasks/config.rake
new file mode 100644 (file)
index 0000000..6067208
--- /dev/null
@@ -0,0 +1,56 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+def diff_hash base, final
+  diffed = {}
+  base.each do |k,v|
+    bk = base[k]
+    fk = final[k]
+    if bk.is_a? Hash
+      d = diff_hash bk, fk
+      if d.length > 0
+        diffed[k] = d
+      end
+    else
+      if bk.to_yaml != fk.to_yaml
+        diffed[k] = fk
+      end
+    end
+  end
+  diffed
+end
+
+namespace :config do
+  desc 'Print items that differ between legacy application.yml and system config.yml'
+  task diff: :environment do
+    diffed = diff_hash $arvados_config_global, $arvados_config
+    cfg = { "Clusters" => {}}
+    cfg["Clusters"][$arvados_config["ClusterID"]] = diffed.select {|k,v| k != "ClusterID"}
+    if cfg["Clusters"][$arvados_config["ClusterID"]].empty?
+      puts "No migrations required for /etc/arvados/config.yml"
+    else
+      puts cfg.to_yaml
+    end
+  end
+
+  desc 'Print config.yml after merging with legacy application.yml'
+  task migrate: :environment do
+    diffed = diff_hash $arvados_config_defaults, $arvados_config
+    cfg = { "Clusters" => {}}
+    cfg["Clusters"][$arvados_config["ClusterID"]] = diffed.select {|k,v| k != "ClusterID"}
+    puts cfg.to_yaml
+  end
+
+  desc 'Print configuration as accessed through Rails.configuration'
+  task dump: :environment do
+    combined = $arvados_config.deep_dup
+    combined.update $remaining_config
+    puts combined.to_yaml
+  end
+
+  desc 'Legacy config check task -- it is a noop now'
+  task check: :environment do
+    # This exists so that build/rails-package-scripts/postinst.sh doesn't fail.
+  end
+end
diff --git a/apps/workbench/lib/tasks/config_check.rake b/apps/workbench/lib/tasks/config_check.rake
deleted file mode 100644 (file)
index 9fd5435..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
-namespace :config do
-  desc 'Ensure site configuration has all required settings'
-  task check: :environment do
-    $application_config.sort.each do |k, v|
-      if ENV.has_key?('QUIET') then
-        # Make sure we still check for the variable to exist
-        eval("Rails.configuration.#{k}")
-      else
-        if /(password|secret)/.match(k) then
-          # Make sure we still check for the variable to exist, but don't print the value
-          eval("Rails.configuration.#{k}")
-          $stderr.puts "%-32s %s" % [k, '*********']
-        else
-          $stderr.puts "%-32s %s" % [k, eval("Rails.configuration.#{k}")]
-        end
-      end
-    end
-  end
-end
diff --git a/apps/workbench/lib/tasks/config_dump.rake b/apps/workbench/lib/tasks/config_dump.rake
deleted file mode 100644 (file)
index ed34960..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
-namespace :config do
-  desc 'Show site configuration'
-  task dump: :environment do
-    puts $application_config.to_yaml
-  end
-end
index 8e6ed7f2ca3127fc6539b2280914b4ad3b85927d..9ab457252adf00cb58c89793d2ce33adb163c76a 100644 (file)
@@ -60,6 +60,14 @@ Clusters:
         ExternalURL: ""
       WebShell:
         InternalURLs: {}
+        # ShellInABox service endpoint URL for a given VM.  If empty, do not
+        # offer web shell logins.
+        #
+        # E.g., using a path-based proxy server to forward connections to shell hosts:
+        # https://webshell.uuid_prefix.arvadosapi.com
+        #
+        # E.g., using a name-based proxy server to forward connections to shell hosts:
+        # https://*.webshell.uuid_prefix.arvadosapi.com
         ExternalURL: ""
       Workbench1:
         InternalURLs: {}
@@ -170,6 +178,11 @@ Clusters:
       NewUserNotificationRecipients: []
       NewInactiveUserNotificationRecipients: []
 
+      # Set anonymous_user_token to enable anonymous user access. You can get
+      # the token by running "bundle exec ./script/get_anonymous_user_token.rb"
+      # in the directory where your API server is running.
+      AnonymousUserToken: ""
+
     AuditLogs:
       # Time to keep audit logs, in seconds. (An audit log is a row added
       # to the "logs" table in the PostgreSQL database each time an
@@ -661,6 +674,11 @@ Clusters:
           FormFieldDescription: ""
           Required: true
       UserProfileFormMessage: 'Welcome to Arvados. All <span style="color:red">required fields</span> must be completed before you can proceed.'
+
+      # Mimetypes of applications for which the view icon
+      # would be enabled in a collection's show page.
+      # It is sufficient to list only applications here.
+      # No need to list text and image types.
       ApplicationMimetypesWithViewIcon:
         cwl: {}
         fasta: {}
index d03512d5989b4f950d7169db58e6cd6feefbf2ed..6d563370834abab8fa686bbcf47fe2aeb61f0bc7 100644 (file)
@@ -1,7 +1,7 @@
 PATH
   remote: .
   specs:
-    arvados-login-sync (1.3.3.20190528194843)
+    arvados-login-sync (1.4.0.20190610174652)
       arvados (~> 1.3.0, >= 1.3.0)
 
 GEM
@@ -60,8 +60,8 @@ GEM
     mocha (1.8.0)
       metaclass (~> 0.0.1)
     multi_json (1.13.1)
-    multipart-post (2.1.1)
-    os (1.0.1)
+    multipart-post (2.0.0)
+    os (1.0.0)
     public_suffix (3.0.3)
     rake (12.3.2)
     retriable (1.4.1)