Skip to content

Commit b052056

Browse files
Merge pull request #1384 from travis-ci/prd_custom_images_us7_am
custom images ms3
2 parents a7d7caf + 2f01543 commit b052056

36 files changed

+633
-10
lines changed

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

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def push(event, payload)
2727
end
2828

2929
def accept?
30-
current_user && permission? && resetable? && billing?
30+
current_user && permission? && resetable? && billing? && custom_image_create_allowed? && custom_image_use_allowed?
3131
end
3232

3333
def billing?
@@ -61,12 +61,64 @@ def billing?
6161
end
6262
end
6363

64+
def custom_image_create_allowed?
65+
return true if !!Travis.config.enterprise
66+
67+
@_custom_image_create_allowed ||= begin
68+
jobs = target.is_a?(Job) ? [target] : target.matrix
69+
jobs.map do |job|
70+
next unless job.config
71+
72+
create_name = job.config.dig(:vm, :create, :name)
73+
if create_name
74+
return false unless !!artifact_manager.create(owner: repository.owner, image_name: create_name, job_restart: true)
75+
end
76+
end
77+
true
78+
rescue Travis::API::V3::Error
79+
false
80+
end
81+
end
82+
83+
84+
def custom_image_use_allowed?
85+
return true if !!Travis.config.enterprise
86+
87+
@_custom_image_use_allowed ||= begin
88+
jobs = target.is_a?(Job) ? [target] : target.matrix
89+
90+
jobs.map do |job|
91+
next unless job.config
92+
93+
use_name = job.config.dig(:vm, :use)
94+
use_name = use_name[:name] if use_name.is_a?(Hash)
95+
96+
if use_name
97+
return false unless can_use_custom_image?(owner: repository.owner, image_name: use_name)
98+
end
99+
end
100+
true
101+
rescue Travis::API::V3::Error
102+
false
103+
end
104+
end
105+
106+
def can_use_custom_image?(owner:, image_name:)
107+
!!artifact_manager.use(owner: , image_name:)
108+
rescue Travis::API::V3::NotFound
109+
true
110+
rescue Travis::API::V3::Error
111+
false
112+
end
113+
64114
def messages
65115
messages = []
66116
messages << { notice: "The #{type} was successfully restarted." } if accept?
67117
messages << { error: 'You do not seem to have sufficient permissions.' } unless permission?
68118
messages << { error: 'You do not have enough credits.' } unless billing?
69119
messages << { error: "This #{type} currently can not be restarted." } unless resetable?
120+
messages << { error: "Image creation build restart not allowed." } unless custom_image_create_allowed?
121+
messages << { error: "Can't use the custom image. Make sure it's already created" } unless custom_image_use_allowed?
70122
messages
71123
end
72124

@@ -88,6 +140,10 @@ def target
88140

89141
private
90142

