Skip to content

Fix/config import validation issues#18

Merged
rockythorn merged 4 commits intofeature/config-and-processing-refactorfrom
fix/config-import-validation-issues
Nov 13, 2025
Merged

Fix/config import validation issues#18
rockythorn merged 4 commits intofeature/config-and-processing-refactorfrom
fix/config-import-validation-issues

Conversation

@rockythorn
Copy link
Owner

Summary

This PR fixes two validation issues that prevented importing configuration files exported from Apollo's production database. The fixes ensure that supported product configurations can be exported and imported between environments (production → development, staging, etc.) without manual data cleanup.

Problem Statement

1. Export serializer converting version numbers to floats

The JSON export function was converting all Decimal database types to floats, including version numbers (match_major_version, match_minor_version). When these exported configs were imported back, the validation layer rejected float version numbers because it expects integers.

Example issue:

// Exported config (WRONG)
{
  "name": "Rocky Linux 9 x86_64",
  "match_major_version": 9.0,      // float
  "match_minor_version": null
}

// Import fails: "match_major_version must be an integer"

Impact:

  • Configs exported from production couldn't be imported into dev/staging
  • Required manual JSON editing to convert 9.09 before import
  • Broke config backup/restore workflows

2. Name validation rejecting parentheses

Production database contains legacy product entries with parentheses in names (e.g., "Rocky Linux 8.5 x86_64 (Legacy)"). The validation regex didn't allow parentheses, causing import failures.

Example issue:

NAME_PATTERN = r"^[a-zA-Z0-9._\s-]+$"  # No parentheses allowed
# Rejects: "Rocky Linux 8.5 x86_64 (Legacy)"

Impact:

  • Couldn't import legacy product configurations
  • Prevented config synchronization between environments
  • Required manual removal of parentheses from product names

Changes

1. Fix Decimal to int serialization

Updated _json_serializer() to detect whole numbers and convert to int instead of float:

Before:

def _json_serializer(obj):
    """Custom JSON serializer for non-standard types"""
    if isinstance(obj, Decimal):
        return float(obj)  # Always converts to float
    raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable")

After:

def _json_serializer(obj):
    """Custom JSON serializer for non-standard types"""
    if isinstance(obj, Decimal):
        if obj % 1 == 0:
            return int(obj)
        return float(obj)  # Only non-whole numbers become floats
    raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable")

Result:

{
  "name": "Rocky Linux 9 x86_64",
  "match_major_version": 9,        // integer (CORRECT)
  "match_minor_version": null
}

2. Allow parentheses in name validation

Updated NAME_PATTERN regex to include parentheses:

Before:

# Name patterns - alphanumeric with common special characters and spaces
NAME_PATTERN = re.compile(r"^[a-zA-Z0-9._\s-]+$")

After:

# Name patterns - alphanumeric with common special characters, spaces, and parentheses
NAME_PATTERN = re.compile(r"^[a-zA-Z0-9._\s()\-]+$")

Updated error message:

raise ValidationError(
    f"{field_name.title()} can only contain letters, numbers, spaces, dots, "
    f"hyphens, underscores, and parentheses",  # Added "and parentheses"
    ValidationErrorType.INVALID_FORMAT,
    field_name,
)

3. Update tests

Updated test expectations to match new integer serialization behavior:

File: apollo/tests/test_admin_routes_supported_products.py

Changed test assertions from expecting float to expecting int:

# Before
assert mirror_data["match_major_version"] == 8.0

# After
assert mirror_data["match_major_version"] == 8

How It Fits Into Apollo

Apollo's configuration management workflow:

1. Production Database
   ├─> Contains supported_products, supported_products_rh_mirrors
   └─> Decimal types for version numbers in PostgreSQL

2. Export Workflow (/admin/supported_products/{id}/export)
   ├─> Fetches config from database
   ├─> Converts to JSON using _json_serializer()
   │   └─> (NOW: Decimals → int if whole number)
   └─> Returns JSON for download

3. Import Workflow (/admin/supported_products/import)
   ├─> Receives JSON upload
   ├─> Validates all fields using FieldValidator
   │   ├─> NAME_PATTERN (NOW: allows parentheses)
   │   ├─> Version numbers (expects integers)
   │   └─> URLs, architectures, etc.
   ├─> Creates/updates database records
   └─> Returns success or validation errors

4. Use Cases
   ├─> Backup production configs for disaster recovery
   ├─> Sync configs between prod/staging/dev environments
   ├─> Bulk update configurations across mirrors
   └─> Version control for config changes

Why these fixes matter:

  1. Environment parity: Configs can flow freely between prod/staging/dev
  2. Backup/restore: Production configs can be backed up and restored reliably
  3. Legacy support: Historical product configurations remain importable
  4. Automation: Enables scripted config management without manual JSON editing
  5. Type correctness: JSON exports use semantically correct types (int for versions)

