Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,43 @@ jobs:
**/build/test-results/**
**/build/reports/tests/**

license-checks:
name: License and NOTICE Checks
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Set up JDK 21
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with: *setup-java-vars
- name: Setup test environment
uses: ./.github/actions/setup-test-env
- name: Prepare Gradle build cache
uses: ./.github/actions/ci-incr-build-cache-prepare
- name: Run distribution merge and Quarkus license report validation
env: *gradle_env_vars
run: |
./gradlew \
:polaris-distribution:licenseNoticeMerge \
:polaris-server:generateLicenseReport \
:polaris-admin:generateLicenseReport \
--continue
- name: Save partial Gradle build cache
uses: ./.github/actions/ci-incr-build-cache-save
- name: Archive license validation outputs
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: failure()
with:
name: upload-${{ github.job }}-license-reports
path: |
runtime/distribution/build/merged-license-notice/**
runtime/server/build/reports/dependency-license/**
runtime/admin/build/reports/dependency-license/**

runtime-service-tests:
name: Runtime Service Tests
runs-on: ubuntu-latest
Expand Down Expand Up @@ -552,6 +589,7 @@ jobs:
name: "Required Checks"
needs:
- build-checks
- license-checks
- runtime-service-tests
- runtime-service-int-tests
- admin-tool-tests
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti
- Added support for **Apache Ranger** as an external authorizer (Beta).

### Changes
- Added a dedicated `license-checks` CI job for distribution LICENSE/NOTICE merge validation and Quarkus license reports; Spark bundle validation (`checkBundleLicense`, `checkBundleJarLicenseNotice`) runs via the standard `check` task. Spark checks validate direct `runtimeClasspath` Maven mentions and bundle JAR LICENSE/NOTICE presence; transitive attribution inside fat runtime JARs remains manually maintained in `BUNDLE-LICENSE`.
- Improved Python CLI error messages and exit codes for invalid arguments and configuration errors.
- Removed unused `PolarisAuthorizableOperation` values: `REVOKE_PRINCIPAL_GRANT_FROM_PRINCIPAL_ROLE`, `REVOKE_PRINCIPAL_ROLE_GRANT_FROM_PRINCIPAL_ROLE`, `LIST_GRANTS_ON_ROOT`, `ADD_PRINCIPAL_GRANT_TO_PRINCIPAL_ROLE`, `LIST_GRANTS_ON_PRINCIPAL`, `ADD_PRINCIPAL_ROLE_GRANT_TO_PRINCIPAL_ROLE`, `LIST_GRANTS_ON_PRINCIPAL_ROLE`, `ADD_CATALOG_ROLE_GRANT_TO_CATALOG_ROLE`, `REVOKE_CATALOG_ROLE_GRANT_FROM_CATALOG_ROLE`, `LIST_GRANTS_ON_CATALOG_ROLE`, `LIST_GRANTS_ON_CATALOG`, `LIST_GRANTS_ON_NAMESPACE`, `LIST_GRANTS_ON_TABLE`, `LIST_GRANTS_ON_VIEW`.
- Changed deprecated APIs in JUnit 5. This change will force downstream projects that pull in the Polaris test packages to adopt JUnit 6.
Expand Down
4 changes: 4 additions & 0 deletions build-logic/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,8 @@ dependencies {
implementation(baselibs.nexus.publish)
implementation(baselibs.shadow)
implementation(baselibs.spotless)

testImplementation(kotlin("test-junit5"))
}

tasks.test { useJUnitPlatform() }
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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
*
* http://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.
*/

package licenses

import java.util.zip.ZipFile
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.work.DisableCachingByDefault

/**
* Validates that a shadow bundle JAR contains top-level LICENSE and NOTICE entries (renamed from
* BUNDLE-LICENSE and BUNDLE-NOTICE at package time).
*/
@DisableCachingByDefault(because = "lightweight validation task, not worth caching")
abstract class BundleJarLicenseNoticeValidation : DefaultTask() {

@get:InputFile abstract val bundleJar: RegularFileProperty

@TaskAction
fun validate() {
val jarFile = bundleJar.get().asFile
val missing = mutableListOf<String>()
ZipFile(jarFile).use { zip ->
for (entryName in REQUIRED_ENTRIES) {
val entry = zip.getEntry(entryName)
if (entry == null) {
missing.add(entryName)
continue
}
if (entry.size == 0L) {
missing.add("$entryName (empty)")
}
}
}
if (missing.isNotEmpty()) {
throw GradleException(
"Bundle JAR '${jarFile.name}' is missing required license files: ${missing.joinToString(", ")}"
)
}
}

companion object {
private val REQUIRED_ENTRIES = listOf("LICENSE", "NOTICE")
}
}
104 changes: 104 additions & 0 deletions build-logic/src/main/kotlin/licenses/BundleLicenseValidation.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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
*
* http://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.
*/

