diff --git a/helper-cli/src/main/kotlin/HelperMain.kt b/helper-cli/src/main/kotlin/HelperMain.kt index 6c65931daf843..935be5c2012cc 100644 --- a/helper-cli/src/main/kotlin/HelperMain.kt +++ b/helper-cli/src/main/kotlin/HelperMain.kt @@ -38,6 +38,7 @@ import org.ossreviewtoolkit.helper.commands.classifications.LicenseClassificatio import org.ossreviewtoolkit.helper.commands.dev.DevCommand import org.ossreviewtoolkit.helper.commands.packageconfig.PackageConfigurationCommand import org.ossreviewtoolkit.helper.commands.packagecuration.PackageCurationsCommand +import org.ossreviewtoolkit.helper.commands.provenancestorage.ProvenanceStorageCommand import org.ossreviewtoolkit.helper.commands.repoconfig.RepositoryConfigurationCommand import org.ossreviewtoolkit.helper.commands.scanstorage.ScanStorageCommand import org.ossreviewtoolkit.helper.utils.ORTH_NAME @@ -97,6 +98,7 @@ internal class HelperMain : CliktCommand( MergeRepositoryConfigurationsCommand(), PackageConfigurationCommand(), PackageCurationsCommand(), + ProvenanceStorageCommand(), RepositoryConfigurationCommand(), ScanStorageCommand(), SetDependencyRepresentationCommand(), diff --git a/helper-cli/src/main/kotlin/commands/provenancestorage/DeleteCommand.kt b/helper-cli/src/main/kotlin/commands/provenancestorage/DeleteCommand.kt new file mode 100644 index 0000000000000..13f19c1ec4c17 --- /dev/null +++ b/helper-cli/src/main/kotlin/commands/provenancestorage/DeleteCommand.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2024 The ORT Project Authors (see ) + * + * 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.helper.commands.provenancestorage + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.terminal +import com.github.ajalt.clikt.parameters.options.associate +import com.github.ajalt.clikt.parameters.options.convert +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.flag +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import com.github.ajalt.clikt.parameters.types.file +import com.github.ajalt.mordant.rendering.Theme +import com.github.ajalt.mordant.terminal.YesNoPrompt + +import org.ossreviewtoolkit.model.Identifier +import org.ossreviewtoolkit.model.config.OrtConfiguration +import org.ossreviewtoolkit.scanner.ScanStorages +import org.ossreviewtoolkit.utils.common.expandTilde +import org.ossreviewtoolkit.utils.ort.ORT_CONFIG_FILENAME +import org.ossreviewtoolkit.utils.ort.ortConfigDirectory + +internal class DeleteCommand : CliktCommand( + help = "Deletes stored provenance results matching the options." +) { + private val configFile by option( + "--config", + help = "The path to the ORT configuration file that configures the scan storages." + ).convert { it.expandTilde() } + .file(mustExist = true, canBeFile = true, canBeDir = false, mustBeWritable = false, mustBeReadable = true) + .convert { it.absoluteFile.normalize() } + .default(ortConfigDirectory.resolve(ORT_CONFIG_FILENAME)) + + private val configArguments by option( + "-P", + help = "Override a key-value pair in the configuration file. For example: " + + "-P ort.scanner.storages.postgres.connection.schema=testSchema" + ).associate() + + private val packageId by option( + "--package-id", + help = "Coordinates of the package ID to delete." + ).convert { Identifier(it) } + .required() + + private val forceYes by option( + "--yes", "-y", + help = "Force yes on all prompts." + ).flag() + + override fun run() { + val config = OrtConfiguration.load(configArguments, configFile) + val scanStorages = ScanStorages.createFromConfig(config.scanner) + + val provenances = scanStorages.packageProvenanceStorage.readProvenances(packageId) + if (provenances.isEmpty()) { + val pkgCoords = Theme.Default.success(packageId.toCoordinates()) + echo(Theme.Default.info("No stored provenance found for '$pkgCoords'.")) + return + } + + val count = Theme.Default.warning(provenances.size.toString()) + + echo(Theme.Default.danger("About to delete the following $count provenance(s):")) + provenances.forEach(::echo) + + if (forceYes || YesNoPrompt("Continue?", terminal).ask() == true) { + scanStorages.packageProvenanceStorage.deleteProvenances(packageId) + } + } +} diff --git a/helper-cli/src/main/kotlin/commands/provenancestorage/ProvenanceStorageCommand.kt b/helper-cli/src/main/kotlin/commands/provenancestorage/ProvenanceStorageCommand.kt new file mode 100644 index 0000000000000..3aa7f7f7f0b98 --- /dev/null +++ b/helper-cli/src/main/kotlin/commands/provenancestorage/ProvenanceStorageCommand.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The ORT Project Authors (see ) + * + * 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.helper.commands.provenancestorage + +import com.github.ajalt.clikt.core.NoOpCliktCommand +import com.github.ajalt.clikt.core.subcommands + +internal class ProvenanceStorageCommand : NoOpCliktCommand( + help = "Commands for working with provenance storages." +) { + init { + subcommands( + DeleteCommand() + ) + } +} diff --git a/scanner/src/funTest/kotlin/provenance/DefaultPackageProvenanceResolverFunTest.kt b/scanner/src/funTest/kotlin/provenance/DefaultPackageProvenanceResolverFunTest.kt index cf97ee68d7794..fc9da19864296 100644 --- a/scanner/src/funTest/kotlin/provenance/DefaultPackageProvenanceResolverFunTest.kt +++ b/scanner/src/funTest/kotlin/provenance/DefaultPackageProvenanceResolverFunTest.kt @@ -211,4 +211,6 @@ internal class DummyProvenanceStorage : PackageProvenanceStorage { sourceArtifact: RemoteArtifact, result: PackageProvenanceResolutionResult ) { /* no-op */ } + + override fun deleteProvenances(id: Identifier) { /* no-op */ } } diff --git a/scanner/src/main/kotlin/provenance/FileBasedPackageProvenanceStorage.kt b/scanner/src/main/kotlin/provenance/FileBasedPackageProvenanceStorage.kt index e94409c83aed2..af9379377af09 100644 --- a/scanner/src/main/kotlin/provenance/FileBasedPackageProvenanceStorage.kt +++ b/scanner/src/main/kotlin/provenance/FileBasedPackageProvenanceStorage.kt @@ -112,6 +112,13 @@ class FileBasedPackageProvenanceStorage(val backend: FileStorage) : PackageProve } } } + + override fun deleteProvenances(id: Identifier) { + val path = storagePath(id) + if (!backend.delete(path)) { + logger.warn { "Could not delete resolved provenances for '${id.toCoordinates()}' at path '$path'." } + } + } } private const val FILE_NAME = "resolved_provenance.yml" diff --git a/scanner/src/main/kotlin/provenance/PackageProvenanceStorage.kt b/scanner/src/main/kotlin/provenance/PackageProvenanceStorage.kt index 689b5f0c32063..f03a6a3b11f1d 100644 --- a/scanner/src/main/kotlin/provenance/PackageProvenanceStorage.kt +++ b/scanner/src/main/kotlin/provenance/PackageProvenanceStorage.kt @@ -87,6 +87,11 @@ interface PackageProvenanceStorage { * for [id] and [vcs] it is overwritten. */ fun writeProvenance(id: Identifier, vcs: VcsInfo, result: PackageProvenanceResolutionResult) + + /** + * Delete all [PackageProvenanceResolutionResult]s for the [id]. + */ + fun deleteProvenances(id: Identifier) } /** diff --git a/scanner/src/main/kotlin/provenance/PostgresPackageProvenanceStorage.kt b/scanner/src/main/kotlin/provenance/PostgresPackageProvenanceStorage.kt index 83e03801b7b50..1e2907e6d758b 100644 --- a/scanner/src/main/kotlin/provenance/PostgresPackageProvenanceStorage.kt +++ b/scanner/src/main/kotlin/provenance/PostgresPackageProvenanceStorage.kt @@ -134,6 +134,14 @@ class PostgresPackageProvenanceStorage( } } } + + override fun deleteProvenances(id: Identifier) { + database.transaction { + table.deleteWhere { + table.identifier eq id.toCoordinates() + } + } + } } private class PackageProvenances(tableName: String) : IntIdTable(tableName) { diff --git a/utils/ort/src/main/kotlin/storage/FileStorage.kt b/utils/ort/src/main/kotlin/storage/FileStorage.kt index 71a2d8f05b33f..634426e097e5c 100644 --- a/utils/ort/src/main/kotlin/storage/FileStorage.kt +++ b/utils/ort/src/main/kotlin/storage/FileStorage.kt @@ -41,4 +41,9 @@ interface FileStorage { * provided [inputStream] is closed after writing it to the file. */ fun write(path: String, inputStream: InputStream) + + /** + * Delete the file at the given [path] and return whether the operation was successful. + */ + fun delete(path: String): Boolean } diff --git a/utils/ort/src/main/kotlin/storage/HttpFileStorage.kt b/utils/ort/src/main/kotlin/storage/HttpFileStorage.kt index 0cb0f87406396..b4c0d933732d6 100644 --- a/utils/ort/src/main/kotlin/storage/HttpFileStorage.kt +++ b/utils/ort/src/main/kotlin/storage/HttpFileStorage.kt @@ -136,4 +136,16 @@ class HttpFileStorage( } private fun urlForPath(path: String) = "$url/$path$query" + + override fun delete(path: String): Boolean { + val request = requestBuilder() + .delete() + .url(urlForPath(path)) + .build() + + logger.debug { "Deleting file from storage: ${request.url}" } + + val response = httpClient.execute(request) + return response.isSuccessful + } } diff --git a/utils/ort/src/main/kotlin/storage/LocalFileStorage.kt b/utils/ort/src/main/kotlin/storage/LocalFileStorage.kt index b6c0c307e7f59..bc1564316f1c4 100644 --- a/utils/ort/src/main/kotlin/storage/LocalFileStorage.kt +++ b/utils/ort/src/main/kotlin/storage/LocalFileStorage.kt @@ -76,4 +76,7 @@ open class LocalFileStorage( inputStream.use { it.copyTo(outputStream) } } } + + @Synchronized + override fun delete(path: String): Boolean = directory.resolve(path).delete() } diff --git a/utils/ort/src/main/kotlin/storage/S3FileStorage.kt b/utils/ort/src/main/kotlin/storage/S3FileStorage.kt index 46860cf180263..d8e4454e4a713 100644 --- a/utils/ort/src/main/kotlin/storage/S3FileStorage.kt +++ b/utils/ort/src/main/kotlin/storage/S3FileStorage.kt @@ -34,6 +34,7 @@ import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider import software.amazon.awssdk.core.sync.RequestBody import software.amazon.awssdk.regions.Region import software.amazon.awssdk.services.s3.S3Client +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest import software.amazon.awssdk.services.s3.model.GetObjectRequest import software.amazon.awssdk.services.s3.model.HeadObjectRequest import software.amazon.awssdk.services.s3.model.NoSuchKeyException @@ -136,4 +137,14 @@ class S3FileStorage( if (exception is S3Exception) logger.warn { "Can not write '$path' to S3 bucket '$bucketName'." } } } + + override fun delete(path: String): Boolean { + val request = DeleteObjectRequest.builder() + .key(path) + .bucket(bucketName) + .build() + + val response = s3Client.deleteObject(request) + return response.sdkHttpResponse().isSuccessful + } }