Files
id_configurator/id_configurator/modules/java.py
T
2026-09-02 06:58:20 +02:00

301 lines
11 KiB
Python

"""Add the configured root certificates to every Java trust store detected
under `<src>`, without shelling out to `keytool`, and configure the
Maven-compatible repositories declared in `configurations.java.repositories`
globally for the main JVM build tools.
Java ignores the system CA store and only trusts what is in each JRE/JDK's
own `cacerts` file: JKS format before Java 9, PKCS12 since Java 9. Every
`cacerts` file found under `usr/` or `opt/` in `<src>` is copied into `<dst>`
with the configured root certificates added, entirely in Python: `pyjks`
handles the legacy JKS format, `cryptography` handles PKCS12. The default
`changeit` store password is used, since virtually every JDK ships with it
unchanged; a keystore that doesn't open with it is left untouched and a
warning is logged instead of failing the whole run.
The repositories are written to the well-known global configuration file of
each build tool. Maven's is installation-wide, so every user on the image
picks it up; the others are written under root's home directory, since this
tool targets Docker images, which are built and run as root:
- Maven3: `/etc/maven/settings.xml` (the file `/usr/share/maven/conf/settings.xml`
points to on Debian/Ubuntu, Maven's global, all-users settings file), plus
`conf/settings.xml` in every other Maven installation detected under
`<src>` (found via its `bin/mvn` launcher). Both get an always-active
profile adding every repository, plus a `mirrorOf=*` mirror on the first
one so that even `pom.xml` files hardcoding central are redirected.
- Gradle: `~/.gradle/init.d/id-configurator-repositories.gradle`, an init
script adding the repositories to every project and to plugin management.
- Ivy: `~/.ivy2/ivysettings.xml`, as a chain of `ibiblio` resolvers.
- SBT: `~/.sbt/repositories`, the resolver override file SBT reads for its
own bootstrapping and, when combined with the `sbt.override.build.repos`
system property, for every project's resolvers as well.
- Leiningen: `~/.lein/profiles.clj`, adding the repositories to the `:user`
profile.
"""
from __future__ import annotations
import base64
import json
import logging
import xml.etree.ElementTree as ET
from pathlib import Path
import jks
from cryptography import x509
from cryptography.hazmat.primitives.serialization import (
BestAvailableEncryption,
Encoding,
pkcs12,
)
from id_configurator.config import JavaRepository, NamedContent
from id_configurator.context import Context
from id_configurator.modules.base import Module, register
logger = logging.getLogger(__name__)
_JAVA_SEARCH_ROOTS = ("usr", "opt")
_STORE_PASSWORD = b"changeit"
_MAVEN_GLOBAL_SETTINGS = "etc/maven/settings.xml"
_MAVEN_BIN_GLOBS = ("usr/**/bin/mvn", "opt/**/bin/mvn")
_GRADLE_INIT_SCRIPT = "root/.gradle/init.d/id-configurator-repositories.gradle"
_IVY_SETTINGS = "root/.ivy2/ivysettings.xml"
_SBT_REPOSITORIES = "root/.sbt/repositories"
_LEIN_PROFILES = "root/.lein/profiles.clj"
def _find_cacerts(context: Context) -> list[Path]:
found = {
path
for root in _JAVA_SEARCH_ROOTS
for path in context.glob_src(f"{root}/**/cacerts")
}
return sorted(path for path in found if path.is_file())
def _decode_certificates(certificates: list[NamedContent]) -> list[x509.Certificate]:
return [
x509.load_pem_x509_certificate(base64.b64decode(cert.content))
for cert in certificates
]
def _add_to_pkcs12(
content: bytes, certificates: list[NamedContent], certs: list[x509.Certificate]
) -> bytes:
loaded = pkcs12.load_pkcs12(content, _STORE_PASSWORD)
entries = list(loaded.additional_certs)
for named, cert in zip(certificates, certs, strict=True):
entries.append(pkcs12.PKCS12Certificate(cert, named.name.encode("utf-8")))
return pkcs12.serialize_java_truststore(
entries, BestAvailableEncryption(_STORE_PASSWORD)
)
def _add_to_jks(
content: bytes, certificates: list[NamedContent], certs: list[x509.Certificate]
) -> bytes:
store = jks.KeyStore.loads(content, _STORE_PASSWORD.decode("ascii"))
for named, cert in zip(certificates, certs, strict=True):
store.entries[named.name] = jks.TrustedCertEntry.new(
named.name, cert.public_bytes(Encoding.DER)
)
return store.saves(_STORE_PASSWORD.decode("ascii"))
def _keystore_kind(content: bytes) -> str:
if content[:4] in (jks.MAGIC_NUMBER_JKS, jks.MAGIC_NUMBER_JCEKS):
return "jks"
return "pkcs12"
def _string_literal(value: str) -> str:
return json.dumps(value)
def _render_xml(root: ET.Element) -> bytes:
ET.indent(root)
content = '<?xml version="1.0" encoding="UTF-8"?>\n' + ET.tostring(
root, encoding="unicode"
)
return (content + "\n").encode("utf-8")
def _write_xml(context: Context, relative: str, root: ET.Element) -> None:
context.write_file(relative, _render_xml(root))
def _find_maven_homes(context: Context) -> list[Path]:
found = {
path.parent.parent
for pattern in _MAVEN_BIN_GLOBS
for path in context.glob_src(pattern)
if path.is_file()
}
return sorted(found)
def _build_maven_settings(repositories: list[JavaRepository]) -> ET.Element:
settings = ET.Element(
"settings",
{
"xmlns": "http://maven.apache.org/SETTINGS/1.1.0",
"xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
"xsi:schemaLocation": (
"http://maven.apache.org/SETTINGS/1.1.0 "
"http://maven.apache.org/xsd/settings-1.1.0.xsd"
),
},
)
profile = ET.SubElement(ET.SubElement(settings, "profiles"), "profile")
ET.SubElement(profile, "id").text = "id-configurator"
activation = ET.SubElement(profile, "activation")
ET.SubElement(activation, "activeByDefault").text = "true"
repositories_el = ET.SubElement(profile, "repositories")
plugin_repositories_el = ET.SubElement(profile, "pluginRepositories")
for repository in repositories:
for parent, tag in (
(repositories_el, "repository"),
(plugin_repositories_el, "pluginRepository"),
):
entry = ET.SubElement(parent, tag)
ET.SubElement(entry, "id").text = repository.name
ET.SubElement(entry, "url").text = repository.url
active_profiles = ET.SubElement(settings, "activeProfiles")
ET.SubElement(active_profiles, "activeProfile").text = "id-configurator"
mirror = ET.SubElement(ET.SubElement(settings, "mirrors"), "mirror")
ET.SubElement(mirror, "id").text = repositories[0].name
ET.SubElement(mirror, "url").text = repositories[0].url
ET.SubElement(mirror, "mirrorOf").text = "*"
return settings
def _write_maven_settings(context: Context, repositories: list[JavaRepository]) -> None:
content = _render_xml(_build_maven_settings(repositories))
relatives = {_MAVEN_GLOBAL_SETTINGS}
relatives.update(
f"{home.relative_to(context.src).as_posix()}/conf/settings.xml"
for home in _find_maven_homes(context)
)
for relative in sorted(relatives):
context.write_file(relative, content)
logger.info(
"configured maven mirrors for all users in %d location(s)", len(relatives)
)
def _gradle_repository_lines(repositories: list[JavaRepository], indent: str) -> str:
return "\n".join(
f"{indent}maven {{ url = uri({_string_literal(repository.url)}) }}"
for repository in repositories
)
def _write_gradle_init_script(
context: Context, repositories: list[JavaRepository]
) -> None:
content = (
"allprojects {\n"
" repositories {\n"
f"{_gradle_repository_lines(repositories, ' ')}\n"
" }\n"
"}\n"
"\n"
"settingsEvaluated { settings ->\n"
" settings.pluginManagement {\n"
" repositories {\n"
f"{_gradle_repository_lines(repositories, ' ')}\n"
" }\n"
" }\n"
"}\n"
)
context.write_file(_GRADLE_INIT_SCRIPT, content.encode("utf-8"))
logger.info("configured gradle to use %d repositories", len(repositories))
def _write_ivy_settings(context: Context, repositories: list[JavaRepository]) -> None:
ivysettings = ET.Element("ivysettings")
ET.SubElement(ivysettings, "settings", {"defaultResolver": "id-configurator"})
chain = ET.SubElement(
ET.SubElement(ivysettings, "resolvers"), "chain", {"name": "id-configurator"}
)
for repository in repositories:
ET.SubElement(
chain,
"ibiblio",
{"name": repository.name, "m2compatible": "true", "root": repository.url},
)
_write_xml(context, _IVY_SETTINGS, ivysettings)
logger.info("configured ivy to use %d repositories", len(repositories))
def _write_sbt_repositories(
context: Context, repositories: list[JavaRepository]
) -> None:
lines = ["[repositories]", " local"]
lines.extend(
f" {repository.name}: {repository.url}" for repository in repositories
)
context.write_file(_SBT_REPOSITORIES, ("\n".join(lines) + "\n").encode("utf-8"))
logger.info("configured sbt to use %d repositories", len(repositories))
def _write_lein_profiles(context: Context, repositories: list[JavaRepository]) -> None:
entries = " ".join(
f"[{_string_literal(repository.name)} {{:url {_string_literal(repository.url)}}}]"
for repository in repositories
)
content = f"{{:user {{:repositories [{entries}]}}}}\n"
context.write_file(_LEIN_PROFILES, content.encode("utf-8"))
logger.info("configured leiningen to use %d repositories", len(repositories))
def _configure_repositories(
context: Context, repositories: list[JavaRepository]
) -> None:
_write_maven_settings(context, repositories)
_write_gradle_init_script(context, repositories)
_write_ivy_settings(context, repositories)
_write_sbt_repositories(context, repositories)
_write_lein_profiles(context, repositories)
@register
class JavaModule(Module):
name = "java"
def is_relevant(self, context: Context) -> bool:
has_certificates = bool(context.config.configurations.certificates) and bool(
_find_cacerts(context)
)
has_repositories = bool(context.config.configurations.java.repositories)
return has_certificates or has_repositories
def apply(self, context: Context) -> None:
certificates = context.config.configurations.certificates
if certificates:
certs = _decode_certificates(certificates)
for path in _find_cacerts(context):
relative = path.relative_to(context.src).as_posix()
content = path.read_bytes()
kind = _keystore_kind(content)
try:
if kind == "jks":
updated = _add_to_jks(content, certificates, certs)
else:
updated = _add_to_pkcs12(content, certificates, certs)
context.write_file(relative, updated)
logger.info(
"added %d certificate(s) to java keystore %s",
len(certificates),
relative,
)
except (jks.KeystoreException, ValueError) as exc:
logger.warning("skipping java keystore %s: %s", relative, exc)
repositories = context.config.configurations.java.repositories
if repositories:
_configure_repositories(context, repositories)