jets-api 0.1.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.
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: []