diff --git a/.github/local/test-connect-ci.yml b/.github/local/test-connect-ci.yml index 39ce7318..359e6789 100644 --- a/.github/local/test-connect-ci.yml +++ b/.github/local/test-connect-ci.yml @@ -3,7 +3,7 @@ services: connect1: hostname: connect1 - image: rstudio/rstudio-connect:${RSC_VERSION} + image: rstudio/rstudio-connect:${CONNECT_VERSION} ports: - 3939 privileged: true @@ -11,7 +11,7 @@ services: - RSC_LICENSE connect2: hostname: connect2 - image: rstudio/rstudio-connect:${RSC_VERSION} + image: rstudio/rstudio-connect:${CONNECT_VERSION} ports: - 3939 privileged: true diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml index 11d39232..23e740ad 100644 --- a/.github/workflows/integration-tests.yaml +++ b/.github/workflows/integration-tests.yaml @@ -11,8 +11,20 @@ name: integration-tests jobs: integration-tests: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + version: + - "2024.03.0" # jammy + - "2023.09.0" # jammy + - "2023.03.0" # bionic + - "2022.09.0" # bionic + - "2022.03.2" + - "2021.09.0" + - "1.8.8.2" + env: - RSC_VERSION: 2022.09.0 + CONNECT_VERSION: ${{ matrix.version }} GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} RSC_LICENSE: ${{ secrets.RSC_LICENSE }} CONNECT_LICENSE_FILE: ${{ secrets.CONNECT_LICENSE_FILE }} @@ -58,5 +70,5 @@ jobs: if: failure() uses: actions/upload-artifact@main with: - name: ${{ runner.os }}-r${{ matrix.config.r }}-results + name: ${{ runner.os }}-${{ matrix.version }}-results path: check diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index bbed78cd..10ffbe23 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} - RSC_VERSION: 2022.09.0 + CONNECT_VERSION: 2024.03.0 RSC_LICENSE: ${{ secrets.RSC_LICENSE }} CONNECT_LICENSE_FILE: ${{ secrets.CONNECT_LICENSE_FILE }} diff --git a/.github/workflows/pr-commands.yaml b/.github/workflows/pr-commands.yaml index b9ea1e71..2d3f44aa 100644 --- a/.github/workflows/pr-commands.yaml +++ b/.github/workflows/pr-commands.yaml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} - RSC_VERSION: 2022.09.0 + CONNECT_VERSION: 2024.03.0 RSC_LICENSE: ${{ secrets.RSC_LICENSE }} CONNECT_LICENSE_FILE: ${{ secrets.CONNECT_LICENSE_FILE }} CONNECTAPI_INTEGRATED: true diff --git a/.github/workflows/test-coverage.yaml b/.github/workflows/test-coverage.yaml index db7f8d89..d2b0a6b0 100644 --- a/.github/workflows/test-coverage.yaml +++ b/.github/workflows/test-coverage.yaml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} - RSC_VERSION: 2022.09.0 + CONNECT_VERSION: 2024.03.0 RSC_LICENSE: ${{ secrets.RSC_LICENSE }} CONNECT_LICENSE_FILE: ${{ secrets.CONNECT_LICENSE_FILE }} CONNECTAPI_INTEGRATED: true diff --git a/Makefile b/Makefile index 85c35159..6df74bda 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ PWD := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) PROJECT=connectapi NETWORK=${PROJECT}_default -RSC_VERSION=2022.09.0 +CONNECT_VERSION=2024.03.0 #--------------------------------------------- # Network @@ -30,10 +30,10 @@ network-down: # Update versions #--------------------------------------------- update-versions: - @sed -i '' "s/RSC_VERSION\:.*/RSC_VERSION: ${RSC_VERSION}/g" .github/workflows/pkgdown.yaml - @sed -i '' "s/RSC_VERSION\:.*/RSC_VERSION: ${RSC_VERSION}/g" .github/workflows/test-coverage.yaml - @sed -i '' "s/RSC_VERSION\:.*/RSC_VERSION: ${RSC_VERSION}/g" .github/workflows/integration-tests.yaml - @sed -i '' "s/^current_connect_version <- .*/current_connect_version <- '${RSC_VERSION}'/g" R/connectapi.R + @sed -i '' "s/CONNECT_VERSION\:.*/CONNECT_VERSION: ${CONNECT_VERSION}/g" .github/workflows/pkgdown.yaml + @sed -i '' "s/CONNECT_VERSION\:.*/CONNECT_VERSION: ${CONNECT_VERSION}/g" .github/workflows/test-coverage.yaml + @sed -i '' "s/CONNECT_VERSION\:.*/CONNECT_VERSION: ${CONNECT_VERSION}/g" .github/workflows/pr-commands.yaml + @sed -i '' "s/^current_connect_version <- .*/current_connect_version <- '${CONNECT_VERSION}'/g" R/connectapi.R #--------------------------------------------- # Helpers @@ -49,7 +49,7 @@ mail-down: connect-up: NETWORK=${NETWORK} \ RSC_LICENSE=$(RSC_LICENSE) \ - RSC_VERSION=$(RSC_VERSION) \ + CONNECT_VERSION=$(CONNECT_VERSION) \ docker compose -f inst/ci/test-connect.yml -f .github/local/make-network.yml up -d connect-down: @@ -59,7 +59,7 @@ connect-down: connect-file-up: NETWORK=${NETWORK} \ RSC_LICENSE=$(RSC_LICENSE) \ - RSC_VERSION=$(RSC_VERSION) \ + CONNECT_VERSION=$(CONNECT_VERSION) \ docker compose -f inst/ci/test-connect-lic.yml -f .github/local/make-network.yml up -d connect-file-down: @@ -69,7 +69,7 @@ connect-file-down: test-env-up: NETWORK=${NETWORK} \ RSC_LICENSE=$(RSC_LICENSE) \ - RSC_VERSION=$(RSC_VERSION) \ + CONNECT_VERSION=$(CONNECT_VERSION) \ docker compose -f .github/local/test-connect-ci.yml -f .github/local/make-network.yml up -d test-env-down: diff --git a/NEWS.md b/NEWS.md index e849acb3..9556a6ef 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # connectapi (development version) +- The package is now tested against many versions of Connect, back to 1.8.8.2 (May 2021). + There are now fewer warnings about version mismatches: you should only see a warning if your Connect server is older than that. (#244) - Now correctly provides methods for `tbl_connect`, rather than `tbl_lazy`, preventing problems when also using dplyr (#177). - BREAKING: The functions `Connect$download_bundle()` and @@ -126,7 +128,7 @@ # connectapi 0.1.0.9017 -BREAKING: +BREAKING: * Switch from `RSTUDIO_CONNECT_*` variables to `CONNECT_*` variables * Rename a handful of functions: - `connect$activate_bundle` to `connect$content_deploy` @@ -144,7 +146,7 @@ BREAKING: - `cache_apps` - `tag_page` -OTHER: +OTHER: * Add some endpoints: - `content` - `audit_logs` diff --git a/R/acl.R b/R/acl.R index 96228d0f..7a8d12c4 100644 --- a/R/acl.R +++ b/R/acl.R @@ -35,7 +35,7 @@ get_acl_user <- function(content) { content_info <- content$get_content_remote() prep <- get_acl_user_impl(content) - out <- parse_connectapi_typed(prep, !!!connectapi_ptypes$acl_user) + out <- parse_connectapi_typed(prep, connectapi_ptypes$acl_user) out$content_guid <- content_info$guid out$content_access_type <- content_info$access_type @@ -50,7 +50,7 @@ get_acl_group <- function(content) { content_info <- content$get_content_remote() prep <- get_acl_group_impl(content) - out <- parse_connectapi_typed(prep, !!!connectapi_ptypes$acl_group) + out <- parse_connectapi_typed(prep, connectapi_ptypes$acl_group) if (nrow(out) > 0) { out$content_guid <- content_info$guid out$content_access_type <- content_info$access_type diff --git a/R/connect.R b/R/connect.R index bf7876a7..f9f09a76 100644 --- a/R/connect.R +++ b/R/connect.R @@ -1004,11 +1004,7 @@ connect <- function( tryCatch( { check_connect_license(con) - - # check Connect is accessible - srv <- safe_server_settings(con) - - check_connect_version(using_version = srv$version) + check_connect_version(using_version = safe_server_version(con)) }, error = function(err) { if (.check_is_fatal) { diff --git a/R/connectapi.R b/R/connectapi.R index 30ef12a4..a3d783ef 100644 --- a/R/connectapi.R +++ b/R/connectapi.R @@ -18,4 +18,4 @@ utils::globalVariables( ) ) -current_connect_version <- "2022.09.0" +current_connect_version <- "2024.03.0" diff --git a/R/content.R b/R/content.R index b1b0d743..ffa3cc38 100644 --- a/R/content.R +++ b/R/content.R @@ -585,7 +585,7 @@ get_jobs <- function(content) { validate_R6_class(content, "Content") jobs <- content$jobs() - parse_connectapi_typed(jobs, !!!connectapi_ptypes$jobs) + parse_connectapi_typed(jobs, connectapi_ptypes$jobs) } # TODO: Need to test `logged_error` on a real error @@ -602,7 +602,7 @@ get_job <- function(content, key) { job$stderr <- strsplit(job$stderr, "\n")[[1]] # a bit of an abuse # since stdout / stderr / logged_error are here now... - parse_connectapi_typed(list(job), !!!connectapi_ptypes$job) + parse_connectapi_typed(list(job), connectapi_ptypes$job) } #' Set RunAs User @@ -779,7 +779,7 @@ get_bundles <- function(content, limit = Inf) { validate_R6_class(content, "Content") bundles <- content$get_bundles() - parse_connectapi_typed(bundles, !!!connectapi_ptypes$bundles) + parse_connectapi_typed(bundles, connectapi_ptypes$bundles) } #' @rdname get_bundles @@ -951,5 +951,5 @@ get_group_permission <- function(content, guid) { get_content_permissions <- function(content, add_owner = TRUE) { validate_R6_class(content, "Content") res <- content$permissions(add_owner = add_owner) - parse_connectapi_typed(res, !!!connectapi_ptypes$permissions) + parse_connectapi_typed(res, connectapi_ptypes$permissions) } diff --git a/R/get.R b/R/get.R index 8380aaac..478754fd 100644 --- a/R/get.R +++ b/R/get.R @@ -48,7 +48,7 @@ get_users <- function(src, page_size = 20, prefix = NULL, limit = 25) { limit = limit ) - out <- parse_connectapi_typed(res, !!!connectapi_ptypes$users) + out <- parse_connectapi_typed(res, connectapi_ptypes$users) return(out) } @@ -88,7 +88,7 @@ get_groups <- function(src, page_size = 20, prefix = NULL, limit = 25) { res <- page_offset(src, src$groups(page_size = page_size, prefix = prefix), limit = limit) - out <- parse_connectapi_typed(res, !!!connectapi_ptypes$groups) + out <- parse_connectapi_typed(res, connectapi_ptypes$groups) return(out) } @@ -280,7 +280,7 @@ get_content <- function(src, guid = NULL, owner_guid = NULL, name = NULL, ..., . res <- res %>% purrr::keep(.p = .p) } - out <- parse_connectapi_typed(res, !!!connectapi_ptypes$content) + out <- parse_connectapi_typed(res, connectapi_ptypes$content) return(out) } @@ -359,7 +359,7 @@ content_list_by_tag <- function(src, tag) { res <- src$GET(glue::glue("v1/tags/{tag_id}/content")) - out <- parse_connectapi_typed(res, !!!connectapi_ptypes$content) + out <- parse_connectapi_typed(res, connectapi_ptypes$content) return(out) } @@ -516,7 +516,7 @@ get_content_old <- function(src, filter = NULL, limit = 25, page_size = 25) { page_size = page_size ) - out <- parse_connectapi_typed(res, !!!connectapi_ptypes$content_old) + out <- parse_connectapi_typed(res, connectapi_ptypes$content_old) return(out) } @@ -602,7 +602,7 @@ get_usage_shiny <- function(src, content_guid = NULL, res <- page_cursor(src, res, limit = limit) - out <- parse_connectapi_typed(res, !!!connectapi_ptypes$usage_shiny) + out <- parse_connectapi_typed(res, connectapi_ptypes$usage_shiny) return(out) } @@ -695,7 +695,7 @@ get_usage_static <- function(src, content_guid = NULL, res <- page_cursor(src, res, limit = limit) - out <- parse_connectapi_typed(res, !!!connectapi_ptypes$usage_static) + out <- parse_connectapi_typed(res, connectapi_ptypes$usage_static) return(out) } @@ -754,7 +754,7 @@ get_audit_logs <- function(src, limit = 20L, previous = NULL, res <- page_cursor(src, res, limit = limit) - out <- parse_connectapi_typed(res, !!!connectapi_ptypes$audit_logs) + out <- parse_connectapi_typed(res, connectapi_ptypes$audit_logs) return(out) } @@ -796,7 +796,7 @@ get_procs <- function(src) { c(list(pid = y), x) } ) - tbl_data <- parse_connectapi_typed(proc_prep, !!!connectapi_ptypes$procs) + tbl_data <- parse_connectapi_typed(proc_prep, connectapi_ptypes$procs) return(tbl_data) } diff --git a/R/lazy.R b/R/lazy.R index 960abf8e..d56ea425 100644 --- a/R/lazy.R +++ b/R/lazy.R @@ -92,7 +92,7 @@ api_build.op_base_connect <- function(op, con, ..., n) { } else { stop(glue::glue("'{op$x}' is not recognized")) } - parse_connectapi_typed(res, !!!op$ptype) + parse_connectapi_typed(res, op$ptype) } cat_line <- function(...) { diff --git a/R/parse.R b/R/parse.R index ea73cc52..83d35932 100644 --- a/R/parse.R +++ b/R/parse.R @@ -29,11 +29,13 @@ swap_timestamp_format <- function(.col) { } } -ensure_columns <- function(.data, ...) { - defaults <- rlang::list2(...) - names <- names(defaults) - for (i in seq_along(defaults)) { - .data <- ensure_column(.data, defaults[[i]], names[[i]]) +ensure_columns <- function(.data, ptype) { + # Given a prototype, ensure that all columns are present and cast to the correct type. + # If a column is missing in .data, it will be created with all missing values of the correct type. + # If a column is present in both, it will be cast to the correct type. + # If a column is present in .data but not in ptype, it will be left as is. + for (i in names(ptype)) { + .data <- ensure_column(.data, ptype[[i]], i) } .data } @@ -66,8 +68,8 @@ ensure_column <- function(data, default, name) { data } -parse_connectapi_typed <- function(data, ...) { - ensure_columns(parse_connectapi(data), ...) +parse_connectapi_typed <- function(data, ptype) { + ensure_columns(parse_connectapi(data), ptype) } parse_connectapi <- function(data) { diff --git a/R/utils-ci.R b/R/utils-ci.R index e0fd7a45..ff1e8014 100644 --- a/R/utils-ci.R +++ b/R/utils-ci.R @@ -25,7 +25,27 @@ determine_license_env <- function(license) { } } -compose_start <- function(connect_license = Sys.getenv("RSC_LICENSE"), rsc_version, clean = TRUE) { +version_to_docker_tag <- function(version) { + # Prior to 2022.09.0, the plain version number was the tag + # After, it's "-" + # If you want a specific image tag, just pass it in and it will go through unchanged + try( + { + numver <- numeric_version(version) + if (numver > "2023.06") { + version <- paste0("jammy-", version) + } else if (numver >= "2022.09") { + # There's both jammy and bionic for these, but the bionic ones + # seem more reliable in CI + version <- paste0("bionic-", version) + } + }, + silent = TRUE + ) + version +} + +compose_start <- function(connect_license = Sys.getenv("RSC_LICENSE"), connect_version, clean = TRUE) { warn_dire("compose_start") scoped_dire_silence() @@ -39,7 +59,7 @@ compose_start <- function(connect_license = Sys.getenv("RSC_LICENSE"), rsc_versi compose_file_path <- system.file(compose_file, package = "connectapi") env_vars <- c( - RSC_VERSION = rsc_version, + CONNECT_VERSION = version_to_docker_tag(connect_version), PATH = Sys.getenv("PATH"), license_details$env_params ) @@ -106,11 +126,12 @@ update_renviron_creds <- function(server, api_key, prefix, .file = ".Renviron") build_test_env <- function(connect_license = Sys.getenv("RSC_LICENSE"), clean = TRUE, username = "admin", - password = "admin0", rsc_version = current_connect_version) { + password = "admin0", + connect_version = Sys.getenv("CONNECT_VERSION", current_connect_version)) { warn_dire("build_test_env") scoped_dire_silence() - compose_start(connect_license = connect_license, clean = clean, rsc_version = rsc_version) + compose_start(connect_license = connect_license, clean = clean, connect_version = connect_version) # It was ci_connect before but it's ci-connect on my machine now; # this is a regex so it will match either diff --git a/R/utils.R b/R/utils.R index d4f7a0ce..dd278765 100644 --- a/R/utils.R +++ b/R/utils.R @@ -141,10 +141,6 @@ warn_once <- function(msg, id = msg) { } warn_env <- new.env(parent = emptyenv()) -tested_connect_version <- function() { - current_connect_version -} - check_connect_license <- function(url) { if (is_R6_class(url, "Connect")) { res <- url$GET_RESULT_URL(url$server) @@ -177,48 +173,42 @@ safe_server_settings <- function(client) { } safe_server_version <- function(client) { - srv <- safe_server_settings(client) - return(srv$version) + version <- safe_server_settings(client)$version + if (is.null(version) || nchar(version) == 0) { + message("Version information is not exposed by this Posit Connect instance.") + # Return 0 so this will always show up as "too old" + version <- "0" + } + version } -# TODO: switch compare_connect_version ordering... error_if_less_than <- function(client, tested_version) { - comp <- compare_connect_version(using_version = safe_server_version(client), tested_version = tested_version) - if (comp > 0) { - stop(glue::glue("ERROR: This API requires Posit Connect version {tested_version}, but you are using {srv$version}. Please use a previous version of the `connectapi` package, upgrade Posit Connect, or review the API documentation corresponding to your version.")) + server_version <- safe_server_version(client) + comp <- compare_connect_version(server_version, tested_version) + if (comp < 0) { + msg <- paste0( + "ERROR: This API requires Posit Connect version ", tested_version, + " but you are using", server_version, ". Please use a previous version", + " of the `connectapi` package, upgrade Posit Connect, or review the API ", + "documentation corresponding to your version." + ) + stop(msg) } invisible() } compare_connect_version <- function(using_version, tested_version) { - if (is.null(using_version)) { - message("Version information is not exposed by this Posit Connect instance.") - return(0) - } else if (nchar(using_version) == 0) { - message("Version information is not exposed by this Posit Connect instance.") - return(0) - } else { - minor_using_version <- simplify_version(using_version) - minor_tested_version <- simplify_version(tested_version) - return(compareVersion(minor_tested_version, minor_using_version)) - } + compareVersion(simplify_version(using_version), simplify_version(tested_version)) } -check_connect_version <- function(using_version, tested_version = tested_connect_version()) { - comp <- compare_connect_version(using_version = using_version, tested_version = tested_version) - - msg <- switch(as.character(comp), - "0" = NULL, - "1" = warn_once(glue::glue( +check_connect_version <- function(using_version, minimum_tested_version = "1.8.8.2") { + comp <- compare_connect_version(using_version, minimum_tested_version) + if (comp < 0) { + warn_once(glue::glue( "You are using an older version of Posit Connect ", - "({using_version}) than was tested ({tested_version}). ", + "({using_version}) than is tested ({minimum_tested_version}). ", "Some APIs may not function as expected." - ), id = "old-connect"), - "-1" = warn_once(glue::glue( - "You are using a newer version of Posit Connect ", - "({using_version}) than was tested ({tested_version}). ", - "Most APIs should function as expected." - ), id = "new-connect") - ) + ), id = "old-connect") + } invisible() } diff --git a/R/variant.R b/R/variant.R index 071d114b..30dd4a2a 100644 --- a/R/variant.R +++ b/R/variant.R @@ -261,7 +261,7 @@ get_variants <- function(content) { variants <- content$variants() - parse_connectapi_typed(variants, !!!connectapi_ptypes$variant) + parse_connectapi_typed(variants, connectapi_ptypes$variant) } #' @rdname variant @@ -307,7 +307,7 @@ get_variant_renderings <- function(variant) { validate_R6_class(variant, "Variant") renders <- variant$renderings() - parse_connectapi_typed(renders, !!!connectapi_ptypes$rendering) + parse_connectapi_typed(renders, connectapi_ptypes$rendering) } #' @rdname render diff --git a/README.Rmd b/README.Rmd index 05c82450..3c8afc3a 100644 --- a/README.Rmd +++ b/README.Rmd @@ -23,29 +23,9 @@ knitr::opts_chunk$set( # connectapi This package provides an R client for the [Posit Connect Server -API](https://docs.posit.co/connect/api/) as well as helpful functions that -utilize the client. The package is based on the `rsconnect` -[package](https://rstudio.github.io/rsconnect/), but is publicly exported to be -easier to use, is extensible via an R6 class, and is separated from the -`rsconnect` package for easier support and maintenance. - -## Disclaimer - -Because many of these functions are experimental, it is advisable to be cautious -about (1) upgrading the package, (2) upgrading Posit Connect when you care -about the reproducibility of workflows that use `connectapi`. As a result, we -would advise: - -- managing package versions with [`renv`](https://rstudio.github.io/renv/) -- test your dependent content before and after upgrading Posit Connect - -Please pay careful attention to the lifecycle badges of the various functions -and warnings present when you are using experimental features. - -**Also, [please share -feedback!!](https://community.rstudio.com/c/r-admin/posit-connect/27) We love -hearing how the Posit Connect Server API is helpful and what additional -endpoints would be useful!!** +API](https://docs.posit.co/connect/api/), as well as helpful functions that +utilize the client. +It is designed to work with all supported versions of Connect, though some features may only be available to newer versions. ## Installation @@ -183,12 +163,10 @@ This is likely due to either (1) `Connect$server` or `Connect$api_key` being defined improperly or (2) you do not have access to the Posit Connect cluster to do the operation in question -**Constant warning about version numbers** +**Warning about version numbers** -This warning is intentionally chatty. While version number mismatches between -Posit Connect and `connectapi` can be benign, we want you to be clear that -`connectapi` is tightly coupled to a version of Posit Connect (because Posit -Connect's APIs change over time). +We test the package against a range of versions of Connect, as old as 1.8.8.2 (May 2021). +If your Connect server is older than that, the package may still work, but `connectapi` will warn you. We strive to: @@ -221,7 +199,7 @@ Community](https://community.rstudio.com/c/r-admin/posit-connect/27) **Other ideas for FAQs or Common Issues?** -Please submit a PR! We would love to have your contribution! +Please open an issue or PR! We would love to have your contribution! ## Code of Conduct diff --git a/README.md b/README.md index 6d8c6e66..1e574c51 100644 --- a/README.md +++ b/README.md @@ -15,31 +15,10 @@ status](https://github.com/rstudio/connectapi/workflows/R-CMD-check/badge.svg)]( # connectapi This package provides an R client for the [Posit Connect Server -API](https://docs.posit.co/connect/api/) as well as helpful functions -that utilize the client. The package is based on the `rsconnnect` -[package](https://rstudio.github.io/rsconnect/), but is publicly -exported to be easier to use, is extensible via an R6 class, and is -separated from the `rsconnect` package for easier support and -maintenance. - -## Disclaimer - -Because many of these functions are experimental, it is advisable to be -cautious about (1) upgrading the package, (2) upgrading Posit Connect -when you care about the reproducibility of workflows that use -`connectapi`. As a result, we would advise: - -- managing package versions with - [`renv`](https://rstudio.github.io/renv/) -- test your dependent content before and after upgrading Posit Connect - -Please pay careful attention to the lifecycle badges of the various -functions and warnings present when you are using experimental features. - -**Also, [please share -feedback!!](https://community.rstudio.com/c/r-admin/posit-connect/27) We -love hearing how the Posit Connect Server API is helpful and what -additional endpoints would be useful!!** +API](https://docs.posit.co/connect/api/), as well as helpful functions +that utilize the client. It is designed to work with all supported +versions of Connect, though some features may only be available to newer +versions. ## Installation @@ -62,8 +41,8 @@ To create a client: ``` r library(connectapi) client <- connect( - server = 'https://connect.example.com', - api_key = '' + server = "https://connect.example.com", + api_key = "" ) ``` @@ -179,12 +158,11 @@ This is likely due to either (1) `Connect$server` or `Connect$api_key` being defined improperly or (2) you do not have access to the Posit Connect cluster to do the operation in question -**Constant warning about version numbers** +**Warning about version numbers** -This warning is intentionally chatty. While version number mismatches -between Posit Connect and `connectapi` can be benign, we want you to be -clear that `connectapi` is tightly coupled to a version of Posit Connect -(because Posit Connect's APIs changes over time). +We test the package against a range of versions of Connect, as old as +1.8.8.2 (May 2021). If your Connect server is older than that, the +package may still work, but `connectapi` will warn you. We strive to: @@ -224,7 +202,7 @@ Community](https://community.rstudio.com/c/r-admin/posit-connect/27) **Other ideas for FAQs or Common Issues?** -Please submit a PR! We would love to have your contribution! +Please open an issue or PR! We would love to have your contribution! ## Code of Conduct diff --git a/inst/ci/test-connect-lic.yml b/inst/ci/test-connect-lic.yml index d304b11a..2ae59c8e 100644 --- a/inst/ci/test-connect-lic.yml +++ b/inst/ci/test-connect-lic.yml @@ -3,7 +3,7 @@ services: connect: hostname: connect - image: ghcr.io/rstudio/rstudio-connect:bionic-${RSC_VERSION} + image: ghcr.io/rstudio/rstudio-connect:${CONNECT_VERSION} scale: 2 ports: - 3939 diff --git a/inst/ci/test-connect.yml b/inst/ci/test-connect.yml index e38c3a07..cddecdde 100644 --- a/inst/ci/test-connect.yml +++ b/inst/ci/test-connect.yml @@ -3,7 +3,7 @@ services: connect: hostname: connect - image: ghcr.io/rstudio/rstudio-connect:bionic-${RSC_VERSION} + image: ghcr.io/rstudio/rstudio-connect:${CONNECT_VERSION} scale: 2 ports: - 3939 diff --git a/tests/integrated/helper.R b/tests/integrated/helper.R new file mode 100644 index 00000000..a00d6a38 --- /dev/null +++ b/tests/integrated/helper.R @@ -0,0 +1,15 @@ +expect_ptype_equal <- function(actual, expected, exact = TRUE) { + if (!exact) { + # Keep only the columns from each that are in the other + shared_names <- intersect(names(actual), names(expected)) + actual <- actual[, shared_names] + expected <- expected[, shared_names] + } + expect_equal(vctrs::vec_ptype(actual), vctrs::vec_ptype(expected)) +} + +skip_if_connect_older_than <- function(client, version) { + if (numeric_version(safe_server_version(client)) < numeric_version(version)) { + skip(paste("Requires Connect >=", version)) + } +} diff --git a/tests/integrated/test-deploy.R b/tests/integrated/test-deploy.R index 480f177e..bd0cecbd 100644 --- a/tests/integrated/test-deploy.R +++ b/tests/integrated/test-deploy.R @@ -432,9 +432,20 @@ test_that("deployment timestamps respect timezone", { # will fail without the png package invisible(tryCatch(test_conn_1$GET_URL(myc$get_url()), error = function(e) {})) - allusg <- get_usage_static(test_conn_1, content_guid = myc_guid) + all_usage <- get_usage_static(test_conn_1, content_guid = myc_guid) + for (i in 1:5) { + if (nrow(all_usage) == 0) { + # We may need to wait a beat for the metrics to show up. + # Retry a few times just in case. + # This did not show up testing against Connect versions <= 2022.09.0, + # but 2023.03.0 and newer seemed to hit this + Sys.sleep(1) + all_usage <- get_usage_static(test_conn_1, content_guid = myc_guid) + } + } + expect_equal(nrow(all_usage), 1) # we just did this, so it should be less than 1 minute ago... # (really protecting against being off by hours b/c of timezone differences) - expect_true(any((Sys.time() - allusg$time) < lubridate::make_difftime(60, "seconds"))) + expect_lt(Sys.time() - all_usage$time, lubridate::make_difftime(60, "seconds")) }) diff --git a/tests/integrated/test-get.R b/tests/integrated/test-get.R index bd34bacc..bc451d32 100644 --- a/tests/integrated/test-get.R +++ b/tests/integrated/test-get.R @@ -23,7 +23,7 @@ test_that("get_groups works", { groups_list <- get_groups(test_conn_1) expect_is(groups_list, c("tbl_df", "tbl", "data.frame")) - expect_equal(vctrs::vec_ptype(groups_list), vctrs::vec_ptype(connectapi_ptypes$groups)) + expect_ptype_equal(groups_list, connectapi_ptypes$groups) }) test_that("get_content works", { @@ -31,30 +31,33 @@ test_that("get_content works", { content_list <- get_content(test_conn_1) expect_is(content_list, c("tbl_df", "tbl", "data.frame")) - # https://github.com/r-lib/testthat/issues/985 - skip("currently segfaults") - expect_equal(vctrs::vec_ptype(content_list), vctrs::vec_ptype(connectapi_ptypes$content)) + # various attributes have been added over the years, so exact match + # doesn't work against all versions of Connect + expect_ptype_equal(content_list, connectapi_ptypes$content, exact = FALSE) }) test_that("get_usage_shiny works", { shiny_usage <- get_usage_shiny(test_conn_1) expect_is(shiny_usage, c("tbl_df", "tbl", "data.frame")) - expect_equal(vctrs::vec_ptype(shiny_usage), vctrs::vec_ptype(connectapi_ptypes$usage_shiny)) + expect_ptype_equal(shiny_usage, connectapi_ptypes$usage_shiny) }) test_that("get_usage_static works", { content_visits <- get_usage_static(test_conn_1) expect_is(content_visits, c("tbl_df", "tbl", "data.frame")) - expect_equal(vctrs::vec_ptype(content_visits), vctrs::vec_ptype(connectapi_ptypes$usage_static)) + # path was added to usage_static in 2024 + expect_ptype_equal(content_visits, connectapi_ptypes$usage_static, exact = FALSE) }) test_that("get_audit_logs works", { audit_list <- get_audit_logs(test_conn_1) expect_is(audit_list, c("tbl_df", "tbl", "data.frame")) - expect_equal(vctrs::vec_ptype(audit_list), vctrs::vec_ptype(connectapi_ptypes$audit_logs)) + # This is different on older versions, not sure it's worth worrying about how + skip_if_connect_older_than(test_conn_1, "2022.09.0") + expect_ptype_equal(audit_list, connectapi_ptypes$audit_logs) }) test_that("get_procs works", { @@ -64,7 +67,7 @@ test_that("get_procs works", { # TODO: This is not a great test, since no processes are running # we could always start a content restoration... expect_is(proc_data, "tbl_df") - expect_equal(vctrs::vec_ptype(proc_data), vctrs::vec_ptype(connectapi_ptypes$procs)) + expect_ptype_equal(proc_data, connectapi_ptypes$procs) }) # experimental -------------------------------------------- diff --git a/tests/integrated/test-lazy.R b/tests/integrated/test-lazy.R index 67d4e33d..6aa88007 100644 --- a/tests/integrated/test-lazy.R +++ b/tests/integrated/test-lazy.R @@ -54,7 +54,8 @@ test_that("usage_static works", { expect_is(colnames(content_visits), "character") expect_gt(length(colnames(content_visits)), 1) - expect_equal(vctrs::vec_ptype(content_visits_local), vctrs::vec_ptype(connectapi_ptypes$usage_static)) + # path was added in 2024 + expect_ptype_equal(content_visits_local, connectapi_ptypes$usage_static, exact = FALSE) }) test_that("usage_shiny works", { @@ -68,7 +69,7 @@ test_that("usage_shiny works", { expect_is(colnames(shiny_usage), "character") expect_gt(length(colnames(shiny_usage)), 1) - expect_equal(vctrs::vec_ptype(shiny_usage_local), vctrs::vec_ptype(connectapi_ptypes$usage_shiny)) + expect_ptype_equal(shiny_usage_local, connectapi_ptypes$usage_shiny) }) test_that("content works", { @@ -83,7 +84,9 @@ test_that("content works", { expect_is(colnames(content_list), "character") expect_gt(length(colnames(content_list)), 1) - expect_equal(vctrs::vec_ptype(content_list_local), vctrs::vec_ptype(connectapi_ptypes$content)) + # various attributes have been added over the years, so exact match + # doesn't work against all versions of Connect + expect_ptype_equal(content_list_local, connectapi_ptypes$content, exact = FALSE) }) test_that("groups works", { @@ -98,7 +101,7 @@ test_that("groups works", { expect_is(colnames(groups_list), "character") expect_gt(length(colnames(groups_list)), 1) - expect_equal(vctrs::vec_ptype(groups_list_local), vctrs::vec_ptype(connectapi_ptypes$groups)) + expect_ptype_equal(groups_list_local, connectapi_ptypes$groups) }) test_that("audit_logs works", { @@ -113,5 +116,7 @@ test_that("audit_logs works", { expect_is(colnames(audit_list), "character") expect_gt(length(colnames(audit_list)), 1) - expect_equal(vctrs::vec_ptype(audit_list_local), vctrs::vec_ptype(connectapi_ptypes$audit_logs)) + # This is different on older versions, not sure it's worth worrying about how + skip_if_connect_older_than(test_conn_1, "2022.09.0") + expect_ptype_equal(audit_list_local, connectapi_ptypes$audit_logs) }) diff --git a/tests/testthat/_snaps/utils.md b/tests/testthat/_snaps/utils.md index 4a1939cc..7a0f8d5a 100644 --- a/tests/testthat/_snaps/utils.md +++ b/tests/testthat/_snaps/utils.md @@ -3,14 +3,13 @@ Code capture_warning(check_connect_version("2022.02", "2022.01")) Output - + NULL --- Code capture_warning(check_connect_version("2022.01", "2022.02")) Output - diff --git a/tests/testthat/test-parse.R b/tests/testthat/test-parse.R index d8cbf151..a1adcf89 100644 --- a/tests/testthat/test-parse.R +++ b/tests/testthat/test-parse.R @@ -122,7 +122,7 @@ test_that("works for bad inputs", { end_time = NULL, app_guid = uuid::UUIDgenerate() ) - res <- connectapi:::parse_connectapi_typed(list(job), !!!connectapi:::connectapi_ptypes$job) + res <- connectapi:::parse_connectapi_typed(list(job), connectapi:::connectapi_ptypes$job) expect_is(res$stdout, "list") expect_is(res$origin, "character") expect_is(res$start_time, "POSIXct") diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index 0d5e4983..1d3ce3d0 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -1,6 +1,6 @@ context("utils") -test_that("safequery handles values correctly", { +test_that("safe_query handles values correctly", { pref <- "prefixed" nullval <- NULL expect_identical(safe_query(nullval, pref), "") @@ -38,21 +38,22 @@ test_that("error_if_less_than errors as expected", { test_that("check_connect_version works", { # silent for patch version changes - expect_silent(check_connect_version("1.8.2-4", "1.8.2.1-10")) + expect_silent(check_connect_version("1.8.2.1-10", "1.8.2-4")) + + # silent if newer + expect_silent(check_connect_version("1.8.2-4", "1.8.0.5-1")) # warnings for minor version changes - expect_warning(check_connect_version("1.8.2-4", "1.8.0.5-1"), "newer") - warn_clear("new-connect") expect_warning(check_connect_version("1.8.2-4", "2.8.0.5-1"), "older") warn_clear("old-connect") }) test_that("check_connect_version warning snapshot", { # warning messages seem to cause issues in different environments based on color codes - testthat::skip_on_cran() + skip_on_cran() local_edition(3) + # No warning expect_snapshot(capture_warning(check_connect_version("2022.02", "2022.01"))) - warn_clear("new-connect") expect_snapshot(capture_warning(check_connect_version("2022.01", "2022.02"))) warn_clear("old-connect") })