Skip to content

[M2-US4] Implement deleting custom images #1369

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: prd_custom_images_us3_am
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion lib/travis/api/enqueue/services/restart_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def push(event, payload)
end

def accept?
current_user && permission? && resetable? && billing?
current_user && permission? && resetable? && billing? && custom_image_create_allowed? && custom_image_use_allowed?
end

def billing?
Expand Down Expand Up @@ -61,12 +61,64 @@ def billing?
end
end

def custom_image_create_allowed?
return true if !!Travis.config.enterprise

@_custom_image_create_allowed ||= begin
jobs = target.is_a?(Job) ? [target] : target.matrix
jobs.map do |job|
next unless job.config

create_name = job.config.dig(:vm, :create, :name)
if create_name
return false unless !!artifact_manager.create(owner: repository.owner, image_name: create_name, job_restart: true)
end
end
true
rescue Travis::API::V3::Error
false
end
end


def custom_image_use_allowed?
return true if !!Travis.config.enterprise

@_custom_image_use_allowed ||= begin
jobs = target.is_a?(Job) ? [target] : target.matrix

jobs.map do |job|
next unless job.config

use_name = job.config.dig(:vm, :use)
use_name = use_name[:name] if use_name.is_a?(Hash)

if use_name
return false unless can_use_custom_image?(owner: repository.owner, image_name: use_name)
end
end
true
rescue Travis::API::V3::Error
false
end
end

def can_use_custom_image?(owner:, image_name:)
!!artifact_manager.use(owner: , image_name:)
rescue Travis::API::V3::NotFound
true
rescue Travis::API::V3::Error
false
end

def messages
messages = []
messages << { notice: "The #{type} was successfully restarted." } if accept?
messages << { error: 'You do not seem to have sufficient permissions.' } unless permission?
messages << { error: 'You do not have enough credits.' } unless billing?
messages << { error: "This #{type} currently can not be restarted." } unless resetable?
messages << { error: "Image creation build restart not allowed." } unless custom_image_create_allowed?
messages << { error: "Can't use the custom image. Make sure it's already created" } unless custom_image_use_allowed?
messages
end

Expand All @@ -88,6 +140,10 @@ def target

private

def artifact_manager
@_artifact_manger = Travis::API::V3::ArtifactManagerClient.new(current_user&.id)
end

def subscription
Subscription.where(owner: repository.owner)&.first
end
Expand Down
99 changes: 99 additions & 0 deletions lib/travis/api/v3/artifact_manager_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
module Travis::API::V3
class ArtifactManagerClient
class ConfigurationError < StandardError; end

def initialize(user_id)
@user_id = user_id
end

def create(owner:, image_name:, job_restart: false)
params = {
owner_type: owner.class.name.downcase,
id: owner.id,
name: image_name,
job_restart:
}
handle_errors_and_respond(connection.post("/create", params)) do |body|
body.include?('image_id') ? body['image_id'] : false
end
rescue Faraday::Error
raise ArtifactManagerConnectionError
end

def use(owner:, image_name:)
handle_errors_and_respond(connection.get("/image/#{owner.class.name.downcase}/#{owner.id}/#{image_name}")) do |body|
return body['image_id'] if body.include?('image_id')

return body['image']['id'] if body.include?('image')

false
end
rescue Faraday::Error
raise ArtifactManagerConnectionError
end

def images(owner_type, owner_id)
response = connection.get("/images?owner_type=#{owner_type}&id=#{owner_id}")
handle_images_response(response)
end

def delete_images(owner_type, owner_id, image_ids)
image_ids.each do |image_id|
response = connection.delete("/image/#{owner_type.downcase}/#{owner_id}/#{image_id}")
handle_errors_and_respond(response)
end
end

private

def handle_images_response(response)
handle_errors_and_respond(response) do |r|
r['images'].map { |image_data| Travis::API::V3::Models::CustomImage.new(image_data) }
end
end

def handle_errors_and_respond(response)
body = response.body.is_a?(String) && response.body.length.positive? ? JSON.parse(response.body) : response.body

case response.status
when 200, 201
yield(body) if block_given?
when 202
true
when 204
true
when 400
raise Travis::API::V3::ClientError, body['error']
when 403
raise Travis::API::V3::InsufficientAccess, body['rejection_code']
when 404
raise Travis::API::V3::NotFound, body['error']
when 422
raise Travis::API::V3::UnprocessableEntity, body['error']
else
raise Travis::API::V3::ServerError, 'Artifact manager failed'
end
end

def connection(timeout: 10)
@connection ||= Faraday.new(url: artifact_manager_url, ssl: { ca_path: '/usr/lib/ssl/certs' }) do |conn|
conn.request(:authorization, :basic, '_', artifact_manager_auth_key)
conn.headers['X-Travis-User-Id'] = @user_id.to_s
conn.headers['Content-Type'] = 'application/json'
conn.request :json
conn.response :json
conn.options[:open_timeout] = timeout
conn.options[:timeout] = timeout
conn.adapter :net_http
end
end

def artifact_manager_url
Travis.config.artifact_manager&.url || raise(ConfigurationError, 'No artifact manager url configured')
end

def artifact_manager_auth_key
Travis.config.artifact_manager&.auth_key || raise(ConfigurationError, 'No artifact manager auth key configured')
end
end
end
2 changes: 1 addition & 1 deletion lib/travis/api/v3/models/custom_image.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class Models::CustomImage < Model
scope :available, -> { where(state: 'available') }

def created_by
user_id = custom_image_logs.where(action: 'created').first&.sender_id
user_id = custom_image_logs.created.first&.sender_id
return unless user_id

User.find(user_id)
Expand Down
1 change: 1 addition & 0 deletions lib/travis/api/v3/models/custom_image_log.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ module Travis::API::V3
class Models::CustomImageLog < Model
after_initialize :readonly!
belongs_to :custom_image
enum action: { created: 'created', used: 'used', deleted: 'deleted', other: 'other' }
end
end
7 changes: 6 additions & 1 deletion lib/travis/api/v3/queries/custom_images.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
module Travis::API::V3
class Queries::CustomImages < Query
def for_owner(owner)
Models::CustomImage.where(owner_id: owner.id, owner_type: owner_type(owner))
Models::CustomImage.available.where(owner_id: owner.id, owner_type: owner_type(owner)).order('created_at DESC')
end

def delete(image_ids, owner, sender)
client = ArtifactManagerClient.new(sender.id)
client.delete_images(owner_type(owner), owner.id, image_ids)
end

private
Expand Down
6 changes: 6 additions & 0 deletions lib/travis/api/v3/renderer/membership.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module Travis::API::V3
class Renderer::Membership < ModelRenderer
representation(:minimal, :organization_id, :build_permission)
representation(:standard, :user_id, :organization_id, :role, :build_permission)
end
end
2 changes: 1 addition & 1 deletion lib/travis/api/v3/renderer/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

def email
Expand Down
1 change: 1 addition & 0 deletions lib/travis/api/v3/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ module Routes
resource :custom_images do
route '/custom_images'
get :for_owner
delete :delete
end
end

Expand Down
21 changes: 21 additions & 0 deletions lib/travis/api/v3/services/custom_images/delete.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module Travis::API::V3
class Services::CustomImages::Delete < Service
params :image_ids

def run!
raise LoginRequired unless access_control.full_access_or_logged_in?

owner = query(:owner).find
raise NotFound unless owner

if owner.is_a?(Travis::API::V3::Models::User)
access_control.permissions(owner).sync!
else
access_control.permissions(owner).admin!
end

query.delete(params['image_ids'], owner, access_control.user)
deleted
end
end
end
13 changes: 10 additions & 3 deletions lib/travis/api/v3/services/custom_images/for_owner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@ def run!
owner = query(:owner).find

raise NotFound unless owner
repo = owner.repositories.first
raise InsufficientAccess unless repo
access_control.permissions(repo).build_create!

if owner.is_a?(Travis::API::V3::Models::User)
raise InsufficientAccess unless access_control.user.id == owner.id
else
membership = Models::Membership.where(organization_id: owner.id).joins(:user).includes(:user).first
raise NotFound unless membership

build_permission = membership.build_permission.nil? ? true : membership.build_permission
raise InsufficientAccess unless build_permission
end

