From e067c777abe8e8ed21ae980d53adc3738399ad2a Mon Sep 17 00:00:00 2001 From: Michael Marchetti Date: Mon, 1 Apr 2024 14:54:21 -0400 Subject: [PATCH 1/4] accept config name when creating a deployment --- internal/services/api/deployment_dto.go | 16 +++- internal/services/api/post_deployments.go | 18 +++++ .../services/api/post_deployments_test.go | 77 +++++++++++++++++++ 3 files changed, 109 insertions(+), 2 deletions(-) diff --git a/internal/services/api/deployment_dto.go b/internal/services/api/deployment_dto.go index 2d2183ca3..bcd47817a 100644 --- a/internal/services/api/deployment_dto.go +++ b/internal/services/api/deployment_dto.go @@ -32,12 +32,14 @@ type preDeploymentDTO struct { SaveName string `json:"saveName"` CreatedAt string `json:"createdAt"` Error *types.AgentError `json:"error,omitempty"` + ConfigName string `json:"configName,omitempty"` + ConfigPath string `json:"configurationPath,omitempty"` } type fullDeploymentDTO struct { deploymentLocation deployment.Deployment - ConfigPath string `json:"configurationPath,omitempty"` + ConfigPath string `json:"configurationPath"` SaveName string `json:"saveName"` } @@ -55,6 +57,8 @@ func getConfigPath(base util.AbsolutePath, configName string) util.AbsolutePath func deploymentAsDTO(d *deployment.Deployment, err error, base util.AbsolutePath, path util.AbsolutePath) any { saveName := deployment.SaveNameFromPath(path) + configPath := "" + if err != nil { return &deploymentErrorDTO{ deploymentLocation: deploymentLocation{ @@ -65,6 +69,9 @@ func deploymentAsDTO(d *deployment.Deployment, err error, base util.AbsolutePath Error: types.AsAgentError(err), } } else if d.ID != "" { + if d.ConfigName != "" { + configPath = getConfigPath(base, d.ConfigName).String() + } return &fullDeploymentDTO{ deploymentLocation: deploymentLocation{ State: deploymentStateDeployed, @@ -72,10 +79,13 @@ func deploymentAsDTO(d *deployment.Deployment, err error, base util.AbsolutePath Path: path.String(), }, Deployment: *d, - ConfigPath: getConfigPath(base, d.ConfigName).String(), + ConfigPath: configPath, SaveName: saveName, // TODO: remove this duplicate (remove frontend references first) } } else { + if d.ConfigName != "" { + configPath = getConfigPath(base, d.ConfigName).String() + } return preDeploymentDTO{ deploymentLocation: deploymentLocation{ State: deploymentStateNew, @@ -87,6 +97,8 @@ func deploymentAsDTO(d *deployment.Deployment, err error, base util.AbsolutePath ServerURL: d.ServerURL, SaveName: saveName, // TODO: remove this duplicate (remove frontend references first) CreatedAt: d.CreatedAt, + ConfigName: d.ConfigName, + ConfigPath: configPath, Error: d.Error, } } diff --git a/internal/services/api/post_deployments.go b/internal/services/api/post_deployments.go index 985f69ba3..a0c5c9cc3 100644 --- a/internal/services/api/post_deployments.go +++ b/internal/services/api/post_deployments.go @@ -6,6 +6,7 @@ import ( "net/http" "github.com/rstudio/connect-client/internal/accounts" + "github.com/rstudio/connect-client/internal/config" "github.com/rstudio/connect-client/internal/deployment" "github.com/rstudio/connect-client/internal/logging" "github.com/rstudio/connect-client/internal/util" @@ -15,6 +16,7 @@ import ( type PostDeploymentsRequestBody struct { AccountName string `json:"account"` + ConfigName string `json:"config"` SaveName string `json:"saveName"` } @@ -49,6 +51,7 @@ func PostDeploymentsHandlerFunc( } } + // Deployment must not exist path := deployment.GetDeploymentPath(base, b.SaveName) exists, err := path.Exists() if err != nil { @@ -60,9 +63,24 @@ func PostDeploymentsHandlerFunc( return } + if b.ConfigName != "" { + // Config must exist + configPath := config.GetConfigPath(base, b.ConfigName) + exists, err = configPath.Exists() + if err != nil { + InternalError(w, req, log, err) + return + } + if !exists { + w.WriteHeader(http.StatusNotFound) + return + } + } + d := deployment.New() d.ServerURL = acct.URL d.ServerType = acct.ServerType + d.ConfigName = b.ConfigName err = d.WriteFile(path) if err != nil { diff --git a/internal/services/api/post_deployments_test.go b/internal/services/api/post_deployments_test.go index 35d3e7b6f..6aaafb62e 100644 --- a/internal/services/api/post_deployments_test.go +++ b/internal/services/api/post_deployments_test.go @@ -13,6 +13,7 @@ import ( "testing" "github.com/rstudio/connect-client/internal/accounts" + "github.com/rstudio/connect-client/internal/config" "github.com/rstudio/connect-client/internal/logging" "github.com/rstudio/connect-client/internal/util" "github.com/rstudio/connect-client/internal/util/utiltest" @@ -48,6 +49,56 @@ func (s *PostDeploymentsSuite) TestPostDeployments() { h := PostDeploymentsHandlerFunc(s.cwd, logging.New(), lister) + cfg := config.New() + err := cfg.WriteFile(config.GetConfigPath(s.cwd, "myConfig")) + s.NoError(err) + + rec := httptest.NewRecorder() + body := strings.NewReader(`{ + "account": "myAccount", + "config": "myConfig", + "saveName": "newDeployment" + }`) + req, err := http.NewRequest("POST", "/api/deployments", body) + s.NoError(err) + h(rec, req) + + s.Equal(http.StatusOK, rec.Result().StatusCode) + s.Equal("application/json", rec.Header().Get("content-type")) + + res := preDeploymentDTO{} + dec := json.NewDecoder(rec.Body) + dec.DisallowUnknownFields() + s.NoError(dec.Decode(&res)) + + actualPath, err := util.NewPath(res.Path, s.cwd.Fs()).Rel(s.cwd) + s.NoError(err) + s.Equal(filepath.Join(".posit", "publish", "deployments", "newDeployment.toml"), actualPath.String()) + + s.Equal("newDeployment", res.Name) + s.Equal("newDeployment", res.SaveName) + s.Equal("myConfig", res.ConfigName) + s.Equal("myConfig.toml", filepath.Base(res.ConfigPath)) + s.Equal(accounts.ServerTypeConnect, res.ServerType) + s.Equal(acct.URL, res.ServerURL) + s.Equal(deploymentStateNew, res.State) +} + +func (s *PostDeploymentsSuite) TestPostDeploymentsNoConfig() { + lister := &accounts.MockAccountList{} + acct := &accounts.Account{ + Name: "myAccount", + URL: "https://connect.example.com", + ServerType: accounts.ServerTypeConnect, + } + lister.On("GetAccountByName", "myAccount").Return(acct, nil) + + h := PostDeploymentsHandlerFunc(s.cwd, logging.New(), lister) + + cfg := config.New() + err := cfg.WriteFile(config.GetConfigPath(s.cwd, "myConfig")) + s.NoError(err) + rec := httptest.NewRecorder() body := strings.NewReader(`{ "account": "myAccount", @@ -71,11 +122,37 @@ func (s *PostDeploymentsSuite) TestPostDeployments() { s.Equal("newDeployment", res.Name) s.Equal("newDeployment", res.SaveName) + s.Equal("", res.ConfigName) + s.Equal("", res.ConfigPath) s.Equal(accounts.ServerTypeConnect, res.ServerType) s.Equal(acct.URL, res.ServerURL) s.Equal(deploymentStateNew, res.State) } +func (s *PostDeploymentsSuite) TestPostDeploymentsNonexistentConfig() { + lister := &accounts.MockAccountList{} + acct := &accounts.Account{ + Name: "myAccount", + URL: "https://connect.example.com", + ServerType: accounts.ServerTypeConnect, + } + lister.On("GetAccountByName", "myAccount").Return(acct, nil) + + h := PostDeploymentsHandlerFunc(s.cwd, logging.New(), lister) + + rec := httptest.NewRecorder() + body := strings.NewReader(`{ + "account": "myAccount", + "config": "myConfig", + "saveName": "newDeployment" + }`) + req, err := http.NewRequest("POST", "/api/deployments", body) + s.NoError(err) + h(rec, req) + + s.Equal(http.StatusNotFound, rec.Result().StatusCode) +} + func (s *PostDeploymentsSuite) TestPostDeploymentsBadRequest() { h := PostDeploymentsHandlerFunc(s.cwd, logging.New(), nil) From 8b9c68a969adde8e19782124a224c1a1beef50f1 Mon Sep 17 00:00:00 2001 From: Michael Marchetti Date: Mon, 1 Apr 2024 14:57:03 -0400 Subject: [PATCH 2/4] frontend API deployment type includes configurationName and configurationPath --- extensions/vscode/src/api/types/deployments.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/extensions/vscode/src/api/types/deployments.ts b/extensions/vscode/src/api/types/deployments.ts index 3ecaabec4..9417a8930 100644 --- a/extensions/vscode/src/api/types/deployments.ts +++ b/extensions/vscode/src/api/types/deployments.ts @@ -27,12 +27,13 @@ type DeploymentRecord = { serverUrl: string; saveName: string; createdAt: string; - configurationName: string; } & DeploymentLocation; export type PreDeployment = { state: DeploymentState.NEW; error: AgentError | null; + configurationName: string | undefined; + configurationPath: string | undefined; } & DeploymentRecord; export type Deployment = { @@ -45,6 +46,8 @@ export type Deployment = { deployedAt: string; state: DeploymentState.DEPLOYED; deploymentError: AgentError | null; + configurationName: string; + configurationPath: string; } & DeploymentRecord & Configuration; From d64c6f586815fdcc0f5f670616ed66eb0ea12cde Mon Sep 17 00:00:00 2001 From: Michael Marchetti Date: Wed, 3 Apr 2024 13:39:39 -0400 Subject: [PATCH 3/4] API fixes for configuration name --- internal/services/api/deployment_dto.go | 2 +- internal/services/api/post_deployments.go | 4 +++- internal/services/api/post_deployments_test.go | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/services/api/deployment_dto.go b/internal/services/api/deployment_dto.go index bcd47817a..ef46543db 100644 --- a/internal/services/api/deployment_dto.go +++ b/internal/services/api/deployment_dto.go @@ -32,7 +32,7 @@ type preDeploymentDTO struct { SaveName string `json:"saveName"` CreatedAt string `json:"createdAt"` Error *types.AgentError `json:"error,omitempty"` - ConfigName string `json:"configName,omitempty"` + ConfigName string `json:"configurationName,omitempty"` ConfigPath string `json:"configurationPath,omitempty"` } diff --git a/internal/services/api/post_deployments.go b/internal/services/api/post_deployments.go index a0c5c9cc3..105041dc8 100644 --- a/internal/services/api/post_deployments.go +++ b/internal/services/api/post_deployments.go @@ -3,6 +3,7 @@ package api import ( "encoding/json" "errors" + "fmt" "net/http" "github.com/rstudio/connect-client/internal/accounts" @@ -72,7 +73,8 @@ func PostDeploymentsHandlerFunc( return } if !exists { - w.WriteHeader(http.StatusNotFound) + w.WriteHeader(http.StatusUnprocessableEntity) + w.Write([]byte(fmt.Sprintf("configuration %s not found", b.ConfigName))) return } } diff --git a/internal/services/api/post_deployments_test.go b/internal/services/api/post_deployments_test.go index 6aaafb62e..df0b224ea 100644 --- a/internal/services/api/post_deployments_test.go +++ b/internal/services/api/post_deployments_test.go @@ -150,7 +150,7 @@ func (s *PostDeploymentsSuite) TestPostDeploymentsNonexistentConfig() { s.NoError(err) h(rec, req) - s.Equal(http.StatusNotFound, rec.Result().StatusCode) + s.Equal(http.StatusUnprocessableEntity, rec.Result().StatusCode) } func (s *PostDeploymentsSuite) TestPostDeploymentsBadRequest() { From a6e72859fe330675caa2badcb9ce62fb11120dd1 Mon Sep 17 00:00:00 2001 From: Michael Marchetti Date: Wed, 3 Apr 2024 13:56:16 -0400 Subject: [PATCH 4/4] make new subtype for pre-deployment with configuration --- .../vscode/src/api/types/deployments.ts | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/extensions/vscode/src/api/types/deployments.ts b/extensions/vscode/src/api/types/deployments.ts index 473560d78..14ce88a76 100644 --- a/extensions/vscode/src/api/types/deployments.ts +++ b/extensions/vscode/src/api/types/deployments.ts @@ -1,7 +1,7 @@ // Copyright (C) 2023 by Posit Software, PBC. import { AgentError } from "./error"; -import { Configuration } from "./configurations"; +import { Configuration, ConfigurationLocation } from "./configurations"; import { SchemaURL } from "./schema"; import { ServerType } from "./accounts"; @@ -33,10 +33,10 @@ type DeploymentRecord = { export type PreDeployment = { state: DeploymentState.NEW; - configurationName: string | undefined; - configurationPath: string | undefined; } & DeploymentRecord; +export type PreDeploymentWithConfig = PreDeployment & ConfigurationLocation; + export type Deployment = { id: string; bundleId: string; @@ -46,12 +46,14 @@ export type Deployment = { files: string[]; deployedAt: string; state: DeploymentState.DEPLOYED; - configurationName: string; - configurationPath: string; } & DeploymentRecord & Configuration; -export type AllDeploymentTypes = Deployment | PreDeployment | DeploymentError; +export type AllDeploymentTypes = + | Deployment + | PreDeployment + | PreDeploymentWithConfig + | DeploymentError; export function isSuccessful( d: AllDeploymentTypes | undefined, @@ -87,6 +89,16 @@ export function isPreDeployment( return Boolean(d && d.state === DeploymentState.NEW); } +export function isPreDeploymentWithConfig( + d: AllDeploymentTypes | undefined, +): d is PreDeploymentWithConfig { + return Boolean( + d && + d.state === DeploymentState.NEW && + (d as PreDeploymentWithConfig).configurationName !== undefined, + ); +} + export function isSuccessfulPreDeployment( d: AllDeploymentTypes | undefined, ): d is PreDeployment {