forked from OSC/ondemand
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfiguration_singleton.rb
More file actions
545 lines (465 loc) · 16.7 KB
/
configuration_singleton.rb
File metadata and controls
545 lines (465 loc) · 16.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
require 'pathname'
require 'dotenv'
require_relative '../lib/current_user'
require_relative '../app/models/concerns/bool_reader.rb'
# Dashboard app specific configuration singleton definition
# following the first proposal in:
#
# https://8thlight.com/blog/josh-cheek/2012/10/20/implementing-and-testing-the-singleton-pattern-in-ruby.html
#
# to avoid the traditional singleton approach or using class methods, both of
# which make it difficult to write tests against
#
# instead, ConfigurationSingleton is the definition of the configuration
# then the singleton instance used is a new class called "Configuration" which
# we set in config/boot i.e.
#
# Configuration = ConfigurationSingleton.new
#
# This is functionally equivalent to taking every instance method on
# ConfigurationSingleton and defining it as a class method on Configuration.
#
class ConfigurationSingleton
include BoolReader
attr_writer :app_development_enabled
attr_writer :app_sharing_enabled
# FIXME: temporary
attr_accessor :app_sharing_facls_enabled
alias_method :app_sharing_facls_enabled?, :app_sharing_facls_enabled
def initialize
load_dotenv_files
add_boolean_configs
add_string_configs
add_int_configs
end
# All the boolean configurations that can be read through
# environment variables or through the config file.
#
# @return [Hash] key/value pairs of defaults
def boolean_configs
{
:csp_enabled => false,
:csp_report_only => false,
:bc_dynamic_js => false,
:bc_simple_auto_accounts => false,
:bc_clean_old_dirs => false,
:bc_saved_settings => false,
:per_cluster_dataroot => false,
:remote_files_enabled => false,
:remote_files_validation => false,
:host_based_profiles => false,
:disable_bc_shell => false,
:cancel_session_enabled => false,
:hide_app_version => false,
:motd_render_html => false,
:upload_enabled => true,
:download_enabled => true,
:project_size_enabled => true,
:widget_partials_enabled => false,
:unsafe_render_html => false,
}.freeze
end
# All the string configurations that can be read through
# environment variables or through the config file.
#
# @return [Hash] key/value pairs of defaults
def string_configs
{
:module_file_dir => nil,
:user_settings_file => Pathname.new("~/.config/#{ood_portal}/settings.yml").expand_path.to_s,
:facl_domain => nil,
:auto_groups_filter => nil,
:google_analytics_tag_id => nil,
:project_template_dir => "#{config_root}/projects",
:rclone_extra_config => nil,
:default_profile => nil,
:plugins_directory => '/etc/ood/config/plugins'
}.freeze
end
# All the int configs can be read through environment variables
# or through the config file.
#
# @return [Hash] key/value pairs of defaults
def int_configs
{
:bc_clean_old_dirs_days => 30,
:download_dir_timeout_seconds => 5,
:download_dir_max => 10_737_418_240, # 10*1024*1024*1024 bytes
:file_editor_max_size => 12_582_912, # 12*1024*1024 bytes
:file_download_max => 10_737_418_240, # 10*1024*1024*1024 bytes
:project_size_timeout => 15,
:novnc_default_compression => 6,
:novnc_default_quality => 2,
}.freeze
end
# @return [String] memoized version string
def app_version
@app_version ||= (version_from_file(Rails.root) || version_from_git(Rails.root) || "Unknown").strip
end
# @return [String] memoized version string
def ood_version
@ood_version ||= (ood_version_from_env || version_from_file('/opt/ood') || version_from_git('/opt/ood') || "Unknown").strip
end
def ood_bc_ssh_to_compute_node
read_bool(ENV['OOD_BC_SSH_TO_COMPUTE_NODE'] || true)
end
# @return [String, nil] version string from git describe, or nil if not git repo
def version_from_git(dir)
Dir.chdir(Pathname.new(dir)) do
version = `git describe --always --tags 2>/dev/null`
version.blank? ? nil : version
end
rescue Errno::ENOENT
nil
end
def login_clusters
OodCore::Clusters.new(
OodAppkit.clusters
.select(&:allow?)
.reject { |c| c.metadata.hidden }
.select(&:login_allow?)
)
end
# clusters you can submit jobs to
def job_clusters
@job_clusters ||= OodCore::Clusters.new(
OodAppkit.clusters
.select(&:job_allow?)
.reject { |c| c.metadata.hidden }
)
end
# @return [String, nil] version string from VERSION file, or nil if no file avail
def version_from_file(dir)
file = Pathname.new(dir).join("VERSION")
file.read if file.file?
end
def ood_version_from_env
ENV['OOD_VERSION'] || ENV['ONDEMAND_VERSION']
end
# The app's configuration root directory
# @return [Pathname] path to configuration root
def config_root
Pathname.new(ENV["OOD_APP_CONFIG_ROOT"] || "/etc/ood/config/apps/dashboard")
end
def load_external_config?
read_bool(ENV['OOD_LOAD_EXTERNAL_CONFIG'] || (rails_env == 'production'))
end
# The root directory that holds configuration information for Batch Connect
# apps (typically each app will have a sub-directory underneath this)
def bc_config_root
Pathname.new(ENV["OOD_BC_APP_CONFIG_ROOT"] || "/etc/ood/config/apps")
end
def load_external_bc_config?
read_bool(ENV["OOD_LOAD_EXTERNAL_BC_CONFIG"] || (rails_env == "production"))
end
# The paths to the JSON files that store the quota information
# Can be URL or File path. colon delimited string; though colon in URL is
# ignored if URL has format: scheme://path (colons preceding // are ignored)
#
# /path/to/quota.json:https://osc.edu/quota.json
#
#
# @return [Array<String>] quota paths
def quota_paths
# regex uses negative lookahead to ignore : preceding //
ENV.fetch("OOD_QUOTA_PATH", "").strip.split(/:(?!\/\/)/)
end
# The threshold for determining if there is sufficient quota remaining
# @return [Float] threshold factor
def quota_threshold
ENV.fetch("OOD_QUOTA_THRESHOLD", 0.95).to_f
end
# The paths to the JSON files that store the balance information
# Can be URL or File path. colon delimited string; though colon in URL is
# ignored if URL has format: scheme://path (colons preceding // are ignored)
#
# /path/to/balance.json:https://osc.edu/balance.json
#
#
# @return [Array<String>] balance paths
def balance_paths
# regex uses negative lookahead to ignore : preceding //
ENV.fetch("OOD_BALANCE_PATH", "").strip.split(/:(?!\/\/)/)
end
# The threshold for determining if there is sufficient balance remaining
# @return [Float] threshold factor
def balance_threshold
ENV.fetch("OOD_BALANCE_THRESHOLD", 0).to_f
end
# The XMoD host
# @return [String, null] the host, or null if not set
def xdmod_host
ENV["OOD_XDMOD_HOST"]
end
# Whether or not XDMoD integration is enabled
# @return [Boolean]
def xdmod_integration_enabled?
xdmod_host.present?
end
# Support ticket configuration
def support_ticket_enabled?
config.has_key?(:support_ticket) || config.fetch(:profiles, {}).any? { |_, profile| profile.has_key?(:support_ticket) }
end
# Globus configuration
def globus_endpoints
config.fetch(:globus_endpoints, nil)
end
def launcher_default_items
config.fetch(:launcher_default_items, []).to_a
end
def global_bc_form_item(key)
return nil if key.nil? || key.to_s.empty?
all = config.fetch(:global_bc_form_items, {}).to_h
all[key.to_sym]
end
# Load the dotenv local files first, then the /etc dotenv files and
# the .env and .env.production or .env.development files.
#
# Doing this in two separate loads means OOD_APP_CONFIG_ROOT can be specified in
# the .env.local file, which will specify where to look for the /etc dotenv
# files. The default for OOD_APP_CONFIG_ROOT is /etc/ood/config/apps/myjobs and
# both .env and .env.production will be searched for there.
def load_dotenv_files
# .env.local first, so it can override OOD_APP_CONFIG_ROOT
Dotenv.load(*dotenv_local_files)
# load the rest of the dotenv files
Dotenv.load(*dotenv_files)
# load overloads
Dotenv.overload(*(overload_files(dotenv_files)))
Dotenv.overload(*(overload_files(dotenv_local_files)))
end
def dev_apps_root_path
Pathname.new(ENV["OOD_DEV_APPS_ROOT"] || "/dev/null")
end
def app_development_enabled?
return @app_development_enabled if defined? @app_development_enabled
read_bool(ENV['OOD_APP_DEVELOPMENT'] || DevRouter.base_path.directory? || DevRouter.base_path.symlink?)
end
alias_method :app_development_enabled, :app_development_enabled?
def app_sharing_enabled?
return @app_sharing_enabled if defined? @app_sharing_enabled
@app_sharing_enabled = read_bool(ENV['OOD_APP_SHARING'])
end
alias_method :app_sharing_enabled, :app_sharing_enabled?
def batch_connect_global_cache_enabled?
read_bool(ENV["OOD_BATCH_CONNECT_CACHE_ATTR_VALUES"] || true )
end
def developer_docs_url
ENV['OOD_DASHBOARD_DEV_DOCS_URL'] || "https://go.osu.edu/ood-app-dev"
end
def dataroot
# copied from OodAppkit::AppConfig#set_default_configuration
# then modified to ensure dataroot is never nil
#
# FIXME: note that this would be invalid if the dataroot where
# overridden in an initializer by modifying OodAppkit.dataroot
# Solution: in a test, add a custom initializer that changes this, then verify it has
# no effect or it affects both.
#
root = ENV['OOD_DATAROOT'] || ENV['RAILS_DATAROOT']
if rails_env == "production"
root ||= "~/#{ood_portal}/data/#{ENV['APP_TOKEN'] || 'sys/dashboard'}"
else
root ||= app_root.join("data")
end
Pathname.new(root).expand_path
end
def ood_portal
ENV['OOD_PORTAL'] || 'ondemand'
end
def locale
(ENV['OOD_LOCALE'] || 'en').to_sym
end
def locales_root
Pathname.new(ENV['OOD_LOCALES_ROOT'] || "/etc/ood/config/locales")
end
# Set the login host in the Native Instructions VNC session partial
def native_vnc_login_host
ENV['OOD_NATIVE_VNC_LOGIN_HOST']
end
# Set the global configuration directory
def config_directory
Pathname.new(ENV['OOD_CONFIG_D_DIRECTORY'] || "/etc/ood/config/ondemand.d")
end
# Setting terminal functionality in files app
def files_enable_shell_button
can_access_shell? && read_bool(config.fetch(:files_enable_shell_button, true))
end
# Report performance of activejobs table rendering
def console_log_performance_report?
dataroot.join("debug").file? || rails_env != 'production'
end
def can_access_activejobs?
can_access_core_app? 'activejobs'
end
def can_access_files?
can_access_core_app? 'files'
end
def can_access_file_editor?
can_access_core_app? 'file-editor'
end
def can_access_projects?
can_access_core_app? 'projects'
end
def can_access_system_status?
can_access_core_app? 'system-status'
end
def can_access_shell?
can_access_core_app? 'shell'
end
# Maximum file upload size that nginx will allow from clients in bytes
#
# @example No maximum upload size supplied.
# file_upload_max #=> "10737420000"
# @example 20 gigabyte file size upload limit.
# file_upload_max #=> "21474840000"
# @return [String] Maximum upload size for nginx.
def file_upload_max
[ENV['FILE_UPLOAD_MAX']&.to_i, ENV['NGINX_FILE_UPLOAD_MAX']&.to_i].compact.min || 10737420000
end
def allowlist_paths
(ENV['OOD_ALLOWLIST_PATH'] || ENV['WHITELIST_PATH'] || "").split(':').map{ |s| Pathname.new(s) }
end
# default value for opening apps in new window
# that is used if app's manifest doesn't specify
# if not set default is true
#
# @return [Boolean] true if by default open apps in new window
def open_apps_in_new_window?
if ENV['OOD_OPEN_APPS_IN_NEW_WINDOW']
read_bool(ENV['OOD_OPEN_APPS_IN_NEW_WINDOW'])
else
true
end
end
# How many days before a Session record is considered old and ready to delete
def ood_bc_card_time
ood_bc_card_time = ENV['OOD_BC_CARD_TIME']
return 7 if ood_bc_card_time.blank? || /^([+-]\d+|\d+)/.match(ood_bc_card_time.to_s).nil?
ood_bc_card_time_int = ood_bc_card_time.to_i
(ood_bc_card_time_int < 0) ? 0 : ood_bc_card_time_int
end
MIN_POLL_DELAY = 10_000
# Returns the number of milliseconds to wait between calls to the system status page
# The default is 30s and the minimum is 10s.
def status_poll_delay
status_poll_delay = ENV['OOD_STATUS_POLL_DELAY']
status_poll_delay_int = status_poll_delay.nil? ? config.fetch(:status_poll_delay, '30000').to_i : status_poll_delay.to_i
status_poll_delay_int < MIN_POLL_DELAY ? MIN_POLL_DELAY : status_poll_delay_int
end
# Returns the number of milliseconds to wait between calls to the BatchConnect Sessions resource
# to update the sessions card information.
# The default and minimum value is 10s = 10_000
def bc_sessions_poll_delay
bc_poll_delay = ENV['OOD_BC_SESSIONS_POLL_DELAY'] || ENV['POLL_DELAY']
bc_poll_delay_int = bc_poll_delay.nil? ? config.fetch(:bc_sessions_poll_delay, '10000').to_i : bc_poll_delay.to_i
bc_poll_delay_int < MIN_POLL_DELAY ? MIN_POLL_DELAY : bc_poll_delay_int
end
def config
@config ||= read_config
end
# Content security policy value for 'script-src'
def script_sources
sources = [:self]
sources << 'https://www.googletagmanager.com' unless google_analytics_tag_id.nil?
sources
end
# Content security policy value for 'connect-src'
def connect_sources
sources = [:self]
sources << 'https://www.google-analytics.com' unless google_analytics_tag_id.nil?
sources << xdmod_host if xdmod_integration_enabled?
sources
end
def rails_env_production?
rails_env == 'production'
end
def shared_projects_root
# This environment variable will support ':' colon separated paths
ENV['OOD_SHARED_PROJECT_PATH'].to_s.split(":").map { |p| Pathname.new(p) }
end
private
def can_access_core_app?(name)
app_dir = Rails.root.realpath.parent.join(name)
app_dir.directory? && app_dir.join('manifest.yml').readable?
end
def read_config
files = Pathname.glob(config_directory.join("*.{yml,yaml,yml.erb,yaml.erb}"))
files.sort.select do |f|
# only resond to root owned files in production.
rails_env == 'production' ? File.stat(f).uid.zero? : true
end.each_with_object({}) do |f, conf|
begin
content = ERB.new(f.read, trim_mode: "-").result(binding)
yml = YAML.safe_load(content, aliases: true) || {}
conf.deep_merge!(yml.deep_symbolize_keys)
rescue => e
$stderr.puts("Can't read or parse #{f} because of error #{e}")
end
end
end
# The environment
# @return [String] "development", "test", or "production"
def rails_env
ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development"
end
# The app's root directory
# @return [Pathname] path to app root
def app_root
Pathname.new(File.expand_path("../../", __FILE__))
end
def dotenv_local_files
[
app_root.join(".env.#{rails_env}.local"),
(app_root.join(".env.local") unless rails_env == "test"),
].compact
end
def dotenv_files
[
(config_root.join("env") if load_external_config?),
app_root.join(".env.#{rails_env}"),
app_root.join(".env")
].compact
end
# reverse list and suffix every path with '.overload'
def overload_files(files)
files.reverse.map {|p| p.sub(/$/, '.overload')}
end
# private method to add the boolean_config methods to this instances
def add_boolean_configs
boolean_configs.each do |cfg_item, default|
define_singleton_method(cfg_item.to_sym) do
e = ENV["OOD_#{cfg_item.to_s.upcase}"]
if e.nil?
config.fetch(cfg_item, default)
else
read_bool(e.to_s)
end
end
end.each do |cfg_item, _|
define_singleton_method("#{cfg_item}?".to_sym) do
send(cfg_item)
end
end
end
def add_string_configs
string_configs.each do |cfg_item, default|
define_singleton_method(cfg_item.to_sym) do
e = ENV["OOD_#{cfg_item.to_s.upcase}"]
e.nil? ? config.fetch(cfg_item, default) : e.to_s
end
end.each do |cfg_item, _|
define_singleton_method("#{cfg_item}?".to_sym) do
!send(cfg_item).nil?
end
end
end
def add_int_configs
int_configs.each do |cfg_item, default|
define_singleton_method(cfg_item.to_sym) do
e = ENV["OOD_#{cfg_item.to_s.upcase}"]
(e.nil? ? config.fetch(cfg_item, default) : e).to_i
end
end
end
end