Merge branch '13306-arvados-cwl-runner-py3-support'
[arvados.git] / apps / workbench / app / controllers / pipeline_instances_controller.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 class PipelineInstancesController < ApplicationController
6   skip_before_filter :find_object_by_uuid, only: :compare
7   before_filter :find_objects_by_uuid, only: :compare
8   skip_around_filter :require_thread_api_token, if: proc { |ctrl|
9     Rails.configuration.anonymous_user_token and
10     'show' == ctrl.action_name
11   }
12
13   include PipelineInstancesHelper
14   include PipelineComponentsHelper
15
16   def copy
17     template = PipelineTemplate.find?(@object.pipeline_template_uuid)
18
19     source = @object
20     @object = PipelineInstance.new
21     @object.pipeline_template_uuid = source.pipeline_template_uuid
22
23     if params['components'] == 'use_latest' and template
24       @object.components = template.components.deep_dup
25       @object.components.each do |cname, component|
26         # Go through the script parameters of each component
27         # that are marked as user input and copy them over.
28         # Skip any components that are not present in the
29         # source instance (there's nothing to copy)
30         if source.components.include? cname
31           component[:script_parameters].each do |pname, val|
32             if val.is_a? Hash and val[:dataclass]
33               # this is user-inputtable, so check the value from the source pipeline
34               srcvalue = source.components[cname][:script_parameters][pname]
35               if not srcvalue.nil?
36                 component[:script_parameters][pname] = srcvalue
37               end
38             end
39           end
40         end
41       end
42     else
43       @object.components = source.components.deep_dup
44     end
45
46     if params['script'] == 'use_same'
47       # Go through each component and copy the script_version from each job.
48       @object.components.each do |cname, component|
49         if source.components.include? cname and source.components[cname][:job]
50           component[:script_version] = source.components[cname][:job][:script_version]
51         end
52       end
53     end
54
55     @object.components.each do |cname, component|
56       component.delete :job
57     end
58     @object.state = 'New'
59
60     # set owner_uuid to that of source, provided it is a project and writable by current user
61     current_project = Group.find(source.owner_uuid) rescue nil
62     if (current_project && current_project.writable_by.andand.include?(current_user.uuid))
63       @object.owner_uuid = source.owner_uuid
64     end
65
66     super
67   end
68
69   def update
70     @updates ||= params[@object.class.to_s.underscore.singularize.to_sym]
71     if (components = @updates[:components])
72       components.each do |cname, component|
73         if component[:script_parameters]
74           component[:script_parameters].each do |param, value_info|
75             if value_info.is_a? Hash
76               value_info_partitioned = value_info[:value].partition('/') if value_info[:value].andand.class.eql?(String)
77               value_info_value = value_info_partitioned ? value_info_partitioned[0] : value_info[:value]
78               value_info_class = resource_class_for_uuid value_info_value
79               if value_info_class == Link
80                 # Use the link target, not the link itself, as script
81                 # parameter; but keep the link info around as well.
82                 link = Link.find value_info[:value]
83                 value_info[:value] = link.head_uuid
84                 value_info[:link_uuid] = link.uuid
85                 value_info[:link_name] = link.name
86               else
87                 # Delete stale link_uuid and link_name data.
88                 value_info[:link_uuid] = nil
89                 value_info[:link_name] = nil
90               end
91               if value_info_class == Collection
92                 # to ensure reproducibility, the script_parameter for a
93                 # collection should be the portable_data_hash
94                 # keep the collection name and uuid for human-readability
95                 obj = Collection.find value_info_value
96                 if value_info_partitioned
97                   value_info[:value] = obj.portable_data_hash + value_info_partitioned[1] + value_info_partitioned[2]
98                   value_info[:selection_name] = obj.name ? obj.name + value_info_partitioned[1] + value_info_partitioned[2] : obj.name
99                 else
100                   value_info[:value] = obj.portable_data_hash
101                   value_info[:selection_name] = obj.name
102                 end
103                 value_info[:selection_uuid] = obj.uuid
104               end
105             end
106           end
107         end
108       end
109     end
110     super
111   end
112
113   def graph(pipelines)
114     return nil, nil if params['tab_pane'] != "Graph"
115
116     provenance = {}
117     pips = {}
118     n = 1
119
120     # When comparing more than one pipeline, "pips" stores bit fields that
121     # indicates which objects are part of which pipelines.
122
123     pipelines.each do |p|
124       collections = []
125       hashes = []
126       jobs = []
127
128       p[:components].each do |k, v|
129         provenance["component_#{p[:uuid]}_#{k}"] = v
130
131         collections << v[:output_uuid] if v[:output_uuid]
132         jobs << v[:job][:uuid] if v[:job]
133       end
134
135       jobs = jobs.compact.uniq
136       if jobs.any?
137         Job.where(uuid: jobs).each do |j|
138           job_uuid = j.uuid
139
140           provenance[job_uuid] = j
141           pips[job_uuid] = 0 unless pips[job_uuid] != nil
142           pips[job_uuid] |= n
143
144           hashes << j[:output] if j[:output]
145           ProvenanceHelper::find_collections(j) do |hash, uuid|
146             collections << uuid if uuid
147             hashes << hash if hash
148           end
149
150           if j[:script_version]
151             script_uuid = j[:script_version]
152             provenance[script_uuid] = {:uuid => script_uuid}
153             pips[script_uuid] = 0 unless pips[script_uuid] != nil
154             pips[script_uuid] |= n
155           end
156         end
157       end
158
159       hashes = hashes.compact.uniq
160       if hashes.any?
161         Collection.where(portable_data_hash: hashes).each do |c|
162           hash_uuid = c.portable_data_hash
163           provenance[hash_uuid] = c
164           pips[hash_uuid] = 0 unless pips[hash_uuid] != nil
165           pips[hash_uuid] |= n
166         end
167       end
168
169       collections = collections.compact.uniq
170       if collections.any?
171         Collection.where(uuid: collections).each do |c|
172           collection_uuid = c.uuid
173           provenance[collection_uuid] = c
174           pips[collection_uuid] = 0 unless pips[collection_uuid] != nil
175           pips[collection_uuid] |= n
176         end
177       end
178
179       n = n << 1
180     end
181
182     return provenance, pips
183   end
184
185   def show
186     # the #show action can also be called by #compare, which does its own work to set up @pipelines
187     unless defined? @pipelines
188       @pipelines = [@object]
189     end
190
191     provenance, pips = graph(@pipelines)
192     if provenance
193       @prov_svg = ProvenanceHelper::create_provenance_graph provenance, "provenance_svg", {
194         :request => request,
195         :direction => :top_down,
196         :all_script_parameters => true,
197         :combine_jobs => :script_and_version,
198         :pips => pips,
199         :only_components => true,
200         :no_docker => true,
201         :no_log => true}
202     end
203
204     super
205   end
206
207   def compare
208     @breadcrumb_page_name = 'compare'
209
210     @rows = []          # each is {name: S, components: [...]}
211
212     if params['tab_pane'] == "Compare" or params['tab_pane'].nil?
213       # Build a table: x=pipeline y=component
214       @objects.each_with_index do |pi, pi_index|
215         pipeline_jobs(pi).each do |component|
216           # Find a cell with the same name as this component but no
217           # entry for this pipeline
218           target_row = nil
219           @rows.each_with_index do |row, row_index|
220             if row[:name] == component[:name] and !row[:components][pi_index]
221               target_row = row
222             end
223           end
224           if !target_row
225             target_row = {name: component[:name], components: []}
226             @rows << target_row
227           end
228           target_row[:components][pi_index] = component
229         end
230       end
231
232       @rows.each do |row|
233         # Build a "normal" pseudo-component for this row by picking the
234         # most common value for each attribute. If all values are
235         # equally common, there is no "normal".
236         normal = {}              # attr => most common value
237         highscore = {}           # attr => how common "normal" is
238         score = {}               # attr => { value => how common }
239         row[:components].each do |pj|
240           next if pj.nil?
241           pj.each do |k,v|
242             vstr = for_comparison v
243             score[k] ||= {}
244             score[k][vstr] = (score[k][vstr] || 0) + 1
245             highscore[k] ||= 0
246             if score[k][vstr] == highscore[k]
247               # tie for first place = no "normal"
248               normal.delete k
249             elsif score[k][vstr] == highscore[k] + 1
250               # more pipelines have v than anything else
251               highscore[k] = score[k][vstr]
252               normal[k] = vstr
253             end
254           end
255         end
256
257         # Add a hash in component[:is_normal]: { attr => is_the_value_normal? }
258         row[:components].each do |pj|
259           next if pj.nil?
260           pj[:is_normal] = {}
261           pj.each do |k,v|
262             pj[:is_normal][k] = (normal.has_key?(k) && normal[k] == for_comparison(v))
263           end
264         end
265       end
266     end
267
268     if params['tab_pane'] == "Graph"
269       @pipelines = @objects
270     end
271
272     @object = @objects.first
273
274     show
275   end
276
277   def show_pane_list
278     panes = %w(Components Log Graph Advanced)
279     if @object and @object.state.in? ['New', 'Ready']
280       panes = %w(Inputs) + panes - %w(Log)
281     end
282     if not @object.components.values.any? { |x| x[:job] rescue false }
283       panes -= ['Graph']
284     end
285     panes
286   end
287
288   def compare_pane_list
289     %w(Compare Graph)
290   end
291
292   helper_method :unreadable_inputs_present?
293   def unreadable_inputs_present?
294     unless @unreadable_inputs_present.nil?
295       return @unreadable_inputs_present
296     end
297
298     input_uuids = []
299     input_pdhs = []
300     @object.components.each do |k, component|
301       next if !component
302       component[:script_parameters].andand.each do |p, tv|
303         if (tv.is_a? Hash) and ((tv[:dataclass] == "Collection") || (tv[:dataclass] == "File"))
304           if tv[:value]
305             value = tv[:value]
306           elsif tv[:default]
307             value = tv[:default]
308           else
309             value = ''
310           end
311           if value.present?
312             split = value.split '/'
313             if CollectionsHelper.match(split[0])
314               input_pdhs << split[0]
315             else
316               input_uuids << split[0]
317             end
318           end
319         end
320       end
321     end
322
323     input_pdhs = input_pdhs.uniq
324     input_uuids = input_uuids.uniq
325
326     preload_collections_for_objects input_uuids if input_uuids.any?
327     preload_for_pdhs input_pdhs if input_pdhs.any?
328
329     @unreadable_inputs_present = false
330     input_uuids.each do |uuid|
331       if !collections_for_object(uuid).any?
332         @unreadable_inputs_present = true
333         break
334       end
335     end
336     if !@unreadable_inputs_present
337       input_pdhs.each do |pdh|
338         if !collection_for_pdh(pdh).any?
339           @unreadable_inputs_present = true
340           break
341         end
342       end
343     end
344
345     @unreadable_inputs_present
346   end
347
348   def cancel
349     @object.cancel
350     if params[:return_to]
351       redirect_to params[:return_to]
352     else
353       redirect_to @object
354     end
355   end
356
357   protected
358   def for_comparison v
359     if v.is_a? Hash or v.is_a? Array
360       v.to_json
361     else
362       v.to_s
363     end
364   end
365
366   def load_filters_and_paging_params
367     params[:limit] = 20
368     super
369   end
370
371   def find_objects_by_uuid
372     @objects = model_class.where(uuid: params[:uuids])
373   end
374 end