143+
def artifact_manager
144+
@_artifact_manger = Travis::API::V3::ArtifactManagerClient.new(current_user&.id)
145+
end
146+
91147
def subscription
92148
Subscription.where(owner: repository.owner)&.first
93149
end
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
module Travis::API::V3
2+
class ArtifactManagerClient
3+
class ConfigurationError < StandardError; end
4+
5+
def initialize(user_id)
6+
@user_id = user_id
7+
end
8+
9+
def create(owner:, image_name:, job_restart: false)
10+
params = {
11+
owner_type: owner.class.name.downcase,
12+
id: owner.id,
13+
name: image_name,
14+
job_restart:
15+
}
16+
handle_errors_and_respond(connection.post("/create", params)) do |body|
17+
body.include?('image_id') ? body['image_id'] : false
18+
end
19+
rescue Faraday::Error
20+
raise ArtifactManagerConnectionError
21+
end
22+
23+
def use(owner:, image_name:)
24+
handle_errors_and_respond(connection.get("/image/#{owner.class.name.downcase}/#{owner.id}/#{image_name}")) do |body|
25+
return body['image_id'] if body.include?('image_id')
26+
27+
return body['image']['id'] if body.include?('image')
28+
29+
false
30+
end
31+
rescue Faraday::Error
32+
raise ArtifactManagerConnectionError
33+
end
34+
35+
def images(owner_type, owner_id)
36+
response = connection.get("/images?owner_type=#{owner_type}&id=#{owner_id}")
37+
handle_images_response(response)
38+
end
39+
40+
def delete_images(owner_type, owner_id, image_ids)
41+
image_ids.each do |image_id|
42+
response = connection.delete("/image/#{owner_type.downcase}/#{owner_id}/#{image_id}")
43+
handle_errors_and_respond(response)
44+
end
45+
end
46+
47+
private
48+
49+
def handle_images_response(response)
50+
handle_errors_and_respond(response) do |r|
51+
r['images'].map { |image_data| Travis::API::V3::Models::CustomImage.new(image_data) }
52+
end
53+
end
54+
55+
def handle_errors_and_respond(response)
56+
body = response.body.is_a?(String) && response.body.length.positive? ? JSON.parse(response.body) : response.body
57+
58+
case response.status
59+
when 200, 201
60+
yield(body) if block_given?
61+
when 202
62+
true
63+
when 204
64+
true
65+
when 400
66+
raise Travis::API::V3::ClientError, body['error']
67+
when 403
68+
raise Travis::API::V3::InsufficientAccess, body['rejection_code']
69+
when 404
70+
raise Travis::API::V3::NotFound, body['error']
71+
when 422
72+
raise Travis::API::V3::UnprocessableEntity, body['error']
73+
else
74+
raise Travis::API::V3::ServerError, 'Artifact manager failed'
75+
end
76+
end
77+
78+
def connection(timeout: 10)
79+
@connection ||= Faraday.new(url: artifact_manager_url, ssl: { ca_path: '/usr/lib/ssl/certs' }) do |conn|
80+
conn.request(:authorization, :basic, '_', artifact_manager_auth_key)
81+
conn.headers['X-Travis-User-Id'] = @user_id.to_s
82+
conn.headers['Content-Type'] = 'application/json'
83+
conn.request :json
84+
conn.response :json
85+
conn.options[:open_timeout] = timeout
86+
conn.options[:timeout] = timeout
87+
conn.adapter :net_http
88+
end
89+
end
90+
91+
def artifact_manager_url
92+
Travis.config.artifact_manager&.url || raise(ConfigurationError, 'No artifact manager url configured')
93+
end
94+
95+
def artifact_manager_auth_key
96+
Travis.config.artifact_manager&.auth_key || raise(ConfigurationError, 'No artifact manager auth key configured')
97+
end
98+
end
99+
end

lib/travis/api/v3/billing_client.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,20 @@ def executions(owner_type, owner_id, page, per_page, from, to)
4747
executions
4848
end
4949

50+
def storage_usage(owner_type, owner_id, from, to)
51+
response = connection.get("/usage/#{owner_type.downcase}s/#{owner_id}/storage?from=#{from}&to=#{to}")
52+
body(response).map do |usage_data|
53+
usage_data
54+
end
55+
end
56+
57+
def storage_executions_usage(owner_type, owner_id)
58+
response = connection.get("/usage/#{owner_type.downcase}s/#{owner_id}/storage_executions")
59+
body(response).map do |usage_data|
60+
usage_data
61+
end
62+
end
63+
5064
def calculate_credits(users, executions)
5165
response = connection.post("/usage/credits_calculator", users: users, executions: executions)
5266
body(response).map do |calculator_data|
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
module Travis::API::V3
2+
class Models::CustomImage < Model
3+
after_initialize :readonly!
4+
belongs_to :owner, polymorphic: true
5+
has_many :custom_image_logs
6+
7+
scope :available, -> { where(state: 'available') }
8+
9+
def created_by
10+
user_id = custom_image_logs.created.first&.sender_id
11+
return unless user_id
12+
13+
User.find(user_id)
14+
end
15+
16+
def private
17+
true
18+
end
19+
end
20+
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module Travis::API::V3
2+
class Models::CustomImageLog < Model
3+
after_initialize :readonly!
4+
belongs_to :custom_image
5+
enum action: { created: 'created', used: 'used', deleted: 'deleted', other: 'other' }
6+
end
7+
end
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module Travis::API::V3
2+
class Models::CustomImageStorage < Model
3+
end
4+
end

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module Travis::API::V3
22
class Models::V2AddonUsage
3-
attr_reader :id, :addon_id, :addon_quantity, :addon_usage, :remaining, :purchase_date, :valid_to, :active, :status
3+
attr_reader :id, :addon_id, :addon_quantity, :addon_usage, :remaining, :purchase_date, :valid_to, :active, :status,
4+
:quantity_limit_free, :quantity_limit_type, :quantity_limit_charge, :total_usage
45

