From 9ee3121327e6e93438705b0c33fbdd0f7322e8f2 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Mon, 28 Aug 2023 16:31:41 -0500 Subject: [PATCH 1/7] add space parameter for Posit Cloud --- NEWS.md | 2 ++ R/client-cloud.R | 37 +++++++++++++++-------- R/client-shinyapps.R | 2 +- R/deployApp.R | 12 ++++++-- tests/testthat/test-client-cloud.R | 47 +++++++++++++++++++++++++++++- 5 files changed, 83 insertions(+), 17 deletions(-) diff --git a/NEWS.md b/NEWS.md index 112a3cb6..91457d8b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # rsconnect (development version) +* Added `space` parameter to deploy directly to a space in Posit Cloud. + * Fixed analysis of directories that were smaller than the `rsconnect.max.bundle.files=10000` limit but larger than the `renv.config.dependencies.limit=1000` limit. (#968) diff --git a/R/client-cloud.R b/R/client-cloud.R index be0afb60..71730d0e 100644 --- a/R/client-cloud.R +++ b/R/client-cloud.R @@ -161,7 +161,7 @@ cloudClient <- function(service, authInfo) { GET(service, authInfo, path, query) }, - createApplication = function(name, title, template, accountId, appMode) { + createApplication = function(name, title, template, accountId, appMode, spaceId = NULL) { json <- list() json$name <- name json$application_type <- if (appMode %in% c("rmd-static", "quarto-static", "static")) "static" else "connect" @@ -169,15 +169,19 @@ cloudClient <- function(service, authInfo) { json$render_by <- "server" } - currentProjectId <- getCurrentProjectId(service, authInfo) - # in case the source cloud project is a temporary copy, there is no - # content id. The output will be published without a space id. - if (!is.null(currentProjectId)) { - json$project <- currentProjectId + if (is.null(spaceId)) { + currentProjectId <- getCurrentProjectId(service, authInfo) + # in case the source cloud project is a temporary copy, there is no + # content id. The output will be published without a space id. + if (!is.null(currentProjectId)) { + json$project <- currentProjectId - path <- paste0("/content/", currentProjectId) - currentProject <- GET(service, authInfo, path) - json$space <- currentProject$space_id + path <- paste0("/content/", currentProjectId) + currentProject <- GET(service, authInfo, path) + json$space <- currentProject$space_id + } + } else { + json$space <- spaceId } output <- POST_JSON(service, authInfo, "/outputs", json) @@ -230,12 +234,21 @@ cloudClient <- function(service, authInfo) { revision$application_id }, - deployApplication = function(application, bundleId = NULL) { + deployApplication = function(application, bundleId = NULL, spaceId = NULL) { + outputPatchData <- list() + currentProjectId <- getCurrentProjectId(service, authInfo) if (!is.null(currentProjectId)) { + outputPatchData$project <- currentProjectId + } + + if (!is.null(spaceId)) { + outputPatchData$space <- spaceId + } + + if (length(outputPatchData) > 0) { path <- paste0("/outputs/", application$id) - json <- list(project = currentProjectId) - PATCH_JSON(service, authInfo, path, json) + PATCH_JSON(service, authInfo, path, outputPatchData) } path <- paste0("/applications/", application$application_id, "/deploy") diff --git a/R/client-shinyapps.R b/R/client-shinyapps.R index 15349037..70bab3f1 100644 --- a/R/client-shinyapps.R +++ b/R/client-shinyapps.R @@ -90,7 +90,7 @@ shinyAppsClient <- function(service, authInfo) { GET(service, authInfo, path, query) }, - createApplication = function(name, title, template, accountId, appMode) { + createApplication = function(name, title, template, accountId, appMode, spaceId) { json <- list() json$name <- name # the title field is only used on connect diff --git a/R/deployApp.R b/R/deployApp.R index a7401d5b..cafe993b 100644 --- a/R/deployApp.R +++ b/R/deployApp.R @@ -142,6 +142,10 @@ #' @param image Optional. The name of the image to use when building and #' executing this content. If none is provided, Posit Connect will #' attempt to choose an image based on the content requirements. +#' @param space Optional. For Posit Cloud, the id of the space where the content +#' should be deployed. If none is provided, content will be deployed to the +#' deploying user's workspace or deployed to the same space in case of +#' redeploy. #' @examples #' \dontrun{ #' @@ -197,7 +201,8 @@ deployApp <- function(appDir = getwd(), forceGeneratePythonEnvironment = FALSE, quarto = NA, appVisibility = NULL, - image = NULL + image = NULL, + space = NULL ) { check_string(appDir) @@ -365,7 +370,8 @@ deployApp <- function(appDir = getwd(), target$appTitle, "shiny", accountDetails$accountId, - appMetadata$appMode + appMetadata$appMode, + space ) taskComplete(quiet, "Created application with id {.val {application$id}}") } else { @@ -453,7 +459,7 @@ deployApp <- function(appDir = getwd(), if (!quiet) { cli::cli_rule("Deploying to server") } - task <- client$deployApplication(application, bundle$id) + task <- client$deployApplication(application, bundle$id, space) taskId <- if (is.null(task$task_id)) task$id else task$task_id # wait for the deployment to complete (will raise an error if it can't) response <- client$waitForTask(taskId, quiet) diff --git a/tests/testthat/test-client-cloud.R b/tests/testthat/test-client-cloud.R index 2ccb96d2..e3b90aae 100644 --- a/tests/testthat/test-client-cloud.R +++ b/tests/testthat/test-client-cloud.R @@ -303,6 +303,50 @@ test_that("Create application", { expect_equal(app$url, "http://fake-url.test.me/") }) +test_that("Create application with space id", { + mockServer <- mockServerFactory(list( + "^POST /outputs" = list( + content = function(methodAndPath, match, contentFile, ...) { + content <- jsonlite::fromJSON(readChar(contentFile, file.info(contentFile)$size)) + expect_equal(content$application_type, "connect") + expect_equal(content$space, 333) + list( + "id" = 1, + "source_id" = 2, + "url" = "http://fake-url.test.me/", + "state" = "active" + ) + } + ), + "^GET /applications/([0-9]+)" = list( + content = function(methodAndPath, match, ...) { + end <- attr(match, "match.length")[2] + match[2] + application_id <- strtoi(substr(methodAndPath, match[2], end)) + + list( + "id" = application_id, + "content_id" = 1 + ) + }) + )) + + restoreOpt <- options(rsconnect.http = mockServer$impl) + withr::defer(options(restoreOpt)) + + fakeService <- list( + protocol = "test", + host = "unit-test", + port = 42 + ) + client <- cloudClient(fakeService, NULL) + + app <- client$createApplication("test app", "unused?", "unused?", "unused?", "shiny", 333) + + expect_equal(app$id, 1) + expect_equal(app$application_id, 2) + expect_equal(app$url, "http://fake-url.test.me/") +}) + test_that("Create static application", { mockServer <- mockServerFactory(list( "^POST /outputs" = list( @@ -408,6 +452,7 @@ test_that("deployApplication updates the parent project", { content = function(methodAndPath, match, contentFile, ...) { content <- jsonlite::fromJSON(readChar(contentFile, file.info(contentFile)$size)) expect_equal(content$project, 41) + expect_equal(content$space, 333) list( "id" = 41 ) @@ -442,7 +487,7 @@ test_that("deployApplication updates the parent project", { "id" = 100, "application_id" = 101 ) - client$deployApplication(application) + client$deployApplication(application, spaceId = 333) }) test_that("Create static RMD application", { From 98367752fbc12ae514e1fd85027a8e1e90d18987 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Mon, 28 Aug 2023 16:47:46 -0500 Subject: [PATCH 2/7] update documentation with deployApp space parameter --- man/deployApp.Rd | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/man/deployApp.Rd b/man/deployApp.Rd index 4044d44f..6ee18032 100644 --- a/man/deployApp.Rd +++ b/man/deployApp.Rd @@ -30,7 +30,8 @@ deployApp( forceGeneratePythonEnvironment = FALSE, quarto = NA, appVisibility = NULL, - image = NULL + image = NULL, + space = NULL ) } \arguments{ @@ -177,6 +178,11 @@ made. Currently has an effect only on deployments to shinyapps.io.} \item{image}{Optional. The name of the image to use when building and executing this content. If none is provided, Posit Connect will attempt to choose an image based on the content requirements.} + +\item{space}{Optional. For Posit Cloud, the id of the space where the content +should be deployed. If none is provided, content will be deployed to the +deploying user's workspace or deployed to the same space in case of +redeploy.} } \description{ Deploy a \link[shiny:shiny-package]{shiny} application, an From 34e4e6429b66fb656524e9fbaeabe143fae8f297 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Tue, 29 Aug 2023 13:57:24 -0500 Subject: [PATCH 3/7] add extra arguments to other clients to match cloud; fix space/project setting logic in createApplication --- R/client-cloud.R | 24 ++++++++++++------------ R/client-connect.R | 4 ++-- R/client-shinyapps.R | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/R/client-cloud.R b/R/client-cloud.R index 71730d0e..4dcf8455 100644 --- a/R/client-cloud.R +++ b/R/client-cloud.R @@ -169,18 +169,18 @@ cloudClient <- function(service, authInfo) { json$render_by <- "server" } - if (is.null(spaceId)) { - currentProjectId <- getCurrentProjectId(service, authInfo) - # in case the source cloud project is a temporary copy, there is no - # content id. The output will be published without a space id. - if (!is.null(currentProjectId)) { - json$project <- currentProjectId - - path <- paste0("/content/", currentProjectId) - currentProject <- GET(service, authInfo, path) - json$space <- currentProject$space_id - } - } else { + currentProjectId <- getCurrentProjectId(service, authInfo) + # in case the source cloud project is a temporary copy, there is no + # content id. The output will be published without a space id. + if (!is.null(currentProjectId)) { + json$project <- currentProjectId + + path <- paste0("/content/", currentProjectId) + currentProject <- GET(service, authInfo, path) + json$space <- currentProject$space_id + } + + if (is.null(currentProjectId) && !is.null(spaceId)) { json$space <- spaceId } diff --git a/R/client-connect.R b/R/client-connect.R index 98e1de43..59906ddd 100644 --- a/R/client-connect.R +++ b/R/client-connect.R @@ -48,7 +48,7 @@ connectClient <- function(service, authInfo) { listApplicationsRequest(service, authInfo, path, query, "applications") }, - createApplication = function(name, title, template, accountId, appMode) { + createApplication = function(name, title, template, accountId, appMode, spaceId = NULL) { # add name; inject title if specified details <- list(name = name) if (!is.null(title) && nzchar(title)) @@ -80,7 +80,7 @@ connectClient <- function(service, authInfo) { ) }, - deployApplication = function(application, bundleId = NULL) { + deployApplication = function(application, bundleId = NULL, spaceId = NULL) { path <- paste("/applications/", application$id, "/deploy", sep = "") json <- list() json$bundle <- as.numeric(bundleId) diff --git a/R/client-shinyapps.R b/R/client-shinyapps.R index 70bab3f1..70de9f1d 100644 --- a/R/client-shinyapps.R +++ b/R/client-shinyapps.R @@ -90,7 +90,7 @@ shinyAppsClient <- function(service, authInfo) { GET(service, authInfo, path, query) }, - createApplication = function(name, title, template, accountId, appMode, spaceId) { + createApplication = function(name, title, template, accountId, appMode, spaceId = NULL) { json <- list() json$name <- name # the title field is only used on connect @@ -138,7 +138,7 @@ shinyAppsClient <- function(service, authInfo) { ) }, - deployApplication = function(application, bundleId = NULL) { + deployApplication = function(application, bundleId = NULL, spaceId = NULL) { path <- paste("/applications/", application$id, "/deploy", sep = "") json <- list() if (length(bundleId) > 0 && nzchar(bundleId)) From 3ccd0ad40349687f027a49d9e69e7b2d2f9d0f18 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Tue, 5 Sep 2023 13:47:58 -0500 Subject: [PATCH 4/7] refactor tests --- tests/testthat/test-client-cloud.R | 33 ++++++++++-------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/tests/testthat/test-client-cloud.R b/tests/testthat/test-client-cloud.R index e3b90aae..1a327ca5 100644 --- a/tests/testthat/test-client-cloud.R +++ b/tests/testthat/test-client-cloud.R @@ -171,8 +171,7 @@ test_that("Get application", { ) )) - restoreOpt <- options(rsconnect.http = mockServer$impl) - withr::defer(options(restoreOpt)) + withr::local_options(rsconnect.http = mockServer$impl) fakeService <- list( protocol = "test", @@ -237,8 +236,7 @@ test_that("Get application output trashed", { ) )) - restoreOpt <- options(rsconnect.http = mockServer$impl) - withr::defer(options(restoreOpt)) + withr::local_options(rsconnect.http = mockServer$impl) fakeService <- list( protocol = "test", @@ -286,8 +284,7 @@ test_that("Create application", { }) )) - restoreOpt <- options(rsconnect.http = mockServer$impl) - withr::defer(options(restoreOpt)) + withr::local_options(rsconnect.http = mockServer$impl) fakeService <- list( protocol = "test", @@ -330,8 +327,7 @@ test_that("Create application with space id", { }) )) - restoreOpt <- options(rsconnect.http = mockServer$impl) - withr::defer(options(restoreOpt)) + withr::local_options(rsconnect.http = mockServer$impl) fakeService <- list( protocol = "test", @@ -373,8 +369,7 @@ test_that("Create static application", { }) )) - restoreOpt <- options(rsconnect.http = mockServer$impl) - withr::defer(options(restoreOpt)) + withr::local_options(rsconnect.http = mockServer$impl) fakeService <- list( protocol = "test", @@ -417,8 +412,7 @@ test_that("Create static server-side-rendered application", { }) )) - restoreOpt <- options(rsconnect.http = mockServer$impl) - withr::defer(options(restoreOpt)) + withr::local_options(rsconnect.http = mockServer$impl) fakeService <- list( protocol = "test", @@ -517,8 +511,7 @@ test_that("Create static RMD application", { }) )) - restoreOpt <- options(rsconnect.http = mockServer$impl) - withr::defer(options(restoreOpt)) + withr::local_options(rsconnect.http = mockServer$impl) fakeService <- list( protocol = "test", @@ -569,8 +562,7 @@ test_that("Create application with linked source project", { ) )) - restoreOpt <- options(rsconnect.http = mockServer$impl) - withr::defer(options(restoreOpt)) + withr::local_options(rsconnect.http = mockServer$impl) Sys.setenv(LUCID_APPLICATION_ID = "42") withr::defer(Sys.unsetenv("LUCID_APPLICATION_ID")) @@ -618,8 +610,7 @@ test_that("deploymentTargetForApp() results in correct Cloud API calls", { ) )) - restoreOpt <- options(rsconnect.http = mockServer$impl) - withr::defer(options(restoreOpt)) + withr::local_options(rsconnect.http = mockServer$impl) testAccount <- configureTestAccount() withr::defer(removeAccount(testAccount)) @@ -802,8 +793,7 @@ test_that("deployApp() for shiny results in correct Cloud API calls", { mock <- deployAppMockServerFactory(expectedAppType = "connect", outputState = "active") mockServer <- mock$server - restoreOpt <- options(rsconnect.http = mockServer$impl) - withr::defer(options(restoreOpt)) + withr::local_options(rsconnect.http = mockServer$impl) testAccount <- configureTestAccount() withr::defer(removeAccount(testAccount)) @@ -896,8 +886,7 @@ test_that("deployDoc() results in correct Cloud API calls", { mock <- deployAppMockServerFactory(expectedAppType = "static", outputState = "active") mockServer <- mock$server - restoreOpt <- options(rsconnect.http = mockServer$impl) - withr::defer(options(restoreOpt)) + withr::local_options(rsconnect.http = mockServer$impl) testAccount <- configureTestAccount() withr::defer(removeAccount(testAccount)) From a33a91ed9cbd7dcfa1a70cec1d0e9b59c0c94130 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Tue, 12 Sep 2023 10:01:01 -0500 Subject: [PATCH 5/7] fix tests --- R/client-cloud.R | 2 +- tests/testthat/test-client-cloud.R | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/R/client-cloud.R b/R/client-cloud.R index d7b9840e..162c9e25 100644 --- a/R/client-cloud.R +++ b/R/client-cloud.R @@ -161,7 +161,7 @@ cloudClient <- function(service, authInfo) { GET(service, authInfo, path, query) }, - createApplication = function(name, title, template, accountId, appMode, contentCategory = NULL, spaceId = NULL) { + createApplication = function(name, title, template, accountId, appMode, contentCategory = NULL, spaceId = NULL) { json <- list() json$name <- name json$application_type <- if (appMode %in% c("rmd-static", "quarto-static", "static")) "static" else "connect" diff --git a/tests/testthat/test-client-cloud.R b/tests/testthat/test-client-cloud.R index 3f1c09ce..531d8e66 100644 --- a/tests/testthat/test-client-cloud.R +++ b/tests/testthat/test-client-cloud.R @@ -336,7 +336,7 @@ test_that("Create application with space id", { ) client <- cloudClient(fakeService, NULL) - app <- client$createApplication("test app", "unused?", "unused?", "unused?", "shiny", 333) + app <- client$createApplication("test app", "unused?", "unused?", "unused?", "shiny", spaceId = 333) expect_equal(app$id, 1) expect_equal(app$application_id, 2) From d234d40611968a72288f5428d24aae115c3ecc22 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Tue, 12 Sep 2023 10:56:27 -0500 Subject: [PATCH 6/7] make separate calls for space and project update --- R/client-cloud.R | 8 ++------ tests/testthat/test-client-cloud.R | 7 +++++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/R/client-cloud.R b/R/client-cloud.R index 162c9e25..e679d87c 100644 --- a/R/client-cloud.R +++ b/R/client-cloud.R @@ -242,18 +242,14 @@ cloudClient <- function(service, authInfo) { currentProjectId <- getCurrentProjectId(service, authInfo) if (!is.null(currentProjectId)) { - outputPatchData$project <- currentProjectId + PATCH_JSON(service, authInfo, paste0("/outputs/", application$id), list(project = currentProjectId)) } if (!is.null(spaceId)) { + PATCH_JSON(service, authInfo, paste0("/outputs/", application$id), list(space = spaceId)) outputPatchData$space <- spaceId } - if (length(outputPatchData) > 0) { - path <- paste0("/outputs/", application$id) - PATCH_JSON(service, authInfo, path, outputPatchData) - } - path <- paste0("/applications/", application$application_id, "/deploy") json <- list() if (length(bundleId) > 0 && nzchar(bundleId)) diff --git a/tests/testthat/test-client-cloud.R b/tests/testthat/test-client-cloud.R index 531d8e66..ba3ca0e6 100644 --- a/tests/testthat/test-client-cloud.R +++ b/tests/testthat/test-client-cloud.R @@ -447,8 +447,11 @@ test_that("deployApplication updates the parent project", { "^PATCH /outputs" = list( content = function(methodAndPath, match, contentFile, ...) { content <- jsonlite::fromJSON(readChar(contentFile, file.info(contentFile)$size)) - expect_equal(content$project, 41) - expect_equal(content$space, 333) + if (!is.null(content$project)) { + expect_equal(content$project, 41) + } else { + expect_equal(content$space, 333) + } list( "id" = 41 ) From ab46dd1e61cafe1af71e1cf98419f29890044d0b Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Tue, 12 Sep 2023 10:59:49 -0500 Subject: [PATCH 7/7] remove extraneous lines --- R/client-cloud.R | 3 --- 1 file changed, 3 deletions(-) diff --git a/R/client-cloud.R b/R/client-cloud.R index e679d87c..e3182c0e 100644 --- a/R/client-cloud.R +++ b/R/client-cloud.R @@ -238,8 +238,6 @@ cloudClient <- function(service, authInfo) { }, deployApplication = function(application, bundleId = NULL, spaceId = NULL) { - outputPatchData <- list() - currentProjectId <- getCurrentProjectId(service, authInfo) if (!is.null(currentProjectId)) { PATCH_JSON(service, authInfo, paste0("/outputs/", application$id), list(project = currentProjectId)) @@ -247,7 +245,6 @@ cloudClient <- function(service, authInfo) { if (!is.null(spaceId)) { PATCH_JSON(service, authInfo, paste0("/outputs/", application$id), list(space = spaceId)) - outputPatchData$space <- spaceId } path <- paste0("/applications/", application$application_id, "/deploy")