Skip to content

Commit 6da074e

Browse files
authored
feat(creds): Add edit credentials command (#921)
* feat(creds): Add edit credentials command * ensure EDITOR env var is checked * Edit the entire credential set instead of picking each one * fix spaces in EDITOR not opening editor correctly, e.g. using code.exe --wait * add test, change editor to use context.CommandBuilder so it works * add test for windows editor path * Changes from feedback: - shift editor into separate package, get it through editor.New() - embed the context struct into editor directly - use context.FileSystem instead of ioutil/os calls - fix editor to split on spaces, add comment on why and examples - remove "tempStrategy" stuff, edit the actual credentials directly - shift windows-specific check into conditional compilation with editor_windows.go, editor_nix.go etc * fix parsing of editor command, pass this to the shell to do it for us * fix tests * remove restriction on editing an empty credential set (it's possible) * use yaml extension for temp credential file for editor syntax, colors etc * minor variable naming, use Wrap instead of Wrapf, and unnecessary byte casting * minor tweaks to comment * validate source keys are correct after editing credentials * use multierrors for validation errors * Adding tests for CredentialStorage.Validate * show some slightly friendlier errors
1 parent 951ad46 commit 6da074e

11 files changed

+318
-8
lines changed

cmd/porter/credentials.go

+7-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package main
22

33
import (
44
"get.porter.sh/porter/pkg/porter"
5-
"github.com/pkg/errors"
65
"github.com/spf13/cobra"
76
)
87

@@ -24,15 +23,17 @@ func buildCredentialsCommands(p *porter.Porter) *cobra.Command {
2423
}
2524

2625
func buildCredentialsEditCommand(p *porter.Porter) *cobra.Command {
26+
opts := porter.CredentialEditOptions{}
27+
2728
cmd := &cobra.Command{
28-
Use: "edit",
29-
Short: "Edit Credential",
30-
Hidden: true,
29+
Use: "edit",
30+
Short: "Edit Credential",
31+
Long: `Edit a named credential set.`,
3132
PreRunE: func(cmd *cobra.Command, args []string) error {
32-
return nil
33+
return opts.Validate(args)
3334
},
3435
RunE: func(cmd *cobra.Command, args []string) error {
35-
return errors.New("Not implemented")
36+
return p.EditCredential(opts)
3637
},
3738
}
3839
return cmd

docs/content/cli/credentials.md

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Credentials commands
2727

2828
* [porter](/cli/porter/) - I am porter 👩🏽‍✈️, the friendly neighborhood CNAB authoring tool
2929
* [porter credentials delete](/cli/porter_credentials_delete/) - Delete a Credential
30+
* [porter credentials edit](/cli/porter_credentials_edit/) - Edit Credential
3031
* [porter credentials generate](/cli/porter_credentials_generate/) - Generate Credential Set
3132
* [porter credentials list](/cli/porter_credentials_list/) - List credentials
3233
* [porter credentials show](/cli/porter_credentials_show/) - Show a Credential

docs/content/cli/credentials_edit.md

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
title: "porter credentials edit"
3+
slug: porter_credentials_edit
4+
url: /cli/porter_credentials_edit/
5+
---
6+
## porter credentials edit
7+
8+
Edit Credential
9+
10+
### Synopsis
11+
12+
Edit a named credential set.
13+
14+
```
15+
porter credentials edit [flags]
16+
```
17+
18+
### Options
19+
20+
```
21+
-h, --help help for edit
22+
```
23+
24+
### Options inherited from parent commands
25+
26+
```
27+
--debug Enable debug logging
28+
```
29+
30+
### SEE ALSO
31+
32+
* [porter credentials](/cli/porter_credentials/) - Credentials commands
33+

pkg/credentials/credentialProvider.go

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
type CredentialProvider interface {
99
CredentialStore
1010
ResolveAll(creds credentials.CredentialSet) (credentials.Set, error)
11+
Validate(credentials.CredentialSet) error
1112
}
1213

1314
// CredentialStore is an interface representing cnab-go's credentials.Store

pkg/credentials/credentialStorage.go

+28
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package credentials
22

33
import (
4+
"fmt"
5+
"strings"
6+
47
"get.porter.sh/porter/pkg/config"
58
"get.porter.sh/porter/pkg/secrets"
69
secretplugins "get.porter.sh/porter/pkg/secrets/pluginstore"
710
crudplugins "get.porter.sh/porter/pkg/storage/pluginstore"
811
"github.com/cnabio/cnab-go/credentials"
912
cnabsecrets "github.com/cnabio/cnab-go/secrets"
13+
"github.com/cnabio/cnab-go/secrets/host"
1014
"github.com/hashicorp/go-multierror"
1115
"github.com/pkg/errors"
1216
)
@@ -49,3 +53,27 @@ func (s CredentialStorage) ResolveAll(creds credentials.CredentialSet) (credenti
4953

5054
return resolvedCreds, resolveErrors
5155
}
56+
57+
func (s CredentialStorage) Validate(creds credentials.CredentialSet) error {
58+
validSources := []string{"secret", host.SourceValue, host.SourceEnv, host.SourcePath, host.SourceCommand}
59+
var errors error
60+
61+
for _, cs := range creds.Credentials {
62+
valid := false
63+
for _, validSource := range validSources {
64+
if cs.Source.Key == validSource {
65+
valid = true
66+
break
67+
}
68+
}
69+
if valid == false {
70+
errors = multierror.Append(errors, fmt.Errorf(
71+
"%s is not a valid source. Valid sources are: %s",
72+
cs.Source.Key,
73+
strings.Join(validSources, ", "),
74+
))
75+
}
76+
}
77+
78+
return errors
79+
}
+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package credentials
2+
3+
import (
4+
"testing"
5+
6+
"github.com/cnabio/cnab-go/credentials"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestCredentialStorage_Validate_GoodSources(t *testing.T) {
11+
s := CredentialStorage{}
12+
testCreds := credentials.CredentialSet{
13+
Credentials: []credentials.CredentialStrategy{
14+
{
15+
Source: credentials.Source{
16+
Key: "env",
17+
Value: "SOME_ENV",
18+
},
19+
},
20+
{
21+
Source: credentials.Source{
22+
Key: "value",
23+
Value: "somevalue",
24+
},
25+
},
26+
},
27+
}
28+
29+
err := s.Validate(testCreds)
30+
require.NoError(t, err, "Validate did not return errors")
31+
}
32+
33+
func TestCredentialStorage_Validate_BadSources(t *testing.T) {
34+
s := CredentialStorage{}
35+
testCreds := credentials.CredentialSet{
36+
Credentials: []credentials.CredentialStrategy{
37+
{
38+
Source: credentials.Source{
39+
Key: "wrongthing",
40+
Value: "SOME_ENV",
41+
},
42+
},
43+
{
44+
Source: credentials.Source{
45+
Key: "anotherwrongthing",
46+
Value: "somevalue",
47+
},
48+
},
49+
},
50+
}
51+
52+
err := s.Validate(testCreds)
53+
require.Error(t, err, "Validate returned errors")
54+
}

pkg/editor/editor.go

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package editor
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
8+
"get.porter.sh/porter/pkg/context"
9+
)
10+
11+
// Editor displays content to a user using an external text editor, like vi or notepad.
12+
// The content is captured and returned.
13+
//
14+
// The `EDITOR` environment variable is checked to find an editor.
15+
// Failing that, use some sensible default depending on the operating system.
16+
//
17+
// This is useful for editing things like configuration files, especially those
18+
// that might be stored on a remote server. For example: the content could be retrieved
19+
// from the remote store, edited locally, then saved back.
20+
type Editor struct {
21+
*context.Context
22+
contents []byte
23+
tempFilename string
24+
}
25+
26+
// New returns a new Editor with the temp filename and contents provided.
27+
func New(context *context.Context, tempFilename string, contents []byte) *Editor {
28+
return &Editor{
29+
Context: context,
30+
tempFilename: tempFilename,
31+
contents: contents,
32+
}
33+
}
34+
35+
func editorArgs(filename string) []string {
36+
shell := defaultShell
37+
if os.Getenv("SHELL") != "" {
38+
shell = os.Getenv("SHELL")
39+
}
40+
editor := defaultEditor
41+
if os.Getenv("EDITOR") != "" {
42+
editor = os.Getenv("EDITOR")
43+
}
44+
45+
// Example of what will be run:
46+
// on *nix: sh -c "vi /tmp/test.txt"
47+
// on windows: cmd /C "C:\Program Files\Visual Studio Code\Code.exe --wait C:\somefile.txt"
48+
//
49+
// Pass the editor command to the shell so we don't have to parse the command ourselves.
50+
// Passing the editor command that could possibly have an argument (e.g. --wait for VSCode) to the
51+
// shell means we don't have to parse this ourselves, like splitting on spaces.
52+
return []string{shell, shellCommandFlag, fmt.Sprintf("%s %s", editor, filename)}
53+
}
54+
55+
// Run opens the editor, displaying the contents through a temporary file.
56+
// The content is returned once the editor closes.
57+
func (e *Editor) Run() ([]byte, error) {
58+
tempFile, err := e.FileSystem.OpenFile(filepath.Join(os.TempDir(), e.tempFilename), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
59+
if err != nil {
60+
return nil, err
61+
}
62+
defer e.FileSystem.Remove(tempFile.Name())
63+
64+
_, err = tempFile.Write(e.contents)
65+
if err != nil {
66+
return nil, err
67+
}
68+
69+
// close here without defer so cmd can grab the file
70+
tempFile.Close()
71+
72+
args := editorArgs(tempFile.Name())
73+
cmd := e.NewCommand(args[0], args[1:]...)
74+
cmd.Stdout = e.Out
75+
cmd.Stderr = e.Err
76+
cmd.Stdin = e.In
77+
err = cmd.Run()
78+
if err != nil {
79+
return nil, err
80+
}
81+
82+
contents, err := e.FileSystem.ReadFile(tempFile.Name())
83+
if err != nil {
84+
return nil, err
85+
}
86+
87+
return contents, nil
88+
}

pkg/editor/editor_nix.go

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// +build !windows
2+
3+
package editor
4+
5+
const defaultEditor = "vi"
6+
const defaultShell = "sh"
7+
const shellCommandFlag = "-c"

pkg/editor/editor_windows.go

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// +build windows
2+
3+
package editor
4+
5+
const defaultEditor = "notepad"
6+
const defaultShell = "cmd"
7+
const shellCommandFlag = "/C"

pkg/porter/credentials.go

+53-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77

88
"get.porter.sh/porter/pkg/context"
99
"get.porter.sh/porter/pkg/credentialsgenerator"
10+
"get.porter.sh/porter/pkg/editor"
1011
"get.porter.sh/porter/pkg/printer"
12+
"gopkg.in/yaml.v2"
1113

1214
dtprinter "github.com/carolynvs/datetime-printer"
1315
credentials "github.com/cnabio/cnab-go/credentials"
@@ -22,6 +24,10 @@ type CredentialShowOptions struct {
2224
Name string
2325
}
2426

27+
type CredentialEditOptions struct {
28+
Name string
29+
}
30+
2531
// ListCredentials lists saved credential sets.
2632
func (p *Porter) ListCredentials(opts ListOptions) error {
2733
creds, err := p.Credentials.ReadAll()
@@ -137,7 +143,7 @@ func (p *Porter) GenerateCredentials(opts CredentialOptions) error {
137143
return errors.Wrapf(err, "unable to save credentials")
138144
}
139145

140-
// Validate validates the args provided Porter's credential show command
146+
// Validate validates the args provided to Porter's credential show command
141147
func (o *CredentialShowOptions) Validate(args []string) error {
142148
if err := validateCredentialName(args); err != nil {
143149
return err
@@ -146,6 +152,52 @@ func (o *CredentialShowOptions) Validate(args []string) error {
146152
return o.ParseFormat()
147153
}
148154

155+
// Validate validates the args provided to Porter's credential edit command
156+
func (o *CredentialEditOptions) Validate(args []string) error {
157+
if err := validateCredentialName(args); err != nil {
158+
return err
159+
}
160+
o.Name = args[0]
161+
return nil
162+
}
163+
164+
// EditCredential edits the credentials of the provided name.
165+
func (p *Porter) EditCredential(opts CredentialEditOptions) error {
166+
credSet, err := p.Credentials.Read(opts.Name)
167+
if err != nil {
168+
return err
169+
}
170+
171+
contents, err := yaml.Marshal(credSet)
172+
if err != nil {
173+
return errors.Wrap(err, "unable to load credentials")
174+
}
175+
176+
editor := editor.New(p.Context, fmt.Sprintf("porter-%s.yaml", credSet.Name), contents)
177+
output, err := editor.Run()
178+
if err != nil {
179+
return errors.Wrap(err, "unable to open editor to edit credentials")
180+
}
181+
182+
err = yaml.Unmarshal(output, &credSet)
183+
if err != nil {
184+
return errors.Wrap(err, "unable to process credentials")
185+
}
186+
187+
err = p.Credentials.Validate(credSet)
188+
if err != nil {
189+
return errors.Wrap(err, "credentials are invalid")
190+
}
191+
192+
credSet.Modified = time.Now()
193+
err = p.Credentials.Save(credSet)
194+
if err != nil {
195+
return errors.Wrap(err, "unable to save credentials")
196+
}
197+
198+
return nil
199+
}
200+
149201
// ShowCredential shows the credential set corresponding to the provided name, using
150202
// the provided printer.PrintOptions for display.
151203
func (p *Porter) ShowCredential(opts CredentialShowOptions) error {

0 commit comments

Comments
 (0)