zuora_connect 2.0.59 → 2.0.60e
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 +4 -4
- data/app/controllers/zuora_connect/static_controller.rb +83 -12
- data/app/models/zuora_connect/app_instance_base.rb +1 -1
- data/config/initializers/prometheus.rb +78 -23
- data/config/initializers/resque.rb +14 -0
- data/config/initializers/unicorn.rb +29 -1
- data/config/routes.rb +5 -0
- data/lib/middleware/json_parse_errors.rb +13 -2
- data/lib/middleware/metrics_middleware.rb +45 -31
- data/lib/resque/plugins/app_instance_job.rb +1 -1
- data/lib/zuora_connect/controllers/helpers.rb +195 -68
- data/lib/zuora_connect/railtie.rb +9 -5
- data/lib/zuora_connect/version.rb +1 -1
- metadata +29 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9efee2fa39c05fb2cf07a2e9a49e14b76a75e68c8be87f008018287a45551ceb
|
4
|
+
data.tar.gz: f59b6c98c859dc205afc28a2eae57d5037b6d9cc1f41a5dd33e36d3e2b55a307
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 73b79ce54de6008219f2f3fd2abce7defccc27454f3328cf57b02ff116ea7faef8d22d5789566bad372e440c38f486a6bc2062ed2c1ff497d0ea1ac3863cbbdd
|
7
|
+
data.tar.gz: 940145c4211fa0be98c0e099f69df168a3224d17695c8fe3efa70ad4ed6fcc4788377d4ed74f17fd089f3c8adab481a1b224c33a46f71701403b00dbc1d386b6
|
@@ -1,10 +1,11 @@
|
|
1
1
|
module ZuoraConnect
|
2
2
|
class StaticController < ApplicationController
|
3
|
-
before_action :authenticate_connect_app_request, :except => [:metrics, :health, :initialize_app]
|
4
|
-
before_action :clear_connect_app_session, :only => [:metrics, :health, :initialize_app]
|
5
|
-
after_action :persist_connect_app_session, :except => [:metrics, :health, :initialize_app]
|
6
|
-
|
7
|
-
skip_before_action :verify_authenticity_token, :only => [:initialize_app]
|
3
|
+
before_action :authenticate_connect_app_request, :except => [:metrics, :health, :initialize_app, :provision, :instance_user]
|
4
|
+
before_action :clear_connect_app_session, :only => [:metrics, :health, :initialize_app, :provision, :instance_user]
|
5
|
+
after_action :persist_connect_app_session, :except => [:metrics, :health, :initialize_app, :provision, :instance_user]
|
6
|
+
|
7
|
+
skip_before_action :verify_authenticity_token, :only => [:initialize_app, :provision]
|
8
|
+
http_basic_authenticate_with name: ENV['PROVISION_USER'], password: ENV['PROVISION_SECRET'], :only => [:provision, :instance_user]
|
8
9
|
|
9
10
|
def metrics
|
10
11
|
type = params[:type].present? ? params[:type] : "versions"
|
@@ -13,11 +14,11 @@ module ZuoraConnect
|
|
13
14
|
|
14
15
|
def health
|
15
16
|
if params[:error].present?
|
16
|
-
begin
|
17
|
+
begin
|
17
18
|
raise ZuoraConnect::Exceptions::Error.new('This is an error')
|
18
19
|
rescue => ex
|
19
20
|
case params[:error]
|
20
|
-
when 'Log'
|
21
|
+
when 'Log'
|
21
22
|
Rails.logger.error("Error in Health", ex)
|
22
23
|
when 'Exception'
|
23
24
|
raise
|
@@ -34,11 +35,13 @@ module ZuoraConnect
|
|
34
35
|
def initialize_app
|
35
36
|
begin
|
36
37
|
authenticate_connect_app_request
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
38
|
+
unless performed?
|
39
|
+
@appinstance.new_session(:session => @appinstance.data_lookup(:session => session))
|
40
|
+
render json: {
|
41
|
+
message: 'Success',
|
42
|
+
status: 200
|
43
|
+
}, status: 200
|
44
|
+
end
|
42
45
|
rescue => ex
|
43
46
|
Rails.logger.error("Failed to Initialize application", ex)
|
44
47
|
if performed?
|
@@ -52,6 +55,74 @@ module ZuoraConnect
|
|
52
55
|
end
|
53
56
|
end
|
54
57
|
|
58
|
+
def provision
|
59
|
+
create_new_instance
|
60
|
+
unless performed?
|
61
|
+
render json: {
|
62
|
+
status: 200,
|
63
|
+
message: 'Success',
|
64
|
+
app_instance_id: @appinstance.id
|
65
|
+
}, status: 200
|
66
|
+
end
|
67
|
+
rescue StandardError => e
|
68
|
+
message = 'Failed to provision new instance'
|
69
|
+
if performed?
|
70
|
+
Rails.logger.error("#{message}: #{performed?}", e)
|
71
|
+
else
|
72
|
+
Rails.logger.error(message, e)
|
73
|
+
render json: {
|
74
|
+
status: 500,
|
75
|
+
message: message
|
76
|
+
}, status: 500
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def instance_user
|
81
|
+
ZuoraConnect::AppInstance.read_master_db do
|
82
|
+
ZuoraConnect.logger.with_fields = {} if ZuoraConnect.logger.is_a?(Ougai::Logger)
|
83
|
+
Rails.logger.with_fields = {} if Rails.logger.is_a?(Ougai::Logger)
|
84
|
+
|
85
|
+
if defined?(ElasticAPM) && ElasticAPM.running? && ElasticAPM.respond_to?(:set_label)
|
86
|
+
ElasticAPM.set_label(:trace_id, request.uuid)
|
87
|
+
end
|
88
|
+
|
89
|
+
unless params[:id].present?
|
90
|
+
render json: {
|
91
|
+
status: 400,
|
92
|
+
message: 'No app instance id provided'
|
93
|
+
}, status: :bad_request
|
94
|
+
return
|
95
|
+
end
|
96
|
+
|
97
|
+
@appinstance = ZuoraConnect::AppInstance.find(params[:id]).new_session
|
98
|
+
end
|
99
|
+
|
100
|
+
zuora_client = @appinstance.send(ZuoraConnect::AppInstance::LOGIN_TENANT_DESTINATION).client
|
101
|
+
client_describe, = zuora_client.rest_call(
|
102
|
+
url: zuora_client.rest_endpoint('genesis/user/info').gsub('v1/', ''),
|
103
|
+
session_type: zuora_client.class == ZuoraAPI::Oauth ? :bearer : :basic
|
104
|
+
)
|
105
|
+
|
106
|
+
render json: {
|
107
|
+
status: 200,
|
108
|
+
message: 'Success',
|
109
|
+
user_id: client_describe['coreUserId'],
|
110
|
+
username: client_describe['username'],
|
111
|
+
email: client_describe['workEmail']
|
112
|
+
}, status: 200
|
113
|
+
rescue ActiveRecord::RecordNotFound
|
114
|
+
render json: {
|
115
|
+
status: 400,
|
116
|
+
message: 'No app instance found'
|
117
|
+
}, status: :bad_request
|
118
|
+
rescue StandardError => e
|
119
|
+
Rails.logger.error('Error occurred getting user details', e)
|
120
|
+
render json: {
|
121
|
+
status: 500,
|
122
|
+
message: 'Failed to get user details'
|
123
|
+
}, status: 500
|
124
|
+
end
|
125
|
+
|
55
126
|
private
|
56
127
|
|
57
128
|
def clear_connect_app_session
|
@@ -402,7 +402,7 @@ module ZuoraConnect
|
|
402
402
|
end
|
403
403
|
|
404
404
|
def self.write_to_telegraf(*args)
|
405
|
-
if ZuoraConnect.configuration.enable_metrics
|
405
|
+
if ZuoraConnect.configuration.enable_metrics && !defined?(Prometheus)
|
406
406
|
@@telegraf_host = ZuoraConnect::Telegraf.new() if @@telegraf_host == nil
|
407
407
|
unicorn_stats = self.unicorn_listener_stats() if defined?(Unicorn) && Unicorn.respond_to?(:listener_names)
|
408
408
|
@@telegraf_host.write(direction: 'Raindrops', tags: {}, values: unicorn_stats) unless unicorn_stats.blank?
|
@@ -3,38 +3,93 @@ if defined? Prometheus
|
|
3
3
|
require "zuora_connect/version"
|
4
4
|
require "zuora_api/version"
|
5
5
|
|
6
|
+
resque_path = "#{ENV['RESQUE_EXPORTER_PATH'] || Rails.root.join('tmp/resque_exporter')}/*.prom"
|
7
|
+
prometheus_path = Rails.root.join("tmp/prometheus")
|
8
|
+
|
9
|
+
Dir[resque_path, "#{prometheus_path}/*.bin"].each do |file_path|
|
10
|
+
File.unlink(file_path)
|
11
|
+
end
|
12
|
+
|
13
|
+
require 'prometheus/client/data_stores/direct_file_store'
|
14
|
+
Prometheus::Client.config.data_store = Prometheus::Client::DataStores::DirectFileStore.new(
|
15
|
+
dir: prometheus_path
|
16
|
+
)
|
17
|
+
|
18
|
+
class ResqueExporter
|
19
|
+
require 'prometheus/client/formats/text'
|
20
|
+
require 'fileutils'
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@lock = Monitor.new
|
24
|
+
@registry = Prometheus::Client.registry
|
25
|
+
@path = ENV['RESQUE_EXPORTER_PATH'] || Rails.root.join('tmp/resque_exporter')
|
26
|
+
FileUtils.mkdir_p(@path)
|
27
|
+
end
|
28
|
+
|
29
|
+
def export
|
30
|
+
filename = File.join(@path, 'resque_export.prom')
|
31
|
+
@lock.synchronize do
|
32
|
+
File.open(filename, 'w+') do |file|
|
33
|
+
file.write(Prometheus::Client::Formats::Text.marshal(@registry))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
most_recent_aggregation = {}
|
40
|
+
sum_aggregation = {}
|
41
|
+
if defined?(Unicorn)
|
42
|
+
most_recent_aggregation[:aggregation] = :most_recent
|
43
|
+
sum_aggregation[:aggregation] = :sum
|
44
|
+
end
|
45
|
+
|
6
46
|
# Create a default Prometheus registry for our metrics.
|
7
47
|
prometheus = Prometheus::Client.registry
|
8
48
|
|
9
49
|
# Create your metrics.
|
10
|
-
ZUORA_VERSION =
|
11
|
-
CONNECT_VERSION =
|
12
|
-
RAILS_VERSION =
|
13
|
-
RUBY_V =
|
50
|
+
ZUORA_VERSION = prometheus.gauge(:zuora_version, docstring: 'The current Zuora Gem version.', labels: %i(version name), preset_labels: { version: ZuoraAPI::VERSION, name: ZuoraConnect::Telegraf.app_name }, store_settings: most_recent_aggregation)
|
51
|
+
CONNECT_VERSION = prometheus.gauge(:gem_version, docstring: 'The current Connect Gem version.', labels: %i(version name), preset_labels: { version: ZuoraConnect::VERSION, name: ZuoraConnect::Telegraf.app_name }, store_settings: most_recent_aggregation)
|
52
|
+
RAILS_VERSION = prometheus.gauge(:rails_version, docstring: 'The current Rails version.', labels: %i(version name), preset_labels: { version: Rails.version, name: ZuoraConnect::Telegraf.app_name }, store_settings: most_recent_aggregation)
|
53
|
+
RUBY_V = prometheus.gauge(:ruby_version, docstring: 'The current Ruby version.', labels: %i(version name), preset_labels: { version: RUBY_VERSION, name: ZuoraConnect::Telegraf.app_name }, store_settings: most_recent_aggregation)
|
14
54
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
prometheus.register(RUBY_V);RUBY_V.set({version: RUBY_VERSION, name: ZuoraConnect::Telegraf.app_name},0)
|
55
|
+
ZUORA_VERSION.set(0)
|
56
|
+
CONNECT_VERSION.set(0)
|
57
|
+
RAILS_VERSION.set(0)
|
58
|
+
RUBY_V.set(0)
|
20
59
|
|
21
60
|
# Do they have resque jobs?
|
22
61
|
if defined? Resque.redis
|
23
|
-
REDIS_CONNECTION =
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
prometheus.register(REDIS_CONNECTION)
|
31
|
-
prometheus.register(FINISHED_JOBS)
|
32
|
-
prometheus.register(ACTIVE_WORKERS)
|
33
|
-
prometheus.register(WORKERS)
|
34
|
-
prometheus.register(FAILED_JOBS)
|
35
|
-
prometheus.register(PENDING_JOBS)
|
36
|
-
|
62
|
+
REDIS_CONNECTION = prometheus.gauge(:redis_connection, docstring: 'The status of the redis connection, 0 or 1', labels: %i(connection name), preset_labels: {connection:'redis', name: ZuoraConnect::Telegraf.app_name}, store_settings: most_recent_aggregation)
|
63
|
+
JOBS_FINISHED = prometheus.gauge(:jobs_finished, docstring: 'Done resque jobs', labels: %i(type name), preset_labels: {type:'resque', name: ZuoraConnect::Telegraf.app_name}, store_settings: most_recent_aggregation)
|
64
|
+
WORKERS_TOTAL = prometheus.gauge(:workers_total, docstring: 'Total resque workers', labels: %i(type name), preset_labels: {type:'resque', name: ZuoraConnect::Telegraf.app_name}, store_settings: most_recent_aggregation)
|
65
|
+
WORKERS_ACTIVE = prometheus.gauge(:workers_active, docstring: 'Active resque workers', labels: %i(type name), preset_labels: {type:'resque', name: ZuoraConnect::Telegraf.app_name}, store_settings: most_recent_aggregation)
|
66
|
+
JOBS_FAILED = prometheus.gauge(:jobs_failed, docstring: 'Failed resque jobs', labels: %i(type name), preset_labels: {type:'resque', name: ZuoraConnect::Telegraf.app_name}, store_settings: most_recent_aggregation)
|
67
|
+
JOBS_PENDING = prometheus.gauge(:jobs_pending, docstring: 'Pending resque jobs', labels: %i(type name), preset_labels: {type:'resque', name: ZuoraConnect::Telegraf.app_name}, store_settings: most_recent_aggregation)
|
37
68
|
end
|
38
69
|
|
70
|
+
if defined?(Unicorn) && Unicorn.respond_to?(:listener_names)
|
71
|
+
UNICORN_KILLS = prometheus.gauge(
|
72
|
+
:unicorn_kills,
|
73
|
+
docstring: 'Unicorn Kills',
|
74
|
+
labels: %i(type name),
|
75
|
+
preset_labels: {type:'Unicorn-Killer', name: ZuoraConnect::Telegraf.app_name},
|
76
|
+
store_settings: sum_aggregation
|
77
|
+
)
|
78
|
+
|
79
|
+
ZuoraConnect::AppInstanceBase.unicorn_listener_stats.each do |key, _|
|
80
|
+
gauge_name = "unicorn_#{key}".gsub(/[^a-zA-Z0-9_]/, '_')
|
81
|
+
gauge = prometheus.gauge(
|
82
|
+
gauge_name.to_sym,
|
83
|
+
docstring: 'Unicorn Stats',
|
84
|
+
labels: %i(type name),
|
85
|
+
preset_labels: { type: 'unicorn', name: ZuoraConnect::Telegraf.app_name },
|
86
|
+
store_settings: most_recent_aggregation
|
87
|
+
)
|
88
|
+
Prometheus.const_set(
|
89
|
+
gauge_name.upcase,
|
90
|
+
gauge
|
91
|
+
)
|
92
|
+
end
|
93
|
+
end
|
39
94
|
end
|
40
95
|
end
|
@@ -5,6 +5,20 @@ if defined?(Resque::Worker)
|
|
5
5
|
Resque::Job.send(:include, Resque::SelfLookup)
|
6
6
|
end
|
7
7
|
|
8
|
+
if defined?(Resque::Job) && defined?(Prometheus)
|
9
|
+
module ResquePrometheusExtensions
|
10
|
+
EXPORTER = Prometheus::ResqueExporter.new
|
11
|
+
def perform
|
12
|
+
super
|
13
|
+
EXPORTER.export
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Resque::Job
|
18
|
+
prepend ResquePrometheusExtensions
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
8
22
|
Resque.module_eval do
|
9
23
|
# Returns a hash, mapping queue names to queue sizes
|
10
24
|
def queue_sizes
|
@@ -3,7 +3,35 @@ if defined?(Unicorn::WorkerKiller)
|
|
3
3
|
self.singleton_class.send(:alias_method, :kill_self_old, :kill_self)
|
4
4
|
def self.kill_self(logger, start_time)
|
5
5
|
self.kill_self_old(logger, start_time)
|
6
|
-
|
6
|
+
if defined?(Prometheus)
|
7
|
+
Prometheus::UNICORN_KILLS.set(1)
|
8
|
+
else
|
9
|
+
ZuoraConnect::AppInstance.write_to_telegraf(direction: 'Unicorn-Killer', tags: {app_instance: 0}, values: {kill: 1})
|
10
|
+
end
|
7
11
|
end
|
8
12
|
end
|
13
|
+
end
|
14
|
+
|
15
|
+
if defined?(Unicorn::HttpServer) && defined?(Prometheus)
|
16
|
+
module HttpServerExtensions
|
17
|
+
def kill_worker(signal, wpid)
|
18
|
+
Prometheus::UNICORN_KILLS.increment
|
19
|
+
super
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module WorkerExtensions
|
24
|
+
def soft_kill(sig)
|
25
|
+
Prometheus::UNICORN_KILLS.increment
|
26
|
+
super
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Unicorn::HttpServer
|
31
|
+
prepend HttpServerExtensions
|
32
|
+
end
|
33
|
+
|
34
|
+
class Unicorn::Worker
|
35
|
+
prepend WorkerExtensions
|
36
|
+
end
|
9
37
|
end
|
data/config/routes.rb
CHANGED
@@ -3,6 +3,11 @@ ZuoraConnect::Engine.routes.draw do
|
|
3
3
|
get '/internal/data' => 'static#metrics'
|
4
4
|
post '/initialize_app' => 'static#initialize_app'
|
5
5
|
|
6
|
+
if ENV['PROVISION_USER'].present? && ENV['PROVISION_SECRET'].present?
|
7
|
+
post '/provision' => 'static#provision'
|
8
|
+
get '/instance/:id/user' => 'static#instance_user'
|
9
|
+
end
|
10
|
+
|
6
11
|
namespace :api do
|
7
12
|
namespace :v1 do
|
8
13
|
resources :app_instance, :only => [:index], defaults: {format: :json} do
|
@@ -7,7 +7,7 @@ module ZuoraConnect
|
|
7
7
|
def call(env)
|
8
8
|
begin
|
9
9
|
@app.call(env)
|
10
|
-
rescue
|
10
|
+
rescue DynamicRailsError => error
|
11
11
|
if env['HTTP_ACCEPT'] =~ /application\/json/ || env['CONTENT_TYPE'] =~ /application\/json/
|
12
12
|
return [
|
13
13
|
400, { "Content-Type" => "application/json" },
|
@@ -18,5 +18,16 @@ module ZuoraConnect
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
end
|
21
|
+
|
22
|
+
# Note(hartley): remove once the minimum supported version of Rails is 5.2
|
23
|
+
class DynamicRailsError < StandardError
|
24
|
+
def self.===(exception)
|
25
|
+
if Rails.version >= "5.2"
|
26
|
+
exception.is_a?(ActionDispatch::Http::Parameters::ParseError)
|
27
|
+
else
|
28
|
+
exception.is_a?(ActionDispatch::ParamsParser::ParseError)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
21
32
|
end
|
22
|
-
end
|
33
|
+
end
|
@@ -64,6 +64,22 @@ module ZuoraConnect
|
|
64
64
|
#Remove bad headers
|
65
65
|
@bad_headers.each { |header| env.delete(header) }
|
66
66
|
|
67
|
+
if defined?(Prometheus) && env['PATH_INFO'] == '/connect/internal/metrics'
|
68
|
+
# Prometheus Stuff
|
69
|
+
metrics = ZuoraConnect::AppInstance.get_metrics('stats')
|
70
|
+
redis_up = metrics.present? && metrics.dig(:Resque, :Workers_Total).present? ? 1 : 0
|
71
|
+
Prometheus::REDIS_CONNECTION.set(redis_up)
|
72
|
+
|
73
|
+
process_prometheus_metric(metrics: metrics)
|
74
|
+
|
75
|
+
if defined?(Unicorn) && Unicorn.respond_to?(:listener_names)
|
76
|
+
ZuoraConnect::AppInstanceBase.unicorn_listener_stats.each do |key, value|
|
77
|
+
gauge = Prometheus.const_get("unicorn_#{key}".gsub(/[^a-zA-Z0-9_]/, '_').upcase)
|
78
|
+
gauge.set(value) if gauge.present?
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
67
83
|
#Thread.current[:appinstance] = nil
|
68
84
|
start_time = Time.now
|
69
85
|
begin
|
@@ -77,35 +93,6 @@ module ZuoraConnect
|
|
77
93
|
ZuoraConnect::AppInstanceBase.write_to_telegraf(direction: 'request-inbound-assets', tags: tags, values: values)
|
78
94
|
end
|
79
95
|
|
80
|
-
if defined? Prometheus
|
81
|
-
#Prometheus Stuff
|
82
|
-
if env['PATH_INFO'] == '/connect/internal/metrics'
|
83
|
-
|
84
|
-
#Do something before each scrape
|
85
|
-
if defined? Resque.redis
|
86
|
-
begin
|
87
|
-
|
88
|
-
Resque.redis.ping
|
89
|
-
|
90
|
-
Prometheus::REDIS_CONNECTION.set({connection:'redis',name: ZuoraConnect::Telegraf.app_name},1)
|
91
|
-
Prometheus::FINISHED_JOBS.set({type:'resque',name: ZuoraConnect::Telegraf.app_name},Resque.info[:processed])
|
92
|
-
Prometheus::PENDING_JOBS.set({type:'resque',name: ZuoraConnect::Telegraf.app_name},Resque.info[:pending])
|
93
|
-
Prometheus::ACTIVE_WORKERS.set({type:'resque',name: ZuoraConnect::Telegraf.app_name},Resque.info[:working])
|
94
|
-
Prometheus::WORKERS.set({type:'resque',name: ZuoraConnect::Telegraf.app_name},Resque.info[:workers])
|
95
|
-
Prometheus::FAILED_JOBS.set({type:'resque',name: ZuoraConnect::Telegraf.app_name},Resque.info[:failed])
|
96
|
-
|
97
|
-
rescue Redis::CannotConnectError
|
98
|
-
Prometheus::REDIS_CONNECTION.set({connection:'redis',name: ZuoraConnect::Telegraf.app_name},0)
|
99
|
-
end
|
100
|
-
|
101
|
-
if ZuoraConnect.configuration.custom_prometheus_update_block != nil
|
102
|
-
ZuoraConnect.configuration.custom_prometheus_update_block.call()
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
96
|
# Uncomment following block of code for handling engine requests/requests without controller
|
110
97
|
# else
|
111
98
|
# # Handling requests which do not have controllers (engines)
|
@@ -119,10 +106,10 @@ module ZuoraConnect
|
|
119
106
|
content_type = @headers['Content-Type'].split(';')[0] if @headers['Content-Type']
|
120
107
|
content_type = content_type.gsub('text/javascript', 'application/javascript')
|
121
108
|
tags = {status: @status, content_type: content_type}
|
122
|
-
|
109
|
+
|
123
110
|
tags = tags.merge({controller: 'ActionController'})
|
124
111
|
tags = tags.merge({action: 'RoutingError' }) if @status == 404
|
125
|
-
|
112
|
+
|
126
113
|
values = {response_time: ((Time.now - start_time)*1000).round(2) }
|
127
114
|
|
128
115
|
ZuoraConnect::AppInstanceBase.write_to_telegraf(direction: :inbound, tags: tags, values: values)
|
@@ -133,5 +120,32 @@ module ZuoraConnect
|
|
133
120
|
[@status, @headers, @response]
|
134
121
|
end
|
135
122
|
end
|
123
|
+
|
124
|
+
def process_prometheus_metric(type: 'none', metrics: {})
|
125
|
+
return if metrics.blank?
|
126
|
+
|
127
|
+
prometheus = Prometheus::Client.registry
|
128
|
+
most_recent_aggregation = {}
|
129
|
+
if Prometheus::Client.config.data_store.is_a?(Prometheus::Client::DataStores::DirectFileStore)
|
130
|
+
most_recent_aggregation[:aggregation] = :most_recent
|
131
|
+
end
|
132
|
+
metrics.each do |key, value|
|
133
|
+
next if %w[app_name url].include?(key.to_s)
|
134
|
+
|
135
|
+
if value.is_a?(Hash)
|
136
|
+
process_prometheus_metric(type: key.to_s, metrics: value)
|
137
|
+
else
|
138
|
+
gauge_name = key.to_s.downcase.gsub(/[^a-z0-9_]/, '_')
|
139
|
+
gauge = prometheus.get(gauge_name.to_sym) || prometheus.gauge(
|
140
|
+
gauge_name.to_sym,
|
141
|
+
docstring: "#{key} metric",
|
142
|
+
labels: %i(type name),
|
143
|
+
preset_labels: { type: type, name: ZuoraConnect::Telegraf.app_name },
|
144
|
+
store_settings: most_recent_aggregation
|
145
|
+
)
|
146
|
+
gauge.set(value)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
136
150
|
end
|
137
151
|
end
|
@@ -47,7 +47,7 @@ module Resque
|
|
47
47
|
raise
|
48
48
|
end
|
49
49
|
rescue PG::ConnectionBad => exception
|
50
|
-
Rails.logger.
|
50
|
+
Rails.logger.warn("Bad Connection Restart", exception)
|
51
51
|
Resque.enqueue_to(self.job.queue, self.job.payload['class'], args)
|
52
52
|
return
|
53
53
|
rescue ZuoraConnect::Exceptions::ConnectCommunicationError => exception
|
@@ -219,6 +219,80 @@ module ZuoraConnect
|
|
219
219
|
return (request.headers['ZuoraCurrentEntity'].present? || cookies['ZuoraCurrentEntity'].present?)
|
220
220
|
end
|
221
221
|
|
222
|
+
def create_new_instance
|
223
|
+
ZuoraConnect::AppInstance.read_master_db do
|
224
|
+
Thread.current[:appinstance] = nil
|
225
|
+
ZuoraConnect.logger.with_fields = {} if ZuoraConnect.logger.is_a?(Ougai::Logger)
|
226
|
+
Rails.logger.with_fields = {} if Rails.logger.is_a?(Ougai::Logger)
|
227
|
+
|
228
|
+
if defined?(ElasticAPM) && ElasticAPM.running? && ElasticAPM.respond_to?(:set_label)
|
229
|
+
ElasticAPM.set_label(:trace_id, request.uuid)
|
230
|
+
end
|
231
|
+
|
232
|
+
zuora_host = request.headers['zuora-host']
|
233
|
+
zuora_entity_id = (request.headers['zuora-entity-ids'] || '').gsub(
|
234
|
+
'-',
|
235
|
+
''
|
236
|
+
).split(',').first
|
237
|
+
|
238
|
+
# Validate host present
|
239
|
+
if zuora_host.blank?
|
240
|
+
render json: {
|
241
|
+
status: 401,
|
242
|
+
message: 'zuora-host header was not supplied.'
|
243
|
+
}, status: :unauthorized
|
244
|
+
return
|
245
|
+
end
|
246
|
+
|
247
|
+
# Validate entity-ids present
|
248
|
+
if zuora_entity_id.blank?
|
249
|
+
render json: {
|
250
|
+
status: 401,
|
251
|
+
message: 'zuora-entity-ids header was not supplied.'
|
252
|
+
}, status: :unauthorized
|
253
|
+
return
|
254
|
+
end
|
255
|
+
|
256
|
+
rest_domain = ZuoraAPI::Login.new(url: "https://#{zuora_host}").rest_domain
|
257
|
+
app_instance_id = ZuoraConnect::AppInstance.where(
|
258
|
+
'zuora_entity_ids ?& array[:entities] AND zuora_domain = :host',
|
259
|
+
entities: [zuora_entity_id],
|
260
|
+
host: rest_domain
|
261
|
+
).pluck(:id).first
|
262
|
+
|
263
|
+
if app_instance_id.present?
|
264
|
+
render json: {
|
265
|
+
status: 409,
|
266
|
+
message: 'Instance already exists.',
|
267
|
+
app_instance_id: app_instance_id
|
268
|
+
}, status: 409
|
269
|
+
else
|
270
|
+
Apartment::Tenant.switch!("public")
|
271
|
+
retry_count = 3
|
272
|
+
begin
|
273
|
+
@appinstance = new_instance(
|
274
|
+
next_instance_id,
|
275
|
+
zuora_entity_id,
|
276
|
+
rest_domain,
|
277
|
+
retry_count: retry_count
|
278
|
+
)
|
279
|
+
rescue ActiveRecord::RecordNotUnique
|
280
|
+
retry if (retry_count -= 1).positive?
|
281
|
+
return
|
282
|
+
end
|
283
|
+
|
284
|
+
app_instance_id = @appinstance.id
|
285
|
+
end
|
286
|
+
|
287
|
+
begin
|
288
|
+
Apartment::Tenant.switch!('public')
|
289
|
+
Apartment::Tenant.create(app_instance_id.to_s)
|
290
|
+
rescue Apartment::TenantExists
|
291
|
+
ZuoraConnect.logger.debug('Tenant Already Exists')
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
222
296
|
private
|
223
297
|
def setup_instance_via_prod_mode
|
224
298
|
zuora_entity_id = request.headers['ZuoraCurrentEntity'] || cookies['ZuoraCurrentEntity']
|
@@ -226,7 +300,7 @@ module ZuoraConnect
|
|
226
300
|
if zuora_entity_id.present?
|
227
301
|
zuora_tenant_id = cookies['Zuora-Tenant-Id']
|
228
302
|
zuora_user_id = cookies['Zuora-User-Id']
|
229
|
-
zuora_host = request.headers[
|
303
|
+
zuora_host = request.headers['HTTP_X_FORWARDED_HOST'] || request.headers['Zuora-Host'] || 'apisandbox.zuora.com'
|
230
304
|
|
231
305
|
zuora_details = {'host' => zuora_host, 'user_id' => zuora_user_id, 'tenant_id' => zuora_tenant_id, 'entity_id' => zuora_entity_id}
|
232
306
|
auth_headers = {}
|
@@ -328,13 +402,16 @@ module ZuoraConnect
|
|
328
402
|
|
329
403
|
zuora_user_id = cookies['Zuora-User-Id'] || session["ZuoraCurrentIdentity"]['userId']
|
330
404
|
|
331
|
-
#One deployed instance
|
332
405
|
if appinstances.size == 1
|
333
406
|
ZuoraConnect.logger.debug("Instance is #{appinstances.to_h.keys.first}")
|
334
407
|
@appinstance = ZuoraConnect::AppInstance.find(appinstances.to_h.keys.first)
|
408
|
+
end
|
335
409
|
|
410
|
+
# One deployed instance with credentials
|
411
|
+
if defined?(@appinstance) && !@appinstance['zuora_logins'].nil?
|
336
412
|
#Add user/update
|
337
413
|
begin
|
414
|
+
ZuoraConnect::ZuoraUser.reset_table_name
|
338
415
|
@zuora_user = ZuoraConnect::ZuoraUser.where(:zuora_user_id => zuora_user_id).first
|
339
416
|
rescue ActiveRecord::StatementInvalid => ex
|
340
417
|
if ex.message.include?("PG::UndefinedTable") && ex.message.include?("zuora_users")
|
@@ -380,79 +457,85 @@ module ZuoraConnect
|
|
380
457
|
return
|
381
458
|
end
|
382
459
|
Apartment::Tenant.switch!("public")
|
383
|
-
|
384
|
-
|
385
|
-
|
460
|
+
retry_count = 3
|
461
|
+
task_data = {}
|
462
|
+
begin
|
463
|
+
ActiveRecord::Base.transaction do
|
464
|
+
ActiveRecord::Base.connection.execute('LOCK public.zuora_users IN ACCESS EXCLUSIVE MODE')
|
386
465
|
|
387
|
-
|
388
|
-
|
389
|
-
return
|
390
|
-
end
|
466
|
+
unless defined?(@appinstance)
|
467
|
+
appinstances = ZuoraConnect::AppInstance.where("zuora_entity_ids ?& array[:entities] = true AND zuora_domain = :host", entities: [zuora_entity_id], host: zuora_client.rest_domain).pluck(:id, :name)
|
391
468
|
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
'customAuthorities' => [],
|
398
|
-
'additionalInformation' => {
|
399
|
-
'description' => "This user is for #{user} application.",
|
400
|
-
'name' => "#{user} API User #{next_id}"
|
401
|
-
}
|
402
|
-
}
|
403
|
-
|
404
|
-
oauth_response, response = zuora_client.rest_call(
|
405
|
-
method: :post,
|
406
|
-
body: body.to_json,
|
407
|
-
url: zuora_client.rest_endpoint("genesis/clients").gsub('v1/', ''),
|
408
|
-
session_type: zuora_client.class == ZuoraAPI::Oauth ? :bearer : :basic,
|
409
|
-
headers: auth_headers
|
410
|
-
)
|
469
|
+
if appinstances.size > 0
|
470
|
+
redirect_to "https://#{zuora_host}/apps/newlogin.do?retURL=#{request.fullpath}"
|
471
|
+
return
|
472
|
+
end
|
473
|
+
end
|
411
474
|
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
475
|
+
next_id = defined?(@appinstance) ? @appinstance.id : next_instance_id
|
476
|
+
if task_data.blank?
|
477
|
+
user = (ENV['DEIS_APP'] || "Application").split('-').map(&:capitalize).join(' ')
|
478
|
+
body = {
|
479
|
+
'userId' => zuora_user_id,
|
480
|
+
'entityIds' => [zuora_entity_id.unpack("a8a4a4a4a12").join('-')],
|
481
|
+
'customAuthorities' => [],
|
482
|
+
'additionalInformation' => {
|
483
|
+
'description' => "This user is for #{user} application.",
|
484
|
+
'name' => "#{user} API User #{next_id}"
|
485
|
+
}
|
486
|
+
}
|
487
|
+
|
488
|
+
oauth_response, response = zuora_client.rest_call(
|
489
|
+
method: :post,
|
490
|
+
body: body.to_json,
|
491
|
+
url: zuora_client.rest_endpoint("genesis/clients").gsub('v1/', ''),
|
492
|
+
session_type: zuora_client.class == ZuoraAPI::Oauth ? :bearer : :basic,
|
493
|
+
headers: auth_headers
|
494
|
+
)
|
495
|
+
|
496
|
+
new_zuora_client = ZuoraAPI::Oauth.new(url: "https://#{zuora_host}", oauth_client_id: oauth_response["clientId"], oauth_secret: oauth_response["clientSecret"] )
|
497
|
+
if session["ZuoraCurrentUserInfo"].blank?
|
498
|
+
client_describe, response = new_zuora_client.rest_call(url: zuora_client.rest_endpoint("genesis/user/info").gsub('v1/', ''), session_type: :bearer)
|
499
|
+
else
|
500
|
+
client_describe = session["ZuoraCurrentUserInfo"]
|
501
|
+
end
|
418
502
|
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
rescue ActiveRecord::RecordNotUnique => ex
|
443
|
-
if (retry_count += 1) < 3
|
444
|
-
@appinstance.assign_attributes({:api_token => rand(36**64).to_s(36), :token => rand(36**64).to_s(36)})
|
445
|
-
retry
|
503
|
+
available_entities = client_describe["accessibleEntities"].select {|entity| entity['id'] == zuora_entity_id}
|
504
|
+
task_data = {
|
505
|
+
"id": next_id,
|
506
|
+
"name": client_describe["tenantName"],
|
507
|
+
"mode": "Collections",
|
508
|
+
"status": "Running",
|
509
|
+
ZuoraConnect::AppInstance::LOGIN_TENANT_DESTINATION => {
|
510
|
+
"tenant_type": "Zuora",
|
511
|
+
"username": session["ZuoraCurrentIdentity"]["username"],
|
512
|
+
"url": new_zuora_client.url,
|
513
|
+
"status": "Active",
|
514
|
+
"oauth_client_id": oauth_response['clientId'],
|
515
|
+
"oauth_secret": oauth_response['clientSecret'],
|
516
|
+
"authentication_type": "OAUTH",
|
517
|
+
"entities": available_entities.map {|e| e.merge({'displayName' => client_describe["tenantName"]})}
|
518
|
+
},
|
519
|
+
"tenant_ids": available_entities.map{|e| e['entityId']}.uniq,
|
520
|
+
}
|
521
|
+
end
|
522
|
+
|
523
|
+
if defined?(@appinstance)
|
524
|
+
@appinstance.zuora_logins = task_data
|
525
|
+
@appinstance.save(:validate => false)
|
446
526
|
else
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
:
|
452
|
-
|
453
|
-
|
527
|
+
@appinstance = new_instance(
|
528
|
+
next_id,
|
529
|
+
zuora_entity_id,
|
530
|
+
zuora_client.rest_domain,
|
531
|
+
task_data: task_data,
|
532
|
+
retry_count: retry_count
|
533
|
+
)
|
454
534
|
end
|
455
535
|
end
|
536
|
+
rescue ActiveRecord::RecordNotUnique
|
537
|
+
retry if (retry_count -= 1).positive?
|
538
|
+
return
|
456
539
|
end
|
457
540
|
|
458
541
|
Apartment::Tenant.switch!("public")
|
@@ -540,6 +623,50 @@ module ZuoraConnect
|
|
540
623
|
end
|
541
624
|
end
|
542
625
|
|
626
|
+
def next_instance_id
|
627
|
+
min_instance_id = 24_999_999
|
628
|
+
(ZuoraConnect::AppInstance.all.where("id > #{min_instance_id}").order(id: :desc).limit(1).pluck(:id).first || min_instance_id) + 1
|
629
|
+
end
|
630
|
+
|
631
|
+
def new_instance(id, zuora_entity_id, rest_domain, task_data: nil, retry_count: 0)
|
632
|
+
app_instance = ZuoraConnect::AppInstance.new(
|
633
|
+
:id => id,
|
634
|
+
:api_token => generate_token,
|
635
|
+
:token => generate_token,
|
636
|
+
:oauth_expires_at => Time.now + 1000.years,
|
637
|
+
:zuora_domain => rest_domain,
|
638
|
+
:zuora_entity_ids => [zuora_entity_id]
|
639
|
+
)
|
640
|
+
|
641
|
+
if task_data.nil?
|
642
|
+
# no encryption
|
643
|
+
app_instance['zuora_logins'] = task_data
|
644
|
+
else
|
645
|
+
# kms encrypt
|
646
|
+
app_instance.zuora_logins = task_data
|
647
|
+
end
|
648
|
+
|
649
|
+
begin
|
650
|
+
app_instance.save(:validate => false)
|
651
|
+
rescue ActiveRecord::RecordNotUnique
|
652
|
+
raise if retry_count > 1
|
653
|
+
|
654
|
+
Thread.current[:appinstance] = nil
|
655
|
+
session['appInstance'] = nil
|
656
|
+
render 'zuora_connect/static/error_handled', :locals => {
|
657
|
+
:title => 'Application could not create unique tokens.',
|
658
|
+
:message => 'Please contact support or retry launching application.'
|
659
|
+
}, :layout => false
|
660
|
+
return
|
661
|
+
end
|
662
|
+
|
663
|
+
app_instance
|
664
|
+
end
|
665
|
+
|
666
|
+
def generate_token
|
667
|
+
rand(36**64).to_s(36)
|
668
|
+
end
|
669
|
+
|
543
670
|
def setup_instance_via_dev_mode
|
544
671
|
session["appInstance"] = ZuoraConnect.configuration.dev_mode_appinstance
|
545
672
|
user = ZuoraConnect.configuration.dev_mode_user
|
@@ -28,11 +28,6 @@ module ZuoraConnect
|
|
28
28
|
::Rails.configuration.action_dispatch.x_sendfile_header = nil
|
29
29
|
end
|
30
30
|
|
31
|
-
if defined? Prometheus
|
32
|
-
initializer "prometheus.configure_rails_initialization" do |app|
|
33
|
-
app.middleware.use Prometheus::Middleware::Exporter,(options ={:path => '/connect/internal/metrics'})
|
34
|
-
end
|
35
|
-
end
|
36
31
|
initializer "zuora_connect.configure_rails_initialization" do |app|
|
37
32
|
app.middleware.insert_after Rack::Sendfile, ZuoraConnect::MetricsMiddleware
|
38
33
|
app.middleware.insert_after ActionDispatch::RequestId, ZuoraConnect::RequestIdMiddleware
|
@@ -40,6 +35,15 @@ module ZuoraConnect
|
|
40
35
|
app.config.middleware.use ZuoraConnect::JsonParseErrors
|
41
36
|
end
|
42
37
|
|
38
|
+
if defined? Prometheus
|
39
|
+
require 'rack'
|
40
|
+
require 'prometheus/middleware/exporter'
|
41
|
+
initializer "prometheus.configure_rails_initialization" do |app|
|
42
|
+
app.middleware.insert_after ZuoraConnect::MetricsMiddleware, Prometheus::Middleware::Exporter, path: '/connect/internal/metrics'
|
43
|
+
app.config.middleware.use Rack::Deflater, if: ->(env, *) { env['PATH_INFO'] == '/connect/internal/metrics' }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
43
47
|
# hook to process_action
|
44
48
|
ActiveSupport::Notifications.subscribe('process_action.action_controller', ZuoraConnect::PageRequest.new)
|
45
49
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: zuora_connect
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.
|
4
|
+
version: 2.0.60e
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Connect Team
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-08-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: apartment
|
@@ -429,53 +429,53 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
429
429
|
version: '0'
|
430
430
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
431
431
|
requirements:
|
432
|
-
- - "
|
432
|
+
- - ">"
|
433
433
|
- !ruby/object:Gem::Version
|
434
|
-
version:
|
434
|
+
version: 1.3.1
|
435
435
|
requirements: []
|
436
436
|
rubygems_version: 3.0.3
|
437
437
|
signing_key:
|
438
438
|
specification_version: 4
|
439
439
|
summary: Summary of Connect.
|
440
440
|
test_files:
|
441
|
-
- test/integration/navigation_test.rb
|
442
|
-
- test/controllers/zuora_connect/api/v1/app_instance_controller_test.rb
|
443
441
|
- test/fixtures/zuora_connect/app_instances.yml
|
444
|
-
- test/lib/generators/zuora_connect/datatable_generator_test.rb
|
445
442
|
- test/models/zuora_connect/app_instance_test.rb
|
443
|
+
- test/integration/navigation_test.rb
|
444
|
+
- test/controllers/zuora_connect/api/v1/app_instance_controller_test.rb
|
446
445
|
- test/zuora_connect_test.rb
|
447
|
-
- test/
|
448
|
-
- test/dummy/config.ru
|
449
|
-
- test/dummy/public/422.html
|
450
|
-
- test/dummy/public/404.html
|
446
|
+
- test/lib/generators/zuora_connect/datatable_generator_test.rb
|
451
447
|
- test/dummy/public/500.html
|
448
|
+
- test/dummy/public/404.html
|
452
449
|
- test/dummy/public/favicon.ico
|
453
|
-
- test/dummy/
|
454
|
-
- test/dummy/
|
455
|
-
- test/dummy/
|
456
|
-
- test/dummy/app/assets/javascripts/application.js
|
457
|
-
- test/dummy/app/assets/stylesheets/application.css
|
458
|
-
- test/dummy/README.rdoc
|
459
|
-
- test/dummy/bin/rails
|
460
|
-
- test/dummy/bin/rake
|
461
|
-
- test/dummy/bin/bundle
|
462
|
-
- test/dummy/bin/setup
|
450
|
+
- test/dummy/public/422.html
|
451
|
+
- test/dummy/Rakefile
|
452
|
+
- test/dummy/config.ru
|
463
453
|
- test/dummy/config/secrets.yml
|
464
454
|
- test/dummy/config/boot.rb
|
465
|
-
- test/dummy/config/
|
466
|
-
- test/dummy/config/initializers/filter_parameter_logging.rb
|
455
|
+
- test/dummy/config/application.rb
|
467
456
|
- test/dummy/config/initializers/session_store.rb
|
468
|
-
- test/dummy/config/initializers/inflections.rb
|
469
|
-
- test/dummy/config/initializers/cookies_serializer.rb
|
470
457
|
- test/dummy/config/initializers/assets.rb
|
471
458
|
- test/dummy/config/initializers/wrap_parameters.rb
|
459
|
+
- test/dummy/config/initializers/filter_parameter_logging.rb
|
472
460
|
- test/dummy/config/initializers/backtrace_silencers.rb
|
473
|
-
- test/dummy/config/
|
474
|
-
- test/dummy/config/
|
475
|
-
- test/dummy/config/
|
461
|
+
- test/dummy/config/initializers/mime_types.rb
|
462
|
+
- test/dummy/config/initializers/inflections.rb
|
463
|
+
- test/dummy/config/initializers/cookies_serializer.rb
|
476
464
|
- test/dummy/config/routes.rb
|
465
|
+
- test/dummy/config/database.yml
|
466
|
+
- test/dummy/config/environments/test.rb
|
477
467
|
- test/dummy/config/environments/production.rb
|
478
468
|
- test/dummy/config/environments/development.rb
|
479
|
-
- test/dummy/config/
|
469
|
+
- test/dummy/config/environment.rb
|
480
470
|
- test/dummy/config/locales/en.yml
|
471
|
+
- test/dummy/README.rdoc
|
472
|
+
- test/dummy/bin/rake
|
473
|
+
- test/dummy/bin/setup
|
474
|
+
- test/dummy/bin/rails
|
475
|
+
- test/dummy/bin/bundle
|
476
|
+
- test/dummy/app/views/layouts/application.html.erb
|
477
|
+
- test/dummy/app/assets/javascripts/application.js
|
478
|
+
- test/dummy/app/assets/stylesheets/application.css
|
479
|
+
- test/dummy/app/helpers/application_helper.rb
|
480
|
+
- test/dummy/app/controllers/application_controller.rb
|
481
481
|
- test/test_helper.rb
|