Skip to content

Commit b450b70

Browse files
author
Shairyar Baig
authored
Merge pull request #1161 from travis-ci/epic-pricing-rc
The new billing system going live
2 parents c36bb78 + 3e73fc4 commit b450b70

File tree

103 files changed

+2818
-79
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

103 files changed

+2818
-79
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
language: ruby
22

33
import:
4-
- travis-ci/build-configs:db-setup.yml@bionic
4+
- travis-ci/build-configs:db-setup.yml@epic-pricing
55

66
rvm: 2.6.5
77

lib/travis/api/app/endpoint/authorization.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ class Authorization < Endpoint
106106
get '/handshake/?:provider?' do
107107
method = org? ? :handshake : :vcs_handshake
108108
params[:provider] ||= 'github'
109-
110109
send(method) do |user, token, redirect_uri|
111110
if target_ok? redirect_uri
112111
content_type :html
@@ -212,7 +211,9 @@ def vcs_handshake
212211
return
213212
end
214213

215-
yield serialize_user(User.find(vcs_data['user']['id'])), vcs_data['token'], payload(params[:provider])
214+
user = User.find(vcs_data['user']['id'])
215+
update_first_login(user)
216+
yield serialize_user(user), vcs_data['token'], payload(params[:provider])
216217
else
217218
state = vcs_create_state(params[:origin] || params[:redirect_uri])
218219

lib/travis/api/enqueue/services/restart_model.rb

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module Services
44

55
class RestartModel
66
attr_reader :current_user, :target
7-
ABUSE_DETECTED = "abuse_detected"
7+
ABUSE_DETECTED = 'abuse_detected'
88

99
def initialize(current_user, params)
1010
@current_user = current_user
@@ -15,23 +15,43 @@ def initialize(current_user, params)
1515
def push(event, payload)
1616
if current_user && target && accept?
1717
::Sidekiq::Client.push(
18-
'queue' => 'hub',
19-
'class' => 'Travis::Hub::Sidekiq::Worker',
20-
'args' => [event, payload]
21-
)
18+
'queue' => 'hub',
19+
'class' => 'Travis::Hub::Sidekiq::Worker',
20+
'args' => [event, payload]
21+
)
22+
23+
Result.new(value: payload)
2224
else
23-
@cause_of_denial
25+
Result.new(error: @cause_of_denial || 'restart failed')
2426
end
2527
end
2628

2729
def accept?
28-
current_user && permission? && resetable?
30+
current_user && permission? && resetable? && billing?
31+
end
32+
33+
def billing?
34+
@_billing_ok ||= begin
35+
jobs = target.is_a?(Job) ? [target] : target.matrix
36+
37+
jobs_attrs = jobs.map do |job|
38+
job.config ? job.config.slice(:os) : {}
39+
end
40+
41+
client = Travis::API::V3::BillingClient.new(current_user.id)
42+
client.authorize_build(repository, current_user.id, jobs_attrs)
43+
true
44+
rescue Travis::API::V3::InsufficientAccess => e
45+
@cause_of_denial = e.message
46+
false
47+
end
2948
end
3049

3150
def messages
3251
messages = []
3352
messages << { notice: "The #{type} was successfully restarted." } if accept?
3453
messages << { error: 'You do not seem to have sufficient permissions.' } unless permission?
54+
messages << { error: 'You do not have enough credits.' } unless billing?
3555
messages << { error: "This #{type} currently can not be restarted." } unless resetable?
3656
messages
3757
end
@@ -54,23 +74,36 @@ def target
5474

5575
private
5676

57-
def permission?
58-
current_user && current_user.permission?(required_role, repository_id: target.repository_id) && !abusive?
59-
end
77+
def permission?
78+
current_user && current_user.permission?(required_role, repository_id: target.repository_id) && !abusive?
79+
end
6080

61-
def abusive?
62-
abusive = Travis.redis.sismember("abuse:offenders", "#{@target.owner.class.name}:#{@target.owner_id}")
63-
@cause_of_denial = ABUSE_DETECTED if abusive
64-
abusive
65-
end
81+
def abusive?
82+
abusive = Travis.redis.sismember("abuse:offenders", "#{@target.owner.class.name}:#{@target.owner_id}")
83+
@cause_of_denial = ABUSE_DETECTED if abusive
84+
abusive
85+
end
86+
87+
def resetable?
88+
target.resetable?
89+
end
6690

67-
def resetable?
68-
target.resetable?
91+
def required_role
92+
Travis.config.roles.reset_model
93+
end
94+
95+
class Result
96+
attr_reader :error, :value
97+
98+
def initialize(value: nil, error: nil)
99+
@value = value
100+
@error = error
69101
end
70102

71-
def required_role
72-
Travis.config.roles.reset_model
103+
def success?
104+
!@error
73105
end
106+
end
74107
end
75108
end
76109
end

lib/travis/api/v3/billing_client.rb

Lines changed: 122 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,46 @@ module Travis::API::V3
22
class BillingClient
33
class ConfigurationError < StandardError; end
44

