Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8debb3be2d | ||
|
|
6661ecc9e6 | ||
|
|
deb5436930 | ||
|
|
d6e69cb9f5 | ||
|
|
3700a810c7 |
@@ -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')
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user