nexus_api 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.env.template +5 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +10 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +68 -0
- data/LICENSE.txt +21 -0
- data/README.md +185 -0
- data/Rakefile +15 -0
- data/bin/nexus_api +7 -0
- data/bin/setup +8 -0
- data/bin/test +2 -0
- data/lib/nexus_api/cli.rb +29 -0
- data/lib/nexus_api/cli_commands/commands.rb +6 -0
- data/lib/nexus_api/cli_commands/download.rb +23 -0
- data/lib/nexus_api/cli_commands/list.rb +66 -0
- data/lib/nexus_api/cli_commands/script.rb +38 -0
- data/lib/nexus_api/cli_commands/search.rb +34 -0
- data/lib/nexus_api/cli_commands/tag.rb +47 -0
- data/lib/nexus_api/cli_commands/upload.rb +126 -0
- data/lib/nexus_api/cli_utils.rb +64 -0
- data/lib/nexus_api/config_manager.rb +53 -0
- data/lib/nexus_api/docker_manager.rb +112 -0
- data/lib/nexus_api/docker_shell.rb +32 -0
- data/lib/nexus_api/nexus_connection.rb +145 -0
- data/lib/nexus_api/version.rb +4 -0
- data/lib/nexus_api.rb +372 -0
- data/nexus_api.gemspec +35 -0
- data/team_configs/default.yaml +6 -0
- data/team_configs/template.yaml +18 -0
- metadata +189 -0
@@ -0,0 +1,126 @@
|
|
1
|
+
module NexusAPI
|
2
|
+
class Upload < ::Thor
|
3
|
+
attr_accessor :api
|
4
|
+
|
5
|
+
include NexusAPI::CLIUtils
|
6
|
+
|
7
|
+
desc 'docker', 'Upload a docker image'
|
8
|
+
option :image, :aliases => '-i', :desc => 'Docker image to upload', :required => true
|
9
|
+
option :docker_tag, :aliases => '-t', :desc => 'Docker tag', :required => true
|
10
|
+
def docker
|
11
|
+
setup
|
12
|
+
if_file_exists?(file: options[:image].to_s + ':' + options[:docker_tag].to_s, repository: ENV['DOCKER_PUSH_HOSTNAME']) do
|
13
|
+
@api.upload_docker_component(
|
14
|
+
image: options[:image],
|
15
|
+
tag: options[:docker_tag]
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
desc 'maven', 'Upload a maven file'
|
21
|
+
option :filename, :aliases => '-f', :desc => 'Path of file', :required => true
|
22
|
+
option :group_id, :aliases => '-g', :desc => 'Maven groupId for file', :required => true
|
23
|
+
option :artifact_id, :aliases => '-a', :desc => 'Maven artifactId for file', :required => true
|
24
|
+
option :version, :aliases => '-v', :desc => 'File version', :required => true
|
25
|
+
option :repository, :aliases => '-r', :desc => 'Repository name to upload file to; Overrides -e/--team-config; Required if -e not provided'
|
26
|
+
option :tag, :aliases => '-t', :desc => 'Tag to add to file (tag MUST already exist!)'
|
27
|
+
def maven
|
28
|
+
return false unless repository_set?
|
29
|
+
set(repository: :maven_repository)
|
30
|
+
if_file_exists? do
|
31
|
+
@api.upload_maven_component(
|
32
|
+
filename: options[:filename],
|
33
|
+
group_id: options[:group_id],
|
34
|
+
artifact_id: options[:artifact_id],
|
35
|
+
version: options[:version],
|
36
|
+
repository: options[:repository],
|
37
|
+
tag: options[:tag],
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
desc 'npm', 'Upload a npm file'
|
43
|
+
option :filename, :aliases => '-f', :desc => 'Path of file', :required => true
|
44
|
+
option :repository, :aliases => '-r', :desc => 'Repository name to upload file to; Overrides -e/--team-config; Required if -e not provided'
|
45
|
+
option :tag, :aliases => '-t', :desc => 'Tag to add to file (tag MUST already exist!)'
|
46
|
+
def npm
|
47
|
+
return false unless repository_set?
|
48
|
+
set(repository: :npm_repository)
|
49
|
+
if_file_exists? do
|
50
|
+
@api.upload_npm_component(
|
51
|
+
filename: options[:filename],
|
52
|
+
repository: options[:repository],
|
53
|
+
tag: options[:tag],
|
54
|
+
)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
desc 'pypi', 'Upload a pypi file'
|
59
|
+
option :filename, :aliases => '-f', :desc => 'Path of file', :required => true
|
60
|
+
option :repository, :aliases => '-r', :desc => 'Repository name to upload file to; Overrides -e/--team-config; Required if -e not provided'
|
61
|
+
option :tag, :aliases => '-t', :desc => 'Tag to add to file (tag MUST already exist!)'
|
62
|
+
def pypi
|
63
|
+
return false unless repository_set?
|
64
|
+
set(repository: :pypi_repository)
|
65
|
+
if_file_exists? do
|
66
|
+
@api.upload_pypi_component(
|
67
|
+
filename: options[:filename],
|
68
|
+
repository: options[:repository],
|
69
|
+
tag: options[:tag],
|
70
|
+
)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
desc 'raw', 'Upload a raw file'
|
75
|
+
option :filename, :aliases => '-f', :desc => 'Filename', :required => true
|
76
|
+
option :directory, :aliases => '-d', :desc => 'Path to file', :required => true
|
77
|
+
option :repository, :aliases => '-r', :desc => 'Repository name to upload file to; Overrides -e/--team-config; Required if -e not provided'
|
78
|
+
option :tag, :aliases => '-t', :desc => 'Tag to add to file (tag MUST already exist!)'
|
79
|
+
def raw
|
80
|
+
return false unless repository_set?
|
81
|
+
set(repository: :raw_repository)
|
82
|
+
if_file_exists? do
|
83
|
+
@api.upload_raw_component(
|
84
|
+
filename: options[:filename],
|
85
|
+
directory: options[:directory],
|
86
|
+
repository: options[:repository],
|
87
|
+
tag: options[:tag],
|
88
|
+
)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
desc 'rubygems', 'Upload a rubygems file'
|
93
|
+
option :filename, :aliases => '-f', :desc => 'Path of file', :required => true
|
94
|
+
option :repository, :aliases => '-r', :desc => 'Repository name to upload file to; Overrides -e/--team-config; Required if -e not provided'
|
95
|
+
option :tag, :aliases => '-t', :desc => 'Tag to add to file (tag MUST already exist!)'
|
96
|
+
def rubygems
|
97
|
+
return false unless repository_set?
|
98
|
+
set(repository: :rubygems_repository)
|
99
|
+
if_file_exists? do
|
100
|
+
@api.upload_rubygems_component(
|
101
|
+
filename: options[:filename],
|
102
|
+
repository: options[:repository],
|
103
|
+
tag: options[:tag],
|
104
|
+
)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
desc 'yum', 'Upload a yum file'
|
109
|
+
option :filename, :aliases => '-f', :desc => 'Filename', :required => true
|
110
|
+
option :directory, :aliases => '-d', :desc => 'Path to file', :required => true
|
111
|
+
option :repository, :aliases => '-r', :desc => 'Repository name to upload file to; Overrides -e/--team-config; Required if -e not provided'
|
112
|
+
option :tag, :aliases => '-t', :desc => 'Tag to add to file (tag MUST already exist!)'
|
113
|
+
def yum
|
114
|
+
return false unless repository_set?
|
115
|
+
set(repository: :yum_repository)
|
116
|
+
if_file_exists? do
|
117
|
+
@api.upload_yum_component(
|
118
|
+
filename: options[:filename],
|
119
|
+
directory: options[:directory],
|
120
|
+
repository: options[:repository],
|
121
|
+
tag: options[:tag],
|
122
|
+
)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'dotenv'
|
2
|
+
|
3
|
+
module NexusAPI
|
4
|
+
module CLIUtils
|
5
|
+
def setup
|
6
|
+
Dotenv.load(options[:nexus_config])
|
7
|
+
@api = NexusAPI::API.new(
|
8
|
+
username: ENV['NEXUS_USERNAME'],
|
9
|
+
password: ENV['NEXUS_PASSWORD'],
|
10
|
+
hostname: ENV['NEXUS_HOSTNAME'],
|
11
|
+
docker_pull_hostname: ENV['DOCKER_PULL_HOSTNAME'],
|
12
|
+
docker_push_hostname: ENV['DOCKER_PUSH_HOSTNAME'],
|
13
|
+
team_config: options[:team_config]
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
def print_element(action:, params:, filter:)
|
18
|
+
setup
|
19
|
+
element = @api.send(action, params)
|
20
|
+
puts options[:full] ? element : element[filter]
|
21
|
+
end
|
22
|
+
|
23
|
+
def print_paginating_set(action:, params:, filter:, proc: nil)
|
24
|
+
setup
|
25
|
+
set = Array.new.tap do |set|
|
26
|
+
loop do
|
27
|
+
params[:paginate] = true
|
28
|
+
set.concat(Array(@api.send(action, params)))
|
29
|
+
break unless @api.paginate?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
proc = proc { set.map{ |element| element[filter] } } if proc.nil?
|
33
|
+
puts options[:full] ? set : proc.call(set)
|
34
|
+
end
|
35
|
+
|
36
|
+
def print_set(action:, filter:)
|
37
|
+
setup
|
38
|
+
set = @api.send(action)
|
39
|
+
puts options[:full] ? set : set.map{ |element| element[filter] }
|
40
|
+
end
|
41
|
+
|
42
|
+
def repository_set?
|
43
|
+
if options[:repository].nil? && options[:team_config].nil?
|
44
|
+
puts "No value provided for required option '--repository' or '--team_config' (only need 1)"
|
45
|
+
return false
|
46
|
+
end
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
def set(repository:)
|
51
|
+
setup
|
52
|
+
options[:repository] = @api.team_config.send(repository) if options[:repository].nil?
|
53
|
+
end
|
54
|
+
|
55
|
+
def if_file_exists?(file: options[:filename], repository: options[:repository])
|
56
|
+
begin
|
57
|
+
puts "Sending '#{file}' to the '#{repository}' repository in Nexus!"
|
58
|
+
yield
|
59
|
+
rescue Errno::ENOENT
|
60
|
+
puts "'#{file}' does not exist locally."
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module NexusAPI
|
4
|
+
class ConfigManager
|
5
|
+
def initialize(config_path:)
|
6
|
+
if File.exist?(config_path)
|
7
|
+
@config = YAML.safe_load(File.read(config_path)) || {}
|
8
|
+
else
|
9
|
+
raise "ERROR: Specified config '#{config_path}' does not exist."
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def assets_repository
|
14
|
+
@config['assets']
|
15
|
+
end
|
16
|
+
|
17
|
+
def components_repository
|
18
|
+
@config['components']
|
19
|
+
end
|
20
|
+
|
21
|
+
def search_repository
|
22
|
+
@config['search']
|
23
|
+
end
|
24
|
+
|
25
|
+
def tag_repository
|
26
|
+
@config['tag']
|
27
|
+
end
|
28
|
+
|
29
|
+
def maven_repository
|
30
|
+
@config['maven']
|
31
|
+
end
|
32
|
+
|
33
|
+
def npm_repository
|
34
|
+
@config['npm']
|
35
|
+
end
|
36
|
+
|
37
|
+
def pypi_repository
|
38
|
+
@config['pypi']
|
39
|
+
end
|
40
|
+
|
41
|
+
def raw_repository
|
42
|
+
@config['raw']
|
43
|
+
end
|
44
|
+
|
45
|
+
def rubygems_repository
|
46
|
+
@config['rubygems']
|
47
|
+
end
|
48
|
+
|
49
|
+
def yum_repository
|
50
|
+
@config['yum']
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'nexus_api/docker_shell'
|
2
|
+
|
3
|
+
module NexusAPI
|
4
|
+
class DockerManager
|
5
|
+
|
6
|
+
def initialize(docker:, options:)
|
7
|
+
@docker = docker
|
8
|
+
@username = options['username']
|
9
|
+
@password = options['password']
|
10
|
+
@pull_host = options['pull_host']
|
11
|
+
@push_host = options['push_host']
|
12
|
+
end
|
13
|
+
|
14
|
+
def download(image_name:, tag:)
|
15
|
+
return false unless docker_valid?
|
16
|
+
image_name = image_name(@pull_host, image_name, tag)
|
17
|
+
begin
|
18
|
+
image = @docker.pull_image(@username, @password, image_name)
|
19
|
+
rescue Docker::Error::NotFoundError => error
|
20
|
+
puts "ERROR: Failed to pull Docker image #{image_name}.\nDoes it exist in Nexus?"
|
21
|
+
return false
|
22
|
+
end
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
def upload(image_name:, tag:)
|
27
|
+
return false unless docker_valid?
|
28
|
+
begin
|
29
|
+
return false unless login
|
30
|
+
image = find_image(image_name(@pull_host, image_name, tag))
|
31
|
+
return false if image.nil?
|
32
|
+
tag(image, @push_host, image_name, tag)
|
33
|
+
image.push(nil, repo_tag: image_name(@push_host, image_name, tag))
|
34
|
+
rescue StandardError => error
|
35
|
+
puts "ERROR: #{error.inspect}"
|
36
|
+
return false
|
37
|
+
end
|
38
|
+
true
|
39
|
+
end
|
40
|
+
|
41
|
+
def exists?(image_name:, tag:)
|
42
|
+
return false unless docker_valid?
|
43
|
+
images = find_images(image_name(@pull_host, image_name, tag))
|
44
|
+
return false if images.empty?
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
def delete(image_name:, tag:)
|
49
|
+
return false unless docker_valid?
|
50
|
+
begin
|
51
|
+
image = find_image(image_name(@pull_host, image_name, tag))
|
52
|
+
return false if image.nil?
|
53
|
+
return false unless image.remove(:force => true)
|
54
|
+
rescue StandardError => error
|
55
|
+
puts "ERROR: #{error.inspect}"
|
56
|
+
return false
|
57
|
+
end
|
58
|
+
true
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def docker_valid?
|
64
|
+
return true if @docker.validate_version!
|
65
|
+
puts 'ERROR: Your installed version of the Docker API is not supported by the docker-api gem!'
|
66
|
+
false
|
67
|
+
end
|
68
|
+
|
69
|
+
def image_name(host, name, tag)
|
70
|
+
"#{host}/#{name}:#{tag}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def login
|
74
|
+
return true if @docker.authenticate!(@username, @password, @push_host)
|
75
|
+
puts "ERROR: Failed to authenticate to #{@push_host} as #{@username}"
|
76
|
+
false
|
77
|
+
end
|
78
|
+
|
79
|
+
def find_images(name)
|
80
|
+
@docker.list_images.select do |image|
|
81
|
+
image.info['RepoTags'].include?(name)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def valid?(images)
|
86
|
+
if images.empty?
|
87
|
+
puts 'ERROR: No matching docker images found'
|
88
|
+
return false
|
89
|
+
end
|
90
|
+
if images.count > 1
|
91
|
+
puts "ERROR: Found multiple images that match: #{images.map {|image| image.info['RepoTags']} }"
|
92
|
+
return false
|
93
|
+
end
|
94
|
+
true
|
95
|
+
end
|
96
|
+
|
97
|
+
def find_image(name)
|
98
|
+
images = find_images(name)
|
99
|
+
if valid?(images)
|
100
|
+
images.first
|
101
|
+
else
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def tag(image, host, name, tag)
|
107
|
+
unless image.info['RepoTags'].include?(image_name(host, name, tag))
|
108
|
+
image.tag('repo'=>"#{host}/#{name}", 'tag'=>tag)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'docker'
|
2
|
+
|
3
|
+
module NexusAPI
|
4
|
+
class DockerShell
|
5
|
+
def validate_version!
|
6
|
+
Docker.validate_version!
|
7
|
+
end
|
8
|
+
|
9
|
+
def pull_image(username, password, image_name)
|
10
|
+
Docker::Image.create(
|
11
|
+
'username' => username,
|
12
|
+
'password' => password,
|
13
|
+
'fromImage' => image_name
|
14
|
+
)
|
15
|
+
rescue Docker::Error::ClientError => error
|
16
|
+
puts "Error: Could not pull Docker image '#{image_name}'"
|
17
|
+
puts error
|
18
|
+
end
|
19
|
+
|
20
|
+
def authenticate!(username, password, host)
|
21
|
+
Docker.authenticate!(
|
22
|
+
'username' => username,
|
23
|
+
'password' => password,
|
24
|
+
'serveraddress' => host
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def list_images
|
29
|
+
Docker::Image.all
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'rest-client'
|
3
|
+
|
4
|
+
module NexusAPI
|
5
|
+
class NexusConnection
|
6
|
+
VALID_RESPONSE_CODES = [200, 204].freeze
|
7
|
+
|
8
|
+
attr_accessor :continuation_token
|
9
|
+
|
10
|
+
def initialize(username:, password:, hostname:)
|
11
|
+
@username = username
|
12
|
+
@password = password
|
13
|
+
@hostname = hostname
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_response(endpoint:, paginate: false, headers: {'Content-Type' => 'application/json'})
|
17
|
+
response = send_get(endpoint, paginate, headers)
|
18
|
+
response.nil? ? Hash.new : jsonize(response)
|
19
|
+
end
|
20
|
+
|
21
|
+
def get(endpoint:, paginate: false, headers: {'Content-Type' => 'application/json'})
|
22
|
+
valid?(send_get(endpoint, paginate, headers))
|
23
|
+
end
|
24
|
+
|
25
|
+
def post(endpoint:, parameters: '', headers: {'Content-Type' => 'application/json'})
|
26
|
+
response = send_request(
|
27
|
+
:post,
|
28
|
+
endpoint,
|
29
|
+
parameters: parameters,
|
30
|
+
headers: headers
|
31
|
+
)
|
32
|
+
valid?(response)
|
33
|
+
end
|
34
|
+
|
35
|
+
def put(endpoint:, parameters: '', headers: {'Content-Type' => 'application/json'})
|
36
|
+
response = send_request(
|
37
|
+
:put,
|
38
|
+
endpoint,
|
39
|
+
parameters: parameters,
|
40
|
+
headers: headers
|
41
|
+
)
|
42
|
+
valid?(response)
|
43
|
+
end
|
44
|
+
|
45
|
+
def delete(endpoint:, headers: {'Content-Type' => 'application/json'})
|
46
|
+
response = send_request(
|
47
|
+
:delete,
|
48
|
+
endpoint,
|
49
|
+
headers: headers
|
50
|
+
)
|
51
|
+
valid?(response)
|
52
|
+
end
|
53
|
+
|
54
|
+
def head(asset_url:)
|
55
|
+
catch_connection_error do
|
56
|
+
RestClient.head(asset_url)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def content_length(asset_url:)
|
61
|
+
response = head(asset_url: asset_url)
|
62
|
+
return -1 unless response.respond_to?(:headers)
|
63
|
+
response.headers[:content_length]
|
64
|
+
end
|
65
|
+
|
66
|
+
def download(url:)
|
67
|
+
catch_connection_error do
|
68
|
+
RestClient.get(url, authorization_header)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def paginate?
|
73
|
+
!@continuation_token.nil?
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def valid?(response)
|
80
|
+
return false if response.nil?
|
81
|
+
VALID_RESPONSE_CODES.include?(response.code) ? true : false
|
82
|
+
end
|
83
|
+
|
84
|
+
def handle(error)
|
85
|
+
puts "ERROR: Request failed"
|
86
|
+
puts error.description if error.is_a?(RestClient::Response)
|
87
|
+
end
|
88
|
+
|
89
|
+
def catch_connection_error
|
90
|
+
begin
|
91
|
+
yield
|
92
|
+
rescue SocketError => error
|
93
|
+
return handle(error)
|
94
|
+
rescue RestClient::Unauthorized => error
|
95
|
+
return handle(error)
|
96
|
+
rescue RestClient::ExceptionWithResponse => error
|
97
|
+
return handle(error.response)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def authorization_header
|
102
|
+
{ :Authorization => 'Basic ' + Base64.strict_encode64( "#{@username}:#{@password}" ) }
|
103
|
+
end
|
104
|
+
|
105
|
+
def send_request(connection_method, endpoint, parameters: '', headers: {})
|
106
|
+
catch_connection_error do
|
107
|
+
RestClient::Request.execute(
|
108
|
+
method: connection_method,
|
109
|
+
url: "https://#{@hostname}/service/rest/v1/#{endpoint}",
|
110
|
+
payload: parameters,
|
111
|
+
headers: authorization_header.merge(headers)
|
112
|
+
)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def send_get(endpoint, paginate, headers)
|
117
|
+
# paginate answers is the user requesting pagination, paginate? answers does a continuation token exist
|
118
|
+
# if an empty continuation token is included in the request we'll get an ArrayIndexOutOfBoundsException
|
119
|
+
endpoint += "&continuationToken=#{@continuation_token}" if paginate && paginate?
|
120
|
+
response = send_request(
|
121
|
+
:get,
|
122
|
+
endpoint,
|
123
|
+
headers: headers
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
127
|
+
# That's right, nexus has inconsistent null values for its api
|
128
|
+
def continuation_token_for(json)
|
129
|
+
return nil if json['continuationToken'].nil?
|
130
|
+
return nil if json['continuationToken'] == 'nil'
|
131
|
+
json['continuationToken']
|
132
|
+
end
|
133
|
+
|
134
|
+
def jsonize(response)
|
135
|
+
json = JSON.parse(response.body)
|
136
|
+
if json.class == Hash
|
137
|
+
@continuation_token = continuation_token_for(json)
|
138
|
+
json = json["items"] if json["items"]
|
139
|
+
end
|
140
|
+
json
|
141
|
+
rescue JSON::ParserError
|
142
|
+
response.body
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|