10231: validate_runtime_constraints should occur before set_container filter.
[arvados.git] / services / api / app / models / container_request.rb
1 require 'whitelist_update'
2
3 class ContainerRequest < ArvadosModel
4   include HasUuid
5   include KindAndEtag
6   include CommonApiTemplate
7   include WhitelistUpdate
8
9   serialize :properties, Hash
10   serialize :environment, Hash
11   serialize :mounts, Hash
12   serialize :runtime_constraints, Hash
13   serialize :command, Array
14
15   before_validation :fill_field_defaults, :if => :new_record?
16   before_validation :validate_runtime_constraints
17   before_validation :set_container
18   validates :command, :container_image, :output_path, :cwd, :presence => true
19   validate :validate_state_change
20   validate :validate_change
21   after_save :update_priority
22   after_save :finalize_if_needed
23   before_create :set_requesting_container_uuid
24
25   api_accessible :user, extend: :common do |t|
26     t.add :command
27     t.add :container_count
28     t.add :container_count_max
29     t.add :container_image
30     t.add :container_uuid
31     t.add :cwd
32     t.add :description
33     t.add :environment
34     t.add :expires_at
35     t.add :filters
36     t.add :mounts
37     t.add :name
38     t.add :output_path
39     t.add :priority
40     t.add :properties
41     t.add :requesting_container_uuid
42     t.add :runtime_constraints
43     t.add :state
44     t.add :use_existing
45   end
46
47   # Supported states for a container request
48   States =
49     [
50      (Uncommitted = 'Uncommitted'),
51      (Committed = 'Committed'),
52      (Final = 'Final'),
53     ]
54
55   State_transitions = {
56     nil => [Uncommitted, Committed],
57     Uncommitted => [Committed],
58     Committed => [Final]
59   }
60
61   def state_transitions
62     State_transitions
63   end
64
65   def skip_uuid_read_permission_check
66     # XXX temporary until permissions are sorted out.
67     %w(modified_by_client_uuid container_uuid requesting_container_uuid)
68   end
69
70   def finalize_if_needed
71     if state == Committed && Container.find_by_uuid(container_uuid).final?
72       reload
73       act_as_system_user do
74         finalize!
75       end
76     end
77   end
78
79   # Finalize the container request after the container has
80   # finished/cancelled.
81   def finalize!
82     update_attributes!(state: Final)
83     c = Container.find_by_uuid(container_uuid)
84     ['output', 'log'].each do |out_type|
85       pdh = c.send(out_type)
86       next if pdh.nil?
87       manifest = Collection.where(portable_data_hash: pdh).first.manifest_text
88       Collection.create!(owner_uuid: owner_uuid,
89                          manifest_text: manifest,
90                          portable_data_hash: pdh,
91                          name: "Container #{out_type} for request #{uuid}",
92                          properties: {
93                            'type' => out_type,
94                            'container_request' => uuid,
95                          })
96     end
97   end
98
99   protected
100
101   def fill_field_defaults
102     self.state ||= Uncommitted
103     self.environment ||= {}
104     self.runtime_constraints ||= {}
105     self.mounts ||= {}
106     self.cwd ||= "."
107     self.container_count_max ||= Rails.configuration.container_count_max
108   end
109
110   # Create a new container (or find an existing one) to satisfy this
111   # request.
112   def resolve
113     c_mounts = mounts_for_container
114     c_runtime_constraints = runtime_constraints_for_container
115     c_container_image = container_image_for_container
116     c = act_as_system_user do
117       c_attrs = {command: self.command,
118                  cwd: self.cwd,
119                  environment: self.environment,
120                  output_path: self.output_path,
121                  container_image: c_container_image,
122                  mounts: c_mounts,
123                  runtime_constraints: c_runtime_constraints}
124
125       reusable = self.use_existing ? Container.find_reusable(c_attrs) : nil
126       if not reusable.nil?
127         reusable
128       else
129         Container.create!(c_attrs)
130       end
131     end
132     self.container_uuid = c.uuid
133   end
134
135   # Return a runtime_constraints hash that complies with
136   # self.runtime_constraints but is suitable for saving in a container
137   # record, i.e., has specific values instead of ranges.
138   #
139   # Doing this as a step separate from other resolutions, like "git
140   # revision range to commit hash", makes sense only when there is no
141   # opportunity to reuse an existing container (e.g., container reuse
142   # is not implemented yet, or we have already found that no existing
143   # containers are suitable).
144   def runtime_constraints_for_container
145     rc = {}
146     runtime_constraints.each do |k, v|
147       if v.is_a? Array
148         rc[k] = v[0]
149       else
150         rc[k] = v
151       end
152     end
153     rc
154   end
155
156   # Return a mounts hash suitable for a Container, i.e., with every
157   # readonly collection UUID resolved to a PDH.
158   def mounts_for_container
159     c_mounts = {}
160     mounts.each do |k, mount|
161       mount = mount.dup
162       c_mounts[k] = mount
163       if mount['kind'] != 'collection'
164         next
165       end
166       if (uuid = mount.delete 'uuid')
167         c = Collection.
168           readable_by(current_user).
169           where(uuid: uuid).
170           select(:portable_data_hash).
171           first
172         if !c
173           raise ArvadosModel::UnresolvableContainerError.new "cannot mount collection #{uuid.inspect}: not found"
174         end
175         if mount['portable_data_hash'].nil?
176           # PDH not supplied by client
177           mount['portable_data_hash'] = c.portable_data_hash
178         elsif mount['portable_data_hash'] != c.portable_data_hash
179           # UUID and PDH supplied by client, but they don't agree
180           raise ArgumentError.new "cannot mount collection #{uuid.inspect}: current portable_data_hash #{c.portable_data_hash.inspect} does not match #{c['portable_data_hash'].inspect} in request"
181         end
182       end
183     end
184     return c_mounts
185   end
186
187   # Return a container_image PDH suitable for a Container.
188   def container_image_for_container
189     coll = Collection.for_latest_docker_image(container_image)
190     if !coll
191       raise ArvadosModel::UnresolvableContainerError.new "docker image #{container_image.inspect} not found"
192     end
193     return coll.portable_data_hash
194   end
195
196   def set_container
197     if (container_uuid_changed? and
198         not current_user.andand.is_admin and
199         not container_uuid.nil?)
200       errors.add :container_uuid, "can only be updated to nil."
201       return false
202     end
203     if state_changed? and state == Committed and container_uuid.nil?
204       resolve
205     end
206     if self.container_uuid != self.container_uuid_was
207       if self.container_count_changed?
208         errors.add :container_count, "cannot be updated directly."
209         return false
210       else
211         self.container_count += 1
212       end
213     end
214   end
215
216   def validate_runtime_constraints
217     case self.state
218     when Committed
219       ['vcpus', 'ram'].each do |k|
220         if not (runtime_constraints.include? k and
221                 runtime_constraints[k].is_a? Integer and
222                 runtime_constraints[k] > 0)
223           errors.add :runtime_constraints, "#{k} must be a positive integer"
224         end
225       end
226
227       if runtime_constraints.include? 'keep_cache_ram' and
228          (!runtime_constraints['keep_cache_ram'].is_a?(Integer) or
229           runtime_constraints['keep_cache_ram'] <= 0)
230             errors.add :runtime_constraints, "keep_cache_ram must be a positive integer"
231       elsif !runtime_constraints.include? 'keep_cache_ram'
232         runtime_constraints['keep_cache_ram'] = Rails.configuration.container_default_keep_cache_ram
233       end
234     end
235   end
236
237   def validate_change
238     permitted = [:owner_uuid]
239
240     case self.state
241     when Uncommitted
242       # Permit updating most fields
243       permitted.push :command, :container_count_max,
244                      :container_image, :cwd, :description, :environment,
245                      :filters, :mounts, :name, :output_path, :priority,
246                      :properties, :requesting_container_uuid, :runtime_constraints,
247                      :state, :container_uuid, :use_existing
248
249     when Committed
250       if container_uuid.nil?
251         errors.add :container_uuid, "has not been resolved to a container."
252       end
253
254       if priority.nil?
255         errors.add :priority, "cannot be nil"
256       end
257
258       # Can update priority, container count, name and description
259       permitted.push :priority, :container_count, :container_count_max, :container_uuid, :name, :description
260
261       if self.state_changed?
262         # Allow create-and-commit in a single operation.
263         permitted.push :command, :container_image, :cwd, :description, :environment,
264                        :filters, :mounts, :name, :output_path, :properties,
265                        :requesting_container_uuid, :runtime_constraints,
266                        :state, :container_uuid
267       end
268
269     when Final
270       if not current_user.andand.is_admin and not (self.name_changed? || self.description_changed?)
271         errors.add :state, "of container request can only be set to Final by system."
272       end
273
274       if self.state_changed? || self.name_changed? || self.description_changed?
275           permitted.push :state, :name, :description
276       else
277         errors.add :state, "does not allow updates"
278       end
279
280     else
281       errors.add :state, "invalid value"
282     end
283
284     check_update_whitelist permitted
285   end
286
287   def update_priority
288     if self.state_changed? or
289         self.priority_changed? or
290         self.container_uuid_changed?
291       act_as_system_user do
292         Container.
293           where('uuid in (?)',
294                 [self.container_uuid_was, self.container_uuid].compact).
295           map(&:update_priority!)
296       end
297     end
298   end
299
300   def set_requesting_container_uuid
301     return !new_record? if self.requesting_container_uuid   # already set
302
303     token_uuid = current_api_client_authorization.andand.uuid
304     container = Container.where('auth_uuid=?', token_uuid).order('created_at desc').first
305     self.requesting_container_uuid = container.uuid if container
306     true
307   end
308 end