Fix 2.4.2 upgrade notes formatting refs #19330
[arvados.git] / services / api / lib / config_loader.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 module Psych
6   module Visitors
7     class YAMLTree < Psych::Visitors::Visitor
8       def visit_ActiveSupport_Duration o
9         seconds = o.to_i
10         outstr = ""
11         if seconds / 3600 > 0
12           outstr += "#{seconds / 3600}h"
13           seconds = seconds % 3600
14         end
15         if seconds / 60 > 0
16           outstr += "#{seconds / 60}m"
17           seconds = seconds % 60
18         end
19         if seconds > 0
20           outstr += "#{seconds}s"
21         end
22         if outstr == ""
23           outstr = "0s"
24         end
25         @emitter.scalar outstr, nil, nil, true, false, Nodes::Scalar::ANY
26       end
27
28       def visit_URI_Generic o
29         @emitter.scalar o.to_s, nil, nil, true, false, Nodes::Scalar::ANY
30       end
31
32       def visit_URI_HTTP o
33         @emitter.scalar o.to_s, nil, nil, true, false, Nodes::Scalar::ANY
34       end
35
36       def visit_Pathname o
37         @emitter.scalar o.to_s, nil, nil, true, false, Nodes::Scalar::ANY
38       end
39     end
40   end
41 end
42
43
44 module Boolean; end
45 class TrueClass; include Boolean; end
46 class FalseClass; include Boolean; end
47
48 class NonemptyString < String
49 end
50
51 class ConfigLoader
52   def initialize
53     @config_migrate_map = {}
54     @config_types = {}
55   end
56
57   def declare_config(assign_to, configtype, migrate_from=nil, migrate_fn=nil)
58     if migrate_from
59       @config_migrate_map[migrate_from] = migrate_fn || ->(cfg, k, v) {
60         ConfigLoader.set_cfg cfg, assign_to, v
61       }
62     end
63     @config_types[assign_to] = configtype
64   end
65
66
67   def migrate_config from_config, to_config
68     remainders = {}
69     from_config.each do |k, v|
70       if @config_migrate_map[k.to_sym]
71         @config_migrate_map[k.to_sym].call to_config, k, v
72       else
73         remainders[k] = v
74       end
75     end
76     remainders
77   end
78
79   def coercion_and_check check_cfg, check_nonempty: true
80     @config_types.each do |cfgkey, cfgtype|
81       cfg = check_cfg
82       k = cfgkey
83       ks = k.split '.'
84       k = ks.pop
85       ks.each do |kk|
86         cfg = cfg[kk]
87         if cfg.nil?
88           break
89         end
90       end
91
92       if cfg.nil?
93         raise "missing #{cfgkey}"
94       end
95
96       if cfgtype == String and !cfg[k]
97         cfg[k] = ""
98       end
99
100       if cfgtype == String and cfg[k].is_a? Symbol
101         cfg[k] = cfg[k].to_s
102       end
103
104       if cfgtype == Pathname and cfg[k].is_a? String
105
106         if cfg[k] == ""
107           cfg[k] = Pathname.new("")
108         else
109           cfg[k] = Pathname.new(cfg[k])
110           if !cfg[k].exist?
111             raise "#{cfgkey} path #{cfg[k]} does not exist"
112           end
113         end
114       end
115
116       if cfgtype == NonemptyString
117         if (!cfg[k] || cfg[k] == "") && check_nonempty
118           raise "#{cfgkey} cannot be empty"
119         end
120         if cfg[k].is_a? String
121           next
122         end
123       end
124
125       if cfgtype == ActiveSupport::Duration
126         if cfg[k].is_a? Integer
127           cfg[k] = cfg[k].seconds
128         elsif cfg[k].is_a? String
129           cfg[k] = ConfigLoader.parse_duration(cfg[k], cfgkey: cfgkey)
130         end
131       end
132
133       if cfgtype == URI
134         cfg[k] = URI(cfg[k])
135       end
136
137       if cfgtype == Integer && cfg[k].is_a?(String)
138         v = cfg[k].sub(/B\s*$/, '')
139         if mt = /(-?\d*\.?\d+)\s*([KMGTPE]i?)$/.match(v)
140           if mt[1].index('.')
141             v = mt[1].to_f
142           else
143             v = mt[1].to_i
144           end
145           cfg[k] = v * {
146             'K' => 1000,
147             'Ki' => 1 << 10,
148             'M' => 1000000,
149             'Mi' => 1 << 20,
150             "G" =>  1000000000,
151             "Gi" => 1 << 30,
152             "T" =>  1000000000000,
153             "Ti" => 1 << 40,
154             "P" =>  1000000000000000,
155             "Pi" => 1 << 50,
156             "E" =>  1000000000000000000,
157             "Ei" => 1 << 60,
158           }[mt[2]]
159         end
160       end
161
162       if !cfg[k].is_a? cfgtype
163         raise "#{cfgkey} expected #{cfgtype} but was #{cfg[k].class}"
164       end
165     end
166   end
167
168   def self.set_cfg cfg, k, v
169     # "foo.bar = baz" --> { cfg["foo"]["bar"] = baz }
170     ks = k.split '.'
171     k = ks.pop
172     ks.each do |kk|
173       cfg = cfg[kk]
174       if cfg.nil?
175         break
176       end
177     end
178     if !cfg.nil?
179       cfg[k] = v
180     end
181   end
182
183   def self.parse_duration(durstr, cfgkey:)
184     sign = 1
185     if durstr[0] == '-'
186       durstr = durstr[1..-1]
187       sign = -1
188     end
189     duration_re = /(\d+(\.\d+)?)(s|m|h)/
190     dursec = 0
191     while durstr != ""
192       mt = duration_re.match durstr
193       if !mt
194         raise "#{cfgkey} not a valid duration: '#{durstr}', accepted suffixes are s, m, h"
195       end
196       multiplier = {s: 1, m: 60, h: 3600}
197       dursec += (Float(mt[1]) * multiplier[mt[3].to_sym] * sign)
198       durstr = durstr[mt[0].length..-1]
199     end
200     return dursec.seconds
201   end
202
203   def self.copy_into_config src, dst
204     src.each do |k, v|
205       dst.send "#{k}=", self.to_OrderedOptions(v)
206     end
207   end
208
209   def self.to_OrderedOptions confs
210     if confs.is_a? Hash
211       opts = ActiveSupport::OrderedOptions.new
212       confs.each do |k,v|
213         opts[k] = self.to_OrderedOptions(v)
214       end
215       opts
216     elsif confs.is_a? Array
217       confs.map { |v| self.to_OrderedOptions v }
218     else
219       confs
220     end
221   end
222
223   def self.load path, erb: false
224     if File.exist? path
225       yaml = IO.read path
226       if erb
227         yaml = ERB.new(yaml).result(binding)
228       end
229       YAML.load(yaml, deserialize_symbols: false)
230     else
231       {}
232     end
233   end
234
235 end