build-cran-binaries/local/rebuild-missing.R
pat-s 5c4cca6cce fix: verify the metadata database certificate with sslmode=verify-full (#235)
Part of https://codefloe.com/rpkgs/bincraft/issues/225. Stacked on #234.

Every connection to the metadata database used `sslmode = 'require'`, which encrypts but does not authenticate the server, so anyone able to impersonate `r-binaries.devxy.io:15432` would receive `PGPASS`.

## Changes

- `.crow/build-all-versions.yaml`, `process-updates.yaml`, `prune-stale-flat-binaries.yaml`, `reindex.yaml`, `weekly-rebuild-reindex.yaml`: `metadata_db_sslmode = 'verify-full'` in every bincraft call.
- `local/*.R`: the same for bincraft calls; scripts that call `DBI::dbConnect()` directly also pass `sslrootcert = bincraft::postgres_sslrootcert("verify-full")`, because libpq looks for `~/.postgresql/root.crt` rather than the system trust store.
- `README.md`: note that the endpoint serves a Let's Encrypt certificate and how to verify it.
- air reformatted three `=` assignments to `<-` in `local/missing-cran-packages-db.R` and `local/packages-without-any-binary.R` while the hook ran on those files.

## Ordering: merge last

1. devxy/k8s-talos#417 must be live: today the server presents an in-cluster certificate from a private CA, which `verify-full` rejects.
2. https://codefloe.com/rpkgs/bincraft/pulls/226 must be released: `local/install-bincraft.R` installs the latest tag, and older releases neither pass a root certificate nor export `postgres_sslrootcert()`.
3. #234 must be merged.

Merging early fails every database connection in CI, and since #234 per-minor reindex failures fail the pipeline.

Reviewed-on: #235
2026-10-17 07:45:46 +00:00

194 lines
5.6 KiB
R

### Rebuild one shard of a slot's missing-binary list.
#
# Usage: Rscript local/rebuild-missing.R <split_into> <split_index> [budget_hours]
#
# The list itself comes from local/fetch-rebuild-packages-from-issue.R, which
# writes $REBUILD_PKG_LIST (default /tmp/rebuild_pkgs.txt).
#
# Two properties matter here and are the reason this is a script rather than an
# `R -q -e` argument in the pipeline:
#
# * it is restartable. The outstanding set is re-derived from the bucket on
# every start, so a shard that died resumes where it stopped without any
# progress file, and without replaying thousands of per-package HEADs.
# * it terminates. A wall-clock budget stops the loop cleanly instead of the
# run having to be killed, which is what previously skipped the re-index and
# CDN purge and left rebuilt binaries hidden behind stale edge copies.
options(error = function() {
cat("ERROR:", geterrmessage(), "\n", file = stdout())
traceback(2)
q(status = 1)
})
library(bincraft, quietly = TRUE)
source(file.path("local", "rebuild-missing-helpers.R"))
args <- parse_rebuild_args(commandArgs(trailingOnly = TRUE))
if (is.na(args$split_into) || is.na(args$split_index)) {
stop("usage: rebuild-missing.R <split_into> <split_index> [budget_hours]")
}
list_file <- Sys.getenv("REBUILD_PKG_LIST", "/tmp/rebuild_pkgs.txt")
pkgs <- if (file.exists(list_file)) readLines(list_file) else character(0)
pkgs <- pkgs[nzchar(pkgs)]
if (length(pkgs) == 0L) {
cat("Nothing to rebuild\n")
q("no")
}
excluded <- jsonlite::fromJSON("local/excluded-packages.json")[["package"]]
pkgs <- setdiff(pkgs, excluded)
mine <- shard_slice(pkgs, args$split_into, args$split_index)
cat(sprintf(
"Shard %s/%s: %s of %s listed packages\n",
args$split_index,
args$split_into,
length(mine),
length(pkgs)
))
### Resume: ask the bucket what is still outstanding
codename <- bincraft::set_codename(NULL)
local_machine <- Sys.info()[["machine"]]
arch <- if (grepl("arm64|aarch64", local_machine)) "arm64" else "amd64"
slot_dir <- sprintf(
"devxy-rpkgs-binaries/%s/%s/latest/src/contrib",
arch,
codename
)
s3fs::s3_file_system(
aws_access_key_id = Sys.getenv("B2_S3_ACCESS_KEY"),
aws_secret_access_key = Sys.getenv("B2_S3_SECRET_KEY"),
endpoint = "https://s3.eu-central-003.backblazeb2.com",
region_name = "eu-central-003",
refresh = TRUE
)
# One paginated listing instead of a HEAD per package. Not recursed: the
# rebuild passes no `is_r_minor_sensitive`, so it only ever targets the flat
# path, and the resume filter matches that scope deliberately.
info <- tryCatch(s3fs::s3_dir_info(slot_dir), error = function(e) NULL)
etag_by_file <- if (is.null(info) || nrow(info) == 0L) {
cat(sprintf(
"WARNING: could not list %s; building the whole shard\n",
slot_dir
))
stats::setNames(character(), character())
} else {
stats::setNames(
gsub('^"|"$', "", as.character(info$etag)),
basename(as.character(info$uri))
)
}
cran <- tryCatch(
{
con <- gzcon(url(
"https://cloud.r-project.org/src/contrib/PACKAGES.gz",
open = "rb"
))
on.exit(close(con), add = TRUE)
read.dcf(con, fields = c("Package", "Version", "MD5sum"))
},
error = function(e) {
cat(sprintf(
"WARNING: could not read CRAN's index (%s)\n",
conditionMessage(e)
))
NULL
}
)
cran_version <- stats::setNames(character(), character())
cran_md5 <- stats::setNames(character(), character())
if (!is.null(cran)) {
cran_version <- stats::setNames(
as.character(cran[, "Version"]),
as.character(cran[, "Package"])
)
keep <- !is.na(cran[, "MD5sum"])
cran_md5 <- stats::setNames(
as.character(cran[keep, "MD5sum"]),
paste(cran[keep, "Package"], cran[keep, "Version"], sep = "_")
)
}
before <- length(mine)
mine <- outstanding_packages(mine, etag_by_file, cran_version, cran_md5)
cat(sprintf(
"Resume: %s of %s already carry a binary; %s outstanding\n",
before - length(mine),
before,
length(mine)
))
if (length(mine) == 0L) {
cat("Nothing outstanding for this shard\n")
q("no")
}
### Build
options(
crayon.enabled = TRUE,
Ncpus = as.integer(Sys.getenv("NCPUS", "2")),
future.globals.onReference = NULL
)
started <- Sys.time()
n <- length(mine)
completed <- 0L
for (i in seq_along(mine)) {
elapsed <- as.numeric(difftime(Sys.time(), started, units = "hours"))
if (elapsed > args$budget_hours) {
cat(sprintf(
"Budget of %sh reached after %d/%d packages; stopping cleanly. The next run resumes from the bucket.\n",
args$budget_hours,
completed,
n
))
break
}
x <- mine[i]
cat(sprintf("[%d/%d] %s\n", i, n, x))
tryCatch(
bincraft::build_binary_package(
x,
tag_limit = 1L,
patches = "local/patches",
s3_endpoint = "https://s3.eu-central-003.backblazeb2.com",
s3_region = "eu-central-003",
s3_bucket = "devxy-rpkgs-binaries",
s3_access_key_id = Sys.getenv("B2_S3_ACCESS_KEY"),
s3_secret_access_key = Sys.getenv("B2_S3_SECRET_KEY"),
metadata_db_host = "r-binaries.devxy.io",
metadata_db_name = "build_metadata",
metadata_db_table = "single_builds",
metadata_db_user = "rpkgs",
metadata_db_password = Sys.getenv("PGPASS"),
metadata_db_sslmode = "verify-full",
metadata_db_port = 15432,
archive = TRUE,
upload = TRUE,
store_build_metadata = TRUE
),
error = function(e) {
cat(sprintf("ERROR building %s - %s\n", x, conditionMessage(e)))
}
)
completed <- completed + 1L
}
cat(sprintf(
"Shard %s/%s finished: %d/%d packages processed in %.1fh\n",
args$split_index,
args$split_into,
completed,
n,
as.numeric(difftime(Sys.time(), started, units = "hours"))
))