nexus_api 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+