-
Notifications
You must be signed in to change notification settings - Fork 308
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(bazel): Add
MultiBazelModuleRegistryService
class
This is a special `BazelModuleRegistryService` implementation that wraps an arbitrary number of other registry services. On receiving a request, it delegates to the other registries and returns the first successful result. This implementation is going to be used to support multiple registries declared in a `.bazelrc` file. Signed-off-by: Oliver Heger <oliver.heger@bosch.io>
- Loading branch information
1 parent
5dd19ff
commit ebd6454
Showing
3 changed files
with
337 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
112 changes: 112 additions & 0 deletions
112
plugins/package-managers/bazel/src/main/kotlin/MultiBazelModuleRegistryService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
/* | ||
* Copyright (C) 2024 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>) | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
* License-Filename: LICENSE | ||
*/ | ||
|
||
package org.ossreviewtoolkit.plugins.packagemanagers.bazel | ||
|
||
import java.io.File | ||
|
||
import org.ossreviewtoolkit.clients.bazelmoduleregistry.BazelModuleRegistryService | ||
import org.ossreviewtoolkit.clients.bazelmoduleregistry.LocalBazelModuleRegistryService | ||
import org.ossreviewtoolkit.clients.bazelmoduleregistry.ModuleMetadata | ||
import org.ossreviewtoolkit.clients.bazelmoduleregistry.ModuleSourceInfo | ||
import org.ossreviewtoolkit.clients.bazelmoduleregistry.RemoteBazelModuleRegistryService | ||
|
||
/** | ||
* A special implementation of [BazelModuleRegistryService] that wraps an arbitrary number of other | ||
* [BazelModuleRegistryService] instances. It can be used for projects that declare multiple registries. | ||
* | ||
* The functions of the interface are implemented by iterating over the wrapped services and returning the first | ||
* successful result. | ||
*/ | ||
internal class MultiBazelModuleRegistryService( | ||
/** The wrapped [BazelModuleRegistryService] instances. */ | ||
private val registryServices: Collection<BazelModuleRegistryService> | ||
) : BazelModuleRegistryService { | ||
companion object { | ||
/** | ||
* Create an instance of [MultiBazelModuleRegistryService] for the given [registryUrls]. Based on the URLs, | ||
* concrete [BazelModuleRegistryService] implementations are created. Local registry services use the given | ||
* [projectDir] as workspace. These services are then queried in the order defined by the passed in collection. | ||
* Note that as the last service a remote module registry for the Bazel Central Registry is added that serves | ||
* as a fallback. | ||
*/ | ||
fun create(registryUrls: Collection<String>, projectDir: File): MultiBazelModuleRegistryService { | ||
val registryServices = registryUrls.mapTo(mutableListOf()) { url -> | ||
LocalBazelModuleRegistryService.createForLocalUrl(url, projectDir) | ||
?: RemoteBazelModuleRegistryService.create(url) | ||
} | ||
|
||
// Add the default Bazel registry as a fallback. | ||
registryServices += RemoteBazelModuleRegistryService.create(null) | ||
|
||
return MultiBazelModuleRegistryService(registryServices) | ||
} | ||
|
||
/** | ||
* Return an exception with a message that combines the messages of all [Throwable]s in this list. | ||
*/ | ||
private fun List<Throwable>.combinedException(caption: String): Throwable = | ||
IllegalArgumentException( | ||
"$caption:\n${joinToString("\n") { it.message.orEmpty() }}" | ||
) | ||
} | ||
|
||
override suspend fun getModuleMetadata(name: String): ModuleMetadata = | ||
queryRegistryServices( | ||
errorMessage = { "Failed to query metadata for package '$name'" }, | ||
query = { it.getModuleMetadata(name) } | ||
) | ||
|
||
override suspend fun getModuleSourceInfo(name: String, version: String): ModuleSourceInfo = | ||
queryRegistryServices( | ||
errorMessage = { "Failed to query source info for package '$name' and version '$version'" }, | ||
query = { it.getModuleSourceInfo(name, version) } | ||
) | ||
|
||
/** | ||
* A generic function for sending a [query] to all managed [BazelModuleRegistryService] instances and returning the | ||
* first successful result. In case no registry service can provide a result, throw an exception with the given | ||
* [errorMessage] and a summary of all failures. | ||
*/ | ||
private suspend fun <T> queryRegistryServices( | ||
errorMessage: () -> String, | ||
query: suspend (BazelModuleRegistryService) -> T | ||
): T { | ||
val failures = mutableListOf<Throwable>() | ||
|
||
tailrec suspend fun queryServices(itServices: Iterator<BazelModuleRegistryService>): T? = | ||
if (!itServices.hasNext()) { | ||
null | ||
} else { | ||
val triedResult = runCatching { query(itServices.next()) } | ||
val result = triedResult.getOrNull() | ||
|
||
// The Elvis operator does not work here because of the tailrec modifier. | ||
if (result != null) { | ||
result | ||
} else { | ||
triedResult.exceptionOrNull()?.let(failures::add) | ||
queryServices(itServices) | ||
} | ||
} | ||
|
||
val info = queryServices(registryServices.iterator()) | ||
return info ?: throw failures.combinedException(errorMessage()) | ||
} | ||
} |
223 changes: 223 additions & 0 deletions
223
plugins/package-managers/bazel/src/test/kotlin/MultiBazelModuleRegistryServiceTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,223 @@ | ||
/* | ||
* Copyright (C) 2024 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>) | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
* License-Filename: LICENSE | ||
*/ | ||
|
||
package org.ossreviewtoolkit.plugins.packagemanagers.bazel | ||
|
||
import io.kotest.assertions.throwables.shouldThrow | ||
import io.kotest.core.spec.style.WordSpec | ||
import io.kotest.engine.spec.tempdir | ||
import io.kotest.matchers.shouldBe | ||
|
||
import io.mockk.coEvery | ||
import io.mockk.every | ||
import io.mockk.mockk | ||
import io.mockk.mockkObject | ||
import io.mockk.unmockkAll | ||
|
||
import java.io.File | ||
|
||
import org.ossreviewtoolkit.clients.bazelmoduleregistry.BazelModuleRegistryService | ||
import org.ossreviewtoolkit.clients.bazelmoduleregistry.LocalBazelModuleRegistryService | ||
import org.ossreviewtoolkit.clients.bazelmoduleregistry.ModuleMetadata | ||
import org.ossreviewtoolkit.clients.bazelmoduleregistry.ModuleSourceInfo | ||
import org.ossreviewtoolkit.clients.bazelmoduleregistry.RemoteBazelModuleRegistryService | ||
|
||
class MultiBazelModuleRegistryServiceTest : WordSpec({ | ||
beforeTest { | ||
mockkObject(LocalBazelModuleRegistryService, RemoteBazelModuleRegistryService) | ||
} | ||
|
||
afterTest { | ||
unmockkAll() | ||
} | ||
|
||
"getModuleMetadata" should { | ||
"throw an exception if no metadata can be obtained from all registries" { | ||
val projectDir = tempdir() | ||
val mockRegistries = MockRegistryServices.create(projectDir) | ||
mockRegistries.prepareFailedMetadata() | ||
|
||
val multiRegistry = MultiBazelModuleRegistryService.create(registryUrls, projectDir) | ||
|
||
shouldThrow<IllegalArgumentException> { | ||
multiRegistry.getModuleMetadata(PACKAGE_NAME) | ||
} | ||
} | ||
|
||
"return the metadata from the first registry that contains it" { | ||
val projectDir = tempdir() | ||
val mockRegistries = MockRegistryServices.create(projectDir) | ||
val metadata = mockk<ModuleMetadata>() | ||
mockRegistries.prepareSuccessMetadata(1, metadata) | ||
|
||
val multiRegistry = MultiBazelModuleRegistryService.create(registryUrls, projectDir) | ||
|
||
multiRegistry.getModuleMetadata(PACKAGE_NAME) shouldBe metadata | ||
} | ||
|
||
"fall back to the default registry if no metadata can be obtained from the other registries" { | ||
val projectDir = tempdir() | ||
val mockRegistries = MockRegistryServices.create(projectDir) | ||
val metadata = mockk<ModuleMetadata>() | ||
mockRegistries.prepareSuccessMetadata(3, metadata) | ||
|
||
val multiRegistry = MultiBazelModuleRegistryService.create(registryUrls, projectDir) | ||
|
||
multiRegistry.getModuleMetadata(PACKAGE_NAME) shouldBe metadata | ||
} | ||
} | ||
|
||
"getModuleSourceInfo" should { | ||
"throw an exception if no source info can be obtained from all registries" { | ||
val projectDir = tempdir() | ||
val mockRegistries = MockRegistryServices.create(projectDir) | ||
mockRegistries.prepareFailedSourceInfo() | ||
|
||
val multiRegistry = MultiBazelModuleRegistryService.create(registryUrls, projectDir) | ||
|
||
shouldThrow<IllegalArgumentException> { | ||
multiRegistry.getModuleSourceInfo(PACKAGE_NAME, PACKAGE_VERSION) | ||
} | ||
} | ||
|
||
"return the source info from the first registry that contains it" { | ||
val projectDir = tempdir() | ||
val mockRegistries = MockRegistryServices.create(projectDir) | ||
val sourceInfo = mockk<ModuleSourceInfo>() | ||
mockRegistries.prepareSuccessModuleInfo(1, sourceInfo) | ||
|
||
val multiRegistry = MultiBazelModuleRegistryService.create(registryUrls, projectDir) | ||
|
||
multiRegistry.getModuleSourceInfo(PACKAGE_NAME, PACKAGE_VERSION) shouldBe sourceInfo | ||
} | ||
|
||
"fall back to the default registry if no source info can be obtained from the other registries" { | ||
val projectDir = tempdir() | ||
val mockRegistries = MockRegistryServices.create(projectDir) | ||
val sourceInfo = mockk<ModuleSourceInfo>() | ||
mockRegistries.prepareSuccessModuleInfo(3, sourceInfo) | ||
|
||
val multiRegistry = MultiBazelModuleRegistryService.create(registryUrls, projectDir) | ||
|
||
multiRegistry.getModuleSourceInfo(PACKAGE_NAME, PACKAGE_VERSION) shouldBe sourceInfo | ||
} | ||
} | ||
}) | ||
|
||
/** Name of a test package. */ | ||
private const val PACKAGE_NAME = "test-package" | ||
|
||
/** Version of a test package. */ | ||
private const val PACKAGE_VERSION = "0.8.15" | ||
|
||
/** A list of URLs for local and remote test registries. */ | ||
private val registryUrls = listOf( | ||
"file://local/registry/url", | ||
"https://bazel-remote.example.com", | ||
"file://%workspace%/registry" | ||
) | ||
|
||
private class MockRegistryServices( | ||
val registryServices: List<BazelModuleRegistryService> | ||
) { | ||
companion object { | ||
/** An exception thrown by mock registries to simulate a failure. */ | ||
private val testException = Exception("Test exception: Registry invocation failed.") | ||
|
||
/** | ||
* Create an instance of [MockRegistryServices] with mocks for the test registry URLs. The factory methods | ||
* of the local and remote service implementations have been prepared to return the mock instances. | ||
*/ | ||
fun create(projectDir: File): MockRegistryServices { | ||
val localRegistry1 = mockk<LocalBazelModuleRegistryService>() | ||
val localRegistry2 = mockk<LocalBazelModuleRegistryService>() | ||
val remoteRegistry = mockk<RemoteBazelModuleRegistryService>() | ||
val centralRegistry = mockk<RemoteBazelModuleRegistryService>() | ||
|
||
every { | ||
LocalBazelModuleRegistryService.createForLocalUrl(any(), projectDir) | ||
} returns null | ||
every { | ||
LocalBazelModuleRegistryService.createForLocalUrl(registryUrls[0], projectDir) | ||
} returns localRegistry1 | ||
every { | ||
LocalBazelModuleRegistryService.createForLocalUrl(registryUrls[2], projectDir) | ||
} returns localRegistry2 | ||
every { | ||
RemoteBazelModuleRegistryService.create(registryUrls[1]) | ||
} returns remoteRegistry | ||
every { | ||
RemoteBazelModuleRegistryService.create(url = null) | ||
} returns centralRegistry | ||
|
||
return MockRegistryServices(listOf(localRegistry1, localRegistry2, remoteRegistry, centralRegistry)) | ||
} | ||
|
||
/** | ||
* Prepare the given [services] mocks to expect a query for module metadata and to fail with a test exception. | ||
*/ | ||
private fun prepareFailedMetadata(services: Collection<BazelModuleRegistryService>) { | ||
services.forEach { service -> | ||
coEvery { service.getModuleMetadata(PACKAGE_NAME) } throws testException | ||
} | ||
} | ||
|
||
/** | ||
* Prepare the given [services] mocks to expect a query for module source info and to fail with a test | ||
* exception. | ||
*/ | ||
fun prepareFailedSourceInfo(services: Collection<BazelModuleRegistryService>) { | ||
services.forEach { service -> | ||
coEvery { service.getModuleSourceInfo(PACKAGE_NAME, PACKAGE_VERSION) } throws testException | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Prepare the mock registries to expect a query for module metadata and to fail with a test exception. | ||
*/ | ||
fun prepareFailedMetadata() { | ||
prepareFailedMetadata(registryServices) | ||
} | ||
|
||
/** | ||
* Prepare the mock registries to expect a query for module source info and to fail with a test exception. | ||
*/ | ||
fun prepareFailedSourceInfo() { | ||
prepareFailedSourceInfo(registryServices) | ||
} | ||
|
||
/** | ||
* Prepare the mock registries to expect a query for module metadata that will eventually succeed. The registry | ||
* with the given [index] is configured to return the given [metadata]. | ||
*/ | ||
fun prepareSuccessMetadata(index: Int, metadata: ModuleMetadata) { | ||
prepareFailedMetadata(registryServices.take(index)) | ||
coEvery { registryServices[index].getModuleMetadata(PACKAGE_NAME) } returns metadata | ||
} | ||
|
||
/** | ||
* Prepare the mock registries to expect a query for module source info that will eventually succeed. The registry | ||
* with the given [index] is configured to return the given [info]. | ||
*/ | ||
fun prepareSuccessModuleInfo(index: Int, info: ModuleSourceInfo) { | ||
prepareFailedSourceInfo(registryServices.take(index)) | ||
coEvery { registryServices[index].getModuleSourceInfo(PACKAGE_NAME, PACKAGE_VERSION) } returns info | ||
} | ||
} |