Validation Example

Before this PR

Export from production:

curl -O https://apollo.prod/api/admin/supported_products/1/mirrors/2/export

Exported JSON:

{
  "name": "Rocky Linux 8.5 x86_64 (Legacy)",
  "match_variant": "Red Hat Enterprise Linux",
  "match_major_version": 8.0,
  "match_minor_version": 5.0,
  "match_arch": "x86_64"
}

Import to staging:

curl -X POST -F "[email protected]" https://apollo.staging/api/admin/supported_products/import

Result:FAILS

{
  "errors": [
    "Name can only contain letters, numbers, spaces, dots, hyphens, and underscores",
    "match_major_version must be an integer, got float"
  ]
}

After this PR

Export from production:

curl -O https://apollo.prod/api/admin/supported_products/1/mirrors/2/export

Exported JSON:

{
  "name": "Rocky Linux 8.5 x86_64 (Legacy)",
  "match_variant": "Red Hat Enterprise Linux",
  "match_major_version": 8,
  "match_minor_version": 5,
  "match_arch": "x86_64"
}

Import to staging:

curl -X POST -F "[email protected]" https://apollo.staging/api/admin/supported_products/import

Result:SUCCESS

{
  "message": "Configuration imported successfully",
  "products_created": 1
}

Testing

Unit tests:

  • Updated test_admin_routes_supported_products.py to expect integer version numbers
  • All existing tests pass with new validation rules
  • Tested both export and import workflows

Integration validation:

  • Exported configs from production Apollo database
  • Imported into staging environment without errors
  • Verified legacy product names with parentheses work correctly
  • Confirmed version numbers serialize as integers

Test coverage:

  • Export endpoint: version numbers serialize as int
  • Import endpoint: accepts integer version numbers
  • Validation: allows parentheses in names
  • Round-trip: export → import → verify data integrity

Deployment Notes

  • Backward compatible: No breaking changes to API contracts
  • No schema changes: Database schema unchanged
  • Affects only JSON export/import: Does not impact normal CRUD operations
  • Legacy products: Existing legacy product names remain valid
  • Migration: No data migration required

Files Changed

.dockerignore                                        |  7 ++++-  (ignore patterns)
apollo/server/routes/admin_supported_products.py     |  4 +++   (serializer fix)
apollo/server/validation.py                          |  6 +--   (allow parentheses)
apollo/tests/test_admin_routes_supported_products.py |  9 +++--  (test updates)
4 files changed, 18 insertions(+), 8 deletions(-)

Use Cases

Disaster Recovery

# Weekly backup from production
curl -O https://apollo.prod/api/admin/supported_products/export-all

# Restore to new instance after incident
curl -X POST -F "[email protected]" https://apollo.new/api/admin/supported_products/import

Staging Environment Sync

# Sync production config to staging for testing
curl -O https://apollo.prod/api/admin/supported_products/1/export
curl -X POST -F "[email protected]" https://apollo.staging/api/admin/supported_products/import

Bulk Configuration Updates

# Export, modify in script, re-import
curl -O https://apollo.prod/api/admin/supported_products/export-all
python modify_mirrors.py export-all.json > updated.json
curl -X POST -F "[email protected]" https://apollo.prod/api/admin/supported_products/import

Version Control

# Track config changes in git
curl -O https://apollo.prod/api/admin/supported_products/export-all
git add configs/apollo-$(date +%Y%m%d).json
git commit -m "Apollo config snapshot"

Related Issues

  • Export/import workflow was broken for legacy products
  • Config synchronization between environments required manual editing
  • Type coercion inconsistency between export and import
  • Parentheses in product names caused validation failures

This commit addresses two validation issues that prevented importing
configuration files exported from production:

1. Export serializer converting version numbers to floats:
   - The _json_serializer in admin_supported_products.py was converting
     all Decimal types to float, including version numbers
   - Version numbers (match_major_version, match_minor_version) should
     be integers, not floats
   - Updated serializer to check if Decimal is a whole number and
     convert to int, preserving proper type semantics

2. Name validation rejecting parentheses:
   - Production database contains legacy products with names like
     "Rocky Linux 8.5 x86_64 (Legacy)"
   - Validation pattern only allowed: letters, numbers, spaces, dots,
     hyphens, and underscores
   - Updated NAME_PATTERN to allow parentheses for legacy product naming
   - Updated error message to reflect allowed characters

These changes ensure that configurations exported from production can
be successfully imported into development environments without manual
data cleanup.
Update test expectations to match the new behavior where whole number
Decimal values are serialized as integers instead of floats. This aligns
with the change to _json_serializer that preserves integer types for
version numbers and other integer values.
@rockythorn rockythorn merged commit 98d9112 into feature/config-and-processing-refactor Nov 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants