3
3
4
4
from django .conf import settings
5
5
from jsonschema import Draft7Validator
6
- from pulpcore .plugin .models import AsciiArmoredDetachedSigningService , Publication , Remote
6
+ from pulpcore .plugin .models import (
7
+ AsciiArmoredDetachedSigningService ,
8
+ Publication ,
9
+ Remote ,
10
+ Content ,
11
+ RepositoryVersion ,
12
+ )
7
13
from pulpcore .plugin .serializers import (
8
14
DetailRelatedField ,
9
15
DistributionSerializer ,
37
43
)
38
44
from pulp_rpm .app .schema import COPY_CONFIG_SCHEMA
39
45
from urllib .parse import urlparse
46
+ from textwrap import dedent
47
+
48
+ try :
49
+ from pulpcore .plugin .util import resolve_prn
50
+ except ImportError :
51
+
52
+ def resolve_prn (prn ):
53
+ raise serializers .ValidationError (_ ("The pulpcore version in use does not support PRNs." ))
40
54
41
55
42
56
class RpmRepositorySerializer (RepositorySerializer ):
@@ -543,7 +557,32 @@ class CopySerializer(ValidateFieldsMixin, serializers.Serializer):
543
557
"""
544
558
545
559
config = CustomJSONField (
546
- help_text = _ ("A JSON document describing sources, destinations, and content to be copied" ),
560
+ help_text = _ (
561
+ dedent (
562
+ """\
563
+ A JSON document describing sources, destinations, and content to be copied.
564
+
565
+ Its a list of dictionaries with the following available fields:
566
+
567
+ ```json
568
+ [
569
+ {
570
+ "source_repo_version": <RepositoryVersion [pulp_href|prn]>,
571
+ "dest_repo": <RpmRepository [pulp_href|prn]>,
572
+ "dest_base_version": <int>,
573
+ "content": [<Content [pulp_href|prn]>, ...]
574
+ },
575
+ ...
576
+ ]
577
+ ```
578
+
579
+ If domains are enabled, the refered pulp objects must be part of the current domain.
580
+
581
+ For usage examples, refer to the advanced copy guide:
582
+ <https://pulpproject.org/pulp_rpm/docs/user/guides/modify/#advanced-copy-workflow>
583
+ """
584
+ )
585
+ ),
547
586
)
548
587
549
588
dependency_solving = serializers .BooleanField (
@@ -558,29 +597,83 @@ def validate(self, data):
558
597
Check for cross-domain references (if domain-enabled).
559
598
"""
560
599
561
- def check_domain (domain , href , name ):
562
- # We're doing just a string-check here rather than checking objects
563
- # because there can be A LOT of objects, and this is happening in the view-layer
564
- # where we have strictly-limited timescales to work with
565
- if href and domain not in href :
600
+ def raise_validation (field , domain , id = "" ):
601
+ id = f"\n { id } " if id else ""
602
+ raise serializers .ValidationError (
603
+ _ ("The field {} contains object(s) not in {} domain.{}" .format (field , domain , id ))
604
+ )
605
+
606
+ def parse_reference (ref ) -> tuple [str , str , bool ]:
607
+ """Extract info from prn/href to enable checking domains.
608
+
609
+ This is used for:
610
+ 1. In case of HREFS, avoid expensive extract_pk(href) to get pks.
611
+ 2. HREF and PRNs have different information hardcoded available.
612
+ E.g: RepositoryVerseion HREF has its Repository pk; PRNs have the RepoVer pk.
613
+
614
+ Returns a tuple with (pk, class_name, is_prn)
615
+ """
616
+ if ref .startswith ("prn:" ):
617
+ ref_class , pk = resolve_prn (ref )
618
+ return (pk , ref_class , True )
619
+ # content: ${BASE}/content/rpm/packages/${UUID}/
620
+ # repository: ${BASE}/repositories/rpm/rpm/${UUID}/
621
+ # repover: ${BASE}/repositories/rpm/rpm/${UUID}/versions/0/
622
+ url = urlparse (ref ).path .strip ("/" ).split ("/" )
623
+ ref_class = RpmRepository if "/repositories/" in ref else Content
624
+ is_repover_href = url [- 1 ].isdigit () and url [- 2 ] == "versions"
625
+ uuid = url [- 3 ] if is_repover_href else url [- 1 ]
626
+ if len (uuid ) < 32 :
566
627
raise serializers .ValidationError (
567
- _ ("{} must be a part of the {} domain." ) .format (name , domain )
628
+ _ ("The href path should end with a uuid pk: {}" .format (ref ) )
568
629
)
630
+ return (uuid , ref_class , False )
631
+
632
+ def check_domain (entry , name , curr_domain ):
633
+ href_or_prn = entry [name ]
634
+ resource_pk , ref_class , is_prn = parse_reference (href_or_prn )
635
+ try :
636
+ if ref_class is RepositoryVersion and is_prn :
637
+ resource_domain_pk = (
638
+ RepositoryVersion .objects .select_related ("repository" )
639
+ .values ("repository__pulp_domain" )
640
+ .get (pk = resource_pk )["repository__pulp_domain" ]
641
+ )
642
+ else :
643
+ resource_domain_pk = RpmRepository .objects .values ("pulp_domain" ).get (
644
+ pk = resource_pk
645
+ )["pulp_domain" ]
646
+ except RepositoryVersion .DoesNotExit as e :
647
+ raise serializers .ValidationError from e
648
+ except RpmRepository .DoesNotExit as e :
649
+ raise serializers .ValidationError from e
650
+
651
+ if resource_domain_pk != curr_domain .pk :
652
+ raise_validation (name , curr_domain .name , resource_domain_pk )
569
653
570
654
def check_cross_domain_config (cfg ):
571
655
"""Check that all config-elts are in 'our' domain."""
572
656
# copy-cfg is a list of dictionaries.
573
657
# source_repo_version and dest_repo are required fields.
574
658
# Insure curr-domain exists in src/dest/dest_base_version/content-list hrefs
575
- curr_domain_name = get_domain (). name
659
+ curr_domain = get_domain ()
576
660
for entry in cfg :
577
- check_domain (curr_domain_name , entry ["source_repo_version" ], "dest_repo" )
578
- check_domain (curr_domain_name , entry ["dest_repo" ], "dest_repo" )
579
- check_domain (
580
- curr_domain_name , entry .get ("dest_base_version" , None ), "dest_base_version"
581
- )
582
- for content_href in entry .get ("content" , []):
583
- check_domain (curr_domain_name , content_href , "content" )
661
+ # Check required fields individually
662
+ check_domain (entry , "source_repo_version" , curr_domain )
663
+ check_domain (entry , "dest_repo" , curr_domain )
664
+
665
+ # Check content generically to avoid timeout of multiple calls
666
+ content_list = entry .get ("content" , None )
667
+ if content_list :
668
+ content_list = [parse_reference (v )[0 ] for v in content_list ]
669
+ distinct = (
670
+ Content .objects .filter (pk__in = content_list ).values ("pulp_domain" ).distinct ()
671
+ )
672
+ domain_ok = (
673
+ len (distinct ) == 1 and distinct .first ()["pulp_domain" ] == curr_domain .pk
674
+ )
675
+ if not domain_ok :
676
+ raise_validation ("content" , curr_domain .name )
584
677
585
678
super ().validate (data )
586
679
if "config" in data :
0 commit comments