From b96553c07a60abf2455e6eab1ffbaead0f344f55 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Thu, 24 Apr 2025 13:43:49 +0300 Subject: [PATCH 01/20] List custom images endpoint --- lib/travis/api/v3/models/custom_image.rb | 20 +++++++++++++++++++ lib/travis/api/v3/models/custom_image_log.rb | 6 ++++++ lib/travis/api/v3/queries/custom_images.rb | 13 ++++++++++++ lib/travis/api/v3/renderer/custom_image.rb | 20 +++++++++++++++++++ lib/travis/api/v3/renderer/custom_images.rb | 6 ++++++ lib/travis/api/v3/routes.rb | 5 +++++ lib/travis/api/v3/services.rb | 1 + .../v3/services/custom_images/for_owner.rb | 20 +++++++++++++++++++ 8 files changed, 91 insertions(+) create mode 100644 lib/travis/api/v3/models/custom_image.rb create mode 100644 lib/travis/api/v3/models/custom_image_log.rb create mode 100644 lib/travis/api/v3/queries/custom_images.rb create mode 100644 lib/travis/api/v3/renderer/custom_image.rb create mode 100644 lib/travis/api/v3/renderer/custom_images.rb create mode 100644 lib/travis/api/v3/services/custom_images/for_owner.rb diff --git a/lib/travis/api/v3/models/custom_image.rb b/lib/travis/api/v3/models/custom_image.rb new file mode 100644 index 000000000..b0763a940 --- /dev/null +++ b/lib/travis/api/v3/models/custom_image.rb @@ -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.where(action: 'created').first&.sender_id + return unless user_id + + User.find(user_id) + end + + def private + true + end + end +end diff --git a/lib/travis/api/v3/models/custom_image_log.rb b/lib/travis/api/v3/models/custom_image_log.rb new file mode 100644 index 000000000..255763935 --- /dev/null +++ b/lib/travis/api/v3/models/custom_image_log.rb @@ -0,0 +1,6 @@ +module Travis::API::V3 + class Models::CustomImageLog < Model + after_initialize :readonly! + belongs_to :custom_image + end +end diff --git a/lib/travis/api/v3/queries/custom_images.rb b/lib/travis/api/v3/queries/custom_images.rb new file mode 100644 index 000000000..4ceaf0066 --- /dev/null +++ b/lib/travis/api/v3/queries/custom_images.rb @@ -0,0 +1,13 @@ +module Travis::API::V3 + class Queries::CustomImages < Query + def for_owner(owner) + Models::CustomImage.where(owner_id: owner.id, owner_type: owner_type(owner)) + end + + private + + def owner_type(owner) + owner.vcs_type =~ /User/ ? 'User' : 'Organization' + end + end +end diff --git a/lib/travis/api/v3/renderer/custom_image.rb b/lib/travis/api/v3/renderer/custom_image.rb new file mode 100644 index 000000000..ab5bd8085 --- /dev/null +++ b/lib/travis/api/v3/renderer/custom_image.rb @@ -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 + 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 diff --git a/lib/travis/api/v3/renderer/custom_images.rb b/lib/travis/api/v3/renderer/custom_images.rb new file mode 100644 index 000000000..478b978ce --- /dev/null +++ b/lib/travis/api/v3/renderer/custom_images.rb @@ -0,0 +1,6 @@ +module Travis::API::V3 + class Renderer::CustomImages < CollectionRenderer + type :custom_images + collection_key :custom_images + end +end diff --git a/lib/travis/api/v3/routes.rb b/lib/travis/api/v3/routes.rb index d2540ae76..ceebf1ba6 100644 --- a/lib/travis/api/v3/routes.rb +++ b/lib/travis/api/v3/routes.rb @@ -169,6 +169,11 @@ module Routes route '/executions_per_sender' get :for_owner_per_sender end + + resource :custom_images do + route '/custom_images' + get :for_owner + end end resource :credits_calculator do diff --git a/lib/travis/api/v3/services.rb b/lib/travis/api/v3/services.rb index 0598e9b29..c72008071 100644 --- a/lib/travis/api/v3/services.rb +++ b/lib/travis/api/v3/services.rb @@ -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 } diff --git a/lib/travis/api/v3/services/custom_images/for_owner.rb b/lib/travis/api/v3/services/custom_images/for_owner.rb new file mode 100644 index 000000000..5fa4b75f8 --- /dev/null +++ b/lib/travis/api/v3/services/custom_images/for_owner.rb @@ -0,0 +1,20 @@ +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 + repo = owner.repositories.first + raise InsufficientAccess unless repo + access_control.permissions(repo).build_create! + + results = query(:custom_images).for_owner(owner) + result results + end + end +end From 012542cc91aaa03ac53abf2dfc20879666371fcc Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Fri, 25 Apr 2025 12:51:38 +0300 Subject: [PATCH 02/20] Return size_bytes for custom images --- lib/travis/api/v3/renderer/custom_image.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/travis/api/v3/renderer/custom_image.rb b/lib/travis/api/v3/renderer/custom_image.rb index ab5bd8085..1a6622f82 100644 --- a/lib/travis/api/v3/renderer/custom_image.rb +++ b/lib/travis/api/v3/renderer/custom_image.rb @@ -1,7 +1,7 @@ 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 + :created_by, :private, :size_bytes representation :standard, *representations[:minimal] def created_by From 3b5ebadba028b9c21dcf2b68174d89769b45370b Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Thu, 1 May 2025 10:23:19 +0300 Subject: [PATCH 03/20] Implement deleting custom images --- lib/travis/api/v3/artifact_manager_client.rb | 73 +++++++++++++++++++ lib/travis/api/v3/queries/custom_images.rb | 5 ++ lib/travis/api/v3/routes.rb | 1 + .../api/v3/services/custom_images/delete.rb | 21 ++++++ .../v3/services/custom_images/for_owner.rb | 2 + 5 files changed, 102 insertions(+) create mode 100644 lib/travis/api/v3/artifact_manager_client.rb create mode 100644 lib/travis/api/v3/services/custom_images/delete.rb diff --git a/lib/travis/api/v3/artifact_manager_client.rb b/lib/travis/api/v3/artifact_manager_client.rb new file mode 100644 index 000000000..d2e26f7b6 --- /dev/null +++ b/lib/travis/api/v3/artifact_manager_client.rb @@ -0,0 +1,73 @@ +module Travis::API::V3 + class ArtifactManagerClient + class ConfigurationError < StandardError; end + + def initialize(user_id) + @user_id = user_id + 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(image_ids) + response = connection.delete('/images') do |req| + req.body = { image_ids: }.to_json + end + handle_errors_and_respond(response) + 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 diff --git a/lib/travis/api/v3/queries/custom_images.rb b/lib/travis/api/v3/queries/custom_images.rb index 4ceaf0066..c77b5298b 100644 --- a/lib/travis/api/v3/queries/custom_images.rb +++ b/lib/travis/api/v3/queries/custom_images.rb @@ -4,6 +4,11 @@ def for_owner(owner) Models::CustomImage.where(owner_id: owner.id, owner_type: owner_type(owner)) end + def delete(image_ids, sender) + client = ArtifactManagerClient.new(sender.id) + client.delete_images(image_ids) + end + private def owner_type(owner) diff --git a/lib/travis/api/v3/routes.rb b/lib/travis/api/v3/routes.rb index ceebf1ba6..0ca0e725e 100644 --- a/lib/travis/api/v3/routes.rb +++ b/lib/travis/api/v3/routes.rb @@ -173,6 +173,7 @@ module Routes resource :custom_images do route '/custom_images' get :for_owner + delete :delete end end diff --git a/lib/travis/api/v3/services/custom_images/delete.rb b/lib/travis/api/v3/services/custom_images/delete.rb new file mode 100644 index 000000000..9b7ff063d --- /dev/null +++ b/lib/travis/api/v3/services/custom_images/delete.rb @@ -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).write! + else + access_control.permissions(owner).admin! + end + + query.delete(params['image_ids'], access_control.user) + deleted + end + end +end diff --git a/lib/travis/api/v3/services/custom_images/for_owner.rb b/lib/travis/api/v3/services/custom_images/for_owner.rb index 5fa4b75f8..b7c6a44f1 100644 --- a/lib/travis/api/v3/services/custom_images/for_owner.rb +++ b/lib/travis/api/v3/services/custom_images/for_owner.rb @@ -9,8 +9,10 @@ def run! owner = query(:owner).find raise NotFound unless owner + repo = owner.repositories.first raise InsufficientAccess unless repo + access_control.permissions(repo).build_create! results = query(:custom_images).for_owner(owner) From eba366768d7a813ffb1c84537492f65d9f108ff1 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Mon, 5 May 2025 12:25:56 +0300 Subject: [PATCH 04/20] Add specs --- lib/travis/testing/factories.rb | 27 +++++++++++ spec/v3/artifact_manager_client_spec.rb | 32 +++++++++++++ spec/v3/services/custom_images/delete_spec.rb | 45 ++++++++++++++++++ .../services/custom_images/for_owner_spec.rb | 47 +++++++++++++++++++ 4 files changed, 151 insertions(+) create mode 100644 spec/v3/artifact_manager_client_spec.rb create mode 100644 spec/v3/services/custom_images/delete_spec.rb create mode 100644 spec/v3/services/custom_images/for_owner_spec.rb diff --git a/lib/travis/testing/factories.rb b/lib/travis/testing/factories.rb index 2b4a02ca8..81a3a4494 100644 --- a/lib/travis/testing/factories.rb +++ b/lib/travis/testing/factories.rb @@ -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 diff --git a/spec/v3/artifact_manager_client_spec.rb b/spec/v3/artifact_manager_client_spec.rb new file mode 100644 index 000000000..68b427713 --- /dev/null +++ b/spec/v3/artifact_manager_client_spec.rb @@ -0,0 +1,32 @@ +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(image_ids) } + + it 'sends a DELETE request to the artifact manager' do + stub_request(:delete, "#{url}/images") + .with(body: { image_ids: image_ids }.to_json, headers: { 'X-Travis-User-Id' => user_id.to_s }) + .to_return(status: 200, body: { images: [] }.to_json, headers: { 'Content-Type' => 'application/json' }) + + subject + end + + it 'raises an error if the request fails' do + stub_request(:delete, "#{url}/images") + .with(body: { image_ids: image_ids }.to_json, headers: { 'X-Travis-User-Id' => user_id.to_s }) + .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 diff --git a/spec/v3/services/custom_images/delete_spec.rb b/spec/v3/services/custom_images/delete_spec.rb new file mode 100644 index 000000000..33055e521 --- /dev/null +++ b/spec/v3/services/custom_images/delete_spec.rb @@ -0,0 +1,45 @@ +describe Travis::API::V3::Services::CustomImages::Delete, set_app: true do + let(:user) { FactoryBot.create(:user) } + let(:token) { Travis::Api::App::AccessToken.create(user: user, app_id: 1) } + let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }} + let(:parsed_body) { JSON.load(body) } + + before do + Travis.config.host = 'travis-ci.com' + end + + context 'authenticated' do + describe "deleting custom images by id list" do + before do + stub_request(:delete, "#{Travis.config.artifact_manager.url}/images") + .with( + body: { image_ids: ['1', '2', '3'] }.to_json, + headers: { 'X-Travis-User-Id' => user.id.to_s } + ) + .to_return(status: 204, headers: { 'Content-Type' => 'application/json' }) + end + + it 'makes call to artifact manager' do + delete("/v3/owner/#{user.login}/custom_images", { image_ids: [1, 2, 3] }, headers) + + expect(last_response.status).to eq(204) + expect(last_response.body).to be_empty + end + end + + describe "deleting custom images with no permissions" do + let(:organization) { FactoryBot.create(:org) } + + before { stub_request(:get, %r((.+)/roles/org/(.+))).to_return(status: 200, body: JSON.generate({ 'roles' => [] })) } + + it 'returns an error' do + delete("/v3/owner/GitHub/#{organization.name}/custom_images", { image_ids: [1, 2, 3] }, headers) + + expect(last_response.status).to eq(403) + expect(parsed_body).to include( + 'error_type' => 'insufficient_access' + ) + end + end + end +end diff --git a/spec/v3/services/custom_images/for_owner_spec.rb b/spec/v3/services/custom_images/for_owner_spec.rb new file mode 100644 index 000000000..e3687c624 --- /dev/null +++ b/spec/v3/services/custom_images/for_owner_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +RSpec.describe Travis::API::V3::Services::CustomImages::ForOwner, set_app: true do + let(:json_headers) { { 'HTTP_ACCEPT' => 'application/json' } } + let(:authorization) { { 'permissions' => [ 'repository_state_update', 'repository_build_create' ] } } + + before do + Travis.config.host = 'travis-ci.com' + stub_request(:get, %r((.+)/permissions/repo/(.+))).to_return(status: 200, body: JSON.generate(authorization)) + end + + context 'authenticated' do + let(:user) { FactoryBot.create(:user, name: 'Joe', login: 'joe') } + let(:user_token) { Travis::Api::App::AccessToken.create(user: user, app_id: 1) } + let!(:repository) { FactoryBot.create(:repository, owner: user) } + let!(:custom_image) { FactoryBot.create(:custom_image, owner: user) } + let!(:custom_image_log) { FactoryBot.create(:custom_image_log, custom_image: custom_image, sender_id: user.id) } + + context 'when user has custom images' do + it 'returns custom images' do + get("/v3/owner/#{user.login}/custom_images", {}, json_headers.merge('HTTP_AUTHORIZATION' => "token #{user_token}")) + + expect(last_response).to be_ok + expect(JSON.parse(last_response.body)['custom_images'].first).to include( + 'id' => custom_image.id, + 'name' => custom_image.name, + 'size_bytes' => custom_image.size_bytes + ) + end + end + + context 'when user has no build create permission' do + let(:other_user) { FactoryBot.create(:user, name: 'Jane', login: 'jane') } + let(:other_user_token) { Travis::Api::App::AccessToken.create(user: other_user, app_id: 1) } + let!(:repository) { FactoryBot.create(:repository, owner: other_user) } + let(:authorization) { { 'permissions' => [ 'repository_state_update' ] } } + + it 'returns an empty list' do + get("/v3/owner/#{other_user.login}/custom_images", {}, json_headers.merge('HTTP_AUTHORIZATION' => "token #{other_user_token}")) + + expect(JSON.parse(last_response.body)).to include( + 'error_type' => 'insufficient_access' + ) + end + end + end +end From dc885e061cc5156fa55dc27cd933768dd6e22077 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Mon, 5 May 2025 12:39:56 +0300 Subject: [PATCH 05/20] Fix spec --- spec/v3/services/custom_images/delete_spec.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/v3/services/custom_images/delete_spec.rb b/spec/v3/services/custom_images/delete_spec.rb index 33055e521..e21d6eea3 100644 --- a/spec/v3/services/custom_images/delete_spec.rb +++ b/spec/v3/services/custom_images/delete_spec.rb @@ -3,8 +3,12 @@ let(:token) { Travis::Api::App::AccessToken.create(user: user, app_id: 1) } let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }} let(:parsed_body) { JSON.load(body) } + let(:url) { 'https://artifact-manager.travis-ci.com' } + let(:auth_key) { 'super_secret' } before do + Travis.config.artifact_manager.url = url + Travis.config.artifact_manager.auth_key = auth_key Travis.config.host = 'travis-ci.com' end From d12e949634fafa8c6481b9db5e235310ddbfae16 Mon Sep 17 00:00:00 2001 From: gabriel-arc <57348209+GbArc@users.noreply.github.com> Date: Wed, 28 May 2025 12:03:16 +0200 Subject: [PATCH 06/20] custom image restart (#1371) * custom image restart * split to create/use * handling full image response from am on check (#1372) * handling full image response from am on check --------- Co-authored-by: GbArc --- .../api/enqueue/services/restart_model.rb | 58 ++++++++++++++++++- lib/travis/api/v3/artifact_manager_client.rb | 26 +++++++++ .../enqueue/services/restart_model_spec.rb | 24 ++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/lib/travis/api/enqueue/services/restart_model.rb b/lib/travis/api/enqueue/services/restart_model.rb index 7ee56d1e8..c7af11db3 100644 --- a/lib/travis/api/enqueue/services/restart_model.rb +++ b/lib/travis/api/enqueue/services/restart_model.rb @@ -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? @@ -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 @@ -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 diff --git a/lib/travis/api/v3/artifact_manager_client.rb b/lib/travis/api/v3/artifact_manager_client.rb index d2e26f7b6..e0f9a4744 100644 --- a/lib/travis/api/v3/artifact_manager_client.rb +++ b/lib/travis/api/v3/artifact_manager_client.rb @@ -6,6 +6,32 @@ 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) diff --git a/spec/lib/travis/api/enqueue/services/restart_model_spec.rb b/spec/lib/travis/api/enqueue/services/restart_model_spec.rb index 1f167d64c..fda57f1f0 100644 --- a/spec/lib/travis/api/enqueue/services/restart_model_spec.rb +++ b/spec/lib/travis/api/enqueue/services/restart_model_spec.rb @@ -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 From bce0e10ada09ae677e2fe2ace0caf3681a7a09c1 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Mon, 12 May 2025 11:23:46 +0300 Subject: [PATCH 07/20] Address review comments --- lib/travis/api/v3/models/custom_image.rb | 2 +- lib/travis/api/v3/models/custom_image_log.rb | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/travis/api/v3/models/custom_image.rb b/lib/travis/api/v3/models/custom_image.rb index b0763a940..8a5dd3b33 100644 --- a/lib/travis/api/v3/models/custom_image.rb +++ b/lib/travis/api/v3/models/custom_image.rb @@ -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.frist&.sender_id return unless user_id User.find(user_id) diff --git a/lib/travis/api/v3/models/custom_image_log.rb b/lib/travis/api/v3/models/custom_image_log.rb index 255763935..26de4ba02 100644 --- a/lib/travis/api/v3/models/custom_image_log.rb +++ b/lib/travis/api/v3/models/custom_image_log.rb @@ -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 From 6f78487850ca896a58c418c7ac9ea3803af5f2c8 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Wed, 28 May 2025 13:36:13 +0300 Subject: [PATCH 08/20] Fixes --- lib/travis/api/v3/artifact_manager_client.rb | 8 ++++---- lib/travis/api/v3/models/custom_image.rb | 2 +- lib/travis/api/v3/queries/custom_images.rb | 6 +++--- lib/travis/api/v3/services/custom_images/delete.rb | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/travis/api/v3/artifact_manager_client.rb b/lib/travis/api/v3/artifact_manager_client.rb index e0f9a4744..05e6368fe 100644 --- a/lib/travis/api/v3/artifact_manager_client.rb +++ b/lib/travis/api/v3/artifact_manager_client.rb @@ -37,11 +37,11 @@ def images(owner_type, owner_id) handle_images_response(response) end - def delete_images(image_ids) - response = connection.delete('/images') do |req| - req.body = { image_ids: }.to_json + 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 - handle_errors_and_respond(response) end private diff --git a/lib/travis/api/v3/models/custom_image.rb b/lib/travis/api/v3/models/custom_image.rb index 8a5dd3b33..bfe3c30ce 100644 --- a/lib/travis/api/v3/models/custom_image.rb +++ b/lib/travis/api/v3/models/custom_image.rb @@ -7,7 +7,7 @@ class Models::CustomImage < Model scope :available, -> { where(state: 'available') } def created_by - user_id = custom_image_logs.created.frist&.sender_id + user_id = custom_image_logs.created.first&.sender_id return unless user_id User.find(user_id) diff --git a/lib/travis/api/v3/queries/custom_images.rb b/lib/travis/api/v3/queries/custom_images.rb index c77b5298b..045c6d46c 100644 --- a/lib/travis/api/v3/queries/custom_images.rb +++ b/lib/travis/api/v3/queries/custom_images.rb @@ -1,12 +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.where(owner_id: owner.id, owner_type: owner_type(owner)).order('created_at DESC') end - def delete(image_ids, sender) + def delete(image_ids, owner, sender) client = ArtifactManagerClient.new(sender.id) - client.delete_images(image_ids) + client.delete_images(owner_type(owner), owner.id, image_ids) end private diff --git a/lib/travis/api/v3/services/custom_images/delete.rb b/lib/travis/api/v3/services/custom_images/delete.rb index 9b7ff063d..a8e54dfe1 100644 --- a/lib/travis/api/v3/services/custom_images/delete.rb +++ b/lib/travis/api/v3/services/custom_images/delete.rb @@ -14,7 +14,7 @@ def run! access_control.permissions(owner).admin! end - query.delete(params['image_ids'], access_control.user) + query.delete(params['image_ids'], owner, access_control.user) deleted end end From 50b160f02c178e816c76572d22f88e985fec72e7 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Thu, 29 May 2025 15:33:20 +0300 Subject: [PATCH 09/20] Show only active images --- lib/travis/api/v3/queries/custom_images.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/travis/api/v3/queries/custom_images.rb b/lib/travis/api/v3/queries/custom_images.rb index 045c6d46c..364a4b516 100644 --- a/lib/travis/api/v3/queries/custom_images.rb +++ b/lib/travis/api/v3/queries/custom_images.rb @@ -1,7 +1,7 @@ module Travis::API::V3 class Queries::CustomImages < Query def for_owner(owner) - Models::CustomImage.where(owner_id: owner.id, owner_type: owner_type(owner)).order('created_at DESC') + Models::CustomImage.active.where(owner_id: owner.id, owner_type: owner_type(owner)).order('created_at DESC') end def delete(image_ids, owner, sender) From e3aa4cdf27c43936f78f04def9e00cf65ae02cde Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Thu, 29 May 2025 16:27:34 +0300 Subject: [PATCH 10/20] Fix typo --- lib/travis/api/v3/queries/custom_images.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/travis/api/v3/queries/custom_images.rb b/lib/travis/api/v3/queries/custom_images.rb index 364a4b516..c95441494 100644 --- a/lib/travis/api/v3/queries/custom_images.rb +++ b/lib/travis/api/v3/queries/custom_images.rb @@ -1,7 +1,7 @@ module Travis::API::V3 class Queries::CustomImages < Query def for_owner(owner) - Models::CustomImage.active.where(owner_id: owner.id, owner_type: owner_type(owner)).order('created_at DESC') + Models::CustomImage.available.where(owner_id: owner.id, owner_type: owner_type(owner)).order('created_at DESC') end def delete(image_ids, owner, sender) From 72ff6dce0c4c88f8cb6d5cd5a9e16416b8f00bda Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Fri, 30 May 2025 10:04:56 +0300 Subject: [PATCH 11/20] Fix permissions for custom images list --- lib/travis/api/v3/services/custom_images/for_owner.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/travis/api/v3/services/custom_images/for_owner.rb b/lib/travis/api/v3/services/custom_images/for_owner.rb index b7c6a44f1..3187f23b9 100644 --- a/lib/travis/api/v3/services/custom_images/for_owner.rb +++ b/lib/travis/api/v3/services/custom_images/for_owner.rb @@ -10,10 +10,15 @@ def run! raise NotFound unless owner - repo = owner.repositories.first - raise InsufficientAccess unless repo + 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 - access_control.permissions(repo).build_create! + 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 From 165fac06dd6f9f56fd277df292f0fe3cdf550d94 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Fri, 6 Jun 2025 14:35:59 +0300 Subject: [PATCH 12/20] Implement storage usage endpoint --- lib/travis/api/v3/billing_client.rb | 7 +++++++ lib/travis/api/v3/models/v2_addon_usage.rb | 7 ++++++- lib/travis/api/v3/queries/custom_images.rb | 5 +++++ .../api/v3/renderer/custom_image_usage.rb | 7 +++++++ .../api/v3/renderer/custom_image_usages.rb | 6 ++++++ lib/travis/api/v3/renderer/v2_addon_usage.rb | 6 ++++-- lib/travis/api/v3/routes.rb | 1 + .../api/v3/services/custom_images/usage.rb | 18 ++++++++++++++++++ lib/travis/config/defaults.rb | 3 ++- 9 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 lib/travis/api/v3/renderer/custom_image_usage.rb create mode 100644 lib/travis/api/v3/renderer/custom_image_usages.rb create mode 100644 lib/travis/api/v3/services/custom_images/usage.rb diff --git a/lib/travis/api/v3/billing_client.rb b/lib/travis/api/v3/billing_client.rb index 88d2c8a3e..2bf2eb44f 100644 --- a/lib/travis/api/v3/billing_client.rb +++ b/lib/travis/api/v3/billing_client.rb @@ -47,6 +47,13 @@ def executions(owner_type, owner_id, page, per_page, from, to) executions end + def storage_usage(owner_type, owner_id, from, to) + response = connection.get("/usage/#{owner_type.downcase}s/#{owner_id}/storage?from=#{from}&to=#{to}") + body(response).map do |usage_data| + usage_data + end + end + def calculate_credits(users, executions) response = connection.post("/usage/credits_calculator", users: users, executions: executions) body(response).map do |calculator_data| diff --git a/lib/travis/api/v3/models/v2_addon_usage.rb b/lib/travis/api/v3/models/v2_addon_usage.rb index ec68ff9a1..a58d8b6f2 100644 --- a/lib/travis/api/v3/models/v2_addon_usage.rb +++ b/lib/travis/api/v3/models/v2_addon_usage.rb @@ -1,8 +1,10 @@ module Travis::API::V3 class Models::V2AddonUsage - attr_reader :id, :addon_id, :addon_quantity, :addon_usage, :remaining, :purchase_date, :valid_to, :active, :status + attr_reader :id, :addon_id, :addon_quantity, :addon_usage, :remaining, :purchase_date, :valid_to, :active, :status, + :quantity_limit_type, :quantity_limit_charge, :total_usage def initialize(attrs) + pp attrs @id = attrs.fetch('id') @addon_id = attrs.fetch('addon_id') @addon_quantity = attrs.fetch('addon_quantity') @@ -12,6 +14,9 @@ def initialize(attrs) @valid_to = attrs.fetch('valid_to') @active = attrs.fetch('active') @status = attrs.fetch('status') + @quantity_limit_type = attrs.fetch('quantity_limit_type', nil) + @quantity_limit_charge = attrs.fetch('quantity_limit_charge', nil) + @total_usage = attrs.fetch('total_usage', nil) end end end diff --git a/lib/travis/api/v3/queries/custom_images.rb b/lib/travis/api/v3/queries/custom_images.rb index c95441494..edb8cba6f 100644 --- a/lib/travis/api/v3/queries/custom_images.rb +++ b/lib/travis/api/v3/queries/custom_images.rb @@ -9,6 +9,11 @@ def delete(image_ids, owner, sender) client.delete_images(owner_type(owner), owner.id, image_ids) end + def usage(owner, user_id, from, to) + client = BillingClient.new(user_id) + client.storage_usage(owner_type(owner), owner.id, from, to) + end + private def owner_type(owner) diff --git a/lib/travis/api/v3/renderer/custom_image_usage.rb b/lib/travis/api/v3/renderer/custom_image_usage.rb new file mode 100644 index 000000000..5df91f1fb --- /dev/null +++ b/lib/travis/api/v3/renderer/custom_image_usage.rb @@ -0,0 +1,7 @@ +module Travis::API::V3 + class Renderer::CustomImagesUsage < ModelRenderer + representation :minimal, :total_usage, :excess_usage, :free_usage, :quantity_limit_free, :quantity_limit_type, + :quantity_limit_charge + representation :standard, *representations[:minimal] + end +end diff --git a/lib/travis/api/v3/renderer/custom_image_usages.rb b/lib/travis/api/v3/renderer/custom_image_usages.rb new file mode 100644 index 000000000..a65e1ecf2 --- /dev/null +++ b/lib/travis/api/v3/renderer/custom_image_usages.rb @@ -0,0 +1,6 @@ +module Travis::API::V3 + class Renderer::CustomImagesUsages < CollectionRenderer + type :custom_images_usages + collection_key :custom_images_usages + end +end diff --git a/lib/travis/api/v3/renderer/v2_addon_usage.rb b/lib/travis/api/v3/renderer/v2_addon_usage.rb index be1cd7914..3616aea19 100644 --- a/lib/travis/api/v3/renderer/v2_addon_usage.rb +++ b/lib/travis/api/v3/renderer/v2_addon_usage.rb @@ -1,6 +1,8 @@ module Travis::API::V3 class Renderer::V2AddonUsage < ModelRenderer - representation(:standard, :id, :addon_id, :addon_quantity, :addon_usage, :remaining, :purchase_date, :valid_to, :active, :status) - representation(:minimal, :id, :addon_id, :addon_quantity, :addon_usage, :remaining, :purchase_date, :valid_to, :active, :status) + representation(:standard, :id, :addon_id, :addon_quantity, :addon_usage, :remaining, :purchase_date, :valid_to, + :active, :status, :total_usage, :quantity_limit_type, :quantity_limit_charge) + representation(:minimal, :id, :addon_id, :addon_quantity, :addon_usage, :remaining, :purchase_date, :valid_to, + :active, :status, :total_usage, :quantity_limit_type, :quantity_limit_charge) end end diff --git a/lib/travis/api/v3/routes.rb b/lib/travis/api/v3/routes.rb index 0ca0e725e..24f3ab4ec 100644 --- a/lib/travis/api/v3/routes.rb +++ b/lib/travis/api/v3/routes.rb @@ -174,6 +174,7 @@ module Routes route '/custom_images' get :for_owner delete :delete + get :usage, '/usage' end end diff --git a/lib/travis/api/v3/services/custom_images/usage.rb b/lib/travis/api/v3/services/custom_images/usage.rb new file mode 100644 index 000000000..4037c01cc --- /dev/null +++ b/lib/travis/api/v3/services/custom_images/usage.rb @@ -0,0 +1,18 @@ +module Travis::API::V3 + class Services::CustomImages::Usage < Service + result_type :custom_images_usages + params :from, :to + + def run! + raise MethodNotAllowed if Travis.config.org? + raise LoginRequired unless access_control.logged_in? + + owner = query(:owner).find + + raise NotFound unless owner + raise InsufficientAccess unless access_control.visible?(owner) + + result query(:custom_images).usage(owner, access_control.user.id, params['from'], params['to']) + end + end +end diff --git a/lib/travis/config/defaults.rb b/lib/travis/config/defaults.rb index 543113b28..f1d012360 100644 --- a/lib/travis/config/defaults.rb +++ b/lib/travis/config/defaults.rb @@ -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] From e487b8198fb434811a8c40e5e41e14f11b658533 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Mon, 9 Jun 2025 11:20:58 +0300 Subject: [PATCH 13/20] Remove debug --- lib/travis/api/v3/models/v2_addon_usage.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/travis/api/v3/models/v2_addon_usage.rb b/lib/travis/api/v3/models/v2_addon_usage.rb index a58d8b6f2..b83214458 100644 --- a/lib/travis/api/v3/models/v2_addon_usage.rb +++ b/lib/travis/api/v3/models/v2_addon_usage.rb @@ -4,7 +4,6 @@ class Models::V2AddonUsage :quantity_limit_type, :quantity_limit_charge, :total_usage def initialize(attrs) - pp attrs @id = attrs.fetch('id') @addon_id = attrs.fetch('addon_id') @addon_quantity = attrs.fetch('addon_quantity') From becafb399db8e731351f9fb43b95b7d8c5a87e73 Mon Sep 17 00:00:00 2001 From: gabriel-arc <57348209+GbArc@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:18:29 +0200 Subject: [PATCH 14/20] memberships (#1375) * Add default artifact manager config * expose membership /build_permission * specs --------- Co-authored-by: AndriiMysko Co-authored-by: GbArc --- lib/travis/api/v3/renderer/membership.rb | 6 ++++++ lib/travis/api/v3/renderer/user.rb | 2 +- lib/travis/api/v3/services/custom_images/delete.rb | 2 +- spec/v3/artifact_manager_client_spec.rb | 10 ++++------ spec/v3/services/custom_images/delete_spec.rb | 6 +----- spec/v3/services/custom_images/for_owner_spec.rb | 2 +- spec/v3/services/installation/find_spec.rb | 1 + spec/v3/services/owner/find_spec.rb | 4 ++++ spec/v3/services/v2_subscription/executions_spec.rb | 3 +++ 9 files changed, 22 insertions(+), 14 deletions(-) create mode 100644 lib/travis/api/v3/renderer/membership.rb diff --git a/lib/travis/api/v3/renderer/membership.rb b/lib/travis/api/v3/renderer/membership.rb new file mode 100644 index 000000000..af3c66c30 --- /dev/null +++ b/lib/travis/api/v3/renderer/membership.rb @@ -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 diff --git a/lib/travis/api/v3/renderer/user.rb b/lib/travis/api/v3/renderer/user.rb index a336297c4..6fcc34c2b 100644 --- a/lib/travis/api/v3/renderer/user.rb +++ b/lib/travis/api/v3/renderer/user.rb @@ -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 diff --git a/lib/travis/api/v3/services/custom_images/delete.rb b/lib/travis/api/v3/services/custom_images/delete.rb index a8e54dfe1..af2051efc 100644 --- a/lib/travis/api/v3/services/custom_images/delete.rb +++ b/lib/travis/api/v3/services/custom_images/delete.rb @@ -9,7 +9,7 @@ def run! raise NotFound unless owner if owner.is_a?(Travis::API::V3::Models::User) - access_control.permissions(owner).write! + access_control.permissions(owner).sync! else access_control.permissions(owner).admin! end diff --git a/spec/v3/artifact_manager_client_spec.rb b/spec/v3/artifact_manager_client_spec.rb index 68b427713..687ee8a41 100644 --- a/spec/v3/artifact_manager_client_spec.rb +++ b/spec/v3/artifact_manager_client_spec.rb @@ -11,19 +11,17 @@ describe '#delete_images' do let(:image_ids) { [1, 2, 3] } - subject { client.delete_images(image_ids) } + subject { client.delete_images('user', user_id, image_ids) } it 'sends a DELETE request to the artifact manager' do - stub_request(:delete, "#{url}/images") - .with(body: { image_ids: image_ids }.to_json, headers: { 'X-Travis-User-Id' => user_id.to_s }) - .to_return(status: 200, body: { images: [] }.to_json, headers: { 'Content-Type' => 'application/json' }) + 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, "#{url}/images") - .with(body: { image_ids: image_ids }.to_json, headers: { 'X-Travis-User-Id' => user_id.to_s }) + 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') diff --git a/spec/v3/services/custom_images/delete_spec.rb b/spec/v3/services/custom_images/delete_spec.rb index e21d6eea3..dda61cd52 100644 --- a/spec/v3/services/custom_images/delete_spec.rb +++ b/spec/v3/services/custom_images/delete_spec.rb @@ -15,11 +15,7 @@ context 'authenticated' do describe "deleting custom images by id list" do before do - stub_request(:delete, "#{Travis.config.artifact_manager.url}/images") - .with( - body: { image_ids: ['1', '2', '3'] }.to_json, - headers: { 'X-Travis-User-Id' => user.id.to_s } - ) + stub_request(:delete, %r{#{Travis.config.artifact_manager.url}/image/user/#{user.id}/\d+}) .to_return(status: 204, headers: { 'Content-Type' => 'application/json' }) end diff --git a/spec/v3/services/custom_images/for_owner_spec.rb b/spec/v3/services/custom_images/for_owner_spec.rb index e3687c624..36412debc 100644 --- a/spec/v3/services/custom_images/for_owner_spec.rb +++ b/spec/v3/services/custom_images/for_owner_spec.rb @@ -36,7 +36,7 @@ let(:authorization) { { 'permissions' => [ 'repository_state_update' ] } } it 'returns an empty list' do - get("/v3/owner/#{other_user.login}/custom_images", {}, json_headers.merge('HTTP_AUTHORIZATION' => "token #{other_user_token}")) + get("/v3/owner/#{user.login}/custom_images", {}, json_headers.merge('HTTP_AUTHORIZATION' => "token #{other_user_token}")) expect(JSON.parse(last_response.body)).to include( 'error_type' => 'insufficient_access' diff --git a/spec/v3/services/installation/find_spec.rb b/spec/v3/services/installation/find_spec.rb index 884e0db9f..1ebb672f3 100644 --- a/spec/v3/services/installation/find_spec.rb +++ b/spec/v3/services/installation/find_spec.rb @@ -61,6 +61,7 @@ "vcs_type" => user.vcs_type, "avatar_url" => "https://0.gravatar.com/avatar/07fb84848e68b96b69022d333ca8a3e2", "is_syncing" => nil, + "memberships" => [], "synced_at" => nil, "education" => nil, "allowance" => { diff --git a/spec/v3/services/owner/find_spec.rb b/spec/v3/services/owner/find_spec.rb index ade9a5861..e21785b1a 100644 --- a/spec/v3/services/owner/find_spec.rb +++ b/spec/v3/services/owner/find_spec.rb @@ -442,6 +442,7 @@ "vcs_type" => user.vcs_type, "avatar_url" => nil, "is_syncing" => nil, + "memberships" => [], "synced_at" => nil, "education" => nil, "allow_migration"=> false, @@ -480,6 +481,7 @@ "avatar_url" => nil, "education" => nil, "is_syncing" => nil, + "memberships" => [], "synced_at" => nil, "allow_migration"=> false, "allowance" => { @@ -517,6 +519,7 @@ "avatar_url" => nil, "education" => nil, "is_syncing" => nil, + "memberships" => [], "synced_at" => nil, "allow_migration" => false, "allowance" => { @@ -561,6 +564,7 @@ "avatar_url" => nil, "education" => nil, "is_syncing" => nil, + "memberships" => [], "synced_at" => nil, "allow_migration" => false, "allowance" => { diff --git a/spec/v3/services/v2_subscription/executions_spec.rb b/spec/v3/services/v2_subscription/executions_spec.rb index ef5ea0b84..532cec919 100644 --- a/spec/v3/services/v2_subscription/executions_spec.rb +++ b/spec/v3/services/v2_subscription/executions_spec.rb @@ -211,6 +211,7 @@ "account_env_vars" => [], "email"=>"sven@fuchs.com", "is_syncing"=>nil, + "memberships"=>[], "synced_at"=>nil, "recently_signed_up"=>false, "secure_user_hash"=>nil, @@ -271,6 +272,7 @@ "account_env_vars" => [], "email"=>"sven@fuchs.com", "is_syncing"=>nil, + "memberships"=>[], "synced_at"=>nil, "recently_signed_up"=>false, "secure_user_hash"=>nil, @@ -310,6 +312,7 @@ "account_env_vars" => [], "email"=>"sven@fuchs.com", "is_syncing"=>nil, + "memberships"=>[], "synced_at"=>nil, "recently_signed_up"=>false, "secure_user_hash"=>nil, From 73113858c7edc06ccad75c0558d176e61dce7141 Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Tue, 24 Jun 2025 14:20:04 +0300 Subject: [PATCH 15/20] Add quantity_free_limit to addon_usage presentation --- lib/travis/api/v3/models/v2_addon_usage.rb | 3 ++- lib/travis/api/v3/renderer/v2_addon_usage.rb | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/travis/api/v3/models/v2_addon_usage.rb b/lib/travis/api/v3/models/v2_addon_usage.rb index b83214458..a5dee7933 100644 --- a/lib/travis/api/v3/models/v2_addon_usage.rb +++ b/lib/travis/api/v3/models/v2_addon_usage.rb @@ -1,7 +1,7 @@ module Travis::API::V3 class Models::V2AddonUsage attr_reader :id, :addon_id, :addon_quantity, :addon_usage, :remaining, :purchase_date, :valid_to, :active, :status, - :quantity_limit_type, :quantity_limit_charge, :total_usage + :quantity_free_limit, :quantity_limit_type, :quantity_limit_charge, :total_usage def initialize(attrs) @id = attrs.fetch('id') @@ -13,6 +13,7 @@ def initialize(attrs) @valid_to = attrs.fetch('valid_to') @active = attrs.fetch('active') @status = attrs.fetch('status') + @quantity_free_limit = attrs.fetch('quantity_free_limit', 0) @quantity_limit_type = attrs.fetch('quantity_limit_type', nil) @quantity_limit_charge = attrs.fetch('quantity_limit_charge', nil) @total_usage = attrs.fetch('total_usage', nil) diff --git a/lib/travis/api/v3/renderer/v2_addon_usage.rb b/lib/travis/api/v3/renderer/v2_addon_usage.rb index 3616aea19..1afe5abc6 100644 --- a/lib/travis/api/v3/renderer/v2_addon_usage.rb +++ b/lib/travis/api/v3/renderer/v2_addon_usage.rb @@ -1,8 +1,8 @@ module Travis::API::V3 class Renderer::V2AddonUsage < ModelRenderer representation(:standard, :id, :addon_id, :addon_quantity, :addon_usage, :remaining, :purchase_date, :valid_to, - :active, :status, :total_usage, :quantity_limit_type, :quantity_limit_charge) + :active, :status, :total_usage, :quantity_free_limit, :quantity_limit_type, :quantity_limit_charge) representation(:minimal, :id, :addon_id, :addon_quantity, :addon_usage, :remaining, :purchase_date, :valid_to, - :active, :status, :total_usage, :quantity_limit_type, :quantity_limit_charge) + :active, :status, :total_usage, :quantity_free_limit, :quantity_limit_type, :quantity_limit_charge) end end From 0c0df00adf704fb1d2397a3575317cd03b4397cd Mon Sep 17 00:00:00 2001 From: AndriiMysko Date: Wed, 25 Jun 2025 15:33:29 +0300 Subject: [PATCH 16/20] Fix quantity_limit_free --- lib/travis/api/v3/models/v2_addon_usage.rb | 4 ++-- lib/travis/api/v3/renderer/v2_addon_usage.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/travis/api/v3/models/v2_addon_usage.rb b/lib/travis/api/v3/models/v2_addon_usage.rb index a5dee7933..ebf6704fc 100644 --- a/lib/travis/api/v3/models/v2_addon_usage.rb +++ b/lib/travis/api/v3/models/v2_addon_usage.rb @@ -1,7 +1,7 @@ module Travis::API::V3 class Models::V2AddonUsage attr_reader :id, :addon_id, :addon_quantity, :addon_usage, :remaining, :purchase_date, :valid_to, :active, :status, - :quantity_free_limit, :quantity_limit_type, :quantity_limit_charge, :total_usage + :quantity_limit_free, :quantity_limit_type, :quantity_limit_charge, :total_usage def initialize(attrs) @id = attrs.fetch('id') @@ -13,7 +13,7 @@ def initialize(attrs) @valid_to = attrs.fetch('valid_to') @active = attrs.fetch('active') @status = attrs.fetch('status') - @quantity_free_limit = attrs.fetch('quantity_free_limit', 0) + @quantity_limit_free = attrs.fetch('quantity_limit_free', 0) @quantity_limit_type = attrs.fetch('quantity_limit_type', nil) @quantity_limit_charge = attrs.fetch('quantity_limit_charge', nil) @total_usage = attrs.fetch('total_usage', nil) diff --git a/lib/travis/api/v3/renderer/v2_addon_usage.rb b/lib/travis/api/v3/renderer/v2_addon_usage.rb index 1afe5abc6..7742083d2 100644 --- a/lib/travis/api/v3/renderer/v2_addon_usage.rb +++ b/lib/travis/api/v3/renderer/v2_addon_usage.rb @@ -1,8 +1,8 @@ module Travis::API::V3 class Renderer::V2AddonUsage < ModelRenderer representation(:standard, :id, :addon_id, :addon_quantity, :addon_usage, :remaining, :purchase_date, :valid_to, - :active, :status, :total_usage, :quantity_free_limit, :quantity_limit_type, :quantity_limit_charge) + :active, :status, :total_usage, :quantity_limit_free, :quantity_limit_type, :quantity_limit_charge) representation(:minimal, :id, :addon_id, :addon_quantity, :addon_usage, :remaining, :purchase_date, :valid_to, - :active, :status, :total_usage, :quantity_free_limit, :quantity_limit_type, :quantity_limit_charge) + :active, :status, :total_usage, :quantity_limit_free, :quantity_limit_type, :quantity_limit_charge) end end From 9ee23d6cbb99bb6b37001e559d411cd655a593ca Mon Sep 17 00:00:00 2001 From: GbArc Date: Thu, 26 Jun 2025 11:09:14 +0200 Subject: [PATCH 17/20] current image storage api --- lib/travis/api/v3/models/custom_image_storage.rb | 4 ++++ lib/travis/api/v3/queries/custom_images.rb | 4 ++++ .../api/v3/renderer/custom_image_storage.rb | 6 ++++++ lib/travis/api/v3/routes.rb | 1 + .../v3/services/custom_images/current_storage.rb | 16 ++++++++++++++++ 5 files changed, 31 insertions(+) create mode 100644 lib/travis/api/v3/models/custom_image_storage.rb create mode 100644 lib/travis/api/v3/renderer/custom_image_storage.rb create mode 100644 lib/travis/api/v3/services/custom_images/current_storage.rb diff --git a/lib/travis/api/v3/models/custom_image_storage.rb b/lib/travis/api/v3/models/custom_image_storage.rb new file mode 100644 index 000000000..7d52a4a78 --- /dev/null +++ b/lib/travis/api/v3/models/custom_image_storage.rb @@ -0,0 +1,4 @@ +module Travis::API::V3 + class Models::CustomImageStorage < Model + end +end diff --git a/lib/travis/api/v3/queries/custom_images.rb b/lib/travis/api/v3/queries/custom_images.rb index edb8cba6f..ef0023965 100644 --- a/lib/travis/api/v3/queries/custom_images.rb +++ b/lib/travis/api/v3/queries/custom_images.rb @@ -14,6 +14,10 @@ def usage(owner, user_id, from, to) client.storage_usage(owner_type(owner), owner.id, from, to) end + def current_storage(owner, user_id) + Models::CustomImageStorage.where(owner_type: owner_type(owner), owner_id: owner.id).order('id desc').limit(1).first + end + private def owner_type(owner) diff --git a/lib/travis/api/v3/renderer/custom_image_storage.rb b/lib/travis/api/v3/renderer/custom_image_storage.rb new file mode 100644 index 000000000..342ae61c5 --- /dev/null +++ b/lib/travis/api/v3/renderer/custom_image_storage.rb @@ -0,0 +1,6 @@ +module Travis::API::V3 + class Renderer::CustomImageStorage < ModelRenderer + representation :minimal, :id, :owner_id, :owner_type, :current_aggregated_storage, :created_at, :updated_at, :end_date + representation :standard, *representations[:minimal] + end +end diff --git a/lib/travis/api/v3/routes.rb b/lib/travis/api/v3/routes.rb index 24f3ab4ec..49f8f3c79 100644 --- a/lib/travis/api/v3/routes.rb +++ b/lib/travis/api/v3/routes.rb @@ -175,6 +175,7 @@ module Routes get :for_owner delete :delete get :usage, '/usage' + get :current_storage, '/current_storage' end end diff --git a/lib/travis/api/v3/services/custom_images/current_storage.rb b/lib/travis/api/v3/services/custom_images/current_storage.rb new file mode 100644 index 000000000..e2f18e812 --- /dev/null +++ b/lib/travis/api/v3/services/custom_images/current_storage.rb @@ -0,0 +1,16 @@ +module Travis::API::V3 + class Services::CustomImages::CurrentStorage < Service + result_type :custom_image_storage + def run! + raise MethodNotAllowed if Travis.config.org? + raise LoginRequired unless access_control.logged_in? + + owner = query(:owner).find + + raise NotFound unless owner + raise InsufficientAccess unless access_control.visible?(owner) + + result query(:custom_images).current_storage(owner, access_control.user.id) + end + end +end From c98cd80db3d352e1dacebe7c3c8e4117aa42979d Mon Sep 17 00:00:00 2001 From: Stanislav Colotinschi Date: Tue, 1 Jul 2025 16:53:03 +0300 Subject: [PATCH 18/20] Implement storage executions usage endpoint --- lib/travis/api/v3/billing_client.rb | 7 +++++++ lib/travis/api/v3/queries/custom_images.rb | 4 ++++ .../api/v3/renderer/storage_executions_usage.rb | 6 ++++++ .../v3/renderer/storage_executions_usages.rb | 6 ++++++ lib/travis/api/v3/routes.rb | 1 + .../custom_images/storage_executions_usage.rb | 17 +++++++++++++++++ 6 files changed, 41 insertions(+) create mode 100644 lib/travis/api/v3/renderer/storage_executions_usage.rb create mode 100644 lib/travis/api/v3/renderer/storage_executions_usages.rb create mode 100644 lib/travis/api/v3/services/custom_images/storage_executions_usage.rb diff --git a/lib/travis/api/v3/billing_client.rb b/lib/travis/api/v3/billing_client.rb index 2bf2eb44f..1abe9622d 100644 --- a/lib/travis/api/v3/billing_client.rb +++ b/lib/travis/api/v3/billing_client.rb @@ -54,6 +54,13 @@ def storage_usage(owner_type, owner_id, from, to) end end + def storage_executions_usage(owner_type, owner_id) + response = connection.get("/usage/#{owner_type.downcase}s/#{owner_id}/storage_executions") + body(response).map do |usage_data| + usage_data + end + end + def calculate_credits(users, executions) response = connection.post("/usage/credits_calculator", users: users, executions: executions) body(response).map do |calculator_data| diff --git a/lib/travis/api/v3/queries/custom_images.rb b/lib/travis/api/v3/queries/custom_images.rb index ef0023965..682326b02 100644 --- a/lib/travis/api/v3/queries/custom_images.rb +++ b/lib/travis/api/v3/queries/custom_images.rb @@ -18,6 +18,10 @@ def current_storage(owner, user_id) Models::CustomImageStorage.where(owner_type: owner_type(owner), owner_id: owner.id).order('id desc').limit(1).first end + def storage_executions_usage(owner, user_id) + BillingClient.new(user_id).storage_executions_usage(owner_type(owner), owner.id) + end + private def owner_type(owner) diff --git a/lib/travis/api/v3/renderer/storage_executions_usage.rb b/lib/travis/api/v3/renderer/storage_executions_usage.rb new file mode 100644 index 000000000..4db776e41 --- /dev/null +++ b/lib/travis/api/v3/renderer/storage_executions_usage.rb @@ -0,0 +1,6 @@ +module Travis::API::V3 + class Renderer::StorageExecutionsUsage < ModelRenderer + representation :minimal, :estimated_usage + representation :standard, *representations[:minimal] + end +end diff --git a/lib/travis/api/v3/renderer/storage_executions_usages.rb b/lib/travis/api/v3/renderer/storage_executions_usages.rb new file mode 100644 index 000000000..bfd12d02b --- /dev/null +++ b/lib/travis/api/v3/renderer/storage_executions_usages.rb @@ -0,0 +1,6 @@ +module Travis::API::V3 + class Renderer::StorageExecutionsUsages < CollectionRenderer + type :storage_executions_usages + collection_key :storage_executions_usages + end +end diff --git a/lib/travis/api/v3/routes.rb b/lib/travis/api/v3/routes.rb index 49f8f3c79..c7ff32f11 100644 --- a/lib/travis/api/v3/routes.rb +++ b/lib/travis/api/v3/routes.rb @@ -176,6 +176,7 @@ module Routes delete :delete get :usage, '/usage' get :current_storage, '/current_storage' + get :storage_executions_usage, '/storage_executions_usage' end end diff --git a/lib/travis/api/v3/services/custom_images/storage_executions_usage.rb b/lib/travis/api/v3/services/custom_images/storage_executions_usage.rb new file mode 100644 index 000000000..6c22bbba3 --- /dev/null +++ b/lib/travis/api/v3/services/custom_images/storage_executions_usage.rb @@ -0,0 +1,17 @@ +module Travis::API::V3 + class Services::CustomImages::StorageExecutionsUsage < Service + result_type :storage_executions_usages + + def run! + raise MethodNotAllowed if Travis.config.org? + raise LoginRequired unless access_control.logged_in? + + owner = query(:owner).find + + raise NotFound unless owner + raise InsufficientAccess unless access_control.visible?(owner) + + result query(:custom_images).storage_executions_usage(owner, access_control.user.id) + end + end +end From a8a181e2f053da6df1f2524d518217eb38146116 Mon Sep 17 00:00:00 2001 From: Stanislav Colotinschi Date: Tue, 8 Jul 2025 16:37:28 +0300 Subject: [PATCH 19/20] Skip avatar URL from custom image renderer if email is empty This might happen when the Assembla commit was made in a non-standard way --- lib/travis/api/v3/renderer/custom_image.rb | 6 ++++-- .../services/custom_images/for_owner_spec.rb | 19 ++++++++++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/lib/travis/api/v3/renderer/custom_image.rb b/lib/travis/api/v3/renderer/custom_image.rb index 1a6622f82..490716d0d 100644 --- a/lib/travis/api/v3/renderer/custom_image.rb +++ b/lib/travis/api/v3/renderer/custom_image.rb @@ -6,6 +6,7 @@ class Renderer::CustomImage < ModelRenderer def created_by return nil unless user = model.created_by + { '@type' => 'user', '@href' => "/v3/user/#{user.id}", @@ -13,8 +14,9 @@ def created_by 'id' => user.id, 'login' => user.login, 'name' => user.name, - 'avatar_url' => user.avatar_url - } + }.tap do |data| + data['avatar_url'] = user.avatar_url if user.email.present? + end end end end diff --git a/spec/v3/services/custom_images/for_owner_spec.rb b/spec/v3/services/custom_images/for_owner_spec.rb index 36412debc..357d120d4 100644 --- a/spec/v3/services/custom_images/for_owner_spec.rb +++ b/spec/v3/services/custom_images/for_owner_spec.rb @@ -17,15 +17,28 @@ let!(:custom_image_log) { FactoryBot.create(:custom_image_log, custom_image: custom_image, sender_id: user.id) } context 'when user has custom images' do - it 'returns custom images' do - get("/v3/owner/#{user.login}/custom_images", {}, json_headers.merge('HTTP_AUTHORIZATION' => "token #{user_token}")) + subject { get("/v3/owner/#{user.login}/custom_images", {}, json_headers.merge('HTTP_AUTHORIZATION' => "token #{user_token}")) } + it 'returns custom images' do + subject expect(last_response).to be_ok - expect(JSON.parse(last_response.body)['custom_images'].first).to include( + custom_image_response = JSON.parse(last_response.body)['custom_images'].first + expect(custom_image_response).to include( 'id' => custom_image.id, 'name' => custom_image.name, 'size_bytes' => custom_image.size_bytes ) + expect(custom_image_response['created_by']['avatar_url']).to be_present + end + + context 'when user email is blank' do + before { user.update(email: nil) } + + it 'skips avatar_url' do + subject + expect(last_response).to be_ok + expect(JSON.parse(last_response.body)['custom_images'].first['created_by']).not_to have_key('avatar_url') + end end end From 2f01543ee5d9067399128c256578d8ca9a621d9e Mon Sep 17 00:00:00 2001 From: GbArc Date: Wed, 16 Jul 2025 15:29:47 +0200 Subject: [PATCH 20/20] subscription spec fixes -> new fields --- spec/v3/services/v2_subscriptions/all_spec.rb | 12 ++++++++++-- spec/v3/services/v2_subscriptions/create_spec.rb | 12 ++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/spec/v3/services/v2_subscriptions/all_spec.rb b/spec/v3/services/v2_subscriptions/all_spec.rb index 95083de1b..5e9f350e6 100644 --- a/spec/v3/services/v2_subscriptions/all_spec.rb +++ b/spec/v3/services/v2_subscriptions/all_spec.rb @@ -196,7 +196,11 @@ 'purchase_date' => '2017-11-28T00:09:59.502Z', 'valid_to' => '2017-12-28T00:09:59.502Z', 'status' => 'subscribed', - 'active' => true + 'active' => true, + 'total_usage'=> nil, + 'quantity_limit_charge'=> nil, + 'quantity_limit_free'=> 0, + 'quantity_limit_type'=> nil } }, { @@ -218,7 +222,11 @@ 'purchase_date' => '2017-11-28T00:09:59.502Z', 'valid_to' => '', 'status' => 'subscribed', - 'active' => true + 'active' => true, + 'total_usage'=> nil, + 'quantity_limit_charge'=> nil, + 'quantity_limit_free'=> 0, + 'quantity_limit_type'=> nil } } ], diff --git a/spec/v3/services/v2_subscriptions/create_spec.rb b/spec/v3/services/v2_subscriptions/create_spec.rb index efbf62f9e..5a83408cc 100644 --- a/spec/v3/services/v2_subscriptions/create_spec.rb +++ b/spec/v3/services/v2_subscriptions/create_spec.rb @@ -170,7 +170,11 @@ 'status': 'pending', 'active': false, 'created_at': '2017-11-28T00:09:59.502Z', - 'updated_at': '2017-11-28T00:09:59.502Z' + 'updated_at': '2017-11-28T00:09:59.502Z', + 'total_usage': nil, + 'quantity_limit_charge': nil, + 'quantity_limit_free': 0, + 'quantity_limit_type': nil, } }], 'discount' => nil, @@ -320,7 +324,11 @@ 'valid_to' => '2017-11-28T00:09:59.502Z', 'remaining' => 40_000, 'status' => 'pending', - 'active' => false + 'active' => false, + 'total_usage'=> nil, + 'quantity_limit_charge'=> nil, + 'quantity_limit_free'=> 0, + 'quantity_limit_type'=> nil, } }], 'client_secret' => 'client_secret',