Tushar Gupta has proposed merging ~tushar5526/launchpad:add-support-for-excluded-ppas into launchpad:master.
Requested reviews: Launchpad code reviewers (launchpad-reviewers) For more details, see: https://code.launchpad.net/~tushar5526/launchpad/+git/launchpad/+merge/480294 -- Your team Launchpad code reviewers is requested to review the proposed merge of ~tushar5526/launchpad:add-support-for-excluded-ppas into launchpad:master.
diff --git a/lib/lp/archivepublisher/scripts/base.py b/lib/lp/archivepublisher/scripts/base.py index f590791..c00cdb9 100644 --- a/lib/lp/archivepublisher/scripts/base.py +++ b/lib/lp/archivepublisher/scripts/base.py @@ -13,7 +13,14 @@ from optparse import OptionValueError from zope.component import getUtility from lp.registry.interfaces.distribution import IDistributionSet +from lp.services.features import getFeatureFlag from lp.services.scripts.base import LaunchpadCronScript +from lp.soyuz.enums import ArchivePurpose +from lp.soyuz.interfaces.archive import IArchiveSet + +PUBLISHER_EXCLUDED_PPAS_FEATURE_FLAG = ( + "archivepublisher.publisher.excluded_ppas" +) class PublisherScript(LaunchpadCronScript): @@ -62,3 +69,76 @@ class PublisherScript(LaunchpadCronScript): return self.findDerivedDistros() else: return [self.findSelectedDistro()] + + def _setExcludedPPAs(self): + """ + Gets the excluded PPAs from the feature flag. Though feature-rules + don't change within context, this ensures we get a consistent value + within a publisher run. + """ + excluded_ppas_feature_flag = getFeatureFlag( + PUBLISHER_EXCLUDED_PPAS_FEATURE_FLAG + ) + + if not excluded_ppas_feature_flag: + self._excluded_ppas = [] + return + + excluded_ppas = [] + for reference in excluded_ppas_feature_flag.split(" "): + archive = getUtility(IArchiveSet).getByReference(reference) + if not archive: + self.logger.warning( + "Cannot find archive reference: '%s', " + "set in 'archive.publisher.excluded_ppas' feature flag" + % reference + ) + continue + if archive.purpose != ArchivePurpose.PPA: + self.logger.warning( + "Only specify PPAs to be excluded." + "Archive %s is of type %s" % (reference, archive.purpose) + ) + continue + excluded_ppas.append(archive) + + self._excluded_ppas = excluded_ppas + + def getExcludedPPAs(self, distribution): + """ + Retrieve a list of excluded PPAs for a given distribution. + This function checks if the '_excluded_ppas' attribute exists. + If it does not exist, it calls the '_setExcludedPPAs' method + to set it. Then, it filters and returns the PPAs that match the + specified distribution. + """ + if not hasattr(self, "_excluded_ppas"): + self._setExcludedPPAs() + + return [ + archive + for archive in self._excluded_ppas + if archive.distribution == distribution and + archive.purpose == ArchivePurpose.PPA + ] + + def addParallelPublisherOptions(self): + """ + Adds the necessary CLI options needed to enable the parallel publisher + runs. + """ + self.parser.add_option( + "--run-excluded-ppas", + action="store_true", + dest="run_excluded_ppas", + default=False, + help=( + "Run the publisher only for the excluded PPAs." + " Set this option when running another publisher process" + " in parallel with the main publisher process. The" + " excluded PPAs are set via the" + ' "archive.publisher.excluded_ppas" feature flag and' + " these PPAs are excluded from the MAIN publisher" + " run." + ), + ) diff --git a/lib/lp/archivepublisher/scripts/publishdistro.py b/lib/lp/archivepublisher/scripts/publishdistro.py index 38ee406..f143cc0 100644 --- a/lib/lp/archivepublisher/scripts/publishdistro.py +++ b/lib/lp/archivepublisher/scripts/publishdistro.py @@ -75,6 +75,7 @@ class PublishDistro(PublisherScript): def add_my_options(self): self.addDistroOptions() + self.addParallelPublisherOptions() self.parser.add_option( "-C", @@ -276,6 +277,7 @@ class PublishDistro(PublisherScript): self.options.private_ppa, self.options.copy_archive, self.options.archive, + self.options.run_excluded_ppas, ] return len(list(filter(None, exclusive_options))) @@ -375,6 +377,7 @@ class PublishDistro(PublisherScript): def getTargetArchives(self, distribution): """Find the archive(s) selected by the script's options.""" + excluded_ppas = self.getExcludedPPAs(distribution) if self.options.archive: archive = getUtility(IArchiveSet).getByReference( self.options.archive @@ -383,12 +386,26 @@ class PublishDistro(PublisherScript): return [archive] else: return [] + elif self.options.run_excluded_ppas: + return excluded_ppas elif self.options.partner: return [distribution.getArchiveByComponent("partner")] elif self.options.ppa: - return filter(is_ppa_public, self.getPPAs(distribution)) + return [ + archive + for archive in filter( + is_ppa_public, self.getPPAs(distribution) + ) + if archive not in excluded_ppas + ] elif self.options.private_ppa: - return filter(is_ppa_private, self.getPPAs(distribution)) + return [ + archive + for archive in filter( + is_ppa_private, self.getPPAs(distribution) + ) + if archive not in excluded_ppas + ] elif self.options.copy_archive: return self.getCopyArchives(distribution) else: @@ -634,6 +651,7 @@ class PublishDistro(PublisherScript): """Compare the published OVAL files with the incoming one.""" start_dir = Path(config.archivepublisher.oval_data_root) archive_set = getUtility(IArchiveSet) + excluded_ppas = self.getExcludedPPAs(distribution) for owner_path in start_dir.iterdir(): if not owner_path.name.startswith("~"): continue @@ -644,6 +662,29 @@ class PublishDistro(PublisherScript): archive = archive_set.getPPAByDistributionAndOwnerName( distribution, owner_path.name[1:], archive_path.name ) + # Run it only for the excluded archives if the option is + # set, else ALWAYS skip the excluded archives. + if self.options.run_excluded_ppas: + if archive not in excluded_ppas: + continue + self.logger.info( + "Running OVAL data for '~%s/%s/%s' " + "(excluded archive for parallel publisher run).", + owner_path.name[1:], + distribution.name, + archive_path.name, + ) + else: + if archive in excluded_ppas: + self.logger.info( + "Skipping OVAL data for '~%s/%s/%s' " + "(excluded archive for parallel publisher run).", + owner_path.name[1:], + distribution.name, + archive_path.name, + ) + continue + if archive is None: self.logger.info( "Skipping OVAL data for '~%s/%s/%s' " @@ -687,8 +728,18 @@ class PublishDistro(PublisherScript): self.validateOptions() self.logOptions() + # We only run the global rsyncOVALData in the main publisher run, + # and skip it for all other cases. run_excluded_ppas is primarily + # meant to be used in a parallel publisher process, so skip the rsync + # call when it is True. if config.archivepublisher.oval_data_rsync_endpoint: - self.rsyncOVALData() + if not self.options.run_excluded_ppas: + self.rsyncOVALData() + else: + self.logger.info( + "Skipping the OVAL data sync in excluded PPA publisher" + "run." + ) else: self.logger.info( "Skipping the OVAL data sync as no rsync endpoint" diff --git a/lib/lp/archivepublisher/tests/test_publishdistro.py b/lib/lp/archivepublisher/tests/test_publishdistro.py index ad591fa..ef6c629 100644 --- a/lib/lp/archivepublisher/tests/test_publishdistro.py +++ b/lib/lp/archivepublisher/tests/test_publishdistro.py @@ -25,6 +25,9 @@ from lp.archivepublisher.interfaces.archivegpgsigningkey import ( ) from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet from lp.archivepublisher.publishing import Publisher +from lp.archivepublisher.scripts.base import ( + PUBLISHER_EXCLUDED_PPAS_FEATURE_FLAG, +) from lp.archivepublisher.scripts.publishdistro import PublishDistro from lp.archivepublisher.tests.artifactory_fixture import ( FakeArtifactoryFixture, @@ -34,6 +37,7 @@ from lp.registry.interfaces.person import IPersonSet from lp.registry.interfaces.pocket import PackagePublishingPocket from lp.services.config import config from lp.services.database.interfaces import IStore +from lp.services.features.testing import FeatureFixture from lp.services.log.logger import BufferLogger, DevNullLogger from lp.services.osutils import write_file from lp.services.scripts.base import LaunchpadScriptFailure @@ -487,6 +491,101 @@ class TestPublishDistro(TestNativePublishingBase): ) self.assertIsNone(archive.dirty_suites) + def test_checkForUpdatedOVALData_skips_excluded_ppas(self): + """ + Skip excluded PPAs in checkForUpdatedOVALData when the option + "run_excluded_ppas" is not set + """ + + self.setUpOVALDataRsync() + # XXX 29-01-2025 tushar5526: There appears to be an issue with + # FeatureFixture's implementation. It cleans up database objects + # when called, causing reference errors in Storm. Therefore we are + # calling it here with a fixed ppa reference and creating associated + # PPAs with correct owners later. + self.useFixture(FeatureFixture({PUBLISHER_EXCLUDED_PPAS_FEATURE_FLAG: '~test-user/ubuntu/ppa'})) + self.useFixture( + MockPatch("lp.archivepublisher.scripts.publishdistro.check_call") + ) + + owner = self.factory.makePerson(name='test-user') + excluded_ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA, name='ppa',owner=owner) + ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA, owner=owner) + # Disable normal publication so that dirty_suites isn't cleared. + ppa.publish = False + excluded_ppa.publish = False + + for archive in [ppa, excluded_ppa]: + incoming_dir = ( + Path(self.oval_data_root) + / archive.reference + / "breezy-autotest" + / "main" + ) + write_file(str(incoming_dir), b"test") + + self.runPublishDistro( + extra_args=["--ppa"], distribution="ubuntu" + ) + + self.assertEqual(["breezy-autotest"], ppa.dirty_suites) + self.assertIsNone(excluded_ppa.dirty_suites) + self.assertIn( + "INFO Skipping OVAL data for '%s' " + "(excluded archive for parallel publisher run)." % (excluded_ppa.reference), + self.logger.getLogBuffer(), + ) + + def test_checkForUpdatedOVALData_runs_excluded_ppas(self): + """ + Run excluded PPAs in checkForUpdatedOVALData when the option + "run_excluded_ppas" is set + """ + + self.setUpOVALDataRsync() + # XXX 29-01-2025 tushar5526: There appears to be an issue with + # FeatureFixture's implementation. It cleans up database objects + # when called, causing reference errors in Storm. Therefore we are + # calling it here with a fixed ppa reference and creating associated + # PPAs with correct owners later. + self.useFixture(FeatureFixture({PUBLISHER_EXCLUDED_PPAS_FEATURE_FLAG: '~test-user/ubuntu/ppa'})) + self.useFixture( + MockPatch("lp.archivepublisher.scripts.publishdistro.check_call") + ) + + owner = self.factory.makePerson(name='test-user') + excluded_ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA, name='ppa',owner=owner) + ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA, owner=owner) + # Disable normal publication so that dirty_suites isn't cleared. + ppa.publish = False + excluded_ppa.publish = False + + for archive in [ppa, excluded_ppa]: + incoming_dir = ( + Path(self.oval_data_root) + / archive.reference + / "breezy-autotest" + / "main" + ) + write_file(str(incoming_dir), b"test") + + self.runPublishDistro( + extra_args=["--run-excluded-ppas"], distribution="ubuntu" + ) + + self.assertIsNone(ppa.dirty_suites) + self.assertEqual(["breezy-autotest"], excluded_ppa.dirty_suites) + self.assertIn( + "INFO Running OVAL data for '%s' " + "(excluded archive for parallel publisher run)." % (excluded_ppa.reference), + self.logger.getLogBuffer(), + ) + # Further logs should not have any reference to ppa.reference as we skipped it. + self.assertNotIn( + ppa.reference, + self.logger.getLogBuffer(), + ) + @defer.inlineCallbacks def testForPPA(self): """Try to run publish-distro in PPA mode. @@ -954,6 +1053,60 @@ class TestPublishDistroMethods(TestCaseWithFactory): script.logger = DevNullLogger() return script + def makeArchivesForDifferentTypesDistros(self): + """ + Creates a list of PPAs with different + types/publishing-methods/distros. + """ + archives = [] + archives.append( + self.factory.makeArchive(purpose=ArchivePurpose.PPA, private=False) + ) + archives.append( + self.factory.makeArchive( + purpose=ArchivePurpose.PPA, + private=True, + publishing_method=ArchivePublishingMethod.ARTIFACTORY, + ) + ) + archives.append( + self.factory.makeArchive( + purpose=ArchivePurpose.COPY, + private=False, + publishing_method=ArchivePublishingMethod.ARTIFACTORY, + ) + ) + archives.append( + self.factory.makeArchive( + purpose=ArchivePurpose.COPY, + private=True, + publishing_method=ArchivePublishingMethod.ARTIFACTORY, + ) + ) + archives.append( + self.factory.makeArchive( + purpose=ArchivePurpose.PARTNER, + private=False, + publishing_method=ArchivePublishingMethod.ARTIFACTORY, + ) + ) + archives.append( + self.factory.makeArchive( + purpose=ArchivePurpose.PARTNER, + private=True, + publishing_method=ArchivePublishingMethod.ARTIFACTORY, + ) + ) + return archives + + def generateExcludedArchivesFeatureFlagValue(self, excluded_archives): + """ + Returns a space separate string of archive references + from a list of archives + """ + + return " ".join([a.reference for a in excluded_archives]) + def test_isCareful_is_false_if_option_not_set(self): # isCareful normally returns False for a carefulness option that # evaluates to False. @@ -1016,6 +1169,14 @@ class TestPublishDistroMethods(TestCaseWithFactory): 1, self.makeScript(args=["--copy-archive"]).countExclusiveOptions() ) + def test_countExclusiveOptions_counts_run_excluded_ppas(self): + self.assertEqual( + 1, + self.makeScript( + args=["--run-excluded-ppas"] + ).countExclusiveOptions(), + ) + def test_countExclusiveOptions_detects_conflict(self): # If more than one of the exclusive options has been set, that # raises the result from countExclusiveOptions above 1. @@ -1276,40 +1437,138 @@ class TestPublishDistroMethods(TestCaseWithFactory): script = self.makeScript(distro, ["--partner"]) self.assertContentEqual([partner], script.getTargetArchives(distro)) - def test_getTargetArchives_ignores_public_ppas_if_private(self): + def test_getTargetArchives_ignores_public_ppas_if_private_and_ignores_excluded_ppas( # noqa: E501 + self, + ): # If the selected exclusive option is "private-ppa," - # getTargetArchives looks for PPAs but leaves out public ones. + # getTargetArchives looks for PPAs but leaves out public ones + # and excluded PPAs distro = self.makeDistro() + excluded_archives = self.makeArchivesForDifferentTypesDistros() + + # Add different archives from the same distribution as well + excluded_archives.extend([ + self.factory.makeArchive( + distro, purpose=ArchivePurpose.PPA, private=False + ), + self.factory.makeArchive( + distro, purpose=ArchivePurpose.PPA, private=True + ),]) + + self.useFixture( + FeatureFixture( + { + PUBLISHER_EXCLUDED_PPAS_FEATURE_FLAG: self.generateExcludedArchivesFeatureFlagValue( # noqa: E501 + excluded_archives + ) + } + ) + ) self.factory.makeArchive( distro, purpose=ArchivePurpose.PPA, private=False ) script = self.makeScript(distro, ["--private-ppa"]) self.assertContentEqual([], script.getTargetArchives(distro)) - def test_getTargetArchives_gets_private_ppas_if_private(self): + def test_getTargetArchives_gets_private_ppas_if_private_and_ignores_excluded_ppas( # noqa: E501 + self, + ): # If the selected exclusive option is "private-ppa," # getTargetArchives looks for private PPAs. distro = self.makeDistro() + excluded_archives = self.makeArchivesForDifferentTypesDistros() + + # Add different archives from the same distribution as well + excluded_archives.append( + self.factory.makeArchive( + distro, purpose=ArchivePurpose.PPA, private=False + ) + ) + excluded_archives.append( + self.factory.makeArchive( + distro, purpose=ArchivePurpose.PPA, private=True + ) + ) + self.useFixture( + FeatureFixture( + { + PUBLISHER_EXCLUDED_PPAS_FEATURE_FLAG: self.generateExcludedArchivesFeatureFlagValue( # noqa: E501 + excluded_archives + ) + } + ) + ) + ppa = self.factory.makeArchive( distro, purpose=ArchivePurpose.PPA, private=True ) script = self.makeScript(distro, ["--private-ppa", "--careful"]) self.assertContentEqual([ppa], script.getTargetArchives(distro)) - def test_getTargetArchives_gets_public_ppas_if_not_private(self): + def test_getTargetArchives_gets_public_ppas_if_not_private_and_ignores_excluded_ppas( # noqa: E501 + self, + ): # If the selected exclusive option is "ppa," getTargetArchives # looks for public PPAs. distro = self.makeDistro() + excluded_archives = self.makeArchivesForDifferentTypesDistros() + + # Add different archives from the same distribution as well + excluded_archives.append( + self.factory.makeArchive( + distro, purpose=ArchivePurpose.PPA, private=False + ) + ) + excluded_archives.append( + self.factory.makeArchive( + distro, purpose=ArchivePurpose.PPA, private=True + ) + ) + self.useFixture( + FeatureFixture( + { + PUBLISHER_EXCLUDED_PPAS_FEATURE_FLAG: self.generateExcludedArchivesFeatureFlagValue( # noqa: E501 + excluded_archives + ) + } + ) + ) + ppa = self.factory.makeArchive( distro, purpose=ArchivePurpose.PPA, private=False ) script = self.makeScript(distro, ["--ppa", "--careful"]) self.assertContentEqual([ppa], script.getTargetArchives(distro)) - def test_getTargetArchives_ignores_private_ppas_if_not_private(self): + def test_getTargetArchives_ignores_private_ppas_if_not_private_and_ignores_excluded_ppas( # noqa: E501 + self, + ): # If the selected exclusive option is "ppa," getTargetArchives # leaves out private PPAs. distro = self.makeDistro() + excluded_archives = self.makeArchivesForDifferentTypesDistros() + + # Add different archives from the same distribution as well + excluded_archives.append( + self.factory.makeArchive( + distro, purpose=ArchivePurpose.PPA, private=False + ) + ) + excluded_archives.append( + self.factory.makeArchive( + distro, purpose=ArchivePurpose.PPA, private=True + ) + ) + self.useFixture( + FeatureFixture( + { + PUBLISHER_EXCLUDED_PPAS_FEATURE_FLAG: self.generateExcludedArchivesFeatureFlagValue( # noqa: E501 + excluded_archives + ) + } + ) + ) + self.factory.makeArchive( distro, purpose=ArchivePurpose.PPA, private=True ) @@ -1324,6 +1583,47 @@ class TestPublishDistroMethods(TestCaseWithFactory): script = self.makeScript(distro, ["--copy-archive"]) self.assertContentEqual([copy], script.getTargetArchives(distro)) + def test_getTargetArchives_gets_excluded_archives_only(self): + distro = self.makeDistro() + excluded_archives = self.makeArchivesForDifferentTypesDistros() + + # Add different archives from the same distribution as well + ppa = self.factory.makeArchive( + distro, purpose=ArchivePurpose.PPA, private=False + ) + pppa = self.factory.makeArchive( + distro, purpose=ArchivePurpose.PPA, private=True + ) + excluded_archives.extend([ + self.factory.makeArchive( + distro, purpose=ArchivePurpose.COPY, private=True + ), + self.factory.makeArchive( + distro, purpose=ArchivePurpose.PARTNER, private=True + ), + ppa, + pppa + ]) + self.useFixture( + FeatureFixture( + { + PUBLISHER_EXCLUDED_PPAS_FEATURE_FLAG: self.generateExcludedArchivesFeatureFlagValue( # noqa: E501 + excluded_archives + ) + } + ) + ) + + self.factory.makeArchive( + distro, purpose=ArchivePurpose.PPA, private=True + ) + self.factory.makeArchive( + distro, purpose=ArchivePurpose.PPA, private=False + ) + + script = self.makeScript(distro, ["--run-excluded-ppas"]) + self.assertContentEqual([ppa, pppa], script.getTargetArchives(distro)) + def test_getPublisher_returns_publisher(self): # getPublisher produces a Publisher instance. distro = self.makeDistro() @@ -1806,3 +2106,25 @@ class TestPublishDistroMethods(TestCaseWithFactory): self.assertTrue(by_hash_dir.is_dir()) # and still contains the two test files self.assertEqual(2, len(list(by_hash_dir.iterdir()))) + + def test_main_skips_rsyncOVALData_when_run_excluded_ppas_is_set(self): + self.setUpOVALDataRsync() + + script = self.makeScript(args=['--run-excluded-ppas']) + rsyncOVALData_mock = script.rsyncOVALData = FakeMethod() + script.txn = FakeTransaction() + script.findDistros = FakeMethod([]) + script.main() + + self.assertEqual(rsyncOVALData_mock.call_count, 0) + + def test_main_calls_rsyncOVALData_when_run_excluded_ppas_is_not_set(self): + self.setUpOVALDataRsync() + + script = self.makeScript() + rsyncOVALData_mock = script.rsyncOVALData = FakeMethod() + script.txn = FakeTransaction() + script.findDistros = FakeMethod([]) + script.main() + + self.assertEqual(rsyncOVALData_mock.call_count, 1)
_______________________________________________ Mailing list: https://launchpad.net/~launchpad-reviewers Post to : launchpad-reviewers@lists.launchpad.net Unsubscribe : https://launchpad.net/~launchpad-reviewers More help : https://help.launchpad.net/ListHelp