package licenses

import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.work.DisableCachingByDefault

/**
* Validates that every direct dependency in [bundledArtifacts] (in "group:artifactId" form) has a
* corresponding `* Maven group:artifact IDs:` line in the bundle LICENSE file.
*
* This mirrors the Maven-coordinate mention check in [LicenseFileValidation] for Quarkus
* distribution artifacts, adapted for shadow-jar bundle artifacts such as the Spark plugin. It does
* **not** validate prose attribution sections (e.g. "This binary artifact contains Guava") or
* transitive contents inside fat runtime JARs such as `iceberg-spark-runtime`.
*
* The [allowedExtraArtifacts] set lets callers declare entries that are intentionally present in the
* BUNDLE-LICENSE for a reason not reflected in the current build's resolved classpath — for example,
* cross-Scala-version variants of the same artifact (e.g. the `_2.13` variant when building for
* `_2.12`). These entries are excluded from the "superfluous" check so that a single shared
* BUNDLE-LICENSE file can cover multiple build variants without triggering false-positive failures.
*/
@DisableCachingByDefault(because = "lightweight validation task, not worth caching")
abstract class BundleLicenseValidation : DefaultTask() {

@get:InputFile abstract val bundleLicenseFile: RegularFileProperty

/** Set of "group:artifactId" strings that the bundle jar contains. */
@get:Input abstract val bundledArtifacts: SetProperty<String>

/**
* Optional set of "group:artifactId" strings that are intentionally present in the BUNDLE-LICENSE
* but are not part of the current build's resolved classpath (e.g. cross-Scala-version variants).
* Entries listed here are exempt from the "superfluous" check.
*/
@get:Input abstract val allowedExtraArtifacts: SetProperty<String>

@TaskAction
fun validate() {
val error =
validateLicenseMentions(
bundleLicenseFile.get().asFile.readText(),
bundledArtifacts.get(),
allowedExtraArtifacts.get(),
)
if (error != null) {
throw GradleException("BUNDLE-LICENSE validation failed:$error")
}
}

companion object {
/** Returns a validation error message, or null if validation passes. */
internal fun validateLicenseMentions(
licenseText: String,
bundledArtifacts: Set<String>,
allowedExtraArtifacts: Set<String>,
): String? {
val mentioned =
licenseText
.lines()
.filter { it.startsWith(LicenseFileValidation.LICENSE_MENTION_PREFIX) }
.map { it.removePrefix(LicenseFileValidation.LICENSE_MENTION_PREFIX).trim() }
.toSet()

val allKnown = bundledArtifacts + allowedExtraArtifacts
val missing = bundledArtifacts.filter { it !in mentioned }.sorted()
val superfluous = mentioned.filter { it !in allKnown }.sorted()

val errors = StringBuilder()
if (missing.isNotEmpty()) {
errors.append("\nMissing entries in BUNDLE-LICENSE (add these):\n")
missing.forEach { errors.append(" ${LicenseFileValidation.LICENSE_MENTION_PREFIX}$it\n") }
}
if (superfluous.isNotEmpty()) {
errors.append("\nSuperfluous entries in BUNDLE-LICENSE (remove these):\n")
superfluous.forEach { errors.append(" ${LicenseFileValidation.LICENSE_MENTION_PREFIX}$it\n") }
}
return errors.takeIf { it.isNotEmpty() }?.toString()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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
*
* http://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.
*/

package licenses

import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test

class BundleLicenseValidationTest {

@Test
fun validateLicenseMentions_passesWhenAllArtifactsMentioned() {
val licenseText =
"""
* Maven group:artifact IDs: com.example:foo_2.12
* Maven group:artifact IDs: com.example:foo_2.13
"""
.trimIndent()

val error =
BundleLicenseValidation.validateLicenseMentions(
licenseText,
bundledArtifacts = setOf("com.example:foo_2.12"),
allowedExtraArtifacts = setOf("com.example:foo_2.13"),
)

assertNull(error)
}

@Test
fun validateLicenseMentions_failsOnMissingArtifact() {
val error =
BundleLicenseValidation.validateLicenseMentions(
licenseText = "* Maven group:artifact IDs: com.example:other\n",
bundledArtifacts = setOf("com.example:foo_2.12"),
allowedExtraArtifacts = emptySet(),
)

assertNotNull(error)
assert(error!!.contains("com.example:foo_2.12"))
}

@Test
fun validateLicenseMentions_failsOnSuperfluousMention() {
val error =
BundleLicenseValidation.validateLicenseMentions(
licenseText =
"""
* Maven group:artifact IDs: com.example:foo_2.12
* Maven group:artifact IDs: com.example:stale
"""
.trimIndent(),
bundledArtifacts = setOf("com.example:foo_2.12"),
allowedExtraArtifacts = emptySet(),
)

assertNotNull(error)
assert(error!!.contains("com.example:stale"))
}
}
3 changes: 3 additions & 0 deletions plugins/spark/v3.5/spark/BUNDLE-LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,9 @@ This product bundles and includes code from Apache Iceberg.
* plugins/spark/v3.5/spark/src/main/java/org/apache/polaris/spark/PolarisRESTCatalog.java
* plugins/spark/v3.5/spark/src/main/java/org/apache/polaris/spark/SparkCatalog.java

* Maven group:artifact IDs: org.apache.iceberg:iceberg-spark-runtime-3.5_2.12
* Maven group:artifact IDs: org.apache.iceberg:iceberg-spark-runtime-3.5_2.13

Copyright: 2017-2025 The Apache Software Foundation
Project URL: https://iceberg.apache.org/
License: Apache License 2.0 - https://www.apache.org/licenses/LICENSE-2.0.txt
Expand Down
53 changes: 53 additions & 0 deletions plugins/spark/v3.5/spark/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
*/

import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import licenses.BundleJarLicenseNoticeValidation
import licenses.BundleLicenseValidation

plugins { id("polaris-client") }

Expand Down Expand Up @@ -128,6 +130,57 @@ tasks.register<ShadowJar>("createPolarisSparkJar") {
from("${projectDir}/BUNDLE-NOTICE") { rename { "NOTICE" } }
}

val createPolarisSparkJar = tasks.named<ShadowJar>("createPolarisSparkJar")

val checkBundleLicense by
tasks.registering(BundleLicenseValidation::class) {
description =
"Validates direct runtimeClasspath dependencies have " +
"'* Maven group:artifact IDs:' entries in BUNDLE-LICENSE"
group = "verification"
bundleLicenseFile.set(project.file("BUNDLE-LICENSE"))
bundledArtifacts.set(
provider {
configurations
.getByName("runtimeClasspath")
.resolvedConfiguration
.resolvedArtifacts
.filter { it.moduleVersion.id.group != project.group.toString() }
.map { "${it.moduleVersion.id.group}:${it.moduleVersion.id.name}" }
.toSet()
}
)
// The BUNDLE-LICENSE is shared between the _2.12 and _2.13 build variants, so it intentionally
// contains entries for both Scala variants of each artifact. Allow the cross-variant entry so
// the superfluous check does not produce a false positive when building for the other variant.
allowedExtraArtifacts.set(
provider {
val otherScalaVersion = if (scalaVersion == "2.12") "2.13" else "2.12"
configurations
.getByName("runtimeClasspath")
.resolvedConfiguration
.resolvedArtifacts
.filter { it.moduleVersion.id.group != project.group.toString() }
.map {
val baseName =
it.moduleVersion.id.name.replace("_${scalaVersion}", "_${otherScalaVersion}")
"${it.moduleVersion.id.group}:${baseName}"
}
.toSet()
}
)
}

val checkBundleJarLicenseNotice by
tasks.registering(BundleJarLicenseNoticeValidation::class) {
description = "Validates the bundle shadow JAR contains top-level LICENSE and NOTICE entries"
group = "verification"
bundleJar.set(createPolarisSparkJar.flatMap { it.archiveFile })
dependsOn(createPolarisSparkJar)
}

tasks.named("check") { dependsOn(checkBundleLicense, checkBundleJarLicenseNotice) }

// ensure the shadow jar job (which will automatically run license addition) is run for both
// `assemble` and `build` task
tasks.named("assemble") { dependsOn("createPolarisSparkJar") }
Expand Down
Loading