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 ,
14
20
RepositorySyncURLSerializer ,
15
21
ValidateFieldsMixin ,
16
22
)
17
- from pulpcore .plugin .util import get_domain
23
+ from pulpcore .plugin .util import get_domain , resolve_prn
18
24
from rest_framework import serializers
19
25
from pulp_rpm .app .fields import CustomJSONField
20
26
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
40
47
41
48
42
49
class RpmRepositorySerializer (RepositorySerializer ):
@@ -543,7 +550,32 @@ class CopySerializer(ValidateFieldsMixin, serializers.Serializer):
543
550
"""
544
551
545
552
config = CustomJSONField (
546
- help_text = _ ("A JSON document describing sources, destinations, and content to be copied" ),
553
+ help_text = _ (
554
+ dedent (
555
+ """\
556
+ A JSON document describing sources, destinations, and content to be copied.
557
+
558
+ Its a list of dictionaries with the following available fields:
559
+
560
+ ```json
561
+ [
562
+ {
563
+ "source_repo_version": <RepositoryVersion [pulp_href|prn]>,
564
+ "dest_repo": <RpmRepository [pulp_href|prn]>,
565
+ "dest_base_version": <int>,
566
+ "content": [<Content [pulp_href|prn]>, ...]
567
+ },
568
+ ...
569
+ ]
570
+ ```
571
+
572
+ If domains are enabled, the refered pulp objects must be part of the current domain.
573
+
574
+ For usage examples, refer to the advanced copy guide:
575
+ <https://pulpproject.org/pulp_rpm/docs/user/guides/modify/#advanced-copy-workflow>
576
+ """
577
+ )
578
+ ),
547
579
)
548
580
549
581
dependency_solving = serializers .BooleanField (
@@ -558,29 +590,83 @@ def validate(self, data):
558
590
Check for cross-domain references (if domain-enabled).
559
591
"""
560
592
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 :
593
+ def raise_validation (field , domain , id = "" ):
594
+ id = f"\n { id } " if id else ""
595
+ raise serializers .ValidationError (
596
+ _ ("The field {} contains object(s) not in {} domain.{}" .format (field , domain , id ))
597
+ )
598
+
599
+ def parse_reference (ref ) -> tuple [str , str , bool ]:
600
+ """Extract info from prn/href to enable checking domains.
601
+
602
+ This is used for:
603
+ 1. In case of HREFS, avoid expensive extract_pk(href) to get pks.
604
+ 2. HREF and PRNs have different information hardcoded available.
605
+ E.g: RepositoryVerseion HREF has its Repository pk; PRNs have the RepoVer pk.
606
+
607
+ Returns a tuple with (pk, class_name, is_prn)
608
+ """
609
+ if ref .startswith ("prn:" ):
610
+ ref_class , pk = resolve_prn (ref )
611
+ return (pk , ref_class , True )
612
+ # content: ${BASE}/content/rpm/packages/${UUID}/
613
+ # repository: ${BASE}/repositories/rpm/rpm/${UUID}/
614
+ # repover: ${BASE}/repositories/rpm/rpm/${UUID}/versions/0/
615
+ url = urlparse (ref ).path .strip ("/" ).split ("/" )
616
+ ref_class = RpmRepository if "/repositories/" in ref else Content
617
+ is_repover_href = url [- 1 ].isdigit () and url [- 2 ] == "versions"
618
+ uuid = url [- 3 ] if is_repover_href else url [- 1 ]
619
+ if len (uuid ) < 32 :
566
620
raise serializers .ValidationError (
567
- _ ("{} must be a part of the {} domain." ) .format (name , domain )
621
+ _ ("The href path should end with a uuid pk: {}" .format (ref ) )
568
622
)
623
+ return (uuid , ref_class , False )
624
+
625
+ def check_domain (entry , name , curr_domain ):
626
+ href_or_prn = entry [name ]
627
+ resource_pk , ref_class , is_prn = parse_reference (href_or_prn )
628
+ try :
629
+ if ref_class is RepositoryVersion and is_prn :
630
+ resource_domain_pk = (
631
+ RepositoryVersion .objects .select_related ("repository" )
632
+ .values ("repository__pulp_domain" )
633
+ .get (pk = resource_pk )["repository__pulp_domain" ]
634
+ )
635
+ else :
636
+ resource_domain_pk = RpmRepository .objects .values ("pulp_domain" ).get (
637
+ pk = resource_pk
638
+ )["pulp_domain" ]
639
+ except RepositoryVersion .DoesNotExit as e :
640
+ raise serializers .ValidationError from e
641
+ except RpmRepository .DoesNotExit as e :
642
+ raise serializers .ValidationError from e
643
+
644
+ if resource_domain_pk != curr_domain .pk :
645
+ raise_validation (name , curr_domain .name , resource_domain_pk )
569
646
570
647
def check_cross_domain_config (cfg ):
571
648
"""Check that all config-elts are in 'our' domain."""
572
649
# copy-cfg is a list of dictionaries.
573
650
# source_repo_version and dest_repo are required fields.
574
651
# Insure curr-domain exists in src/dest/dest_base_version/content-list hrefs
575
- curr_domain_name = get_domain (). name
652
+ curr_domain = get_domain ()
576
653
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" )
654
+ # Check required fields individually
655
+ check_domain (entry , "source_repo_version" , curr_domain )
656
+ check_domain (entry , "dest_repo" , curr_domain )
657
+
658
+ # Check content generically to avoid timeout of multiple calls
659
+ content_list = entry .get ("content" , None )
660
+ if content_list :
661
+ content_list = [parse_reference (v )[0 ] for v in content_list ]
662
+ distinct = (
663
+ Content .objects .filter (pk__in = content_list ).values ("pulp_domain" ).distinct ()
664
+ )
665
+ domain_ok = (
666
+ len (distinct ) == 1 and distinct .first ()["pulp_domain" ] == curr_domain .pk
667
+ )
668
+ if not domain_ok :
669
+ raise_validation ("content" , curr_domain .name )
584
670
585
671
super ().validate (data )
586
672
if "config" in data :
0 commit comments