diff --git a/DESCRIPTION b/DESCRIPTION index b269d39..b2cbbc4 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Type: Package Package: audit.connect Title: Posit Connect Health Check -Version: 0.8.10 +Version: 0.8.11 Authors@R: person("Jumping", "Rivers", , "info@jumpingrivers.com", role = c("aut", "cre")) Description: Posit Connect Health Check. Deploys various content types to @@ -10,7 +10,7 @@ License: file LICENSE Depends: R (>= 4.1.0) Imports: - audit.base (>= 0.7.2), + audit.base (>= 0.7.6), cli, connectapi (>= 0.12.1), dplyr, diff --git a/NEWS.md b/NEWS.md index 108cfc1..2e11b67 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,10 @@ +# audit.connect 0.8.11 + +## Bug fixes and minor improvements + +* `check()` carries on when the system-library check can't run. An error from the endpoint, an unexpected reply or an unsupported OS gives a warning and `sys_deps = NA`, and the deployment checks still run (#63). +* The sys-deps endpoint lists installed packages on Debian, Rocky Linux and AlmaLinux, as well as Ubuntu and RHEL. Requires audit.base 0.7.6 (#63). + # audit.connect 0.8.10 ## Bug fixes and minor improvements diff --git a/R/check_sys_deps.R b/R/check_sys_deps.R index f457494..6d0d86e 100644 --- a/R/check_sys_deps.R +++ b/R/check_sys_deps.R @@ -39,13 +39,33 @@ check_sys_deps = function(debug_level = 0:2) { # Ping endpoint url = paste0(get_deploy_url(content), "installed") auth_key = paste0("Key ", get_token()) - resp = httr::GET(url, httr::add_headers(Authorization = auth_key)) - rtn = jsonlite::parse_json(rawToChar(resp$content)) - # Read by check_connect_timezone(), as the endpoint is the only code that runs on Connect - .connect$timezone = unlist(rtn$timezone) - - clean_libs = audit.base::clean_libs(rtn$os_release, rtn$libs) - audit.base::check_sys_deps(rtn$os_release, clean_libs, debug_level) + resp = httr::GET(url, httr::add_headers(Authorization = auth_key), httr::timeout(120)) + if (httr::http_error(resp)) { + cli::cli_warn( + "Can't check system libraries: the endpoint returned {httr::http_status(resp)$message}" + ) + return(NA) + } + # An unsupported OS or an unexpected reply warns, so the deployment checks still run + tryCatch( + { + rtn = jsonlite::parse_json(rawToChar(resp$content)) + # Read by check_connect_timezone(), as the endpoint is the only code that runs on Connect + .connect$timezone = unlist(rtn$timezone) + os_release = unlist(rtn$os_release) + libs = unlist(rtn$libs) + clean_libs = audit.base::clean_libs(os_release, libs) + # clean_libs() has warned that it can't read the package list + if (anyNA(clean_libs)) { + return(NA) + } + audit.base::check_sys_deps(os_release, clean_libs, debug_level) + }, + error = function(e) { + cli::cli_warn(c("Can't check system libraries", "x" = conditionMessage(e))) + NA + } + ) } setup_plumber_sys_deps_endpoint = function(app_dir, debug_level) { diff --git a/inst/extdata/check_sys_deps/manifest.json b/inst/extdata/check_sys_deps/manifest.json index 8ef2373..032a2e8 100644 --- a/inst/extdata/check_sys_deps/manifest.json +++ b/inst/extdata/check_sys_deps/manifest.json @@ -674,7 +674,7 @@ "checksum": "9fcd95b1ccc5ca6dba0ad125b62c3747" }, "plumber.R": { - "checksum": "ed571a06f3580f5202a258fb5e8d6bb5" + "checksum": "cb29cfd98c2279f36771c28f0619831e" }, "renv.lock": { "checksum": "a564e763ab0a9b503272f0c962cef9c6" diff --git a/inst/extdata/check_sys_deps/plumber.R b/inst/extdata/check_sys_deps/plumber.R index 668bf02..dfb7cb3 100644 --- a/inst/extdata/check_sys_deps/plumber.R +++ b/inst/extdata/check_sys_deps/plumber.R @@ -3,17 +3,35 @@ # # Commented out. Don't need plumber to deploy library("plumber") + +# apt or yum, from os-release ID and ID_LIKE, so Debian, Rocky and AlmaLinux are listed too. +# audit.base decides whether Posit Package Manager supports the OS. +package_tool = function(os_release) { + ids = sub("^ID(_LIKE)?=", "", os_release[grepl("^ID(_LIKE)?=", os_release)]) + ids = strsplit(tolower(gsub("\"", "", ids)), " ") + ids = unlist(ids) + if (any(ids %in% c("debian", "ubuntu"))) { + "apt" + } else if (any(ids %in% c("rhel", "centos", "fedora"))) { + "yum" + } else { + NA_character_ + } +} + #* @apiTitle Installed Sys Deps #* @apiDescription Returns the OS, installed libraries and timezone of the connect server #* @get /installed function() { os_release = readLines("/etc/os-release") - if (any(grepl("[u|U]buntu", os_release))) { + tool = package_tool(os_release) + if (identical(tool, "apt")) { libs = system2("apt", args = c("list", "--installed"), stdout = TRUE) - } else if (any(grepl("red hat|centos", os_release, ignore.case = TRUE))) { + } else if (identical(tool, "yum")) { libs = system2("yum", args = c("list", "installed"), stdout = TRUE) } else { - os_release = libs = NA + # Still send os_release, so audit.base can say which OS isn't supported + libs = NA } # Sys.timezone() warns when timedatectl fails, but can still find the timezone. diff --git a/tests/testthat/test-cleanup.R b/tests/testthat/test-cleanup.R index 8a1d0e4..9c9051d 100644 --- a/tests/testthat/test-cleanup.R +++ b/tests/testthat/test-cleanup.R @@ -72,3 +72,33 @@ test_that("cleanup_quarto() deletes by title when the deploy left no record", { expect_identical(calls$deleted, "a") expect_false(dir.exists(tmp_dir)) }) + +test_that("A bad endpoint reply or unsupported OS warns instead of stopping check()", { + testthat::local_mocked_bindings( + get_server = function(...) "https://connect.example.com/", + get_token = function(...) "key", + get_deploy_url = function(...) "https://connect.example.com/content/1/", + cleanup_plumber = function(...) NULL, + setup_plumber_sys_deps_endpoint = function(...) structure(list(), class = "Content") + ) + testthat::local_mocked_bindings(poll_task = function(x, ...) x, .package = "connectapi") + reply = function(status, body) { + structure( + list(status_code = status, content = charToRaw(body), headers = list()), + class = "response" + ) + } + replies = list( + reply(500L, "Internal Server Error"), + reply(200L, "not JSON"), + reply( + 200L, + '{"os_release":["PRETTY_NAME=\\"Arch Linux\\"","ID=arch"],"libs":[null],"timezone":[""]}' + ) + ) + for (resp in replies) { + testthat::local_mocked_bindings(GET = function(...) resp, .package = "httr") + expect_warning(sys_deps <- check_sys_deps(debug_level = 0), "system libraries") # nolint + expect_identical(sys_deps, NA) + } +}) diff --git a/tests/testthat/test-timezone.R b/tests/testthat/test-timezone.R index 39ba2a8..1260921 100644 --- a/tests/testthat/test-timezone.R +++ b/tests/testthat/test-timezone.R @@ -1,3 +1,15 @@ +# The endpoint and its helper, without library("plumber") +load_sys_deps_endpoint = function() { + plumber_file = system.file("extdata", "check_sys_deps", "plumber.R", package = "audit.connect") + exprs = parse(plumber_file) + env = new.env() + for (expr in exprs[-c(1L, length(exprs))]) { + eval(expr, env) + } + env$endpoint = eval(exprs[[length(exprs)]], env) + env +} + test_that("check_connect_timezone() reports set, unset and unknown", { on.exit(assign("timezone", NULL, envir = .connect)) .connect$timezone = "Europe/London" @@ -11,10 +23,8 @@ test_that("check_connect_timezone() reports set, unset and unknown", { test_that("The sys deps endpoint returns a timezone that survives JSON", { # Connect only runs on Linux, so the endpoint does too skip_if_not(Sys.info()[["sysname"]] == "Linux") - plumber_file = system.file("extdata", "check_sys_deps", "plumber.R", package = "audit.connect") - exprs = parse(plumber_file) - endpoint = eval(exprs[[length(exprs)]]) - rtn = endpoint() + env = load_sys_deps_endpoint() + rtn = env$endpoint() expect_type(rtn$timezone, "character") expect_length(rtn$timezone, 1) for (auto_unbox in c(TRUE, FALSE)) { @@ -22,3 +32,18 @@ test_that("The sys deps endpoint returns a timezone that survives JSON", { expect_identical(unlist(jsonlite::parse_json(json)$timezone), rtn$timezone) } }) + +test_that("The sys deps endpoint lists packages on Debian and RHEL rebuilds", { + env = load_sys_deps_endpoint() + os_releases = list( + apt = c("ID=ubuntu", "ID_LIKE=debian"), + apt = "ID=debian", + yum = c("ID=\"rocky\"", "ID_LIKE=\"rhel centos fedora\""), + yum = c("ID=\"almalinux\"", "ID_LIKE=\"rhel centos fedora\""), + yum = c("ID=\"rhel\"", "ID_LIKE=\"fedora\"") + ) + for (i in seq_along(os_releases)) { + expect_identical(env$package_tool(os_releases[[i]]), names(os_releases)[i]) + } + expect_identical(env$package_tool(c("ID=\"opensuse-leap\"", "ID_LIKE=\"suse\"")), NA_character_) +})