habitat-client 0.6.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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6b6f661829b14a79b6baf3e8d49567d8b824d3a9
4
+ data.tar.gz: 98edb09a4de342b6914537e0ac5f23bbcf82476e
5
+ SHA512:
6
+ metadata.gz: 76fdffc37cc805ecd11594632dff4ff5c5b65828794f22534b86e1b1f0009147a71ebb5ef6098c4d90ddc3fd35df9dc96d2ebf23276302c0909649f898be61bf
7
+ data.tar.gz: 2dd61ea89763ebc4711bf90ae2aeef7f716e44e7dee455f313a3d8d4939015cc9e4dd6dc2f21a77cb4cff9eaf22265c68bb70619f5e0d505d45e25785a42e759
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,8 @@
1
+ Metrics/MethodLength:
2
+ Enabled: false
3
+
4
+ Metrics/ClassLength:
5
+ Enabled: false
6
+
7
+ Metrics/LineLength:
8
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,38 @@
1
+ # Habitat::Client
2
+
3
+ This is the Ruby client library for Habitat.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'habitat-client'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install habitat-client
20
+
21
+ ## Usage
22
+
23
+ ```ruby
24
+ # Create an instance object
25
+ hc = Habitat::Client.new
26
+
27
+ # Create the instance with a specific Habitat Depot
28
+ hc = Habitat::Client.new('http://localhost:9636/v1/depot')
29
+
30
+ # Upload a Habitat Artifact
31
+ hc.put_package('core-pandas-0.0.1-20160425190407.hart')
32
+
33
+ # Show a package
34
+ hc.show_package('core/pandas')
35
+
36
+ # Promote a package to the `stable` view
37
+ hc.promote_package('core/pandas', 'stable')
38
+ ```
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'habitat/client/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'habitat-client'
8
+ spec.version = Habitat::Client::VERSION
9
+ spec.authors = ['Joshua Timberman']
10
+ spec.email = ['humans@habitat.sh']
11
+
12
+ spec.summary = 'Habitat Depot Client Library'
13
+ spec.homepage = 'https://www.habitat.sh'
14
+
15
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
16
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
17
+ spec.require_paths = ['lib']
18
+
19
+ spec.add_dependency 'rbnacl', '~> 3.3'
20
+ spec.add_dependency 'faraday', '~> 0.9.0'
21
+ spec.add_dependency 'mixlib-shellout', '~> 2.2'
22
+
23
+ spec.add_development_dependency 'pry'
24
+ spec.add_development_dependency 'pry-coolline'
25
+ spec.add_development_dependency 'bundler', '~> 1.11'
26
+ spec.add_development_dependency 'rake', '~> 10.0'
27
+ spec.add_development_dependency 'rspec', '~> 3.0'
28
+ end
@@ -0,0 +1,329 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2016 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require_relative 'client/version'
19
+ require_relative 'exceptions'
20
+ require 'faraday'
21
+ require 'json'
22
+
23
+ module Habitat
24
+ # Habitat Client
25
+ #
26
+ # This class is an API client to the Habitat Depot. It uses Faraday
27
+ # under the covers to give us a nice HTTP interface.
28
+ #
29
+ class Client
30
+ attr_reader :depot, :connection
31
+
32
+ # Creates the Habitat client connection object. The public
33
+ # interface should be used for the common interactions with the
34
+ # API, but more complex operations can be done using the
35
+ # +connection+ attribute through Faraday.
36
+ #
37
+ # === Attributes
38
+ #
39
+ # * +depot+ - URL to the Habitat Depot, defaults to the public
40
+ # service run by the Habitat organization
41
+ # * +connection+ - A Faraday object representing the HTTP connection
42
+ #
43
+ # === Examples
44
+ #
45
+ # hc = Habitat::Client.new
46
+ # hc = Habitat::Client.new('https://habitat-depot.example.com')
47
+ # hc.connection.get('/pkgs')
48
+
49
+ def initialize(depot = 'https://willem.habitat.sh/v1/depot')
50
+ @depot = depot
51
+ @connection = Faraday.new(url: @depot) do |f|
52
+ f.request :multipart
53
+ f.request :url_encoded
54
+ f.adapter :net_http
55
+ end
56
+ end
57
+
58
+ # Downloads the specified key. By default, it will download to the
59
+ # current directory as a file named by the +X-Filename+ HTTP
60
+ # header.
61
+ def fetch_key(_key, _path = '.')
62
+ raise 'Downloading keys is not yet implemented'
63
+ end
64
+
65
+ # Uploads the specified key from the given path location.
66
+ def put_key(_key, _path)
67
+ raise 'Uploading keys is not yet implemented'
68
+ end
69
+
70
+ # Downloads the specified package. It will default to the latest
71
+ # version if not specified, or the latest release of a version if
72
+ # the release is not specified.
73
+ #
74
+ # The file will be downloaded to the current directory as a file
75
+ # named by the +X-Filename+ HTTP header.
76
+ #
77
+ # === Arguments
78
+ #
79
+ # * +pkg+ - A package string, like +core/zlib+
80
+ def fetch_package(pkg, path = '.')
81
+ download(fetch_package_path(pkg), path)
82
+ end
83
+
84
+ # Show details about the specified package using a package
85
+ # identifier string. If the version or release are not specified,
86
+ # +latest+ is assumed.
87
+ #
88
+ # === Examples
89
+ #
90
+ # hc = Habitat::Client.new
91
+ # hc.show_package('core/zlib')
92
+ # hc.show_package('core/zlib/1.2.8')
93
+ # hc.show_package('core/zlib/1.2.8/20160222155343')
94
+ def show_package(pkg)
95
+ JSON.parse(@connection.get(show_package_path(pkg)).body)
96
+ end
97
+
98
+ # Uploads a package from the specified filepath.
99
+ #
100
+ # === Arguments
101
+ #
102
+ # * +file+ - The file to upload
103
+ #
104
+ # === Examples
105
+ def put_package(file)
106
+ upload(upload_package_path(file), file)
107
+ end
108
+
109
+ # Promotes a package to the specified view, for example
110
+ # +core/zlib+ to +staging+ or +production+
111
+ #
112
+ # === Examples
113
+ #
114
+ # hc = Habitat::Client.new
115
+ # hc.promote_package('core/pandas/0.0.1/20160419213120', 'staging')
116
+ def promote_package(pkg, view)
117
+ promote(promote_artifact_path(pkg, view))
118
+ end
119
+
120
+ private
121
+
122
+ # Returns the PackageIdent as a string with the version and/or
123
+ # release qualified if not specified. For example, if +pkg+ is
124
+ # +'core/zlib'+, it will return +'core/zlib/latest'+.
125
+ def package_ident(pkg)
126
+ PackageIdent.new(*pkg.split('/')).to_s
127
+ end
128
+
129
+ # If the PackageIdent has four parts, it's fully qualified.
130
+ def fully_qualified?(pkg)
131
+ parts = package_ident(pkg).split('/')
132
+ parts.count == 4 && parts.last != 'latest'
133
+ end
134
+
135
+ # Returns a URL path for retrieving the PackageIdent specified.
136
+ def show_package_path(pkg)
137
+ ['pkgs', package_ident(pkg)].join('/')
138
+ end
139
+
140
+ # Returns a URL path for retrieving the PackageIdent specified
141
+ # using the download path.
142
+ def fetch_package_path(pkg)
143
+ resolved_path = resolve_latest(package_ident(pkg))
144
+ ['pkgs', resolved_path, 'download'].join('/')
145
+ end
146
+
147
+ # Returns a URL path for retrieving the specified key
148
+ def fetch_key_path(key)
149
+ ['keys', key].join('/')
150
+ end
151
+
152
+ # Returns a URL path for uploading the specified package artifact
153
+ def upload_package_path(path)
154
+ ['pkgs', derive_pkgid_from_file(path)].join('/')
155
+ end
156
+
157
+ # Resolves the latest version of the package returned by +show_package+
158
+ def resolve_latest(pkg)
159
+ latest = show_package(pkg)
160
+ %w(origin name version release).map { |i| latest['ident'][i] }.join('/')
161
+ end
162
+
163
+ def validate_pkg_path!(pkg)
164
+ # rubocop:disable GuardClause
165
+ unless fully_qualified?(pkg)
166
+ raise <<-EOH.gsub(/^\s+/, '')
167
+ You must specify a fully qualified package path, such as:
168
+
169
+ core/pandas/0.0.1/20160419213120
170
+
171
+ You specified `#{pkg}' in `#{caller_locations(1, 1)[0].label}'
172
+ EOH
173
+ end
174
+ end
175
+
176
+ # Opens a Habitat Artifact and reads the IDENT metadata to get the
177
+ # fully qualified package identifier.
178
+ #
179
+ def derive_pkgid_from_file(file)
180
+ require 'mixlib/shellout'
181
+ tail_n = 'tail -n +6'
182
+ xzcat = 'xzcat --decompress'
183
+ tar_toc = 'tar -tf -'
184
+ tar_stdout = 'tar -xOf -'
185
+ subcommand = "#{tail_n} #{file} | #{xzcat} | #{tar_toc} | grep '/IDENT$'"
186
+ command = "#{tail_n} #{file} | #{xzcat} | #{tar_stdout} $(#{subcommand})"
187
+ pkgid = Mixlib::ShellOut.new(command)
188
+ begin
189
+ pkgid.run_command.stdout.chomp
190
+ rescue
191
+ raise "Could not derive a version from #{file}, aborting!"
192
+ end
193
+ end
194
+
195
+ # Returns a hex encoded blake2b hash from the given data.
196
+ def blake2b_checksum(data, size = 32)
197
+ require 'rbnacl'
198
+ require 'digest'
199
+ Digest.hexencode(RbNaCl::Hash.blake2b(data, digest_size: size)).chomp
200
+ end
201
+
202
+ # Returns the URL path for promoting an artifact
203
+ def promote_artifact_path(pkg, view)
204
+ validate_pkg_path!(pkg)
205
+ ['views', view, 'pkgs', pkg, 'promote'].join('/')
206
+ end
207
+
208
+ def promote(url)
209
+ response = @connection.post(url)
210
+ if response.status == 200
211
+ return true
212
+ else
213
+ raise Habitat::PromotionError,
214
+ "Depot returned #{response.status} on promote"
215
+ end
216
+ end
217
+
218
+ # Downloads the file from the depot.
219
+ #
220
+ def download(url, path = '.')
221
+ response = @connection.get(url)
222
+ if response.status == 200
223
+ ::File.open(
224
+ ::File.join(
225
+ path,
226
+ response.headers['x-filename']
227
+ ), 'wb') do |fp|
228
+ fp.write(response.body)
229
+ end
230
+ else
231
+ raise Habitat::DownloadError,
232
+ "Depot returned #{response.status} on download"
233
+ end
234
+ end
235
+
236
+ # Uploads a file to the depot.
237
+ #
238
+ # === Attributes
239
+ # * +url+ - the URL path on the depot server
240
+ # * +path+ - the file path to upload
241
+ #
242
+ def upload(url, path)
243
+ payload = { file: Faraday::UploadIO.new(path, 'application/octet-stream') }
244
+ response = @connection.post do |req|
245
+ req.url url
246
+ req.params['checksum'] = blake2b_checksum(IO.read(path))
247
+ req.body = payload
248
+ end
249
+
250
+ if response.status == 200
251
+ return true
252
+ else
253
+ raise Habitat::UploadError,
254
+ "Depot returned #{response.status} on uploading '#{path}'"
255
+ end
256
+ end
257
+ end
258
+
259
+ # Habitat Package Ident
260
+ #
261
+ # This class builds a Habitat Package Identifier object using the
262
+ # four components of a Habitat package: origin, package name (pkg),
263
+ # version, and release. It subclasses +Struct+ with arguments that
264
+ # have default values - 'latest' for +version+ and +release+. It
265
+ # also implements a method to return the package identifier as a +/+
266
+ # separated string for use in the Habitat Depot API
267
+ # rubocop:disable StructInheritance
268
+ class PackageIdent < Struct.new(:origin, :pkg, :version, :release)
269
+ # Creates the package identifier using the origin and package
270
+ # name. Optionally will also use the version and release, or sets
271
+ # them to latest if not specified.
272
+ #
273
+ # === Attributes
274
+ #
275
+ # * +origin+ - The package origin
276
+ # * +pkg+ - The package name
277
+ # * +version+ - The version of the package
278
+ # * +release+ - The timestamp release of the package
279
+ #
280
+ # === Examples
281
+ #
282
+ # Use the latest core/zlib package:
283
+ #
284
+ # Habitat::PackageIdent.new('core', 'zlib')
285
+ #
286
+ # Use version 1.2.8 of the core/zlib package:
287
+ #
288
+ # Habitat::PackageIdent.new('core', 'zlib', '1.2.8')
289
+ #
290
+ # Use a specific release of version 1.2.8 of the core/zlib
291
+ # package:
292
+ #
293
+ # Habitat::PackageIdent.new('core', 'zlib', '1.2.8', '20160222155343')
294
+ #
295
+ # Pass an array as an argument:
296
+ #
297
+ # Habitat::PackageIdent.new(*['core', 'zlib'])
298
+ #
299
+ # For example, from a +#split+ string:
300
+ #
301
+ # Habitat::PackageIdent.new(*'core/zlib'.split('/'))
302
+
303
+ def initialize(origin, pkg, version = 'latest', release = 'latest')
304
+ super
305
+ end
306
+
307
+ # Returns a string from a +Habitat::PackageIdent+ object separated by
308
+ # +/+ (forward slash).
309
+ #
310
+ # === Examples
311
+ #
312
+ # zlib = Habitat::PackageIdent.new('core', 'zlib')
313
+ # zlib.to_s #=> "core/zlib/latest"
314
+
315
+ def to_s
316
+ parts = if self[:version] == 'latest'
317
+ [self[:origin], self[:pkg], self[:version]]
318
+ else
319
+ [self[:origin], self[:pkg], self[:version], self[:release]]
320
+ end
321
+ parts.join('/')
322
+ end
323
+ end
324
+ end
325
+
326
+ # So users don't have to remember +Habitat+ vs +Hab+
327
+ module Hab
328
+ include Habitat
329
+ end
@@ -0,0 +1,22 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2016 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ module Habitat
19
+ class Client
20
+ VERSION = '0.6.0'.freeze
21
+ end
22
+ end
@@ -0,0 +1,38 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2016 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+ module Habitat
18
+ # Error class for promotion errors
19
+ class PromotionError < StandardError
20
+ def initialize(msg = 'Promotion of package on Depot server failed')
21
+ super
22
+ end
23
+ end
24
+
25
+ # Error class for upload errors
26
+ class UploadError < StandardError
27
+ def initialize(msg = 'Upload of artifact to Depot server failed')
28
+ super
29
+ end
30
+ end
31
+
32
+ # Error class for download errors
33
+ class DownloadError < StandardError
34
+ def initialize(msg = 'Download of artifact from Depot server failed')
35
+ super
36
+ end
37
+ end
38
+ end
metadata ADDED
@@ -0,0 +1,165 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: habitat-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.6.0
5
+ platform: ruby
6
+ authors:
7
+ - Joshua Timberman
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-06-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rbnacl
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.9.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.9.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: mixlib-shellout
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry-coolline
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.11'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.11'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '10.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '10.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '3.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '3.0'
125
+ description:
126
+ email:
127
+ - humans@habitat.sh
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - ".gitignore"
133
+ - ".rspec"
134
+ - ".rubocop.yml"
135
+ - Gemfile
136
+ - README.md
137
+ - Rakefile
138
+ - habitat-client.gemspec
139
+ - lib/habitat/client.rb
140
+ - lib/habitat/client/version.rb
141
+ - lib/habitat/exceptions.rb
142
+ homepage: https://www.habitat.sh
143
+ licenses: []
144
+ metadata: {}
145
+ post_install_message:
146
+ rdoc_options: []
147
+ require_paths:
148
+ - lib
149
+ required_ruby_version: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ required_rubygems_version: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ requirements: []
160
+ rubyforge_project:
161
+ rubygems_version: 2.6.4
162
+ signing_key:
163
+ specification_version: 4
164
+ summary: Habitat Depot Client Library
165
+ test_files: []