jets-api 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ba93fd08108eb85d4cae6dfa1a37f2702213e111d35d09762c8dc30148f0b9c2
4
+ data.tar.gz: 528480b2fa8c3b4635295ac717778bf4d4fb628b97e8fef805df3b946d1029ae
5
+ SHA512:
6
+ metadata.gz: 51b3904fcfbce1d669a45a7692dbbc4c48652ea296890b8ff74a8328a0cdadd2c489fca79fff760d12a99c94c3787c06bc331bd45ad0dcee1289504326a41656
7
+ data.tar.gz: 335178b7d6b2af40a25db803dd9b5aa0974c537f4354776f1c753bcb61d5143e18d9dbf4eeaedc4ec67f0dd7ae447ba585ad605389a7ee4620f45bd90c4ab2f1
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) Tung Nguyen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # Jets API
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/jets-api.png)](http://badge.fury.io/rb/jets-api)
4
+
5
+ [![BoltOps Badge](https://img.boltops.com/boltops/badges/boltops-badge.png)](https://www.boltops.com)
6
+
7
+ [![BoltOps Learn Badge](https://img.boltops.com/boltops-learn/boltops-learn.png)](https://learn.boltops.com)
8
+
9
+ Jets API Client Library for Jets Ruby Serverless Framework.
10
+
11
+ This library works with Jets Pro API to help provide additional features to the [Jets Serverless Framework](https://rubyonjets.com).
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
@@ -0,0 +1,76 @@
1
+ module Jets::Api
2
+ class Agree
3
+ def initialize
4
+ @agree_file = "#{ENV['HOME']}/.jets/agree"
5
+ end
6
+
7
+ # Only prompts if hasnt prompted before and saved a ~/.jets/agree file
8
+ def prompt
9
+ return if bypass_prompt
10
+ return if File.exist?(@agree_file) && File.mtime(@agree_file) > Time.parse("2021-04-12")
11
+
12
+ puts <<~EOL
13
+ Jets uses precompiled binary gems from the Jets Api service to
14
+ provide a user-friendly developer experience. Jets Api
15
+ rate limits free gem download requests daily. You can upgrade to a paid plan
16
+ for unlimited gem download requests. Open Source projects may also qualify
17
+ for a free unlimited plan. More info:
18
+
19
+ https://pro.rubyonjets.com/rate-limits
20
+
21
+ Reporting gems to Jets Api also allows it to build new gems typically
22
+ within a few minutes. So if you run into missing gems, you can try deploying
23
+ again after a few minutes. Non-reported gems may take several days or longer.
24
+ Jets Api only collects the info it needs to run the service.
25
+ More info: https://pro.rubyonjets.com/privacy
26
+ This message will only appear once on this machine.
27
+
28
+ You can also automatically skip this message by setting:
29
+ JETS_AGREE=yes or JETS_AGREE=no
30
+
31
+ Is it okay to send your gem data to Jets Api? (Y/n)?
32
+ EOL
33
+
34
+ answer = $stdin.gets.strip
35
+ value = answer =~ /y/i ? 'yes' : 'no'
36
+
37
+ write_file(value)
38
+ end
39
+
40
+ # Allow user to bypass prompt with JETS_AGREE=1 JETS_AGREE=yes etc
41
+ # Useful for CI/CD pipelines.
42
+ def bypass_prompt
43
+ agree = ENV['JETS_AGREE']
44
+ return false unless agree
45
+
46
+ if %w[1 yes true].include?(agree.downcase)
47
+ write_file('yes')
48
+ else
49
+ write_file('no')
50
+ end
51
+
52
+ true
53
+ end
54
+
55
+ def yes?
56
+ File.exist?(@agree_file) && IO.read(@agree_file).strip == 'yes'
57
+ end
58
+
59
+ def no?
60
+ File.exist?(@agree_file) && IO.read(@agree_file).strip == 'no'
61
+ end
62
+
63
+ def yes!
64
+ write_file("yes")
65
+ end
66
+
67
+ def no!
68
+ write_file("no")
69
+ end
70
+
71
+ def write_file(content)
72
+ FileUtils.mkdir_p(File.dirname(@agree_file))
73
+ IO.write(@agree_file, content)
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,26 @@
1
+ require "zeitwerk"
2
+
3
+ # Make sure to define module Jets in case Jets::Api is loaded before jets
4
+ module Jets
5
+ module Api
6
+ class Autoloader
7
+ class Inflector < Zeitwerk::Inflector
8
+ def camelize(basename, _abspath)
9
+ map = { cli: "CLI", version: "VERSION" }
10
+ map[basename.to_sym] || super
11
+ end
12
+ end
13
+
14
+ class << self
15
+ def setup
16
+ loader = Zeitwerk::Loader.new
17
+ loader.inflector = Inflector.new
18
+ lib = File.expand_path('../..', __dir__)
19
+ loader.push_dir(lib) # lib
20
+ loader.ignore("#{lib}/jets-api.rb")
21
+ loader.setup
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ module Jets::Api
2
+ class Base
3
+ include Jets::Api
4
+ delegate :global_params, to: :class
5
+ class << self
6
+ include Jets::Api
7
+
8
+ def global_params
9
+ Jets.boot
10
+ params = {}
11
+ params[:jets_env] = Jets.env.to_s
12
+ params[:jets_extra] = Jets.extra.to_s if Jets.extra
13
+ params[:name] = Jets.project_namespace
14
+ params[:region] = Jets.aws.region
15
+ params[:account] = Jets.aws.account
16
+ params[:project_id] = Jets.project_name
17
+ params[:jets_api_version] = Jets::Api::VERSION
18
+ params[:jets_version] = Jets::VERSION
19
+ params[:ruby_version] = RUBY_VERSION
20
+ params[:ruby_folder] = Jets::Api::Gems.ruby_folder
21
+ params
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,8 @@
1
+ require 'open-uri'
2
+
3
+ module Jets::Api
4
+ class Client
5
+ include Core
6
+ delegate :endpoint, to: Jets::Api
7
+ end
8
+ end
@@ -0,0 +1,62 @@
1
+ require "singleton"
2
+
3
+ module Jets::Api
4
+ class Config
5
+ include Singleton
6
+ extend Memoist
7
+
8
+ def initialize(options={})
9
+ @options = options
10
+ @config_path = "#{ENV['HOME']}/.jets/config.yml"
11
+ end
12
+
13
+ def token
14
+ data['token'] || data['key'] # keep key for backwards compatibility
15
+ end
16
+
17
+ def data
18
+ @data ||= load
19
+ end
20
+
21
+ # Ensure a Hash is returned
22
+ def load
23
+ return {} unless File.exist?(@config_path)
24
+
25
+ data = YAML.load_file(@config_path)
26
+ if data.is_a?(Hash)
27
+ data
28
+ else
29
+ puts "WARN: #{@config_path} is not in the correct format. Loading an empty hash.".color(:yellow)
30
+ {}
31
+ end
32
+ end
33
+
34
+ def prompt
35
+ puts <<~EOL
36
+ You are about to configure your #{pretty_path(@config_path)}
37
+ You can get a token from pro.rubyonjets.com
38
+ EOL
39
+ print "Please provide your token: "
40
+ $stdin.gets.strip
41
+ end
42
+
43
+ # interface method: do not remove
44
+ def update_token(token=nil)
45
+ token ||= prompt
46
+ write(token: token) # specify keys to allow
47
+ end
48
+
49
+ def write(values={})
50
+ data = load
51
+ data.merge!(values.deep_stringify_keys)
52
+ data.delete('key') if data.key?('key') # remove legacy key that used to store token
53
+ FileUtils.mkdir_p(File.dirname(@config_path))
54
+ IO.write(@config_path, YAML.dump(data))
55
+ puts "Updated #{pretty_path(@config_path)}"
56
+ end
57
+
58
+ def pretty_path(path)
59
+ path.sub(ENV['HOME'], '~')
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,122 @@
1
+ require "aws-sdk-core"
2
+
3
+ module Jets::Api
4
+ module Core
5
+ extend Memoist
6
+
7
+ @@max_retries = 3
8
+
9
+ # Always translate raw json response to ruby Hash
10
+ def request(klass, path, data={})
11
+ raw_response = data.delete(:raw_response)
12
+ url = url(path)
13
+ req = build_request(klass, url, data)
14
+ @retries = 0
15
+ begin
16
+ resp = http.request(req) # send request
17
+ raw_response ? resp : load_json(url, resp)
18
+ rescue SocketError, OpenURI::HTTPError, OpenSSL::SSL::SSLError, Errno::ECONNREFUSED => e
19
+ @retries += 1
20
+ if @retries <= @@max_retries
21
+ delay = 2**@retries
22
+ puts "Error: #{e.class} #{e.message} retrying after #{delay} seconds..." if ENV['JETS_API_DEBUG']
23
+ sleep delay
24
+ retry
25
+ else
26
+ puts "Error: #{e.class} #{e.message} giving up after #{@retries} retries"
27
+ raise Jets::Api::RequestError.new(e)
28
+ end
29
+ end
30
+ end
31
+
32
+ def build_request(klass, url, data={})
33
+ req = klass.new(url) # url includes query string and uri.path does not, must used url
34
+ set_headers!(req)
35
+ if [Net::HTTP::Delete, Net::HTTP::Patch, Net::HTTP::Post, Net::HTTP::Put].include?(klass)
36
+ text = JSON.dump(data)
37
+ req.body = text
38
+ req.content_length = text.bytesize
39
+ end
40
+ req
41
+ end
42
+
43
+ def set_headers!(req)
44
+ req['Authorization'] = token if token
45
+ req['x-account'] = account if account
46
+ req['Content-Type'] = "application/vnd.api+json"
47
+ end
48
+
49
+ def token
50
+ Jets::Api.token
51
+ end
52
+
53
+ def load_json(url, res)
54
+ uri = URI(url)
55
+ if ENV['JETS_API_DEBUG']
56
+ puts "res.code #{res.code}"
57
+ puts "res.body #{res.body}"
58
+ end
59
+ if processable?(res.code)
60
+ JSON.load(res.body)
61
+ else
62
+ puts "Error: Non-successful http response status code: #{res.code}"
63
+ puts "headers: #{res.each_header.to_h.inspect}"
64
+ puts "Jets API #{url}" if ENV['JETS_API_DEBUG']
65
+ raise "Jets API called failed: #{uri.host}"
66
+ end
67
+ end
68
+
69
+ # Note: 422 is Unprocessable Entity. This means an invalid data payload was sent.
70
+ # We want that to error and raise
71
+ def processable?(http_code)
72
+ http_code =~ /^20/ || http_code =~ /^40/
73
+ end
74
+
75
+ def http
76
+ uri = URI(endpoint)
77
+ http = Net::HTTP.new(uri.host, uri.port)
78
+ http.open_timeout = http.read_timeout = 30
79
+ http.use_ssl = true if uri.scheme == 'https'
80
+ http
81
+ end
82
+ memoize :http
83
+
84
+ # API does not include the /. IE: https://app.terraform.io/api/v2
85
+ def url(path)
86
+ "#{endpoint}/#{path}"
87
+ end
88
+
89
+ def get(path, query={})
90
+ path = path_with_query(path, query)
91
+ request(Net::HTTP::Get, path, raw_response: query[:raw_response])
92
+ end
93
+
94
+ def path_with_query(path, query={})
95
+ return path if query.empty?
96
+ separator = path.include?("?") ? "&" : "?"
97
+ "#{path}#{separator}#{query.to_query}"
98
+ end
99
+
100
+ def post(path, data={})
101
+ request(Net::HTTP::Post, path, data)
102
+ end
103
+
104
+ def patch(path, data={})
105
+ request(Net::HTTP::Patch, path, data)
106
+ end
107
+
108
+ def delete(path, data={})
109
+ request(Net::HTTP::Delete, path, data)
110
+ end
111
+
112
+ def account
113
+ sts.get_caller_identity.account rescue nil
114
+ end
115
+ memoize :account
116
+
117
+ def sts
118
+ Aws::STS::Client.new
119
+ end
120
+ memoize :sts
121
+ end
122
+ end
@@ -0,0 +1,230 @@
1
+ # Assumes gems were just built and checks the filesystem to find and detect for
2
+ # compiled gems. Unless the cli option is set to true, the it'll just check
3
+ # based on the gemspecs.
4
+ class Jets::Api::Gems
5
+ class Check
6
+ extend Memoist
7
+
8
+ attr_reader :missing_gems
9
+ def initialize(options={})
10
+ @options = options
11
+ @missing_gems = [] # keeps track of gems that are not found in any of the Jets Api source
12
+ end
13
+
14
+ delegate :endpoint, to: Jets::Api
15
+
16
+ def run!
17
+ puts "Jets API endpoint: #{endpoint}" if @options[:verbose]
18
+ run(exit_early: true)
19
+ end
20
+
21
+ # Checks whether the gem is found at the Jets Api source.
22
+ def run(exit_early: false)
23
+ puts "Checking project for compiled gems..."
24
+ compiled_gems.each do |gem_name|
25
+ puts "Checking #{gem_name}..." if @options[:verbose]
26
+ exist = Jets::Api::Gems::Exist.new
27
+ data = exist.check(gem_name)
28
+ @missing_gems << data unless data["exist"]
29
+ end
30
+
31
+ if exit_early && !@missing_gems.empty?
32
+ # Exits early if not all the linux gems are available.
33
+ # Better to error now than deploy a broken package to AWS Lambda.
34
+ # Apivide users with message about missing gems.
35
+ puts missing_message
36
+ names = @missing_gems.map {|i| i['gem_name']}
37
+ Report.new(@options).report(names) if agree.yes?
38
+ exit 1
39
+ end
40
+
41
+ compiled_gems
42
+ end
43
+
44
+ def missing?
45
+ !@missing_gems.empty?
46
+ end
47
+
48
+ def missing_message
49
+ template = <<-EOL
50
+ Your project requires compiled gems that are not currently available. Unavailable precompiled gems:
51
+ <% missing_gems.each do |missing_gem|
52
+ available = missing_gem['available'].reject { |v| missing_gem['gem_name'].include?(v) }
53
+ %>
54
+ * Unavailable: <%= missing_gem['gem_name'] -%> Available versions: <%= available.join(' ') %>
55
+ <% end %>
56
+ Your current Jets API endpoint: #{endpoint}
57
+
58
+ Jets is unable to build a deployment package that will work on AWS Lambda without the required precompiled gems.
59
+ To remedy this, you can:
60
+
61
+ * Use another gem that does not require compilation.
62
+ * Create your own custom layer with the gem: http://rubyonjets.com/docs/extras/custom-lambda-layers/
63
+ <% if agree.yes? -%>
64
+ * No need to report this to us, as we've already been notified.
65
+ * Usually, missing gems can be built within a few minutes.
66
+ * Some gems may take days or even longer.
67
+ <% elsif agree.no? -%>
68
+ * You have choosen not to report data to Jets Api so we will not be notified about these missing gems.
69
+ * You can edit ~/.jets/agree to change this.
70
+ * Reporting gems generally allows Jets Api to build the missing gems within a few minutes.
71
+ * You can try redeploying again after a few minutes.
72
+ * Non-reported gems may take days or even longer to be built.
73
+ <% end -%>
74
+
75
+ Compiled gems usually take some time to figure out how to build as they each depend on different libraries and packages.
76
+ More info: http://rubyonjets.com/docs/jets-pro/
77
+
78
+ EOL
79
+ erb = ERB.new(template, trim_mode: '-') # trim mode https://stackoverflow.com/questions/4632879/erb-template-removing-the-trailing-line
80
+ erb.result(binding)
81
+ end
82
+
83
+ def agree
84
+ Jets::Api::Agree.new
85
+ end
86
+ memoize :agree
87
+
88
+ # Context, observations, and history:
89
+ #
90
+ # Two ways to check if gem is compiled.
91
+ #
92
+ # 1. compiled_gem_paths - look for .so and .bundle extension files in the folder itself.
93
+ # 2. gemspec - uses the gemspec metadata.
94
+ #
95
+ # Observations:
96
+ #
97
+ # * The gemspec approach generally finds more compiled gems than the compiled_gem_paths approach.
98
+ # * So when using the compiled_gem_paths some compiled are missed and not properly detected like http-parser.
99
+ # * However, some gemspec found compiled gems like json are weird and they don't work when they get replaced.
100
+ #
101
+ # History:
102
+ #
103
+ # * Started with compiled_gem_paths approach
104
+ # * Tried to gemspec approach, but ran into json-2.1.0 gem issues. bundler removes? http://bit.ly/39T8uln
105
+ # * Went to selective checking approach with `cli: true` option. This helped gather more data.
106
+ # * jets deploy - compiled_gem_paths
107
+ # * jets gems:check - gemspec_compiled_gems
108
+ # * Going back to compiled_gem_paths with:
109
+ # * Using the `weird_gem?` check to filter out gems removed by bundler. Note: Only happens with specific versions of json.
110
+ # * Keeping compiled_gem_paths for Jets Afterburner mode. Default to gemspec_compiled_gems otherwise
111
+ #
112
+ #
113
+ def compiled_gems
114
+ # @use_gemspec option finds compile gems with Gem::Specification
115
+ # The normal build process does not use this and checks the file system.
116
+ # So @use_gemspec is only used for this command:
117
+ #
118
+ # jets gems:check
119
+ #
120
+ # This is because it seems like some gems like json are remove and screws things up.
121
+ # We'll filter out for the json gem as a hacky workaround, unsure if there are more
122
+ # gems though that exhibit this behavior.
123
+ if @options[:use_gemspec] == false
124
+ # Afterburner mode
125
+ compiled_gems = compiled_gem_paths.map { |p| gem_name_from_path(p) }.uniq
126
+ # Double check that the gems are also in the gemspec list since that
127
+ # one is scoped to Bundler and will only included gems used in the project.
128
+ # This handles the possiblity of stale gems leftover from previous builds
129
+ # in the cache.
130
+ # TODO: figure out if we need
131
+ # compiled_gems.select { |g| gemspec_compiled_gems.include?(g) }
132
+ else
133
+ # default when use_gemspec not set
134
+ #
135
+ # jets deploy
136
+ # jets gems:check
137
+ #
138
+ gems = gemspec_compiled_gems
139
+ gems += other_compiled_gems
140
+ gems += registered_compiled_gems
141
+ gems.uniq
142
+ end
143
+ end
144
+
145
+ # note 2.7.0/gems vs 2.7.0/extensions
146
+ #
147
+ # /tmp/jets/demo/stage/opt/ruby/gems/2.7.0/gems/nokogiri-1.11.1-x86_64-darwin/
148
+ # /tmp/jets/demo/stage/opt/ruby/gems/2.7.0/extensions/x86_64-linux/2.7.0/
149
+ #
150
+ # Also the platform is appended to the gem folder now
151
+ #
152
+ # /tmp/jets/demo/stage/opt/ruby/gems/2.7.0/gems/nokogiri-1.11.1-x86_64-darwin
153
+ # /tmp/jets/demo/stage/opt/ruby/gems/2.7.0/gems/nokogiri-1.11.1-x86_64-linux
154
+ #
155
+ # On new 2021 macbook with m1 chip: the gems are being saved in a folder like so:
156
+ # nokogiri-1.12.5-arm64-darwin
157
+ # The GEM_REGEXP accounts for this case.
158
+ GEM_REGEXP = /-(arm|x)\d+.*-(darwin|linux)/
159
+ def other_compiled_gems
160
+ paths = Dir.glob("#{Jets.build_root}/stage/opt/ruby/gems/#{Jets::Api::Gems.ruby_folder}/gems/*{-darwin,-linux}")
161
+ paths.map { |p| File.basename(p).sub(GEM_REGEXP,'') }
162
+ end
163
+
164
+ def registered_compiled_gems
165
+ registered = Jets::Api::Gems::Registered.new
166
+ registered_gems = registered.all # no version numbers in this list
167
+
168
+ paths = Dir.glob("#{Jets.build_root}/stage/opt/ruby/gems/#{Jets::Api::Gems.ruby_folder}/gems/*")
169
+ project_gems = paths.map { |p| File.basename(p).sub(GEM_REGEXP,'') }
170
+ project_gems.select do |name|
171
+ name_only = name.sub(/-\d+\.\d+\.\d+.*/,'')
172
+ registered_gems.include?(name_only)
173
+ end
174
+ end
175
+
176
+ # Use precompiled gem because the gem could have development header shared
177
+ # object file dependencies. The shared dependencies are packaged up as part
178
+ # of the precompiled gem so it is available in the Lambda execution environment.
179
+ #
180
+ # Example paths:
181
+ # Macosx:
182
+ # opt/ruby/gems/2.5.0/extensions/x86_64-darwin-16/2.5.0-static/nokogiri-1.8.1
183
+ # opt/ruby/gems/2.5.0/extensions/x86_64-darwin-16/2.5.0-static/byebug-9.1.0
184
+ # Official AWS Lambda Linux AMI:
185
+ # opt/ruby/gems/2.5.0/extensions/x86_64-linux/2.5.0-static/nokogiri-1.8.1
186
+ # Circleci Ubuntu based Linux:
187
+ # opt/ruby/gems/2.5.0/extensions/x86_64-linux/2.5.0/pg-0.21.0
188
+ def compiled_gem_paths
189
+ Dir.glob("#{Jets.build_root}/stage/opt/ruby/gems/*/extensions/**/**/*.{so,bundle}")
190
+ end
191
+
192
+ # Input: opt/ruby/gems/2.5.0/extensions/x86_64-darwin-16/2.5.0-static/byebug-9.1.0
193
+ # Output: byebug-9.1.0
194
+ def gem_name_from_path(path)
195
+ regexp = %r{opt/ruby/gems/\d+\.\d+\.\d+/extensions/.*?/.*?/(.*?)/}
196
+ path.match(regexp)[1] # gem_name
197
+ end
198
+
199
+ # So can also check for compiled gems with Gem::Specification
200
+ # But then also includes the json gem, which then bundler removes?
201
+ # We'll figure out the the json gems.
202
+ # https://gist.github.com/tongueroo/16f4aa5ac5393424103347b0e529495e
203
+ #
204
+ # This is a faster way to check but am unsure if there are more gems than just
205
+ # json that exhibit this behavior. So only using this technique for this commmand:
206
+ #
207
+ # jets gems:check
208
+ #
209
+ # Thanks: https://gist.github.com/aelesbao/1414b169a79162b1d795 and
210
+ # https://stackoverflow.com/questions/5165950/how-do-i-get-a-list-of-gems-that-are-installed-that-have-native-extensions
211
+ def gemspec_compiled_gems
212
+ specs = Gem::Specification.each.select { |spec| spec.extensions.any? }
213
+ specs.reject! { |spec| weird_gem?(spec.name) }
214
+ specs.map(&:full_name)
215
+ end
216
+
217
+ # Filter out the weird special case gems that bundler deletes?
218
+ # Apibably to fix some bug.
219
+ #
220
+ # $ bundle show json
221
+ # The gem json has been deleted. It was installed at:
222
+ # /home/ec2-user/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/json-2.1.0
223
+ #
224
+ def weird_gem?(name)
225
+ command = "bundle show #{name} 2>&1"
226
+ output = `#{command}`
227
+ output.include?("has been deleted")
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,13 @@
1
+ class Jets::Api::Gems
2
+ class Exist
3
+ include Jets::Api
4
+
5
+ # gem_name IE: nokogiri-1.1.1
6
+ def check(gem_name)
7
+ Jets::Api::Gems.exist(gem_name: gem_name) # data = {"exist": ..., "available"}
8
+ rescue Jets::Api::RequestError => e
9
+ puts "WARNING: #{e.class}: #{e.message}"
10
+ {"exist" => false, gem_name: gem_name, available: [] }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,64 @@
1
+ require "open-uri"
2
+
3
+ module Jets::Api::Gems::Extract
4
+ class Base
5
+ class NotFound < RuntimeError; end
6
+
7
+ attr_reader :project_root
8
+ def initialize(name, options={})
9
+ @name = name
10
+ @options = options
11
+ ruby_folder = Jets::Api::Gems.ruby_folder
12
+ @downloads_root = options[:downloads_root] || "/tmp/jets/#{Jets.config.project_name}/jets-api/#{ruby_folder}"
13
+ end
14
+
15
+ def clean_downloads(folder)
16
+ path = "#{@downloads_root}/downloads/#{folder}"
17
+ say "Removing cache: #{path}"
18
+ FileUtils.rm_rf(path)
19
+ end
20
+
21
+ # Using ` > /dev/null 2>&1` to suppress stderr message:
22
+ #
23
+ # lchmod (file attributes) error: Function not implemented
24
+ #
25
+ def unzip(zipfile_path, parent_folder_dest)
26
+ sh("cd #{parent_folder_dest} && unzip -qo #{zipfile_path} > /dev/null 2>&1")
27
+ end
28
+
29
+ def sh(command)
30
+ say "=> #{command}".color(:green)
31
+ success = system(command)
32
+ abort("Command Failed #{command}") unless success
33
+ success
34
+ end
35
+
36
+ # Returns the dest path
37
+ def download_file(url, dest)
38
+ if File.exist?(dest) && ENV['JETS_API_FORCE_DOWNLOAD'].blank?
39
+ say "File already downloaded #{dest}"
40
+ return dest
41
+ end
42
+
43
+ say "Downloading..."
44
+ downloaded = URI.open(url, 'rb') { |read_file| read_file.read }
45
+
46
+ FileUtils.mkdir_p(File.dirname(dest)) # ensure parent folder exists
47
+
48
+ File.open(dest, 'wb') { |saved_file| saved_file.write(downloaded) }
49
+
50
+ dest
51
+ end
52
+
53
+ @@log_level = :info # default level is :info
54
+ # @@log_level = :debug # uncomment to debug
55
+ def log_level=(val)
56
+ @@log_level = val
57
+ end
58
+
59
+ def say(message, level=:info)
60
+ enabled = @@log_level == :debug || level == :debug
61
+ puts(message) if enabled
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,117 @@
1
+ require "gems"
2
+
3
+ module Jets::Api::Gems::Extract
4
+ class Gem < Base
5
+ VERSION_PATTERN = /-(\d+\.\d+.*)/
6
+ include Jets::Api
7
+
8
+ def run
9
+ say "Will download and extract gem: #{full_gem_name}"
10
+ clean_downloads(:gems) if @options[:clean]
11
+ zipfile_path = download_gem
12
+ remove_current_gem if Jets.config.pro.clean || Jets.config.gems.clean
13
+ unzip_file(zipfile_path)
14
+ say("Gem #{full_gem_name} unpacked at #{project_root}")
15
+ end
16
+
17
+ def unzip_file(zipfile_path)
18
+ dest = "#{Jets.build_root}/stage/opt"
19
+ say "Unpacking into #{dest}"
20
+ FileUtils.mkdir_p(dest)
21
+ unzip(zipfile_path, dest)
22
+ end
23
+
24
+ # ensure that we always have the full gem name
25
+ def full_gem_name
26
+ return @full_gem_name if @full_gem_name
27
+
28
+ if @name.match(VERSION_PATTERN)
29
+ @full_gem_name = @name
30
+ return @full_gem_name
31
+ end
32
+
33
+ # name doesnt have a version yet, so grab the latest version and add it
34
+ version = Api.versions(@name).first
35
+ @full_gem_name = "#{@name}-#{version["number"]}"
36
+ end
37
+
38
+ def gem_name
39
+ full_gem_name.gsub(VERSION_PATTERN,'') # folder: byebug
40
+ end
41
+
42
+ # Downloads and extracts the linux gem into the proper directory.
43
+ # Extracts to: . (current directory)
44
+ #
45
+ # It produces a `bundled` folder.
46
+ # The folder contains the re-produced directory structure. Example with
47
+ # the gem: byebug-9.1.0
48
+ #
49
+ # vendor/gems/ruby/2.5.0/extensions/x86_64-darwin-16/2.5.0-static/byebug-9.1.0
50
+ #
51
+ def download_gem
52
+ # download - also move to /tmp/jets/demo/compiled_gems folder
53
+ begin
54
+ @retries ||= 0
55
+ url = gem_url
56
+ basename = File.basename(url).gsub(/\?.*/,'') # remove query string info
57
+ tarball_dest = download_file(url, download_path(basename))
58
+ rescue OpenURI::HTTPError => e
59
+ url_without_query = url.gsub(/\?.*/,'')
60
+ puts "Error downloading #{url_without_query}"
61
+ @retries += 1
62
+ if @retries < 3
63
+ sleep 1
64
+ puts "Retrying download. Retry attempt: #{@retries}"
65
+ retry
66
+ else
67
+ raise e
68
+ end
69
+ end
70
+
71
+ unless tarball_dest
72
+ message = "Url: #{url} not found"
73
+ if @options[:exit_on_error]
74
+ say message
75
+ exit
76
+ else
77
+ raise NotFound.new(message)
78
+ end
79
+ end
80
+ say "Downloaded to: #{tarball_dest}"
81
+ tarball_dest
82
+ end
83
+
84
+ # full_gem_name: byebug-9.1.0
85
+ def gem_url
86
+ resp = Jets::Api::Gems.download(gem_name: full_gem_name)
87
+ if resp["download_url"]
88
+ resp["download_url"]
89
+ else
90
+ puts resp["message"].color(:red)
91
+ exit 1
92
+ end
93
+ end
94
+
95
+ def download_path(filename)
96
+ "#{@downloads_root}/downloads/gems/#{filename}"
97
+ end
98
+
99
+ # Finds any currently install gems that matched with the gem name and version
100
+ # and remove them first.
101
+ # We clean up the current install gems first in case it was previously installed
102
+ # and has different *.so files that can be accidentally required. This
103
+ # happened with the pg gem.
104
+ def remove_current_gem
105
+ say "Removing current #{full_gem_name} gem installation:"
106
+ gem_dirs = Dir.glob("#{project_root}/**/*").select do |path|
107
+ File.directory?(path) &&
108
+ path =~ %r{vendor/gems} &&
109
+ File.basename(path) == full_gem_name
110
+ end
111
+ gem_dirs.each do |dir|
112
+ say " rm -rf #{dir}"
113
+ FileUtils.rm_rf(dir)
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,13 @@
1
+ class Jets::Api::Gems
2
+ class Registered
3
+ include Jets::Api
4
+
5
+ def all
6
+ resp = Jets::Api::Gems.registered
7
+ resp["gems"]
8
+ rescue Jets::Api::RequestError => e
9
+ puts "WARNING: #{e.class}: #{e.message}"
10
+ []
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ require "net/http"
2
+
3
+ class Jets::Api::Gems
4
+ class Report
5
+ include Jets::Api
6
+
7
+ def initialize(options={})
8
+ @options = options
9
+ end
10
+
11
+ def report(gems)
12
+ threads = []
13
+ gems.each do |gem_name|
14
+ threads << Thread.new do
15
+ Jets::Api::Gems.report(gem_name: gem_name)
16
+ end
17
+ end
18
+ # Wait for request to finish because the command might finish before
19
+ # the Threads even send the request. So we join them just case
20
+ threads.each(&:join)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,30 @@
1
+ module Jets::Api
2
+ class Gems < Base
3
+ class << self
4
+ def download(params={})
5
+ params = global_params.merge(params)
6
+ api.post("gems/download", params)
7
+ end
8
+
9
+ def exist(params={})
10
+ params = global_params.merge(params)
11
+ api.post("gems/exist", params)
12
+ end
13
+
14
+ def report(params={})
15
+ params = global_params.merge(params)
16
+ api.post("gems/report", params)
17
+ end
18
+
19
+ def registered(params={})
20
+ params = global_params.merge(params)
21
+ api.post("gems/registered", params)
22
+ end
23
+
24
+ def ruby_folder
25
+ major, minor, _ = RUBY_VERSION.split('.')
26
+ [major, minor, '0'].join('.') # 2.5.1 => 2.5.0
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,10 @@
1
+ module Jets::Api
2
+ class Project < Base
3
+ class << self
4
+ def list(params={})
5
+ params = global_params.merge(params)
6
+ api.get("projects", params)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ module Jets::Api
2
+ class Release < Base
3
+ class << self
4
+ def list(params={})
5
+ params = global_params.merge(params)
6
+ api.get("releases", params)
7
+ end
8
+
9
+ def retrieve(id, params={})
10
+ params = global_params.merge(params)
11
+ api.get("releases/#{id}", params)
12
+ end
13
+
14
+ def create(params={})
15
+ params = global_params.merge(params)
16
+ api.post("releases", params)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ module Jets::Api
2
+ class Stack < Base
3
+ class << self
4
+ def list(params={})
5
+ params = global_params.merge(params)
6
+ api.get("stacks", params)
7
+ end
8
+
9
+ def retrieve(id, params={})
10
+ params = global_params.merge(params)
11
+ api.get("stacks/#{id}", params)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ module Jets
2
+ module Api
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
data/lib/jets/api.rb ADDED
@@ -0,0 +1,38 @@
1
+ require "jets"
2
+
3
+ $:.unshift(File.expand_path("../../", __FILE__))
4
+ require "jets/api/autoloader"
5
+ Jets::Api::Autoloader.setup
6
+
7
+ require "memoist"
8
+ require "yaml"
9
+
10
+ require "cli-format"
11
+ CliFormat.default_format = "table"
12
+
13
+ module Jets
14
+ module Api
15
+ class RequestError < StandardError
16
+ def initialize(original_error)
17
+ message = "#{original_error.class} #{original_error.message}"
18
+ super(message)
19
+ end
20
+ end
21
+
22
+ extend Memoist
23
+ def api
24
+ Jets::Api::Client.new
25
+ end
26
+ memoize :api
27
+
28
+ def token
29
+ Jets::Api::Config.instance.token
30
+ end
31
+ module_function :token
32
+
33
+ def endpoint
34
+ ENV['JETS_API'] || Jets.config.pro.endpoint || 'https://api.rubyonjets.com/v1'
35
+ end
36
+ module_function :endpoint
37
+ end
38
+ end
data/lib/jets-api.rb ADDED
@@ -0,0 +1 @@
1
+ require_relative "jets/api"
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jets-api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tung Nguyen
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-12-02 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - tongueroo@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE.txt
21
+ - README.md
22
+ - Rakefile
23
+ - lib/jets-api.rb
24
+ - lib/jets/api.rb
25
+ - lib/jets/api/agree.rb
26
+ - lib/jets/api/autoloader.rb
27
+ - lib/jets/api/base.rb
28
+ - lib/jets/api/client.rb
29
+ - lib/jets/api/config.rb
30
+ - lib/jets/api/core.rb
31
+ - lib/jets/api/gems.rb
32
+ - lib/jets/api/gems/check.rb
33
+ - lib/jets/api/gems/exist.rb
34
+ - lib/jets/api/gems/extract/base.rb
35
+ - lib/jets/api/gems/extract/gem.rb
36
+ - lib/jets/api/gems/registered.rb
37
+ - lib/jets/api/gems/report.rb
38
+ - lib/jets/api/project.rb
39
+ - lib/jets/api/release.rb
40
+ - lib/jets/api/stack.rb
41
+ - lib/jets/api/version.rb
42
+ homepage: https://github.com/rubyonjets/jets-api
43
+ licenses:
44
+ - MIT
45
+ metadata:
46
+ homepage_uri: https://github.com/rubyonjets/jets-api
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 2.7.0
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubygems_version: 3.4.20
63
+ signing_key:
64
+ specification_version: 4
65
+ summary: Jets API Client Library for Jets Ruby Serverless Framework
66
+ test_files: []