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
+ Content to be copied into the given destinations from the given sources.
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,91 @@ 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
+ """Check domain for RpmRepository and RepositoryVersion objects."""
627
+ href_or_prn = entry [name ]
628
+ resource_pk , ref_class , is_prn = parse_reference (href_or_prn )
629
+ try :
630
+ if ref_class is RepositoryVersion and is_prn :
631
+ resource_domain_pk = (
632
+ RepositoryVersion .objects .select_related ("repository" )
633
+ .values ("repository__pulp_domain" )
634
+ .get (pk = resource_pk )["repository__pulp_domain" ]
635
+ )
636
+ elif ref_class is RpmRepository :
637
+ resource_domain_pk = RpmRepository .objects .values ("pulp_domain" ).get (
638
+ pk = resource_pk
639
+ )["pulp_domain" ]
640
+ else :
641
+ raise serializers .ValidationError (
642
+ _ (
643
+ "Expected RpmRepository or RepositoryVersion ref_class. "
644
+ "Got {} from {}." .format (ref_class , href_or_prn )
645
+ )
646
+ )
647
+ except RepositoryVersion .DoesNotExit as e :
648
+ raise serializers .ValidationError from e
649
+ except RpmRepository .DoesNotExit as e :
650
+ raise serializers .ValidationError from e
651
+
652
+ if resource_domain_pk != curr_domain .pk :
653
+ raise_validation (name , curr_domain .name , resource_domain_pk )
569
654
570
655
def check_cross_domain_config (cfg ):
571
656
"""Check that all config-elts are in 'our' domain."""
572
657
# copy-cfg is a list of dictionaries.
573
658
# source_repo_version and dest_repo are required fields.
574
659
# Insure curr-domain exists in src/dest/dest_base_version/content-list hrefs
575
- curr_domain_name = get_domain (). name
660
+ curr_domain = get_domain ()
576
661
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" )
662
+ # Check required fields individually
663
+ check_domain (entry , "source_repo_version" , curr_domain )
664
+ check_domain (entry , "dest_repo" , curr_domain )
665
+
666
+ # Check content generically to avoid timeout of multiple calls
667
+ content_list = entry .get ("content" , None )
668
+ if content_list :
669
+ content_list = [parse_reference (v )[0 ] for v in content_list ]
670
+ distinct = (
671
+ Content .objects .filter (pk__in = content_list ).values ("pulp_domain" ).distinct ()
672
+ )
673
+ domain_ok = (
674
+ len (distinct ) == 1 and distinct .first ()["pulp_domain" ] == curr_domain .pk
675
+ )
676
+ if not domain_ok :
677
+ raise_validation ("content" , curr_domain .name )
584
678
585
679
super ().validate (data )
586
680
if "config" in data :
0 commit comments