[PULP-1749] Add migrate endpoint for push repositories - #2406
Conversation
|
Do you plan to add a management command to migrate all remaining repositories? Do you think we need to keep the old href's around as an alias? (Or by returning a clever redirect?) |
Yeah. I think I'll do it in a separate PR once I get this right.
No, this is one downside of the migration is that it's a completely new object, old hrefs won't work. I think it won't be a big deal since these repos are auto-created on push and auto-deleted when the distro is removed, so I don't expect many users are keeping track of these hrefs. Adding a compatibility layer is a lot of effort so my plan is to make the docs explicit on how the migration works so users understand what is happening. |
Provide an async API to convert legacy ContainerPushRepository instances into ContainerRepository while preserving content and distribution links. Co-authored-by: Cursor <cursoragent@cursor.com>
| original_name = push_repository.name | ||
| temp_name = f"{original_name}__migrating__{push_repository.pk}" | ||
|
|
||
| with transaction.atomic(): |
There was a problem hiding this comment.
Is there any structural difference between the ContainerRepository and the ContainerPushRepository table?
(I am wondering if we could do something clever in SQL to just change the DNA of the existing object.)
As far as I know the inheritance here is managed with a one-to-one relation so that the pk of the parent model is also the pk of the child model. And since the content models in those repositories as well as the repository versions are compatible (as far as I know), this could be a single create and delete in a transaction.
There was a problem hiding this comment.
For example, we used this code in an old pulp_file migration:
def migrate_data_from_old_model_to_new_model_up(apps, schema_editor):
""" Move objects from FileDistribution to NewFileDistribution."""
FileDistribution = apps.get_model('file', 'FileDistribution')
NewFileDistribution = apps.get_model('file', 'NewFileDistribution')
for file_distribution in FileDistribution.objects.all():
with transaction.atomic():
NewFileDistribution(
pulp_id=file_distribution.pulp_id,
pulp_created=file_distribution.pulp_created,
pulp_last_updated=file_distribution.pulp_last_updated,
pulp_type=file_distribution.pulp_type,
name=file_distribution.name,
base_path=file_distribution.base_path,
content_guard=file_distribution.content_guard,
remote=file_distribution.remote,
publication=file_distribution.publication
).save()
file_distribution.delete()
Maybe the "trick" here is that the raw Django migration level objects don't "know" about the pulp specific MasterDetailModel and can carefully work around that.
There was a problem hiding this comment.
e.g. to turn a file_repository into a pgp keyring (yes, we should not do that becuase they are not as compatible...) i needed to call these three commands:
insert into core_openpgpkeyring (repository_ptr_id) VALUES ('019fd0fc-103f-775a-bb16-f92440ba53dc');
update core_repository set pulp_type = 'core.openpgp' where pulp_id = '019fd0fc-103f-775a-bb16-f92440ba53dc';
delete from file_filerepository where repository_ptr_id = '019fd0fc-103f-775a-bb16-f92440ba53dc';
There was a problem hiding this comment.
Ok, I've tested this out and yeah the ORM code in the migration behaves differently from normal ORM code and runs into issues with the multi-table inheritance. But we can still do the SQL migrate manually and we can preserve mostly everything. See my latest commit.
mdellweg
left a comment
There was a problem hiding this comment.
I very much like this approach!
| pk=push_repository_pk | ||
| ) | ||
| if push_repository.pulp_type != ContainerPushRepository.get_pulp_type(): | ||
| raise RuntimeError( |
There was a problem hiding this comment.
This is a sanity check, since we expect the transition down below to happen in a transaction.
Maybe with a comment we can prevent future readers to panic.
| push_repository_pk (str): The primary key for the push repository to migrate. | ||
| """ | ||
| with transaction.atomic(): | ||
| push_repository = ContainerPushRepository.objects.select_for_update().get( |
There was a problem hiding this comment.
I'm not sure what the select_for_update is supposed to protect here, but why not play it extra safe...
OTOH, does this "select for update" the base Repository row too?
There was a problem hiding this comment.
How about adding cursor.execute("set transaction isolation level serializable") as the very first statement in this transaction?
It could be the single one measure to protect all the reads happening here, including the GenericForeignKeys.
It may be a hit on performance, and could lead to SerializationErrors but for this kind of once in a lifetime migration operation it's probably a good tradeoff.
There was a problem hiding this comment.
Yes, select_for_update locks both parents and childs row. Added the SET transaction level.
| # Deleting the push child cascades its pending M2M rows; parent repo is kept. | ||
| cursor.execute( | ||
| """ | ||
| DELETE FROM container_containerpushrepository | ||
| WHERE repository_ptr_id = %s | ||
| """, | ||
| [push_repository_pk], | ||
| ) |
There was a problem hiding this comment.
Is Django really setting up cascade delete in the database?
I guess this is just something to keep looking out for.
There was a problem hiding this comment.
Apparently not, good call out, fixed.
| UserRole.objects.filter(content_type=push_ct, object_id=str(push_repository_pk)).update( | ||
| content_type=container_ct | ||
| ) | ||
| GroupRole.objects.filter(content_type=push_ct, object_id=str(push_repository_pk)).update( | ||
| content_type=container_ct | ||
| ) |
| [push_repository_pk, signing_service_id], | ||
| ) | ||
|
|
||
| cursor.execute( |
There was a problem hiding this comment.
Hah, I guess it wouldn't be dramatic to loose the pending items accidentally, but yes, good call too.
Reminds me that "pending content" should probably be a pulpcore thing by now.
| f"Container repository child row already exists for {push_repository_pk}." | ||
| ) | ||
|
|
||
| signing_service_id = push_repository.manifest_signing_service_id |
There was a problem hiding this comment.
This is the one "normal" field we have to transfer from the child table, right?
Summary
migrateaction on push repositories that dispatches a task to convertContainerPushRepositoryintoContainerRepositorycopy_versionsto preserve full repository version history (defaults to copying only the latest version)Test plan
pytest pulp_container/tests/functional/api/test_migrate_push_repository.pyPOST /pulp/api/v3/repositories/container/container-push/{pk}/migrate/returns 202 and completes successfullycopy_versions: truewhen multiple repository versions existMade with Cursor