Skip to content
Merged
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
20 changes: 20 additions & 0 deletions lib/travis/api/v3/models/custom_image.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module Travis::API::V3
class Models::CustomImage < Model
after_initialize :readonly!
belongs_to :owner, polymorphic: true
has_many :custom_image_logs

scope :available, -> { where(state: 'available') }

def created_by
user_id = custom_image_logs.created.first&.sender_id
return unless user_id

User.find(user_id)
end

def private
true
end
end
end
7 changes: 7 additions & 0 deletions lib/travis/api/v3/models/custom_image_log.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
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
18 changes: 18 additions & 0 deletions lib/travis/api/v3/queries/custom_images.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module Travis::API::V3
class Queries::CustomImages < Query
def for_owner(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

def owner_type(owner)
owner.vcs_type =~ /User/ ? 'User' : 'Organization'
end
end
end
20 changes: 20 additions & 0 deletions lib/travis/api/v3/renderer/custom_image.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module Travis::API::V3
class Renderer::CustomImage < ModelRenderer
representation :minimal, :id, :owner_id, :owner_type, :name, :usage, :created_at, :updated_at, :os_version,
:created_by, :private, :size_bytes
representation :standard, *representations[:minimal]

def created_by
return nil unless user = model.created_by
{
'@type' => 'user',
'@href' => "/v3/user/#{user.id}",
'@representation' => 'minimal'.freeze,
'id' => user.id,
'login' => user.login,
'name' => user.name,
'avatar_url' => user.avatar_url
}
end
end
end
6 changes: 6 additions & 0 deletions lib/travis/api/v3/renderer/custom_images.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module Travis::API::V3
class Renderer::CustomImages < CollectionRenderer
type :custom_images
collection_key :custom_images
end
end
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
6 changes: 6 additions & 0 deletions lib/travis/api/v3/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@ module Routes
route '/executions_per_sender'
get :for_owner_per_sender
end

resource :custom_images do
route '/custom_images'
get :for_owner
delete :delete
end
end

resource :credits_calculator do
Expand Down
1 change: 1 addition & 0 deletions lib/travis/api/v3/services.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module Services
Crons = Module.new { extend Services }
CustomKey = Module.new { extend Services }
CustomKeys = Module.new { extend Services }
CustomImages = Module.new { extend Services }
EmailSubscription = Module.new { extend Services }
EnvVar = Module.new { extend Services }
EnvVars = Module.new { extend Services }
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
27 changes: 27 additions & 0 deletions lib/travis/api/v3/services/custom_images/for_owner.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module Travis::API::V3
class Services::CustomImages::ForOwner < Service
result_type :custom_images

def run!
raise MethodNotAllowed if Travis.config.org?
raise LoginRequired unless access_control.logged_in?

owner = query(:owner).find

raise NotFound unless owner

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
end
end
end
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
Loading