Compare commits

...
9 changed files with 285 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
class CloudflareCleanupJob < ApplicationJob
queue_as :housekeeping
def perform
Rails.logger.info 'CloudflareCleanupJob: No cleanup needed (Community Edition)'
end
end
CloudflareCleanupJob.prepend_mod_with('CloudflareCleanupJob')
+7
View File
@@ -46,3 +46,10 @@ delete_accounts_job:
cron: '0 1 * * *'
class: 'Internal::DeleteAccountsJob'
queue: scheduled_jobs
# executed weekly on Sunday at 0200 UTC
# to cleanup orphaned Cloudflare custom hostnames
cloudflare_cleanup_job:
cron: '0 2 * * 0'
class: 'CloudflareCleanupJob'
queue: housekeeping
@@ -0,0 +1,39 @@
module Enterprise::CloudflareCleanupJob
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
def perform
return unless ChatwootApp.chatwoot_cloud?
Rails.logger.info 'Starting Cloudflare custom hostname cleanup'
result = Cloudflare::ListCustomHostnamesService.new.perform
if result[:errors].present?
Rails.logger.error "Failed to fetch custom hostnames from Cloudflare: #{result[:errors]}"
return
end
cloudflare_hostnames = result[:data] || []
existing_domains = Portal.where.not(custom_domain: [nil, '']).pluck(:custom_domain)
orphaned_hostnames = cloudflare_hostnames.reject do |hostname|
existing_domains.include?(hostname['hostname'])
end
Rails.logger.info "Found #{orphaned_hostnames.size} orphaned custom hostnames to cleanup"
orphaned_hostnames.each do |hostname|
cleanup_result = Cloudflare::DeleteCustomHostnameService.new(
hostname_id: hostname['id']
).perform
if cleanup_result[:errors].present?
Rails.logger.error "Failed to delete hostname #{hostname['hostname']}: #{cleanup_result[:errors]}"
else
Rails.logger.info "Successfully deleted orphaned hostname: #{hostname['hostname']}"
end
end
Rails.logger.info 'Completed Cloudflare custom hostname cleanup'
end
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
end
@@ -0,0 +1,17 @@
class Cloudflare::DeleteCustomHostnameService < Cloudflare::BaseCloudflareZoneService
pattr_initialize [:hostname_id!]
def perform
return { errors: ['Cloudflare API token or zone ID not found'] } if api_token.blank? || zone_id.blank?
return { errors: ['Hostname ID is required'] } if @hostname_id.blank?
response = HTTParty.delete(
"#{BASE_URI}/zones/#{zone_id}/custom_hostnames/#{@hostname_id}",
headers: headers
)
return { errors: response.parsed_response['errors'] } unless response.success?
{ success: true }
end
end
@@ -0,0 +1,14 @@
class Cloudflare::ListCustomHostnamesService < Cloudflare::BaseCloudflareZoneService
def perform
return { errors: ['Cloudflare API token or zone ID not found'] } if api_token.blank? || zone_id.blank?
response = HTTParty.get(
"#{BASE_URI}/zones/#{zone_id}/custom_hostnames",
headers: headers
)
return { errors: response.parsed_response['errors'] } unless response.success?
{ data: response.parsed_response['result'] }
end
end
@@ -0,0 +1,53 @@
require 'rails_helper'
RSpec.describe CloudflareCleanupJob, type: :job do
let(:list_service) { instance_double(Cloudflare::ListCustomHostnamesService) }
let(:delete_service) { instance_double(Cloudflare::DeleteCustomHostnameService) }
before do
allow(Cloudflare::ListCustomHostnamesService).to receive(:new).and_return(list_service)
allow(Cloudflare::DeleteCustomHostnameService).to receive(:new).and_return(delete_service)
end
describe '#perform' do
context 'when not chatwoot cloud' do
it 'skips cleanup' do
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(false)
expect(list_service).not_to receive(:perform)
described_class.perform_now
end
end
context 'when chatwoot cloud' do
before do
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
end
it 'calls list service and processes results' do
allow(list_service).to receive(:perform).and_return({ data: [] })
described_class.perform_now
expect(list_service).to have_received(:perform)
end
it 'calls delete service for orphaned hostnames' do
create(:portal, custom_domain: 'existing.com')
hostnames = [
{ 'id' => 'keep-id', 'hostname' => 'existing.com' },
{ 'id' => 'delete-id', 'hostname' => 'orphaned.com' }
]
allow(list_service).to receive(:perform).and_return({ data: hostnames })
allow(delete_service).to receive(:perform).and_return({ success: true })
described_class.perform_now
expect(Cloudflare::DeleteCustomHostnameService).to have_received(:new)
.with(hostname_id: 'delete-id')
expect(delete_service).to have_received(:perform)
end
end
end
end
@@ -0,0 +1,61 @@
require 'rails_helper'
RSpec.describe Cloudflare::DeleteCustomHostnameService do
let(:hostname_id) { 'hostname-id-123' }
let(:service) { described_class.new(hostname_id: hostname_id) }
before do
create(:installation_config, name: 'CLOUDFLARE_API_KEY', value: 'test-api-token')
create(:installation_config, name: 'CLOUDFLARE_ZONE_ID', value: 'test-zone-id')
end
describe '#perform' do
context 'when API token or zone ID is missing' do
it 'returns error when api_token is blank' do
InstallationConfig.find_by(name: 'CLOUDFLARE_API_KEY').update(value: '')
result = service.perform
expect(result[:errors]).to eq(['Cloudflare API token or zone ID not found'])
end
it 'returns error when zone_id is blank' do
InstallationConfig.find_by(name: 'CLOUDFLARE_ZONE_ID').update(value: '')
result = service.perform
expect(result[:errors]).to eq(['Cloudflare API token or zone ID not found'])
end
end
context 'when hostname_id is missing' do
it 'returns error' do
service_without_id = described_class.new(hostname_id: '')
result = service_without_id.perform
expect(result[:errors]).to eq(['Hostname ID is required'])
end
end
context 'when API request succeeds' do
it 'returns success' do
stub_request(:delete, "https://api.cloudflare.com/client/v4/zones/test-zone-id/custom_hostnames/#{hostname_id}")
.to_return(status: 200, body: {}.to_json, headers: { 'Content-Type' => 'application/json' })
result = service.perform
expect(result[:success]).to be_truthy
end
end
context 'when API request fails' do
it 'returns API errors' do
error_response = {
'errors' => [{ 'code' => 1234, 'message' => 'Hostname not found' }]
}
stub_request(:delete, "https://api.cloudflare.com/client/v4/zones/test-zone-id/custom_hostnames/#{hostname_id}")
.to_return(status: 404, body: error_response.to_json, headers: { 'Content-Type' => 'application/json' })
result = service.perform
expect(result[:errors]).to eq(error_response['errors'])
end
end
end
end
@@ -0,0 +1,75 @@
require 'rails_helper'
RSpec.describe Cloudflare::ListCustomHostnamesService do
let(:service) { described_class.new }
before do
create(:installation_config, name: 'CLOUDFLARE_API_KEY', value: 'test-api-token')
create(:installation_config, name: 'CLOUDFLARE_ZONE_ID', value: 'test-zone-id')
end
describe '#perform' do
context 'when API token or zone ID is missing' do
it 'returns error when api_token is blank' do
InstallationConfig.find_by(name: 'CLOUDFLARE_API_KEY').update(value: '')
result = service.perform
expect(result[:errors]).to eq(['Cloudflare API token or zone ID not found'])
end
it 'returns error when zone_id is blank' do
InstallationConfig.find_by(name: 'CLOUDFLARE_ZONE_ID').update(value: '')
result = service.perform
expect(result[:errors]).to eq(['Cloudflare API token or zone ID not found'])
end
end
context 'when API request succeeds' do
let(:response_body) do
{
'success' => true,
'errors' => [],
'messages' => [],
'result' => [
{
'id' => '023e105f4ecef8ad9ca31a8372d0c353',
'hostname' => 'app.example.com',
'status' => 'active',
'ssl' => { 'id' => '0d89c70d-ad9f-4843-b99f-6cc0252067e9', 'status' => 'active', 'method' => 'http', 'type' => 'dv' }
},
{
'id' => '124e205f4ecef8ad9ca31a8372d0c454',
'hostname' => 'portal.example.com',
'status' => 'pending',
'ssl' => { 'id' => '1d89c70d-ad9f-4843-b99f-6cc0252067f0', 'status' => 'pending', 'method' => 'http', 'type' => 'dv' }
}
],
'result_info' => { 'page' => 1, 'per_page' => 20, 'count' => 2, 'total_count' => 2 }
}
end
it 'returns custom hostnames data' do
stub_request(:get, 'https://api.cloudflare.com/client/v4/zones/test-zone-id/custom_hostnames')
.to_return(status: 200, body: response_body.to_json, headers: { 'Content-Type' => 'application/json' })
result = service.perform
expect(result[:data]).to eq(response_body['result'])
end
end
context 'when API request fails' do
it 'returns API errors' do
error_response = {
'errors' => [{ 'code' => 1234, 'message' => 'API Error' }]
}
stub_request(:get, 'https://api.cloudflare.com/client/v4/zones/test-zone-id/custom_hostnames')
.to_return(status: 400, body: error_response.to_json, headers: { 'Content-Type' => 'application/json' })
result = service.perform
expect(result[:errors]).to eq(error_response['errors'])
end
end
end
end
+10
View File
@@ -0,0 +1,10 @@
require 'rails_helper'
RSpec.describe CloudflareCleanupJob, type: :job do
subject(:job) { described_class.perform_later }
it 'enqueues the job' do
expect { job }.to have_enqueued_job(described_class)
.on_queue('housekeeping')
end
end