Skip to content

Commit ef0dcbe

Browse files
Merge pull request #1367 from travis-ci/prd_custom_images_us3_am
Custom images list implementation
2 parents a7d7caf + fd3ddfe commit ef0dcbe

File tree

22 files changed

+472
-3
lines changed

22 files changed

+472
-3
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
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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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+
private
13+
14+
def owner_type(owner)
15+
owner.vcs_type =~ /User/ ? 'User' : 'Organization'
16+
end
17+
end
18+
end
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 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+
'@type' => 'user',
11+
'@href' => "/v3/user/#{user.id}",
12+
'@representation' => 'minimal'.freeze,
13+
'id' => user.id,
14+
'login' => user.login,
15+
'name' => user.name,
16+
'avatar_url' => user.avatar_url
17+
}
18+
end
19+
end
20+
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::CustomImages < CollectionRenderer
3+
type :custom_images
4+
collection_key :custom_images
5+
end
6+
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::Membership < ModelRenderer
3+
representation(:minimal, :organization_id, :build_permission)
4+
representation(:standard, :user_id, :organization_id, :role, :build_permission)
5+
end
6+
end

lib/travis/api/v3/renderer/user.rb

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

33
module Travis::API::V3
44
class Renderer::User < Renderer::Owner
5-
representation(:standard, :email, :is_syncing, :synced_at, :recently_signed_up, :secure_user_hash, :ro_mode, :confirmed_at, :custom_keys, :internal, :last_activity_at)
5+
representation(:standard, :email, :is_syncing, :synced_at, :recently_signed_up, :secure_user_hash, :ro_mode, :confirmed_at, :custom_keys, :internal, :last_activity_at, :memberships)
66
representation(:additional, :emails, :collaborator)
77

88
def email

lib/travis/api/v3/routes.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,12 @@ module Routes
169169
route '/executions_per_sender'
170170
get :for_owner_per_sender
171171
end
172+
173+
resource :custom_images do
174+
route '/custom_images'
175+
get :for_owner
176+
delete :delete
177+
end
172178
end
173179

174180
resource :credits_calculator do

0 commit comments

Comments
 (0)