5+
ALLOWANCE_TIMEOUT = 1 # second
6+
57
def initialize(user_id)
68
@user_id = user_id
79
end
810

11+
def allowance(owner_type, owner_id)
12+
response = connection(timeout: ALLOWANCE_TIMEOUT).get("/usage/#{owner_type.downcase}s/#{owner_id}/allowance")
13+
return BillingClient.default_allowance_response unless response.status == 200
14+
15+
Travis::API::V3::Models::Allowance.new(2, owner_id, response.body)
16+
end
17+
18+
def authorize_build(repo, sender_id, jobs)
19+
response = connection.post("/#{repo.owner.class.name.downcase.pluralize}/#{repo.owner.id}/authorize_build", { repository: { private: repo.private? }, sender_id: sender_id, jobs: jobs })
20+
handle_errors_and_respond(response)
21+
end
22+
23+
def self.default_allowance_response(id = 0)
24+
Travis::API::V3::Models::Allowance.new(1, id, {
25+
"public_repos" => true,
26+
"private_repos" => false,
27+
"concurrency_limit" => 1,
28+
"user_usage" => false,
29+
"pending_user_licenses" => false
30+
}.freeze)
31+
end
32+
33+
def self.minimal_allowance_response(id = 0)
34+
Travis::API::V3::Models::Allowance.new(2, id, {})
35+
end
36+
37+
def executions(owner_type, owner_id, page, per_page, from, to)
38+
response = connection.get("/usage/#{owner_type.downcase}s/#{owner_id}/executions?page=#{page}&per_page=#{per_page}&from=#{from}&to=#{to}")
39+
executions = response.body.map do |execution_data|
40+
Travis::API::V3::Models::Execution.new(execution_data)
41+
end
42+
executions
43+
end
44+
945
def all
1046
data = connection.get('/subscriptions').body
1147
subscriptions = data.fetch('subscriptions').map do |subscription_data|
@@ -16,17 +52,38 @@ def all
1652
Travis::API::V3::Models::SubscriptionsCollection.new(subscriptions, permissions)
1753
end
1854

55+
def all_v2
56+
data = connection.get('/v2/subscriptions').body
57+
subscriptions = data.fetch('plans').map do |subscription_data|
58+
Travis::API::V3::Models::V2Subscription.new(subscription_data)
59+
end
60+
permissions = data.fetch('permissions')
61+
62+
Travis::API::V3::Models::SubscriptionsCollection.new(subscriptions, permissions)
63+
end
64+
1965
def get_subscription(id)
2066
response = connection.get("/subscriptions/#{id}")
2167
handle_subscription_response(response)
2268
end
2369

70+
def get_v2_subscription(id)
71+
response = connection.get("/v2/subscriptions/#{id}")
72+
handle_v2_subscription_response(response)
73+
end
74+
2475
def get_invoices_for_subscription(id)
2576
connection.get("/subscriptions/#{id}/invoices").body.map do |invoice_data|
2677
Travis::API::V3::Models::Invoice.new(invoice_data)
2778
end
2879
end
2980

81+
def get_invoices_for_v2_subscription(id)
82+
connection.get("/v2/subscriptions/#{id}/invoices").body.map do |invoice_data|
83+
Travis::API::V3::Models::Invoice.new(invoice_data)
84+
end
85+
end
86+
3087
def trials
3188
connection.get('/trials').body.map do | trial_data |
3289
Travis::API::V3::Models::Trial.new(trial_data)
@@ -43,11 +100,21 @@ def update_address(subscription_id, address_data)
43100
handle_subscription_response(response)
44101
end
45102

103+
def update_v2_address(subscription_id, address_data)
104+
response = connection.patch("/v2/subscriptions/#{subscription_id}/address", address_data)
105+
handle_v2_subscription_response(response)
106+
end
107+
46108
def update_creditcard(subscription_id, creditcard_token)
47109
response = connection.patch("/subscriptions/#{subscription_id}/creditcard", token: creditcard_token)
48110
handle_subscription_response(response)
49111
end
50112

113+
def update_v2_creditcard(subscription_id, creditcard_token)
114+
response = connection.patch("/v2/subscriptions/#{subscription_id}/creditcard", token: creditcard_token)
115+
handle_v2_subscription_response(response)
116+
end
117+
51118
def update_plan(subscription_id, plan_data)
52119
response = connection.patch("/subscriptions/#{subscription_id}/plan", plan_data)
53120
handle_subscription_response(response)
@@ -58,6 +125,44 @@ def create_subscription(subscription_data)
58125
handle_subscription_response(response)
59126
end
60127

