diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..a9ec550 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,10 @@ +FROM ruby:2.6.6 +WORKDIR /app +COPY Gemfile Gemfile +COPY Gemfile.lock Gemfile.lock +COPY little_monster.gemspec little_monster.gemspec +RUN mkdir .git +RUN mkdir -p lib/little_monster +COPY lib/little_monster/version.rb lib/little_monster/version.rb +RUN gem install bundler:2.3.16 +RUN bundle install diff --git a/Gemfile.lock b/Gemfile.lock index fddb782..fa8c381 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,56 +1,64 @@ PATH remote: . specs: - little_monster (0.1.29) - activesupport - multi_json - thor - tilt - toiler - typhoeus + little_monster (0.1.30) + activesupport (= 6.1.7.6) + moneta (= 1.6.0) + multi_json (= 1.15.0) + thor (= 1.2.1) + tilt (= 2.1.0) + toiler (= 0.7.1) + typhoeus (= 1.4.0) GEM remote: https://rubygems.org/ specs: - activesupport (7.0.4.2) + activesupport (6.1.7.6) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - addressable (2.8.1) + zeitwerk (~> 2.3) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) ast (2.4.2) - aws-eventstream (1.2.0) - aws-partitions (1.716.0) - aws-sdk-core (3.170.0) - aws-eventstream (~> 1, >= 1.0.2) + aws-eventstream (1.3.0) + aws-partitions (1.889.0) + aws-sdk-core (3.191.1) + aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.5) + aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-sqs (1.53.0) - aws-sdk-core (~> 3, >= 3.165.0) + aws-sdk-sqs (1.70.0) + aws-sdk-core (~> 3, >= 3.191.0) aws-sigv4 (~> 1.1) - aws-sigv4 (1.5.2) + aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) + base64 (0.2.0) + bigdecimal (3.1.6) byebug (11.1.3) codeclimate-test-reporter (1.0.7) simplecov coderay (1.1.3) - concurrent-ruby (1.2.2) + concurrent-ruby (1.2.3) concurrent-ruby-edge (0.7.0) concurrent-ruby (~> 1.2.0) + crack (1.0.0) + bigdecimal + rexml diff-lcs (1.5.0) docile (1.4.0) ethon (0.16.0) ffi (>= 1.15.0) - faraday (2.7.4) + faraday (2.8.1) + base64 faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) faraday-net_http (3.0.2) - faraday-retry (2.0.0) + faraday-retry (2.2.0) faraday (~> 2.0) - ffi (1.15.5) - gapic-common (0.17.1) + ffi (1.16.3) + gapic-common (0.20.0) faraday (>= 1.9, < 3.a) faraday-retry (>= 1.0, < 3.a) google-protobuf (~> 3.14) @@ -58,54 +66,54 @@ GEM googleapis-common-protos-types (>= 1.3.1, < 2.a) googleauth (~> 1.0) grpc (~> 1.36) - google-cloud-core (1.6.0) - google-cloud-env (~> 1.0) + google-cloud-core (1.6.1) + google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.3.0) - google-cloud-pubsub (2.15.1) + google-cloud-errors (1.3.1) + google-cloud-pubsub (2.15.5) concurrent-ruby (~> 1.1) google-cloud-core (~> 1.5) google-cloud-pubsub-v1 (~> 0.8) retriable (~> 3.1) - google-cloud-pubsub-v1 (0.15.1) - gapic-common (>= 0.17.1, < 2.a) + google-cloud-pubsub-v1 (0.19.0) + gapic-common (>= 0.20.0, < 2.a) google-cloud-errors (~> 1.0) google-iam-v1 (>= 0.4, < 2.a) - google-iam-v1 (0.4.0) - gapic-common (>= 0.17.1, < 2.a) + google-iam-v1 (0.6.0) + gapic-common (>= 0.20.0, < 2.a) google-cloud-errors (~> 1.0) grpc-google-iam-v1 (~> 1.1) - google-protobuf (3.22.0) + google-protobuf (3.23.4) googleapis-common-protos (1.4.0) google-protobuf (~> 3.14) googleapis-common-protos-types (~> 1.2) grpc (~> 1.27) - googleapis-common-protos-types (1.5.0) - google-protobuf (~> 3.14) - googleauth (1.3.0) + googleapis-common-protos-types (1.11.0) + google-protobuf (~> 3.18) + googleauth (1.8.1) faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) - memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) - grpc (1.52.0) - google-protobuf (~> 3.21) + grpc (1.58.0) + google-protobuf (~> 3.23) googleapis-common-protos-types (~> 1.0) - grpc-google-iam-v1 (1.2.0) - google-protobuf (~> 3.14) - googleapis-common-protos (>= 1.3.12, < 2.0) - grpc (~> 1.27) - i18n (1.12.0) + grpc-google-iam-v1 (1.5.0) + google-protobuf (~> 3.18) + googleapis-common-protos (~> 1.4) + grpc (~> 1.41) + hashdiff (1.1.0) + i18n (1.14.1) concurrent-ruby (~> 1.0) jmespath (1.6.2) json (2.6.3) - jwt (2.7.0) - memoist (0.16.2) + jwt (2.7.1) method_source (1.0.0) - minitest (5.17.0) + minitest (5.22.2) + moneta (1.6.0) multi_json (1.15.0) newrelic_rpm (9.0.0) oj (3.14.2) @@ -116,7 +124,7 @@ GEM pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) - public_suffix (5.0.1) + public_suffix (5.0.4) rainbow (3.1.1) rake (13.0.6) regexp_parser (2.7.0) @@ -150,7 +158,7 @@ GEM parser (>= 3.2.1.0) ruby-progressbar (1.11.0) ruby2_keywords (0.0.5) - signet (0.17.0) + signet (0.18.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) @@ -173,23 +181,29 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.4.2) + webmock (3.20.0) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) + zeitwerk (2.6.13) PLATFORMS ruby DEPENDENCIES bundler - byebug + byebug (= 11.1.3) codeclimate-test-reporter little_monster! newrelic_rpm - oj - pry - rake - require_all - rspec - rubocop - simplecov + oj (= 3.14.2) + pry (= 0.14.2) + rake (= 13.0.6) + require_all (= 3.0.0) + rspec (= 3.12.0) + rubocop (= 1.46.0) + simplecov (= 0.22.0) + webmock (= 3.20.0) BUNDLED WITH 2.3.16 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d68a3b0 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +build: + docker build -f Dockerfile.dev -t fury-little_monster-gem-dev . + +rspec: + docker run -it -v .:/app fury-little_monster-gem-dev bundle exec rspec + +rubocop: + docker run -it -v .:/app fury-little_monster-gem-dev bundle exec rubocop lib spec --format simple diff --git a/lib/little_monster.rb b/lib/little_monster.rb index 50848a2..e8a0b43 100644 --- a/lib/little_monster.rb +++ b/lib/little_monster.rb @@ -51,7 +51,10 @@ def default_config_values job_requests_retries: 4, job_requests_retry_wait: 1, heartbeat_execution_interval: 10, - default_job_retries: -1 + default_job_retries: -1, + tiger_api_url: 'http://tiger', + shark_login_file_path: '/var/run/secrets/kubernetes.io/serviceaccount/token', + enable_tiger_token: true } end diff --git a/lib/little_monster/config.rb b/lib/little_monster/config.rb index 81778e1..e827fe5 100644 --- a/lib/little_monster/config.rb +++ b/lib/little_monster/config.rb @@ -2,7 +2,8 @@ module LittleMonster class Config attr_accessor :api_url, :worker_concurrency, :worker_queue, :worker_provider, :formatter, :request_timeout, :default_request_retries, :default_request_retry_wait, :task_requests_retries, :task_requests_retry_wait, - :job_requests_retries, :job_requests_retry_wait, :heartbeat_execution_interval, :default_job_retries + :job_requests_retries, :job_requests_retry_wait, :heartbeat_execution_interval, :default_job_retries, + :tiger_api_url, :shark_login_file_path, :enable_tiger_token def initialize(params = {}) params.to_hash.each do |key, value| diff --git a/lib/little_monster/core.rb b/lib/little_monster/core.rb index 502005e..3eb2fc8 100644 --- a/lib/little_monster/core.rb +++ b/lib/little_monster/core.rb @@ -12,6 +12,8 @@ require 'little_monster/core/errors/ownership_lost_error' require 'little_monster/core/errors/task_not_found_error' +require 'little_monster/tiger/auth' +require 'little_monster/tiger/cache' require 'little_monster/core/tagged_logger' require 'little_monster/core/loggable' # must be required first to satisfy job and task dependencies require 'little_monster/core/api' diff --git a/lib/little_monster/core/api.rb b/lib/little_monster/core/api.rb index 6817223..7b909aa 100644 --- a/lib/little_monster/core/api.rb +++ b/lib/little_monster/core/api.rb @@ -41,6 +41,7 @@ def request(method, path, params = {}, retries: LittleMonster.default_request_re params[:headers] ||= {} params[:headers]['Content-Type'] = 'application/json' unless params[:headers]['Content-Type'] params[:headers]['X-Request-ID'] = request_id + params[:headers]['X-Tiger-Token'] = LittleMonster::Tiger::API.bearer_token params[:timeout] ||= LittleMonster.request_timeout diff --git a/lib/little_monster/tiger/auth.rb b/lib/little_monster/tiger/auth.rb new file mode 100644 index 0000000..776b50f --- /dev/null +++ b/lib/little_monster/tiger/auth.rb @@ -0,0 +1,44 @@ +require 'jwt' +require 'typhoeus' + +module LittleMonster + module Tiger + module API + module_function + + def bearer_token + token = cached_shark_token + "Bearer #{token}" if token + end + + def cached_shark_token + shark_token = Cache.instance.get(:shark_token) + return shark_token if shark_token + + shark_token = new_shark_token + return nil if shark_token.nil? + Cache.instance.set(:shark_token, shark_token, 60 * 60) + shark_token + end + + def new_shark_token + return nil unless LittleMonster.enable_tiger_token + shark_token = File.read(LittleMonster.shark_login_file_path) + response = make_call(:post, 'login/shark', body: { token: shark_token }.to_json) + return nil if response.failure? + + MultiJson.load(response.body, symbolize_keys: true)[:token] + end + + def make_call(method, endpoint, options = {}) + Typhoeus::Request.new( + "#{LittleMonster.tiger_api_url}/#{endpoint}", + method: method, + params: options[:params], + headers: { 'Content-Type': 'application/json' }, + body: options[:body] + ).run + end + end + end +end diff --git a/lib/little_monster/tiger/cache.rb b/lib/little_monster/tiger/cache.rb new file mode 100644 index 0000000..d98fd9d --- /dev/null +++ b/lib/little_monster/tiger/cache.rb @@ -0,0 +1,27 @@ +require 'moneta' + +module LittleMonster + module Tiger + class Cache + include Singleton + + attr_reader :cache + + def initialize + @cache = Moneta.new(:Memory, expires: true) + end + + def set(key, value, expires = 0) + @cache.store(key, value, expires: expires) + end + + def get(key) + @cache[key] + end + + def clear + @cache.clear + end + end + end +end diff --git a/little_monster.gemspec b/little_monster.gemspec index c84db87..421f9e8 100644 --- a/little_monster.gemspec +++ b/little_monster.gemspec @@ -26,21 +26,24 @@ Gem::Specification.new do |spec| spec.executables = ['lm'] spec.require_paths = ['lib'] - spec.add_runtime_dependency 'activesupport' - spec.add_runtime_dependency 'multi_json' - spec.add_runtime_dependency 'thor' - spec.add_runtime_dependency 'tilt' - spec.add_runtime_dependency 'toiler' - spec.add_runtime_dependency 'typhoeus' + spec.add_runtime_dependency 'activesupport', '6.1.7.6'# '7.0.4.2' + spec.add_runtime_dependency 'multi_json', '1.15.0' + spec.add_runtime_dependency 'thor', '1.2.1' + spec.add_runtime_dependency 'tilt', '2.1.0' + spec.add_runtime_dependency 'toiler', '0.7.1' + spec.add_runtime_dependency 'typhoeus', '1.4.0' + spec.add_runtime_dependency 'moneta', '1.6.0' spec.add_development_dependency 'bundler' - spec.add_development_dependency 'byebug' - spec.add_development_dependency 'oj' - spec.add_development_dependency 'pry' - spec.add_development_dependency 'rake' - spec.add_development_dependency 'require_all' - spec.add_development_dependency 'rspec' - spec.add_development_dependency 'rubocop' - spec.add_development_dependency 'simplecov' + spec.add_development_dependency 'byebug', '11.1.3' + spec.add_development_dependency 'oj', '3.14.2' + spec.add_development_dependency 'pry', '0.14.2' + spec.add_development_dependency 'rake', '13.0.6' + spec.add_development_dependency 'require_all', '3.0.0' + spec.add_development_dependency 'rspec', '3.12.0' + spec.add_development_dependency 'rubocop', '1.46.0' + spec.add_development_dependency 'simplecov', '0.22.0' + spec.add_development_dependency 'webmock', '3.20.0' + spec.metadata['rubygems_mfa_required'] = 'true' end diff --git a/spec/lib/little_monster/core/api_spec.rb b/spec/lib/little_monster/core/api_spec.rb index c2235df..cc703fb 100644 --- a/spec/lib/little_monster/core/api_spec.rb +++ b/spec/lib/little_monster/core/api_spec.rb @@ -8,9 +8,12 @@ let(:options) { { critical: false } } let(:response) { double(code: 200, effective_url: '', success?: true, return_code: :ok) } let(:request_id) { '123' } + let(:body_str) { File.open('./spec/mock/responses/tiger_token.json', 'r').read } before :each do allow(SecureRandom).to receive(:uuid).and_return(request_id) + allow(File).to receive(:read).with(LittleMonster.shark_login_file_path).and_return('') + LoginSharkMock.new(self).login_request_success(body_str) end describe '::get' do @@ -120,6 +123,9 @@ end context 'request built' do + let(:body_str) { File.open('./spec/mock/responses/tiger_token.json', 'r').read } + let(:body) { JSON.parse(body_str) } + it 'has body dumped to json' do body = { a: :b } params[:body] = body @@ -137,7 +143,13 @@ it 'has content type set to json if it was not specified' do subject.request method, path, params, **options expect(Typhoeus).to have_received(method) - .with(url, hash_including(headers: { 'Content-Type' => 'application/json', 'X-Request-ID' => request_id })) + .with(url, hash_including( + headers: { + 'Content-Type' => 'application/json', + 'X-Request-ID' => request_id, + 'X-Tiger-Token' => "Bearer #{body['token']}" + } + )) end it 'has content type set to json if specified' do diff --git a/spec/lib/little_monster/tiger/api_spec.rb b/spec/lib/little_monster/tiger/api_spec.rb new file mode 100644 index 0000000..5400504 --- /dev/null +++ b/spec/lib/little_monster/tiger/api_spec.rb @@ -0,0 +1,147 @@ +require 'spec_helper' + +describe LittleMonster::Tiger::API do + subject(:api) { described_class } + + before do + LittleMonster::Tiger::Cache.instance.cache.clear + allow(File).to receive(:read).with(LittleMonster.shark_login_file_path).and_return('') + end + + describe '.bearer_token' do + context 'when the shark login success' do + let(:body_str) { File.open('./spec/mock/responses/tiger_token.json', 'r').read } + let(:body) { JSON.parse(body_str) } + + before do + LoginSharkMock.new(self).login_request_success(body_str) + end + + it 'return correct bearer token' do + expect(api.bearer_token).to eq("Bearer #{body['token']}") + end + end + + context 'when the shark login fail' do + before do + LoginSharkMock.new(self).login_request_failure + end + + it 'return nil' do + expect(api.bearer_token).to be_nil + end + end + end + + describe '.cached_shark_token' do + context 'when the shark login success' do + let(:body_str) { File.open('./spec/mock/responses/tiger_token.json', 'r').read } + let(:body) { JSON.parse(body_str) } + + before do + LoginSharkMock.new(self).login_request_success(body_str) + end + + it 'return correct token' do + expect(api.cached_shark_token).to eq(body['token']) + end + end + + context 'when the shark login fail' do + before do + LoginSharkMock.new(self).login_request_failure + end + + it 'return nil' do + expect(api.cached_shark_token).to be_nil + end + end + + context 'when called cached_shark_token the next call' do + let(:body_str) { File.open('./spec/mock/responses/tiger_token.json', 'r').read } + let(:body) { JSON.parse(body_str) } + + before do + LoginSharkMock.new(self).login_request_success(body_str) + api.cached_shark_token + allow(Typhoeus::Request).to receive(:new).and_call_original + end + + it 'dont request to login endpoint' do + + api.cached_shark_token + expect(Typhoeus::Request).not_to have_received(:new) + end + end + + context 'when called cached_shark_token for first time' do + let(:body_str) { File.open('./spec/mock/responses/tiger_token.json', 'r').read } + let(:body) { JSON.parse(body_str) } + + before do + LoginSharkMock.new(self).login_request_success(body_str) + allow(Typhoeus::Request).to receive(:new).and_call_original + end + + it 'request to login endpoint' do + api.cached_shark_token + expect(Typhoeus::Request).to have_received(:new) + end + end + end + + describe '.new_shark_token' do + context 'when the shark login success' do + let(:body_str) { File.open('./spec/mock/responses/tiger_token.json', 'r').read } + let(:body) { JSON.parse(body_str) } + + before do + LoginSharkMock.new(self).login_request_success(body_str) + end + + it 'return correct token' do + expect(api.new_shark_token).to eq(body['token']) + end + end + + context 'when the shark login fail' do + before do + LoginSharkMock.new(self).login_request_failure + end + + it 'return nil' do + expect(api.new_shark_token).to be_nil + end + end + end + + describe '.make_call' do + let(:method) { :post } + let(:endpoint) { 'login/shark' } + let(:options) { { body: body } } + let(:body) { { token: '' }.to_json } + + context 'when the shark login success' do + let(:body_str) { File.open('./spec/mock/responses/tiger_token.json', 'r').read } + let(:body) { { token: '' }.to_json } + + before do + LoginSharkMock.new(self).login_request_success(body_str) + end + + it 'return success request' do + expect(api.make_call(method, endpoint, options)).to be_success + end + end + + context 'when the shark login fail' do + before do + LoginSharkMock.new(self).login_request_failure + end + + it 'return correct bearer token' do + expect(api.make_call(method, endpoint, options)).to be_failure + end + end + end +end diff --git a/spec/lib/little_monster/tiger/cache_spec.rb b/spec/lib/little_monster/tiger/cache_spec.rb new file mode 100644 index 0000000..d8ba9bc --- /dev/null +++ b/spec/lib/little_monster/tiger/cache_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe LittleMonster::Tiger::Cache do + subject(:cache) { described_class } + + let(:key) { SecureRandom.uuid } + + before do + LittleMonster::Tiger::Cache.instance.cache.clear + cache.instance.set(:key, key) + end + + describe '.set' do + context 'when set a key and value' do + before do + cache.instance.set(:key, key) + end + + it 'store that value for that key' do + expect(cache.instance.get(:key)).to eq(key) + end + end + + context 'when set a key, value and expires' do + let(:expires) { 1 } + + before do + cache.instance.set(:key, key, expires) + end + + it 'store that value for that key for `expires` seconds' do + expect(cache.instance.get(:key)).to eq(key) + Kernel.sleep(expires + 1) + expect(cache.instance.get(:key)).to be_nil + end + end + end + + describe '.get' do + context 'when get from a not exist key' do + it 'return nil' do + expect(cache.instance.get(:foo)).to be_nil + end + end + end + + describe '.clear' do + context 'when get a exist key after clear cache' do + + before do + LittleMonster::Tiger::Cache.instance.cache.clear + end + + it 'return nil' do + expect(cache.instance.get(:key)).to be_nil + end + end + end +end diff --git a/spec/lib/little_monster_spec.rb b/spec/lib/little_monster_spec.rb index 0bbb3f8..7d1eeea 100644 --- a/spec/lib/little_monster_spec.rb +++ b/spec/lib/little_monster_spec.rb @@ -38,7 +38,7 @@ it 'is set to nil' do LittleMonster.init - expect(LittleMonster.logger.instance_variable_get(:@logdev)).to eq(nil) + expect(LittleMonster.logger.instance_variable_get(:@logdev).filename).to eq('/dev/null') end end diff --git a/spec/mock/login_shark_mock.rb b/spec/mock/login_shark_mock.rb new file mode 100644 index 0000000..8add789 --- /dev/null +++ b/spec/mock/login_shark_mock.rb @@ -0,0 +1,14 @@ +class LoginSharkMock + def initialize(rspec_context) + @rspec_context = rspec_context + end + + def login_request_success(body) + headers = { 'Content-Type' => 'application/json'} + @rspec_context.stub_request(:post, %r{.*/login/shark}).to_return(status: 201, headers: headers, body: body) + end + + def login_request_failure + @rspec_context.stub_request(:post, %r{.*/login/shark}).to_return(status: 401) + end +end diff --git a/spec/mock/responses/tiger_token.json b/spec/mock/responses/tiger_token.json new file mode 100644 index 0000000..11f99ba --- /dev/null +++ b/spec/mock/responses/tiger_token.json @@ -0,0 +1,3 @@ +{ + "token": "foo" +} diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 85055fc..7690dc9 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,6 +5,7 @@ require 'rspec' require 'byebug' require 'require_all' +require 'webmock/rspec' require 'little_monster' require 'little_monster/rspec'