results = query(:custom_images).for_owner(owner)
result results
Expand Down
3 changes: 2 additions & 1 deletion lib/travis/config/defaults.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ def fallback_logs_api_auth_token
recaptcha: { endpoint: 'https://www.google.com', secret: ENV['RECAPTCHA_SECRET_KEY'] || '' },
antifraud: { captcha_max_failed_attempts: 3, captcha_block_duration: 24, credit_card_max_failed_attempts: 3, credit_card_block_duration: 24 },
legacy_roles: false,
internal_users: [{id: 0, login: 'cron'}]
internal_users: [{id: 0, login: 'cron'}],
artifact_manager: { url: 'http://artifact_manager:3434', auth_key: 'secret' }

default :_access => [:key]

Expand Down
27 changes: 27 additions & 0 deletions lib/travis/testing/factories.rb
Original file line number Diff line number Diff line change
Expand Up @@ -244,4 +244,31 @@
owner_type { 'User' }
accepted_at { nil }
end

factory :custom_image, class: Travis::API::V3::Models::CustomImage do
name { 'custom_image_name' }
description { 'custom_image_description' }
state { 'available' }
owner { User.first || FactoryBot.create(:user) }
owner_type { 'User' }
architecture { 'x86' }
size_bytes { 1024 }
created_at { Time.now.utc }
updated_at { Time.now.utc }

after(:build) do |custom_image|
custom_image.define_singleton_method(:readonly?) { false }
end
end

factory :custom_image_log, class: Travis::API::V3::Models::CustomImageLog do
custom_image { CustomImage.first || FactoryBot.create(:custom_image) }
action { 'created' }
sender_id { User.first.id || FactoryBot.create(:user).id }
created_at { Time.now.utc }

after(:build) do |custom_image_log|
custom_image_log.define_singleton_method(:readonly?) { false }
end
end
end
24 changes: 24 additions & 0 deletions spec/lib/travis/api/enqueue/services/restart_model_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,30 @@
end
end
end

context 'when trying to restart a custom image build' do
let(:job) { FactoryBot.create(:job, repository: repository, state: 'canceled', config: { vm: { create: { name: 'testimg1'} } }) }

before do
repository.permissions.create(user: user, build: true)
Travis.config.artifact_manager = { url: 'http://localhost:9911' , auth_key: 'test_test'}
stub_request(:post, "http://localhost:9911/create").to_return(status: 200, body: { image_id: 1}.to_json)
end

include_examples 'restarts the job'
end

context 'when trying to restart a custom image build without permission' do
let(:job) { FactoryBot.create(:job, repository: repository, state: 'canceled', config: { vm: { create: { name: 'testimg1'} } }) }

before do
repository.permissions.create(user: user, build: true)
Travis.config.artifact_manager = { url: 'http://localhost:9911' , auth_key: 'test_test'}
stub_request(:post, "http://localhost:9911/create").to_return(status: 401)
end

include_examples 'does not restart the job'
end
end
end

Expand Down
30 changes: 30 additions & 0 deletions spec/v3/artifact_manager_client_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
describe Travis::API::V3::ArtifactManagerClient do
let(:client) { described_class.new(user_id) }
let(:url) { 'https://artifact-manager.travis-ci.com' }
let(:user_id) { rand(999) }
let(:auth_key) { 'super_secret' }

before do
Travis.config.artifact_manager.url = url
Travis.config.artifact_manager.auth_key = auth_key
end

describe '#delete_images' do
let(:image_ids) { [1, 2, 3] }
subject { client.delete_images('user', user_id, image_ids) }

it 'sends a DELETE request to the artifact manager' do
stub_request(:delete, %r{#{url}/image/user/#{user_id}/\d+})
.to_return(status: 200, headers: { 'Content-Type' => 'application/json' })

subject
end

it 'raises an error if the request fails' do
stub_request(:delete, %r{#{url}/image/user/#{user_id}/\d+})
.to_return(status: 400, body: { error: 'Bad Request' }.to_json, headers: { 'Content-Type' => 'application/json' })

expect { subject }.to raise_error(Travis::API::V3::ClientError, 'Bad Request')
end
end
end
Loading