nexus_api 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,4 @@
1
+ module NexusAPI
2
+ VERSION = '1.0.0'
3
+ end
4
+