Skip to content

Commit

Permalink
Merge pull request #2273 from posit-dev/dotnomad/deploy-with-secrets
Browse files Browse the repository at this point in the history
Add ability to send secrets to `POST /api/deployments/$NAME` endpoint
  • Loading branch information
dotNomad committed Sep 18, 2024
2 parents b7c6d0e + e085f8b commit 4044d1c
Show file tree
Hide file tree
Showing 10 changed files with 310 additions and 18 deletions.
2 changes: 1 addition & 1 deletion cmd/publisher/commands/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func (cmd *DeployCmd) Run(args *cli_types.CommonArgs, ctx *cli_types.CLIContext)
if err != nil {
return err
}
stateStore, err := state.New(absPath, cmd.AccountName, cmd.ConfigName, "", cmd.SaveName, ctx.Accounts)
stateStore, err := state.New(absPath, cmd.AccountName, cmd.ConfigName, "", cmd.SaveName, ctx.Accounts, nil)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/publisher/commands/redeploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (cmd *RedeployCmd) Run(args *cli_types.CommonArgs, ctx *cli_types.CLIContex
if err != nil {
return fmt.Errorf("invalid deployment name '%s': %w", cmd.TargetName, err)
}
stateStore, err := state.New(absPath, "", cmd.ConfigName, cmd.TargetName, "", ctx.Accounts)
stateStore, err := state.New(absPath, "", cmd.ConfigName, cmd.TargetName, "", ctx.Accounts, nil)
if err != nil {
return err
}
Expand Down
9 changes: 9 additions & 0 deletions internal/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ type Config struct {
Connect *Connect `toml:"connect,omitempty" json:"connect,omitempty"`
}

func (c *Config) HasSecret(secret string) bool {
for _, s := range c.Secrets {
if s == secret {
return true
}
}
return false
}

type Environment = map[string]string

type Python struct {
Expand Down
43 changes: 43 additions & 0 deletions internal/config/types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package config

// Copyright (C) 2024 by Posit Software, PBC.

import (
"testing"

"github.com/stretchr/testify/suite"
)

type ConfigHasSecretSuite struct {
suite.Suite
}

func TestConfig_HasSecret(t *testing.T) {
suite.Run(t, new(ConfigHasSecretSuite))
}

func (s *ConfigHasSecretSuite) TestSecretExists() {
c := &Config{
Secrets: []string{"SECRET1", "SECRET2", "SECRET3"},
}
s.True(c.HasSecret("SECRET2"))
}

func (s *ConfigHasSecretSuite) TestSecretDoesNotExist() {
c := &Config{
Secrets: []string{"SECRET1", "SECRET2", "SECRET3"},
}
s.False(c.HasSecret("SECRET4"))
}

func (s *ConfigHasSecretSuite) TestEmptySecretsList() {
c := &Config{}
s.False(c.HasSecret("SECRET1"))
}

func (s *ConfigHasSecretSuite) TestCaseSensitiveCheck() {
c := &Config{
Secrets: []string{"SECRET1", "SECRET2", "SECRET3"},
}
s.False(c.HasSecret("secret2"))
}
17 changes: 15 additions & 2 deletions internal/publish/set_env_vars.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package publish
// Copyright (C) 2023 by Posit Software, PBC.

import (
"maps"

"github.com/posit-dev/publisher/internal/clients/connect"
"github.com/posit-dev/publisher/internal/events"
"github.com/posit-dev/publisher/internal/logging"
Expand All @@ -17,7 +19,8 @@ func (p *defaultPublisher) setEnvVars(
contentID types.ContentID) error {

env := p.Config.Environment
if len(env) == 0 {
secrets := p.Secrets
if len(env) == 0 && len(secrets) == 0 {
return nil
}

Expand All @@ -30,7 +33,17 @@ func (p *defaultPublisher) setEnvVars(
for name, value := range env {
log.Info("Setting environment variable", "name", name, "value", value)
}
err := client.SetEnvVars(contentID, env, log)

for name := range secrets {
log.Info("Setting secret as environment variable", "name", name)
}

// Combine env and secrets into one environment for Connect
combinedEnv := make(map[string]string)
maps.Copy(combinedEnv, env)
maps.Copy(combinedEnv, secrets)

err := client.SetEnvVars(contentID, combinedEnv, log)
if err != nil {
return types.OperationError(op, err)
}
Expand Down
115 changes: 115 additions & 0 deletions internal/publish/set_env_vars_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package publish

// Copyright (C) 2024 by Posit Software, PBC.

import (
"testing"

"github.com/posit-dev/publisher/internal/clients/connect"
"github.com/posit-dev/publisher/internal/events"
"github.com/posit-dev/publisher/internal/logging"
"github.com/posit-dev/publisher/internal/state"
"github.com/posit-dev/publisher/internal/types"
"github.com/posit-dev/publisher/internal/util/utiltest"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)

type SetEnvVarsSuite struct {
utiltest.Suite
}

func TestSetEnvVarsSuite(t *testing.T) {
suite.Run(t, new(SetEnvVarsSuite))
}

func (s *SetEnvVarsSuite) TestSetEnvVarsWithNoEnvironmentOrSecrets() {
stateStore := state.Empty()
log := logging.New()
emitter := events.NewCapturingEmitter()

publisher := &defaultPublisher{
State: stateStore,
log: log,
emitter: emitter,
}
client := connect.NewMockClient()

err := publisher.setEnvVars(client, types.ContentID("test-content-id"))
s.NoError(err)

// No calls to the Connect API to set environment variables should be made
s.Equal(0, len(client.Calls))
}

func (s *SetEnvVarsSuite) TestSetEnvVarsWithSecrets() {
stateStore := state.Empty()
log := logging.New()
emitter := events.NewCapturingEmitter()

stateStore.Secrets = map[string]string{"SOME_SECRET": "some-secret-value", "ANOTHER_SECRET": "another-secret-value"}

publisher := &defaultPublisher{
State: stateStore,
log: log,
emitter: emitter,
}
client := connect.NewMockClient()

client.On("SetEnvVars", types.ContentID("test-content-id"), stateStore.Secrets, mock.Anything).Return(nil)

err := publisher.setEnvVars(client, types.ContentID("test-content-id"))
s.NoError(err)

client.AssertExpectations(s.T())
}

func (s *SetEnvVarsSuite) TestSetEnvVarsWithEnvironment() {
stateStore := state.Empty()
log := logging.New()
emitter := events.NewCapturingEmitter()

stateStore.Config.Environment = map[string]string{"TEST_ENV_VAR": "test-value", "ANOTHER_TEST_ENV_VAR": "another-test-value"}

publisher := &defaultPublisher{
State: stateStore,
log: log,
emitter: emitter,
}
client := connect.NewMockClient()

client.On("SetEnvVars", types.ContentID("test-content-id"), stateStore.Config.Environment, mock.Anything).Return(nil)

err := publisher.setEnvVars(client, types.ContentID("test-content-id"))
s.NoError(err)

client.AssertExpectations(s.T())
}

func (s *SetEnvVarsSuite) TestSetEnvVarsWithSecretsAndEnvironment() {
stateStore := state.Empty()
stateStore.Config.Environment = map[string]string{"TEST_ENV_VAR": "test-value", "ANOTHER_TEST_ENV_VAR": "another-test-value"}
stateStore.Secrets = map[string]string{"SOME_SECRET": "some-secret-value", "ANOTHER_SECRET": "another-secret-value"}
log := logging.New()
emitter := events.NewCapturingEmitter()

publisher := &defaultPublisher{
State: stateStore,
log: log,
emitter: emitter,
}
client := connect.NewMockClient()

combinedEnv := map[string]string{
"TEST_ENV_VAR": "test-value",
"ANOTHER_TEST_ENV_VAR": "another-test-value",
"SOME_SECRET": "some-secret-value",
"ANOTHER_SECRET": "another-secret-value",
}
client.On("SetEnvVars", types.ContentID("test-content-id"), combinedEnv, mock.Anything).Return(nil)

err := publisher.setEnvVars(client, types.ContentID("test-content-id"))
s.NoError(err)

client.AssertExpectations(s.T())
}
8 changes: 4 additions & 4 deletions internal/services/api/post_deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ import (
)

type PostDeploymentRequestBody struct {
AccountName string `json:"account"`
ConfigName string `json:"config"`
AccountName string `json:"account"`
ConfigName string `json:"config"`
Secrets map[string]string `json:"secrets,omitempty"`
}

type PostDeploymentsReponse struct {
Expand Down Expand Up @@ -54,9 +55,8 @@ func PostDeploymentHandlerFunc(
InternalError(w, req, log, err)
return
}
newState, err := stateFactory(projectDir, b.AccountName, b.ConfigName, name, "", accountList)
newState, err := stateFactory(projectDir, b.AccountName, b.ConfigName, name, "", accountList, b.Secrets)
log.Debug("New account derived state created", "account", b.AccountName, "config", b.ConfigName)

if err != nil {
if errors.Is(err, accounts.ErrAccountNotFound) {
NotFound(w, log, err)
Expand Down
67 changes: 63 additions & 4 deletions internal/services/api/post_deployment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ func (s *PostDeploymentHandlerFuncSuite) TestPostDeploymentHandlerFunc() {
stateFactory = func(
path util.AbsolutePath,
accountName, configName, targetName, saveName string,
accountList accounts.AccountList) (*state.State, error) {
accountList accounts.AccountList,
secrets map[string]string) (*state.State, error) {

s.Equal(s.cwd, path)
s.Equal("myTargetName", targetName)
Expand Down Expand Up @@ -121,13 +122,16 @@ func (s *PostDeploymentHandlerFuncSuite) TestPostDeploymentHandlerFuncStateErr()
stateFactory = func(
path util.AbsolutePath,
accountName, configName, targetName, saveName string,
accountList accounts.AccountList) (*state.State, error) {
accountList accounts.AccountList,
secrets map[string]string) (*state.State, error) {
return nil, errors.New("test error from state factory")
}

handler := PostDeploymentHandlerFunc(s.cwd, log, nil, events.NewNullEmitter())
handler(rec, req)
s.Equal(http.StatusBadRequest, rec.Result().StatusCode)
body, _ := io.ReadAll(rec.Body)
s.Contains(string(body), "test error from state factory")
}

func (s *PostDeploymentHandlerFuncSuite) TestPostDeploymentHandlerFuncWrongServer() {
Expand Down Expand Up @@ -183,7 +187,8 @@ func (s *PostDeploymentHandlerFuncSuite) TestPostDeploymentHandlerFuncPublishErr
stateFactory = func(
path util.AbsolutePath,
accountName, configName, targetName, saveName string,
accountList accounts.AccountList) (*state.State, error) {
accountList accounts.AccountList,
secrets map[string]string) (*state.State, error) {

st := state.Empty()
st.Account = &accounts.Account{}
Expand Down Expand Up @@ -235,7 +240,8 @@ func (s *PostDeploymentHandlerFuncSuite) TestPostDeploymentSubdir() {
stateFactory = func(
path util.AbsolutePath,
accountName, configName, targetName, saveName string,
accountList accounts.AccountList) (*state.State, error) {
accountList accounts.AccountList,
secrets map[string]string) (*state.State, error) {

s.Equal(s.cwd, path)
s.Equal("myTargetName", targetName)
Expand All @@ -253,3 +259,56 @@ func (s *PostDeploymentHandlerFuncSuite) TestPostDeploymentSubdir() {

s.Equal(http.StatusAccepted, rec.Result().StatusCode)
}

func (s *PostDeploymentHandlerFuncSuite) TestPostDeploymentHandlerFuncWithSecrets() {
log := logging.New()

rec := httptest.NewRecorder()
req, err := http.NewRequest("POST", "/api/deployments/myTargetName", nil)
s.NoError(err)
req = mux.SetURLVars(req, map[string]string{"name": "myTargetName"})

lister := &accounts.MockAccountList{}
req.Body = io.NopCloser(strings.NewReader(
`{
"account": "local",
"config": "default",
"secrets": {
"API_KEY": "secret123",
"DB_PASSWORD": "password456"
}
}`))

publisher := &mockPublisher{}
publisher.On("PublishDirectory", mock.Anything).Return(nil)
publisherFactory = func(*state.State, events.Emitter, logging.Logger) (publish.Publisher, error) {
return publisher, nil
}

stateFactory = func(
path util.AbsolutePath,
accountName, configName, targetName, saveName string,
accountList accounts.AccountList,
secrets map[string]string) (*state.State, error) {

s.Equal(s.cwd, path)
s.Equal("myTargetName", targetName)
s.Equal("local", accountName)
s.Equal("default", configName)
s.Equal("", saveName)
s.Equal(map[string]string{
"API_KEY": "secret123",
"DB_PASSWORD": "password456",
}, secrets)

st := state.Empty()
st.Account = &accounts.Account{}
st.Target = deployment.New()
return st, nil
}

handler := PostDeploymentHandlerFunc(s.cwd, log, lister, events.NewNullEmitter())
handler(rec, req)

s.Equal(http.StatusAccepted, rec.Result().StatusCode)
}
12 changes: 11 additions & 1 deletion internal/state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type State struct {
Config *config.Config
Target *deployment.Deployment
LocalID LocalDeploymentID
Secrets map[string]string
}

func loadConfig(path util.AbsolutePath, configName string) (*config.Config, error) {
Expand Down Expand Up @@ -95,7 +96,7 @@ func Empty() *State {

var ErrServerURLMismatch = errors.New("the account provided is for a different server; it must match the server for this deployment")

func New(path util.AbsolutePath, accountName, configName, targetName string, saveName string, accountList accounts.AccountList) (*State, error) {
func New(path util.AbsolutePath, accountName, configName, targetName string, saveName string, accountList accounts.AccountList, secrets map[string]string) (*State, error) {
var target *deployment.Deployment
var account *accounts.Account
var cfg *config.Config
Expand Down Expand Up @@ -143,6 +144,14 @@ func New(path util.AbsolutePath, accountName, configName, targetName string, sav
return nil, err
}
}

// Check that the secrets passed are in the config
for secret := range secrets {
if !cfg.HasSecret(secret) {
return nil, fmt.Errorf("secret '%s' is not in the configuration", secret)
}
}

return &State{
Dir: path,
AccountName: accountName,
Expand All @@ -152,6 +161,7 @@ func New(path util.AbsolutePath, accountName, configName, targetName string, sav
Account: account,
Config: cfg,
Target: target,
Secrets: secrets,
}, nil
}

Expand Down
Loading

0 comments on commit 4044d1c

Please sign in to comment.