Skip to content

Commit

Permalink
chore: implement ask for secret init (aws#2271)
Browse files Browse the repository at this point in the history
Proceeding PR: aws#2266 

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
  • Loading branch information
Lou1415926 authored May 7, 2021
1 parent 355cf83 commit c585dfa
Show file tree
Hide file tree
Showing 2 changed files with 294 additions and 2 deletions.
104 changes: 102 additions & 2 deletions internal/pkg/cli/secret_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,26 @@ package cli
import (
"fmt"

"github.com/aws/copilot-cli/internal/pkg/term/color"

"github.com/spf13/afero"
"github.com/spf13/cobra"

"github.com/aws/copilot-cli/internal/pkg/config"
"github.com/aws/copilot-cli/internal/pkg/term/log"
"github.com/aws/copilot-cli/internal/pkg/term/prompt"
"github.com/aws/copilot-cli/internal/pkg/term/selector"
)

const (
secretInitAppPrompt = "Which application do you want to add the secret to?"
secretInitAppPromptHelp = "The secret can then be versioned by your existing environments inside the application."

secretInitSecretNamePrompt = "What would you like to name this secret?"
secretInitSecretNamePromptHelp = "The name of the secret, such as 'db_password'."

fmtSecretInitSecretValuePrompt = "What is the value of secret %s in environment %s?"
fmtSecretInitSecretValuePromptHelp = "If you do not wish to add the secret %s to environment %s, you can leave this blank by pressing 'Enter' without entering any value."
)

type secretInitVars struct {
Expand All @@ -28,6 +44,9 @@ type secretInitOpts struct {

store store
fs afero.Fs

prompter prompter
selector appSelector
}

func newSecretInitOpts(vars secretInitVars) (*secretInitOpts, error) {
Expand All @@ -36,14 +55,19 @@ func newSecretInitOpts(vars secretInitVars) (*secretInitOpts, error) {
return nil, fmt.Errorf("new config store: %w", err)
}

prompter := prompt.New()
opts := secretInitOpts{
secretInitVars: vars,
store: store,
fs: &afero.Afero{Fs: afero.NewOsFs()},

prompter: prompter,
selector: selector.NewSelect(prompter, store),
}
return &opts, nil
}

// Validate returns an error if the flag values passed by the user are invalid.
func (o *secretInitOpts) Validate() error {
if o.appName != "" {
_, err := o.store.GetApplication(o.appName)
Expand Down Expand Up @@ -74,21 +98,97 @@ func (o *secretInitOpts) Validate() error {
return nil
}

// Ask prompts the user for any required or important fields that are not provided.
func (o *secretInitOpts) Ask() error {
if o.overwrite {
log.Infof("You have specified %s flag. Please note that overwriting an existing secret may break your deployed service.\n", color.HighlightCode("--overwrite"))
}
if err := o.askForAppName(); err != nil {
return err
}
if err := o.askForSecretName(); err != nil {
return err
}
if err := o.askForSecretValues(); err != nil {
return err
}
return nil
}

// Execute creates the secrets.
func (o *secretInitOpts) Execute() error {
return nil
}

// BuildSecretInitCmd build the command for creating or updating a new secret.
func (o *secretInitOpts) askForAppName() error {
if o.appName != "" {
return nil
}

app, err := o.selector.Application(secretInitAppPrompt, secretInitAppPromptHelp)
if err != nil {
return fmt.Errorf("ask for an application to add the secret to: %w", err)
}
o.appName = app
return nil
}

func (o *secretInitOpts) askForSecretName() error {
if o.name != "" {
return nil
}

name, err := o.prompter.Get(secretInitSecretNamePrompt,
secretInitSecretNamePromptHelp,
validateSecretName,
prompt.WithFinalMessage("secret name: "))
if err != nil {
return fmt.Errorf("ask for the secret name: %w", err)
}

o.name = name
return nil
}

func (o *secretInitOpts) askForSecretValues() error {
if o.values != nil {
return nil
}

envs, err := o.store.ListEnvironments(o.appName)
if err != nil {
return fmt.Errorf("list environments in app %s: %w", o.appName, err)
}

if len(envs) == 0 {
log.Errorf("Secrets environment-level resources. Please run %s before running %s.\n",
color.HighlightCode("copilot env init"),
color.HighlightCode("copilot secret init"))
return fmt.Errorf("no environment is found in app %s", o.appName)
}

values := make(map[string]string)
for _, env := range envs {
value, err := o.prompter.GetSecret(
fmt.Sprintf(fmtSecretInitSecretValuePrompt, color.HighlightUserInput(o.name), env.Name),
fmt.Sprintf(fmtSecretInitSecretValuePromptHelp, color.HighlightUserInput(o.name), env.Name))
if err != nil {
return fmt.Errorf("get secret value for %s in environment %s: %w", color.HighlightUserInput(o.name), env.Name, err)
}

values[env.Name] = value
}
o.values = values
return nil
}

// BuildSecretInitCmd build the command for creating a new secret or updating an existing one.
func BuildSecretInitCmd() *cobra.Command {
vars := secretInitVars{}
cmd := &cobra.Command{
Use: "init",
Short: "Create or update an SSM SecureString parameter.",
Example: `secret init`,
Example: ``, // TODO
RunE: runCmdE(func(cmd *cobra.Command, args []string) error {
opts, err := newSecretInitOpts(vars)
if err != nil {
Expand Down
192 changes: 192 additions & 0 deletions internal/pkg/cli/secret_init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package cli

import (
"errors"
"fmt"
"testing"

"github.com/aws/copilot-cli/internal/pkg/config"
Expand Down Expand Up @@ -130,3 +131,194 @@ func TestSecretInitOpts_Validate(t *testing.T) {
})
}
}

type secretInitAskMocks struct {
mockStore *mocks.Mockstore
mockPrompter *mocks.Mockprompter
mockSelector *mocks.MockappSelector
}

func TestSecretInitOpts_Ask(t *testing.T) {
var (
wantedName = "db-password"
wantedApp = "my-app"
wantedValues = map[string]string{
"test": "test-password",
"dev": "dev-password",
"prod": "prod-password",
}
wantedVars = secretInitVars{
appName: wantedApp,
name: wantedName,
values: wantedValues,
}
)
testCases := map[string]struct {
inAppName string
inName string
inValues map[string]string

setupMocks func(m secretInitAskMocks)

wantedVars secretInitVars
wantedError error
}{
"prompt to select an app if not specified": {
inName: wantedName,
inValues: wantedValues,
setupMocks: func(m secretInitAskMocks) {
m.mockSelector.EXPECT().Application(secretInitAppPrompt, gomock.Any()).Return(wantedApp, nil)
},
wantedVars: wantedVars,
},
"error prompting to select an app": {
setupMocks: func(m secretInitAskMocks) {
m.mockSelector.EXPECT().Application(secretInitAppPrompt, gomock.Any()).Return("", errors.New("some error"))
},
wantedError: errors.New("ask for an application to add the secret to: some error"),
},
"do not prompt for app if specified": {
inAppName: wantedApp,
inName: wantedName,
inValues: wantedValues,
setupMocks: func(m secretInitAskMocks) {
m.mockSelector.EXPECT().Application(gomock.Any(), gomock.Any()).Times(0)
},
wantedVars: secretInitVars{
appName: wantedApp,
name: wantedName,
values: wantedValues,
},
},
"ask for a secret name if not specified": {
inAppName: wantedApp,
inValues: wantedValues,
setupMocks: func(m secretInitAskMocks) {
m.mockPrompter.EXPECT().Get(secretInitSecretNamePrompt, gomock.Any(), gomock.Any(), gomock.Any()).
Return("db-password", nil)
},
wantedVars: wantedVars,
},
"error prompting for a secret name": {
inAppName: wantedApp,
inValues: wantedValues,
setupMocks: func(m secretInitAskMocks) {
m.mockPrompter.EXPECT().Get(secretInitSecretNamePrompt, gomock.Any(), gomock.Any(), gomock.Any()).
Return("", errors.New("some error"))
},
wantedError: errors.New("ask for the secret name: some error"),
},
"do not ask for a secret name if specified": {
inName: "db-password",
inAppName: wantedApp,
inValues: wantedValues,
setupMocks: func(m secretInitAskMocks) {
m.mockPrompter.EXPECT().Get(secretInitSecretNamePrompt, gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
},
wantedVars: wantedVars,
},
"ask for values for each existing environment if not specified": {
inAppName: wantedApp,
inName: wantedName,
setupMocks: func(m secretInitAskMocks) {
m.mockStore.EXPECT().ListEnvironments("my-app").Return([]*config.Environment{
{
Name: "test",
},
{
Name: "dev",
},
{
Name: "prod",
},
}, nil)
m.mockPrompter.EXPECT().GetSecret(fmt.Sprintf(fmtSecretInitSecretValuePrompt, "db-password", "test"), gomock.Any()).
Return("test-password", nil)
m.mockPrompter.EXPECT().GetSecret(fmt.Sprintf(fmtSecretInitSecretValuePrompt, "db-password", "dev"), gomock.Any()).
Return("dev-password", nil)
m.mockPrompter.EXPECT().GetSecret(fmt.Sprintf(fmtSecretInitSecretValuePrompt, "db-password", "prod"), gomock.Any()).
Return("prod-password", nil)
},
wantedVars: wantedVars,
},
"error listing environments": {
inAppName: wantedApp,
inName: wantedName,
setupMocks: func(m secretInitAskMocks) {
m.mockStore.EXPECT().ListEnvironments("my-app").Return(nil, errors.New("some error"))
},
wantedError: errors.New("list environments in app my-app: some error"),
},
"error prompting for values": {
inAppName: wantedApp,
inName: wantedName,
setupMocks: func(m secretInitAskMocks) {
m.mockStore.EXPECT().ListEnvironments("my-app").Return([]*config.Environment{
{
Name: "test",
},
{
Name: "dev",
},
{
Name: "prod",
},
}, nil)
m.mockPrompter.EXPECT().GetSecret(fmt.Sprintf(fmtSecretInitSecretValuePrompt, "db-password", "test"), gomock.Any()).
Return("", errors.New("some error"))
m.mockPrompter.EXPECT().GetSecret(fmt.Sprintf(fmtSecretInitSecretValuePrompt, "db-password", "dev"), gomock.Any()).MinTimes(0).MaxTimes(1)
m.mockPrompter.EXPECT().GetSecret(fmt.Sprintf(fmtSecretInitSecretValuePrompt, "db-password", "prod"), gomock.Any()).MinTimes(0).MaxTimes(1)
},
wantedError: errors.New("get secret value for db-password in environment test: some error"),
},
"error if no env is found": {
inAppName: wantedApp,
inName: wantedName,
setupMocks: func(m secretInitAskMocks) {
m.mockStore.EXPECT().ListEnvironments(wantedApp).Return([]*config.Environment{}, nil)
},
wantedError: errors.New("no environment is found in app my-app"),
},
"do not ask for values if specified": {
inAppName: wantedApp,
inName: wantedName,
inValues: wantedValues,
setupMocks: func(m secretInitAskMocks) {},
wantedVars: wantedVars,
},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

m := secretInitAskMocks{
mockPrompter: mocks.NewMockprompter(ctrl),
mockSelector: mocks.NewMockappSelector(ctrl),
mockStore: mocks.NewMockstore(ctrl),
}

opts := secretInitOpts{
secretInitVars: secretInitVars{
appName: tc.inAppName,
name: tc.inName,
values: tc.inValues,
},
prompter: m.mockPrompter,
store: m.mockStore,
selector: m.mockSelector,
}

tc.setupMocks(m)

err := opts.Ask()
if tc.wantedError == nil {
require.NoError(t, err)
require.Equal(t, tc.wantedVars, opts.secretInitVars)
} else {
require.EqualError(t, tc.wantedError, err.Error())
}
})
}
}

0 comments on commit c585dfa

Please sign in to comment.