← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~ruinedyourlife/launchpad-buildd:maven-virtual-remote into launchpad-buildd:master

 

Quentin Debhi has proposed merging ~ruinedyourlife/launchpad-buildd:maven-virtual-remote into launchpad-buildd:master.

Commit message:
Allow maven to pull plugins from AF and disallow external access

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~ruinedyourlife/launchpad-buildd/+git/launchpad-buildd/+merge/490002
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~ruinedyourlife/launchpad-buildd:maven-virtual-remote into launchpad-buildd:master.
diff --git a/lpbuildd/target/build_craft.py b/lpbuildd/target/build_craft.py
index 69106a8..713795f 100644
--- a/lpbuildd/target/build_craft.py
+++ b/lpbuildd/target/build_craft.py
@@ -160,193 +160,180 @@ class BuildCraft(
 
     def setup_cargo_credentials(self):
         """Set up Cargo credentials through environment variables.
-        
+
         Transforms input environment variables from:
             CARGO_ARTIFACTORY1_URL=https://...
             CARGO_ARTIFACTORY1_READ_AUTH=user:token123
-        
+
         Into Cargo registry environment variables:
             CARGO_REGISTRIES_ARTIFACTORY1_INDEX=https://...
             CARGO_REGISTRIES_ARTIFACTORY1_TOKEN=Bearer token123
             CARGO_REGISTRY_GLOBAL_CREDENTIAL_PROVIDERS=cargo:token
-        
-        This allows Cargo to authenticate with private registries while building.
+
+        This allows Cargo to authenticate with private registries.
         """
         env_vars = dict(
-            pair.split("=", maxsplit=1) 
+            pair.split("=", maxsplit=1)
             for pair in self.args.environment_variables
         )
-        
+
         cargo_vars = {}
-        
+
         # Process CARGO_* variables into CARGO_REGISTRIES_* format
         for key, value in env_vars.items():
             if not key.startswith("CARGO_"):
                 continue
-            
+
             # Extract name by removing CARGO_ prefix
             # and the suffix(_URL or _READ_AUTH)
             if key.endswith("_URL"):
                 # Remove CARGO_ prefix and _URL suffix
-                name = key[len("CARGO_"):-len("_URL")]
+                name = key[len("CARGO_") : -len("_URL")]
                 # Convert URL to registry index
                 cargo_vars[f"CARGO_REGISTRIES_{name}_INDEX"] = value
             elif key.endswith("_READ_AUTH"):
                 # Remove CARGO_ prefix and _READ_AUTH suffix
-                name = key[len("CARGO_"):-len("_READ_AUTH")]
+                name = key[len("CARGO_") : -len("_READ_AUTH")]
                 # Extract token from user:token format
                 token = value.split(":", 1)[1]
-                cargo_vars[f"CARGO_REGISTRIES_{name}_TOKEN"] = f"Bearer {token}"
+                cargo_vars[f"CARGO_REGISTRIES_{name}_TOKEN"] = (
+                    f"Bearer {token}"
+                )
 
         if cargo_vars:
             # Tell Cargo to use token-based authentication from environment
-            cargo_vars["CARGO_REGISTRY_GLOBAL_CREDENTIAL_PROVIDERS"] = "cargo:token"
-        
+            cargo_vars["CARGO_REGISTRY_GLOBAL_CREDENTIAL_PROVIDERS"] = (
+                "cargo:token"
+            )
+
         return cargo_vars
 
     def setup_maven_credentials(self):
-        """Set up Maven credential files if needed.
-        
+        """Set up Maven credential files for Artifactory virtual repository.
+
         Creates a Maven settings.xml file with:
-        1. Server configurations for authentication
-           - One for release repository
-           - One for snapshot repository
-           - Uses credentials from MAVEN_*_READ_AUTH variables
-        
-        2. Profile configurations for each repository
-           - Defines both release and snapshot repositories
-           - Configures plugin repositories
-           - Controls snapshot behavior
-        
-        3. Mirror configurations
-           - Redirects all repository requests through Artifactory
-           - Ensures dependencies are fetched through our proxy
-        
-        The resulting settings.xml allows Maven to:
-        - Authenticate with private repositories
-        - Use the correct URLs for artifacts
-        - Handle both release and snapshot dependencies
-        - Use plugin repositories with authentication
-
-        It is up to the user to activate the profile they want to use
-        in the command line (this is done inside sourcecraft.yaml).
+        1. Server configuration for authentication
+        2. Profile configuration for the virtual repository
+        3. Mirror configuration to redirect all requests through Artifactory
+
+        Note: Multi-tenant Maven setups are complex due to how mirrors work.
+        Instead, we use a single Artifactory virtual repository that shadows
+        Maven Central and other remote repositories as needed.
         """
         env_vars = dict(
-            pair.split("=", maxsplit=1) 
+            pair.split("=", maxsplit=1)
             for pair in self.args.environment_variables
         )
-        
+
         # Extract Maven-specific variables
-        maven_vars = {k: v for k, v in env_vars.items() if k.startswith("MAVEN_")}
+        maven_vars = {
+            k: v for k, v in env_vars.items() if k.startswith("MAVEN_")
+        }
         if not maven_vars:
+            logger.info("No Maven environment variables found")
             return
 
         # Create .m2 directory for Maven configuration
         m2_dir = os.path.join(self.buildd_path, ".m2")
         self.backend.run(["mkdir", "-p", m2_dir])
 
-        # Parse repository URLs and credentials from environment variables
-        repositories = {}
+        # Parse single repository URL and credentials from env vars
+        repository = {}
         for key, value in maven_vars.items():
             if key.endswith("_URL"):
-                repo_name = key[6:-4].lower()  # Remove MAVEN_ and _URL
-                repositories.setdefault(repo_name, {})["url"] = value
+                repository["url"] = value
             elif key.endswith("_READ_AUTH"):
-                repo_name = key[6:-10].lower()  # Remove MAVEN_ and _READ_AUTH
                 user, token = value.split(":")
-                repositories.setdefault(repo_name, {})["username"] = user
-                repositories.setdefault(repo_name, {})["password"] = token
-
-        # Create settings.xml with server configurations
-        settings_xml = """<?xml version="1.0" encoding="UTF-8"?>
-<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0";
-          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
-          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd";>
-    <servers>
-"""
-        # Add authentication for both release and snapshot repositories
-        for name, repo in repositories.items():
-            if "username" in repo and "password" in repo:
-                settings_xml += f"""        <server>
-            <id>{name}</id>
-            <username>{repo['username']}</username>
-            <password>{repo['password']}</password>
-        </server>
-        <server>
-            <id>{name}-snapshots</id>
-            <username>{repo['username']}</username>
-            <password>{repo['password']}</password>
-        </server>
-"""
-
-        # Add profile configurations for repository definitions
-        settings_xml += """    </servers>
-    <profiles>
-"""
-        for name, repo in repositories.items():
-            if "url" in repo:
-                # Each profile contains both release and snapshot repositories
-                settings_xml += f"""        <profile>
-            <id>{name}</id>
-            <repositories>
-                <repository>
-                    <id>{name}</id>
-                    <name>{name}</name>
-                    <url>{repo['url']}</url>
-                    <snapshots>
-                        <enabled>false</enabled>
-                    </snapshots>
-                </repository>
-                <repository>
-                    <id>{name}-snapshots</id>
-                    <name>{name}</name>
-                    <url>{repo['url']}</url>
-                    <snapshots>
-                        <enabled>true</enabled>
-                    </snapshots>
-                </repository>
-            </repositories>
-            <pluginRepositories>
-                <pluginRepository>
-                    <id>{name}</id>
-                    <name>{name}</name>
-                    <url>{repo['url']}</url>
-                    <snapshots>
-                        <enabled>false</enabled>
-                    </snapshots>
-                </pluginRepository>
-                <pluginRepository>
-                    <id>{name}-snapshots</id>
-                    <name>{name}</name>
-                    <url>{repo['url']}</url>
-                    <snapshots>
-                        <enabled>true</enabled>
-                    </snapshots>
-                </pluginRepository>
-            </pluginRepositories>
-        </profile>
-"""
-
-        # Add mirror configurations to ensure all requests go through Artifactory
-        settings_xml += """    </profiles>
-    <mirrors>
-"""
-        for name, repo in repositories.items():
-            if "url" in repo:
-                settings_xml += f"""        <mirror>
-            <id>{name}</id>
-            <name>Maven Repository Manager running on {name}</name>
-            <url>{repo['url']}</url>
-            <mirrorOf>*</mirrorOf>
-        </mirror>
-"""
-
-        settings_xml += """    </mirrors>
-</settings>
-"""
+                repository["username"] = user
+                repository["password"] = token
+
+        # Get repository name from environment variable
+        # (e.g., MAVEN_ARTIFACTORY_URL -> artifactory)
+        repo_name = None
+        for key in maven_vars.keys():
+            if key.endswith("_URL"):
+                repo_name = key[6:-4].lower()  # Remove MAVEN_ and _URL
+                break
+
+        if not repo_name or not repository.get("url"):
+            return
+
+        # Generate Maven settings.xml
+        settings_xml = self._generate_maven_settings_xml(repo_name, repository)
+
         with self.backend.open(os.path.join(m2_dir, "settings.xml"), "w") as f:
             f.write(settings_xml)
 
+    def _generate_maven_settings_xml(self, repo_name, repository):
+        """Generate Maven settings.xml for single virtual repository.
+
+        :param repo_name: Name/ID of the repository
+        :param repository: Dict with repository configuration
+        :return: XML content as string
+        """
+        # XML header
+        header = (
+            '<?xml version="1.0" encoding="UTF-8"?>\n'
+            '<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"\n'
+            '        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n'
+        )
+
+        schema = (
+            '        xsi:schemaLocation="'
+            "http://maven.apache.org/SETTINGS/1.0.0 "
+            'http://maven.apache.org/xsd/settings-1.0.0.xsd";>\n'
+        )
+
+        # Servers section for authentication
+        servers = "    <servers>\n"
+        if "username" in repository and "password" in repository:
+            servers += (
+                "        <server>\n"
+                f"            <id>{repo_name}</id>\n"
+                f"            <username>{repository['username']}</username>\n"
+                f"            <password>{repository['password']}</password>\n"
+                "        </server>\n"
+            )
+        servers += "    </servers>\n"
+
+        # Profiles section with virtual repository
+        profiles = "    <profiles>\n"
+        profiles += (
+            "        <profile>\n"
+            f"            <id>{repo_name}</id>\n"
+            "            <repositories>\n"
+            "                <repository>\n"
+            f"                    <id>{repo_name}</id>\n"
+            f"                    <name>Artifactory Virtual Repo</name>\n"
+            f"                    <url>{repository['url']}</url>\n"
+            "                </repository>\n"
+            "            </repositories>\n"
+            "            <pluginRepositories>\n"
+            "                <pluginRepository>\n"
+            f"                    <id>{repo_name}</id>\n"
+            f"                    <name>Artifactory Virtual Repo</name>\n"
+            f"                    <url>{repository['url']}</url>\n"
+            "                </pluginRepository>\n"
+            "            </pluginRepositories>\n"
+            "        </profile>\n"
+        )
+        profiles += "    </profiles>\n"
+
+        # Mirrors section - redirect all requests through Artifactory
+        mirrors = (
+            "    <mirrors>\n"
+            "        <mirror>\n"
+            f"            <id>{repo_name}</id>\n"
+            f"            <name>Artifactory Virtual Repo</name>\n"
+            f"            <url>{repository['url']}</url>\n"
+            "            <mirrorOf>*</mirrorOf>\n"
+            "        </mirror>\n"
+            "    </mirrors>\n"
+        )
+
+        # Combine all parts
+        return header + schema + servers + profiles + mirrors + "</settings>"
+
     def build(self):
         """Running build phase..."""
         # Set up credential files before building
diff --git a/lpbuildd/target/tests/test_build_craft.py b/lpbuildd/target/tests/test_build_craft.py
index fe342c4..0e83514 100644
--- a/lpbuildd/target/tests/test_build_craft.py
+++ b/lpbuildd/target/tests/test_build_craft.py
@@ -1296,7 +1296,7 @@ class TestBuildCraft(TestCase):
             "--branch",
             "lp:foo",
             "--environment-variable",
-            "MAVEN_ARTIFACTORY_URL=https://canonical.example.com/artifactory/api/maven/maven-upstream/";,
+            "MAVEN_ARTIFACTORY_URL=https://canonical.example.com/artifactory/virtual-maven/";,
             "--environment-variable",
             "MAVEN_ARTIFACTORY_READ_AUTH=user:token123",
             "test-image",
@@ -1309,33 +1309,50 @@ class TestBuildCraft(TestCase):
         self.assertTrue(build_craft.backend.path_exists(maven_settings_path))
         with build_craft.backend.open(maven_settings_path) as f:
             settings_content = f.read()
-            
+
             # Check server configurations
-            self.assertIn('        <server>', settings_content)
-            self.assertIn('            <id>artifactory</id>', settings_content)
-            self.assertIn('            <username>user</username>', settings_content)
-            self.assertIn('            <password>token123</password>', settings_content)
-            self.assertIn('            <id>artifactory-snapshots</id>', settings_content)
-            
+            self.assertIn("        <server>", settings_content)
+            self.assertIn("            <id>artifactory</id>", settings_content)
+            self.assertIn(
+                "            <username>user</username>", settings_content
+            )
+            self.assertIn(
+                "            <password>token123</password>", settings_content
+            )
+
             # Check profile configuration
-            self.assertIn('        <profile>', settings_content)
-            self.assertIn('            <id>artifactory</id>', settings_content)
-            
-            # Check repositories
-            self.assertIn('                <repository>', settings_content)
-            self.assertIn('                    <url>https://canonical.example.com/artifactory/api/maven/maven-upstream/</url>', settings_content)
-            self.assertIn('                        <enabled>false</enabled>', settings_content)
-            self.assertIn('                        <enabled>true</enabled>', settings_content)
-            
-            # Check plugin repositories
-            self.assertIn('                <pluginRepository>', settings_content)
-            
-            # Check mirrors
-            self.assertIn('        <mirror>', settings_content)
-            self.assertIn('            <mirrorOf>*</mirrorOf>', settings_content)
-            
-            # Verify activeProfiles section is not present
-            self.assertNotIn('<activeProfiles>', settings_content)
+            self.assertIn("        <profile>", settings_content)
+            self.assertIn("            <id>artifactory</id>", settings_content)
+
+            # Check repository configuration
+            self.assertIn("                <repository>", settings_content)
+            self.assertIn(
+                "                    <name>Artifactory Virtual Repo</name>",
+                settings_content,
+            )
+            self.assertIn(
+                "                    <url>https://canonical.example.com/artifactory/virtual-maven/</url>",
+                settings_content,
+            )
+
+            # Check plugin repository configuration
+            self.assertIn(
+                "                <pluginRepository>", settings_content
+            )
+            self.assertIn(
+                "                    <name>Artifactory Virtual Repo</name>",
+                settings_content,
+            )
+
+            # Check mirror configuration
+            self.assertIn("        <mirror>", settings_content)
+            self.assertIn(
+                "            <name>Artifactory Virtual Repo</name>",
+                settings_content,
+            )
+            self.assertIn(
+                "            <mirrorOf>*</mirrorOf>", settings_content
+            )
 
         # Verify the build command was run
         self.assertThat(
@@ -1351,7 +1368,8 @@ class TestBuildCraft(TestCase):
             ),
         )
 
-    def test_build_with_multiple_maven_credentials(self):
+    def test_build_without_maven_credentials(self):
+        """Test that no Maven settings.xml is created with no Maven vars ."""
         args = [
             "build-craft",
             "--backend=fake",
@@ -1360,56 +1378,20 @@ class TestBuildCraft(TestCase):
             "1",
             "--branch",
             "lp:foo",
-            "--environment-variable",
-            "MAVEN_ARTIFACTORY1_URL=https://canonical.example.com/artifactory/api/maven/maven-upstream1/";,
-            "--environment-variable",
-            "MAVEN_ARTIFACTORY1_READ_AUTH=user1:token1",
-            "--environment-variable",
-            "MAVEN_ARTIFACTORY2_URL=https://canonical.example.com/artifactory/api/maven/maven-upstream2/";,
-            "--environment-variable",
-            "MAVEN_ARTIFACTORY2_READ_AUTH=user2:token2",
             "test-image",
         ]
         build_craft = parse_args(args=args).operation
         build_craft.build()
 
-        # Check that .m2/settings.xml was created correctly
+        # Check that .m2/settings.xml was NOT created
         maven_settings_path = "/home/buildd/test-image/.m2/settings.xml"
-        self.assertTrue(build_craft.backend.path_exists(maven_settings_path))
-        with build_craft.backend.open(maven_settings_path) as f:
-            settings_content = f.read()
-            
-            # Check server configurations for both repositories
-            for i, creds in enumerate([("user1", "token1"), ("user2", "token2")], 1):
-                user, token = creds
-                repo_name = f"artifactory{i}"
-                
-                # Check servers with exact indentation
-                self.assertIn(f'            <id>{repo_name}</id>', settings_content)
-                self.assertIn(f'            <username>{user}</username>', settings_content)
-                self.assertIn(f'            <password>{token}</password>', settings_content)
-                self.assertIn(f'            <id>{repo_name}-snapshots</id>', settings_content)
-                
-                # Check profiles with exact indentation
-                self.assertIn(f'            <id>{repo_name}</id>', settings_content)
-                
-                # Check repositories with exact indentation
-                url = f'https://canonical.example.com/artifactory/api/maven/maven-upstream{i}/'
-                self.assertIn(f'                    <url>{url}</url>', settings_content)
-                
-                # Check mirrors with exact indentation
-                self.assertIn(f'            <id>{repo_name}</id>', settings_content)
-                self.assertIn('            <mirrorOf>*</mirrorOf>', settings_content)
-            
-            # Verify activeProfiles section is not present
-            self.assertNotIn('<activeProfiles>', settings_content)
+        self.assertFalse(build_craft.backend.path_exists(maven_settings_path))
 
-        # Verify the build command was run
+        # Verify only the build command was run (no mkdir for .m2)
         self.assertThat(
             build_craft.backend.run.calls,
             MatchesListwise(
                 [
-                    RanCommand(["mkdir", "-p", "/home/buildd/test-image/.m2"]),
                     RanBuildCommand(
                         ["sourcecraft", "pack", "-v", "--destructive-mode"],
                         cwd="/home/buildd/test-image/.",

Follow ups