56
def initialize(attrs)
67
@id = attrs.fetch('id')
@@ -12,6 +13,10 @@ def initialize(attrs)
1213
@valid_to = attrs.fetch('valid_to')
1314
@active = attrs.fetch('active')
1415
@status = attrs.fetch('status')
16+
@quantity_limit_free = attrs.fetch('quantity_limit_free', 0)
17+
@quantity_limit_type = attrs.fetch('quantity_limit_type', nil)
18+
@quantity_limit_charge = attrs.fetch('quantity_limit_charge', nil)
19+
@total_usage = attrs.fetch('total_usage', nil)
1520
end
1621
end
1722
end
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
module Travis::API::V3
2+
class Queries::CustomImages < Query
3+
def for_owner(owner)
4+
Models::CustomImage.available.where(owner_id: owner.id, owner_type: owner_type(owner)).order('created_at DESC')
5+
end
6+
7+
def delete(image_ids, owner, sender)
8+
client = ArtifactManagerClient.new(sender.id)
9+
client.delete_images(owner_type(owner), owner.id, image_ids)
10+
end
11+
12+
def usage(owner, user_id, from, to)
13+
client = BillingClient.new(user_id)
14+
client.storage_usage(owner_type(owner), owner.id, from, to)
15+
end
16+
17+
def current_storage(owner, user_id)
18+
Models::CustomImageStorage.where(owner_type: owner_type(owner), owner_id: owner.id).order('id desc').limit(1).first
19+
end
20+
21+
def storage_executions_usage(owner, user_id)
22+
BillingClient.new(user_id).storage_executions_usage(owner_type(owner), owner.id)
23+
end
24+
25+
private
26+
27+
def owner_type(owner)
28+
owner.vcs_type =~ /User/ ? 'User' : 'Organization'
29+
end
30+
end
31+
end
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
module Travis::API::V3
2+
class Renderer::CustomImage < ModelRenderer
3+
representation :minimal, :id, :owner_id, :owner_type, :name, :usage, :created_at, :updated_at, :os_version,
4+
:created_by, :private, :size_bytes
5+
representation :standard, *representations[:minimal]
6+
7+
def created_by
8+
return nil unless user = model.created_by
9+
10+
{
11+
'@type' => 'user',
12+
'@href' => "/v3/user/#{user.id}",
13+
'@representation' => 'minimal'.freeze,
14+
'id' => user.id,
15+
'login' => user.login,
16+
'name' => user.name,
17+
}.tap do |data|
18+
data['avatar_url'] = user.avatar_url if user.email.present?
19+
end
20+
end
21+
end
22+
end
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module Travis::API::V3
2+
class Renderer::CustomImageStorage < ModelRenderer
3+
representation :minimal, :id, :owner_id, :owner_type, :current_aggregated_storage, :created_at, :updated_at, :end_date
4+
representation :standard, *representations[:minimal]
5+
end
6+
end

0 commit comments

Comments
 (0)