Skip to content

Commit fd3ddfe

Browse files
Merge pull request #1369 from travis-ci/prd_custom_images_us4_am
[M2-US4] Implement deleting custom images
2 parents 56ca7a9 + 4a66eb4 commit fd3ddfe

File tree

18 files changed

+385
-8
lines changed

18 files changed

+385
-8
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/models/custom_image.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class Models::CustomImage < Model
77
scope :available, -> { where(state: 'available') }
88

99
def created_by
10-
user_id = custom_image_logs.created.frist&.sender_id
10+
user_id = custom_image_logs.created.first&.sender_id
1111
return unless user_id
1212

1313
User.find(user_id)

lib/travis/api/v3/queries/custom_images.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
module Travis::API::V3
22
class Queries::CustomImages < Query
33
def for_owner(owner)
4-
Models::CustomImage.where(owner_id: owner.id, owner_type: owner_type(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)
510
end
611

712
private
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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ module Routes
173173
resource :custom_images do
174174
route '/custom_images'
175175
get :for_owner
176+
delete :delete
176177
end
177178
end
178179

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module Travis::API::V3
2+
class Services::CustomImages::Delete < Service
3+
params :image_ids
4+
5+
def run!
6+
raise LoginRequired unless access_control.full_access_or_logged_in?
7+
8+
owner = query(:owner).find
9+
raise NotFound unless owner
10+
11+
if owner.is_a?(Travis::API::V3::Models::User)
12+
access_control.permissions(owner).sync!
13+
else
14+
access_control.permissions(owner).admin!
15+
end
16+
17+
query.delete(params['image_ids'], owner, access_control.user)
18+
deleted
19+
end
20+
end
21+
end

lib/travis/api/v3/services/custom_images/for_owner.rb

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,16 @@ def run!
99
owner = query(:owner).find
1010

1111
raise NotFound unless owner
12-
repo = owner.repositories.first
13-
raise InsufficientAccess unless repo
14-
access_control.permissions(repo).build_create!
12+
13+
if owner.is_a?(Travis::API::V3::Models::User)
14+
raise InsufficientAccess unless access_control.user.id == owner.id
15+
else
16+
membership = Models::Membership.where(organization_id: owner.id).joins(:user).includes(:user).first
17+
raise NotFound unless membership
18+
19+
build_permission = membership.build_permission.nil? ? true : membership.build_permission
20+
raise InsufficientAccess unless build_permission
21+
end
1522

1623
results = query(:custom_images).for_owner(owner)
1724
result results

lib/travis/config/defaults.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ def fallback_logs_api_auth_token
106106
recaptcha: { endpoint: 'https://www.google.com', secret: ENV['RECAPTCHA_SECRET_KEY'] || '' },
107107
antifraud: { captcha_max_failed_attempts: 3, captcha_block_duration: 24, credit_card_max_failed_attempts: 3, credit_card_block_duration: 24 },
108108
legacy_roles: false,
109-
internal_users: [{id: 0, login: 'cron'}]
109+
internal_users: [{id: 0, login: 'cron'}],
110+
artifact_manager: { url: 'http://artifact_manager:3434', auth_key: 'secret' }
110111

111112
default :_access => [:key]
112113

0 commit comments

Comments
 (0)