Merge branch '8784-dir-listings'
[arvados.git] / sdk / cli / bin / arv-tag
1 #! /usr/bin/env ruby
2 # Copyright (C) The Arvados Authors. All rights reserved.
3 #
4 # SPDX-License-Identifier: Apache-2.0
5
6 # arv tag usage:
7 #   arv tag add tag1 [tag2 ...] --object obj_uuid1 [--object obj_uuid2 ...]
8 #   arv tag remove tag1 [tag2 ...] --object obj_uuid1 [--object obj_uuid2 ...]
9 #   arv tag remove tag1 [tag2 ...] --all
10
11 def usage_string
12   return "\nUsage:\n" +
13     "arv tag add tag1 [tag2 ...] --object object_uuid1 [object_uuid2...]\n" +
14     "arv tag remove tag1 [tag2 ...] --object object_uuid1 [object_uuid2...]\n" +
15     "arv tag remove --all\n"
16 end
17
18 def usage
19   abort usage_string
20 end
21
22 def api_call(method, parameters:{}, request_body:{})
23   result = $client.execute(:api_method => method,
24                            :parameters => parameters,
25                            :body_object => request_body,
26                            :authenticated => false,
27                            :headers => {
28                              authorization: "OAuth2 #{ENV['ARVADOS_API_TOKEN']}",
29                            })
30
31   begin
32     results = JSON.parse result.body
33   rescue JSON::ParserError => e
34     abort "Failed to parse server response:\n" + e.to_s
35   end
36
37   if results["errors"]
38     abort "Error: #{results["errors"][0]}"
39   end
40
41   return results
42 end
43
44 def tag_add(tag, obj_uuid)
45   return api_call($arvados.links.create,
46                   request_body: {
47                     :link => {
48                       :name       => tag,
49                       :link_class => :tag,
50                       :head_uuid  => obj_uuid,
51                     }
52                   })
53 end
54
55 def tag_remove(tag, obj_uuids=nil)
56   # If we got a list of objects to untag, look up the uuids for the
57   # links that need to be deleted.
58   link_uuids = []
59   if obj_uuids
60     obj_uuids.each do |uuid|
61       link = api_call($arvados.links.list,
62                       request_body: {
63                         :where => {
64                           :link_class => :tag,
65                           :name => tag,
66                           :head_uuid => uuid,
67                         }
68                       })
69       if link['items_available'] > 0
70         link_uuids.push link['items'][0]['uuid']
71       end
72     end
73   else
74     all_tag_links = api_call($arvados.links.list,
75                              request_body: {
76                                :where => {
77                                  :link_class => :tag,
78                                  :name => tag,
79                                }
80                              })
81     link_uuids = all_tag_links['items'].map { |obj| obj['uuid'] }
82   end
83
84   results = []
85   if link_uuids
86     link_uuids.each do |uuid|
87       results.push api_call($arvados.links.delete, parameters:{ :uuid => uuid })
88     end
89   else
90     $stderr.puts "no tags found to remove"
91   end
92
93   return results
94 end
95
96 if RUBY_VERSION < '1.9.3' then
97   abort <<-EOS
98 #{$0.gsub(/^\.\//,'')} requires Ruby version 1.9.3 or higher.
99 EOS
100 end
101
102 $arvados_api_version = ENV['ARVADOS_API_VERSION'] || 'v1'
103 $arvados_api_host = ENV['ARVADOS_API_HOST'] or
104   abort "#{$0}: fatal: ARVADOS_API_HOST environment variable not set."
105 $arvados_api_token = ENV['ARVADOS_API_TOKEN'] or
106   abort "#{$0}: fatal: ARVADOS_API_TOKEN environment variable not set."
107 $arvados_api_host_insecure = %w(1 true yes).
108   include?((ENV['ARVADOS_API_HOST_INSECURE'] || "").downcase)
109
110 begin
111   require 'rubygems'
112   require 'google/api_client'
113   require 'json'
114   require 'pp'
115   require 'oj'
116   require 'trollop'
117 rescue LoadError
118   abort <<-EOS
119 #{$0}: fatal: some runtime dependencies are missing.
120 Try: gem install pp google-api-client json trollop
121   EOS
122 end
123
124 def debuglog(message, verbosity=1)
125   $stderr.puts "#{File.split($0).last} #{$$}: #{message}" if $debuglevel >= verbosity
126 end
127
128 module Kernel
129   def suppress_warnings
130     original_verbosity = $VERBOSE
131     $VERBOSE = nil
132     result = yield
133     $VERBOSE = original_verbosity
134     return result
135   end
136 end
137
138 if $arvados_api_host_insecure or $arvados_api_host.match /local/
139   # You probably don't care about SSL certificate checks if you're
140   # testing with a dev server.
141   suppress_warnings { OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE }
142 end
143
144 class Google::APIClient
145   def discovery_document(api, version)
146     api = api.to_s
147     return @discovery_documents["#{api}:#{version}"] ||=
148       begin
149         response = self.execute!(
150                                  :http_method => :get,
151                                  :uri => self.discovery_uri(api, version),
152                                  :authenticated => false
153                                  )
154         response.body.class == String ? JSON.parse(response.body) : response.body
155       end
156   end
157 end
158
159 global_opts = Trollop::options do
160   banner usage_string
161   banner ""
162   opt :dry_run, "Don't actually do anything", :short => "-n"
163   opt :verbose, "Print some things on stderr", :short => "-v"
164   opt :uuid, "Return the UUIDs of the objects in the response, one per line (default)", :short => nil
165   opt :json, "Return the entire response received from the API server, as a JSON object", :short => "-j"
166   opt :human, "Return the response received from the API server, as a JSON object with whitespace added for human consumption", :short => "-h"
167   opt :pretty, "Synonym of --human", :short => nil
168   opt :yaml, "Return the response received from the API server, in YAML format", :short => "-y"
169   stop_on ['add', 'remove']
170 end
171
172 p = Trollop::Parser.new do
173   opt(:all,
174       "Remove this tag from all objects under your ownership. Only valid with `tag remove'.",
175       :short => :none)
176   opt(:object,
177       "The UUID of an object to which this tag operation should be applied.",
178       :type => :string,
179       :multi => true,
180       :short => :o)
181 end
182
183 $options = Trollop::with_standard_exception_handling p do
184   p.parse ARGV
185 end
186
187 if $options[:all] and ARGV[0] != 'remove'
188   usage
189 end
190
191 # Set up the API client.
192
193 $client ||= Google::APIClient.
194   new(:host => $arvados_api_host,
195       :application_name => File.split($0).last,
196       :application_version => $application_version.to_s)
197 $arvados = $client.discovered_api('arvados', $arvados_api_version)
198
199 results = []
200 cmd = ARGV.shift
201
202 if ARGV.empty?
203   usage
204 end
205
206 case cmd
207 when 'add'
208   ARGV.each do |tag|
209     $options[:object].each do |obj|
210       results.push(tag_add(tag, obj))
211     end
212   end
213 when 'remove'
214   ARGV.each do |tag|
215     if $options[:all] then
216       results.concat tag_remove(tag)
217     else
218       results.concat tag_remove(tag, $options[:object])
219     end
220   end
221 else
222   usage
223 end
224
225 if global_opts[:human] or global_opts[:pretty] then
226   puts Oj.dump(results, :indent => 1)
227 elsif global_opts[:yaml] then
228   puts results.to_yaml
229 elsif global_opts[:json] then
230   puts Oj.dump(results)
231 else
232   results.each do |r|
233     if r['uuid'].nil?
234       abort("Response did not include a uuid:\n" +
235             Oj.dump(r, :indent => 1) +
236             "\n")
237     else
238       puts r['uuid']
239     end
240   end
241 end