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