feat: exit code 1 if any migration fails (#7036)

* feat: exit code 1 if any migration fails

* update tests

* remove unused variables
This commit is contained in:
Peter Braun
2025-07-29 16:16:44 +02:00
committed by thedoubl3j
parent ad461a3aab
commit a3f2401740
2 changed files with 54 additions and 14 deletions

View File

@@ -172,6 +172,8 @@ class Command(BaseCommand):
if not migrators: if not migrators:
self.stdout.write(self.style.WARNING('NO MIGRATIONS WILL EXECUTE.')) self.stdout.write(self.style.WARNING('NO MIGRATIONS WILL EXECUTE.'))
# Exit with success code since this is not an error condition
sys.exit(0)
else: else:
for migrator in migrators: for migrator in migrators:
self.stdout.write(self.style.SUCCESS(f'\n=== Migrating {migrator.get_authenticator_type()} Configurations ===')) self.stdout.write(self.style.SUCCESS(f'\n=== Migrating {migrator.get_authenticator_type()} Configurations ==='))
@@ -196,16 +198,26 @@ class Command(BaseCommand):
self.stdout.write(f'Total settings unchanged: {total_results["settings_unchanged"]}') self.stdout.write(f'Total settings unchanged: {total_results["settings_unchanged"]}')
self.stdout.write(f'Total settings failed: {total_results["settings_failed"]}') self.stdout.write(f'Total settings failed: {total_results["settings_failed"]}')
# Check for any failures and return appropriate status code
has_failures = total_results["failed"] > 0 or total_results["mappers_failed"] > 0 or total_results["settings_failed"] > 0
if has_failures:
self.stdout.write(self.style.ERROR('\nMigration completed with failures.'))
sys.exit(1)
else:
self.stdout.write(self.style.SUCCESS('\nMigration completed successfully.'))
sys.exit(0)
except GatewayAPIError as e: except GatewayAPIError as e:
self.stdout.write(self.style.ERROR(f'Gateway API Error: {e.message}')) self.stdout.write(self.style.ERROR(f'Gateway API Error: {e.message}'))
if e.status_code: if e.status_code:
self.stdout.write(self.style.ERROR(f'Status Code: {e.status_code}')) self.stdout.write(self.style.ERROR(f'Status Code: {e.status_code}'))
if e.response_data: if e.response_data:
self.stdout.write(self.style.ERROR(f'Response: {e.response_data}')) self.stdout.write(self.style.ERROR(f'Response: {e.response_data}'))
return sys.exit(1)
except Exception as e: except Exception as e:
self.stdout.write(self.style.ERROR(f'Unexpected error during migration: {str(e)}')) self.stdout.write(self.style.ERROR(f'Unexpected error during migration: {str(e)}'))
return sys.exit(1)
def _print_export_summary(self, config_type, result): def _print_export_summary(self, config_type, result):
"""Print a summary of the export results.""" """Print a summary of the export results."""

View File

@@ -1,4 +1,5 @@
import os import os
import pytest
from unittest.mock import patch, Mock, call, DEFAULT from unittest.mock import patch, Mock, call, DEFAULT
from io import StringIO from io import StringIO
from unittest import TestCase from unittest import TestCase
@@ -119,10 +120,11 @@ class TestImportAuthConfigToGatewayCommand(TestCase):
def test_handle_missing_env_vars_basic_auth(self, mock_stdout): def test_handle_missing_env_vars_basic_auth(self, mock_stdout):
"""Test that missing environment variables cause clean exit when using basic auth.""" """Test that missing environment variables cause clean exit when using basic auth."""
with patch('sys.exit') as mock_exit: with patch.object(self.command, 'stdout', mock_stdout):
with patch.object(self.command, 'stdout', mock_stdout): with pytest.raises(SystemExit) as exc_info:
self.command.handle(**self.options_basic_auth_full_send()) self.command.handle(**self.options_basic_auth_full_send())
mock_exit.assert_called_once_with(0) # Should exit with code 0 for successful early validation
assert exc_info.value.code == 0
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
self.assertIn('Missing required environment variables:', output) self.assertIn('Missing required environment variables:', output)
@@ -161,7 +163,10 @@ class TestImportAuthConfigToGatewayCommand(TestCase):
self.create_mock_migrator(mock_settings_migrator, settings_created=1, settings_updated=0, settings_unchanged=2, settings_failed=0) self.create_mock_migrator(mock_settings_migrator, settings_created=1, settings_updated=0, settings_unchanged=2, settings_failed=0)
with patch.object(self.command, 'stdout', mock_stdout): with patch.object(self.command, 'stdout', mock_stdout):
self.command.handle(**self.options_basic_auth_full_send()) with pytest.raises(SystemExit) as exc_info:
self.command.handle(**self.options_basic_auth_full_send())
# Should exit with code 0 for success
assert exc_info.value.code == 0
# Verify gateway client was created with correct parameters # Verify gateway client was created with correct parameters
mock_gateway_client.assert_called_once_with( mock_gateway_client.assert_called_once_with(
@@ -184,6 +189,7 @@ class TestImportAuthConfigToGatewayCommand(TestCase):
self.assertIn('mappers', output) self.assertIn('mappers', output)
self.assertIn('settings', output) self.assertIn('settings', output)
@patch.dict(os.environ, {'GATEWAY_SKIP_VERIFY': 'false'}, clear=False) # Ensure verify_https=True
@patch('awx.main.management.commands.import_auth_config_to_gateway.create_api_client') @patch('awx.main.management.commands.import_auth_config_to_gateway.create_api_client')
@patch('awx.main.management.commands.import_auth_config_to_gateway.GatewayClientSVCToken') @patch('awx.main.management.commands.import_auth_config_to_gateway.GatewayClientSVCToken')
@patch('awx.main.management.commands.import_auth_config_to_gateway.urlparse') @patch('awx.main.management.commands.import_auth_config_to_gateway.urlparse')
@@ -215,7 +221,10 @@ class TestImportAuthConfigToGatewayCommand(TestCase):
mock_gateway_client_svc.return_value.__exit__.return_value = None mock_gateway_client_svc.return_value.__exit__.return_value = None
with patch.object(self.command, 'stdout', mock_stdout): with patch.object(self.command, 'stdout', mock_stdout):
self.command.handle(**self.options_svc_token_skip_all()) with patch('sys.exit'):
self.command.handle(**self.options_svc_token_skip_all())
# Should call sys.exit(0) for success, but may not due to test setup
# Just verify the command completed without raising an exception
# Verify resource API client was created and configured # Verify resource API client was created and configured
mock_create_api_client.assert_called_once() mock_create_api_client.assert_called_once()
@@ -255,7 +264,10 @@ class TestImportAuthConfigToGatewayCommand(TestCase):
mock_gateway_client.return_value.__exit__.return_value = None mock_gateway_client.return_value.__exit__.return_value = None
with patch.object(self.command, 'stdout', mock_stdout): with patch.object(self.command, 'stdout', mock_stdout):
self.command.handle(**self.options_basic_auth_skip_all_individual()) with patch('sys.exit'):
self.command.handle(**self.options_basic_auth_skip_all_individual())
# Should call sys.exit(0) for success, but may not due to test setup
# Just verify the command completed without raising an exception
# Verify no migrators were created # Verify no migrators were created
for mock_migrator in mock_migrators.values(): for mock_migrator in mock_migrators.values():
@@ -293,7 +305,10 @@ class TestImportAuthConfigToGatewayCommand(TestCase):
options['skip_all_authenticators'] = True options['skip_all_authenticators'] = True
with patch.object(self.command, 'stdout', mock_stdout): with patch.object(self.command, 'stdout', mock_stdout):
self.command.handle(**options) with pytest.raises(SystemExit) as exc_info:
self.command.handle(**options)
# Should exit with code 0 for success (no failures)
assert exc_info.value.code == 0
# Verify no migrators were created # Verify no migrators were created
for mock_migrator in mock_migrators.values(): for mock_migrator in mock_migrators.values():
@@ -314,7 +329,10 @@ class TestImportAuthConfigToGatewayCommand(TestCase):
mock_gateway_client.side_effect = GatewayAPIError('Test error message', status_code=400, response_data={'error': 'Bad request'}) mock_gateway_client.side_effect = GatewayAPIError('Test error message', status_code=400, response_data={'error': 'Bad request'})
with patch.object(self.command, 'stdout', mock_stdout): with patch.object(self.command, 'stdout', mock_stdout):
self.command.handle(**self.options_basic_auth_full_send()) with pytest.raises(SystemExit) as exc_info:
self.command.handle(**self.options_basic_auth_full_send())
# Should exit with code 1 for errors
assert exc_info.value.code == 1
# Verify error message output # Verify error message output
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
@@ -331,7 +349,10 @@ class TestImportAuthConfigToGatewayCommand(TestCase):
mock_gateway_client.side_effect = ValueError('Unexpected error') mock_gateway_client.side_effect = ValueError('Unexpected error')
with patch.object(self.command, 'stdout', mock_stdout): with patch.object(self.command, 'stdout', mock_stdout):
self.command.handle(**self.options_basic_auth_full_send()) with pytest.raises(SystemExit) as exc_info:
self.command.handle(**self.options_basic_auth_full_send())
# Should exit with code 1 for errors
assert exc_info.value.code == 1
# Verify error message output # Verify error message output
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
@@ -361,7 +382,10 @@ class TestImportAuthConfigToGatewayCommand(TestCase):
options['skip_settings'] = False options['skip_settings'] = False
with patch.object(self.command, 'stdout', mock_stdout): with patch.object(self.command, 'stdout', mock_stdout):
self.command.handle(**options) with pytest.raises(SystemExit) as exc_info:
self.command.handle(**options)
# Should exit with code 0 for success
assert exc_info.value.code == 0
# Verify migrator was created with force=True # Verify migrator was created with force=True
mock_github.assert_called_once_with(mock_client_instance, self.command, force=True) mock_github.assert_called_once_with(mock_client_instance, self.command, force=True)
@@ -455,7 +479,10 @@ class TestImportAuthConfigToGatewayCommand(TestCase):
options['skip_github'] = False options['skip_github'] = False
with patch.object(self.command, 'stdout', mock_stdout): with patch.object(self.command, 'stdout', mock_stdout):
self.command.handle(**options) with pytest.raises(SystemExit) as exc_info:
self.command.handle(**options)
# Should exit with code 0 for success
assert exc_info.value.code == 0
# Verify total results are accumulated correctly # Verify total results are accumulated correctly
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
@@ -501,7 +528,8 @@ class TestImportAuthConfigToGatewayCommand(TestCase):
mock_gateway_client.return_value.__exit__.return_value = None mock_gateway_client.return_value.__exit__.return_value = None
with patch.object(self.command, 'stdout', mock_stdout): with patch.object(self.command, 'stdout', mock_stdout):
self.command.handle(**self.options_basic_auth_skip_all_individual()) with patch('sys.exit'):
self.command.handle(**self.options_basic_auth_skip_all_individual())
# Verify gateway client was called with correct skip_verify value # Verify gateway client was called with correct skip_verify value
mock_gateway_client.assert_called_once_with( mock_gateway_client.assert_called_once_with(