128+
def create_v2_subscription(subscription_data)
129+
response = connection.post('/v2/subscriptions', subscription_data)
130+
handle_v2_subscription_response(response)
131+
end
132+
133+
def changetofree_v2_subscription(subscription_id, data)
134+
response = connection.patch("/v2/subscriptions/#{subscription_id}/changetofree", data)
135+
handle_v2_subscription_response(response)
136+
end
137+
138+
def update_v2_subscription(subscription_id, plan_data)
139+
response = connection.patch("/v2/subscriptions/#{subscription_id}/plan", plan_data)
140+
handle_v2_subscription_response(response)
141+
end
142+
143+
def purchase_addon(subscription_id, addon_config_id)
144+
response = connection.patch("/v2/subscriptions/#{subscription_id}/addon", { addon: addon_config_id })
145+
handle_v2_subscription_response(response)
146+
end
147+
148+
def v2_subscription_user_usages(subscription_id)
149+
connection.get("/v2/subscriptions/#{subscription_id}/user_usage").body.map do |usage_data|
150+
Travis::API::V3::Models::V2AddonUsage.new(usage_data)
151+
end
152+
end
153+
154+
def v2_plans_for_organization(organization_id)
155+
connection.get("/v2/plans_for/organization/#{organization_id}").body.map do |plan_data|
156+
Travis::API::V3::Models::V2PlanConfig.new(plan_data)
157+
end
158+
end
159+
160+
def v2_plans_for_user
161+
connection.get('/v2/plans_for/user').body.map do |plan_data|
162+
Travis::API::V3::Models::V2PlanConfig.new(plan_data)
163+
end
164+
end
165+
61166
def cancel_subscription(id, reason_data)
62167
response = connection.post("/subscriptions/#{id}/cancel", reason_data)
63168
handle_subscription_response(response)
@@ -70,7 +175,7 @@ def plans_for_organization(organization_id)
70175
end
71176

72177
def plans_for_user
73-
connection.get("/plans_for/user").body.map do |plan_data|
178+
connection.get('/plans_for/user').body.map do |plan_data|
74179
Travis::API::V3::Models::Plan.new(plan_data)
75180
end
76181
end
@@ -85,6 +190,11 @@ def pay(id)
85190
handle_subscription_response(response)
86191
end
87192

193+
def pay_v2(id)
194+
response = connection.post("/v2/subscriptions/#{id}/pay")
195+
handle_v2_subscription_response(response)
196+
end
197+
88198
def get_coupon(code)
89199
response = connection.get("/coupons/#{code}")
90200
handle_coupon_response(response)
@@ -101,6 +211,10 @@ def handle_subscription_response(response)
101211
handle_errors_and_respond(response) { |r| Travis::API::V3::Models::Subscription.new(r) }
102212
end
103213

214+
def handle_v2_subscription_response(response)
215+
handle_errors_and_respond(response) { |r| Travis::API::V3::Models::V2Subscription.new(r) }
216+
end
217+
104218
def handle_coupon_response(response)
105219
handle_errors_and_respond(response) { |r| Travis::API::V3::Models::Coupon.new(r) }
106220
end
@@ -113,24 +227,28 @@ def handle_errors_and_respond(response)
113227
true
114228
when 204
115229
true
116-
when 404
117-
raise Travis::API::V3::NotFound, response.body['error']
118230
when 400
119231
raise Travis::API::V3::ClientError, response.body['error']
232+
when 403
233+
raise Travis::API::V3::InsufficientAccess, response.body['rejection_code']
234+
when 404
235+
raise Travis::API::V3::NotFound, response.body['error']
120236
when 422
121237
raise Travis::API::V3::UnprocessableEntity, response.body['error']
122238
else
123239
raise Travis::API::V3::ServerError, 'Billing system failed'
124240
end
125241
end
126242

127-
def connection
243+
def connection(timeout: 10)
128244
@connection ||= Faraday.new(url: billing_url, ssl: { ca_path: '/usr/lib/ssl/certs' }) do |conn|
129245
conn.basic_auth '_', billing_auth_key
130246
conn.headers['X-Travis-User-Id'] = @user_id.to_s
131247
conn.headers['Content-Type'] = 'application/json'
132248
conn.request :json
133249
conn.response :json
250+
conn.options[:open_timeout] = timeout
251+
conn.options[:timeout] = timeout
134252
conn.use OpenCensus::Trace::Integrations::FaradayMiddleware if Travis::Api::App::Middleware::OpenCensus.enabled?
135253
conn.adapter :net_http
136254
end

lib/travis/api/v3/models/allowance.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module Travis::API::V3
2+
class Models::Allowance
3+
attr_reader :subscription_type, :public_repos, :private_repos, :concurrency_limit, :user_usage, :pending_user_licenses, :id
4+
5+
def initialize(subscription_type, owner_id, attributes = {})
6+
@subscription_type = subscription_type
7+
@id = owner_id
8+
@public_repos = attributes.fetch('public_repos', nil)
9+
@private_repos = attributes.fetch('private_repos', nil)
10+
@concurrency_limit = attributes.fetch('concurrency_limit', nil)
11+
@user_usage = attributes.fetch('user_usage', nil)
12+
@pending_user_licenses = attributes.fetch('pending_user_licenses', nil)
13+
end
14+
end
15+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module Travis::API::V3
2+
class Models::BuildPermission
3+
attr_accessor :user, :permission, :role
4+
5+
def initialize(attrs = {})
6+
@user = attrs.fetch(:user)
7+
@role = attrs.fetch(:role)
8+
@permission = attrs.fetch(:permission)
9+
end
10+
end
11+
end

0 commit comments

Comments
 (0)