Add ssh-key delete command (#6273)
This commit is contained in:
parent
f80f566ab2
commit
5962bc4383
4 changed files with 366 additions and 1 deletions
87
pkg/cmd/ssh-key/delete/delete.go
Normal file
87
pkg/cmd/ssh-key/delete/delete.go
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
package delete
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type DeleteOptions struct {
|
||||
IO *iostreams.IOStreams
|
||||
Config func() (config.Config, error)
|
||||
HttpClient func() (*http.Client, error)
|
||||
|
||||
KeyID string
|
||||
Confirmed bool
|
||||
Prompter prompter.Prompter
|
||||
}
|
||||
|
||||
func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Command {
|
||||
opts := &DeleteOptions{
|
||||
HttpClient: f.HttpClient,
|
||||
Config: f.Config,
|
||||
IO: f.IOStreams,
|
||||
Prompter: f.Prompter,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete <id>",
|
||||
Short: "Delete an SSH key from your GitHub account",
|
||||
Args: cmdutil.ExactArgs(1, "cannot delete: key id required"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.KeyID = args[0]
|
||||
|
||||
if !opts.IO.CanPrompt() && !opts.Confirmed {
|
||||
return cmdutil.FlagErrorf("--confirm required when not running interactively")
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
return deleteRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&opts.Confirmed, "confirm", "y", false, "Skip the confirmation prompt")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func deleteRun(opts *DeleteOptions) error {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err := opts.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
host, _ := cfg.DefaultHost()
|
||||
key, err := getSSHKey(httpClient, host, opts.KeyID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !opts.Confirmed {
|
||||
if err := opts.Prompter.ConfirmDeletion(key.Title); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = deleteSSHKey(httpClient, host, opts.KeyID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
cs := opts.IO.ColorScheme()
|
||||
fmt.Fprintf(opts.IO.Out, "%s SSH key %q (%s) deleted from your account\n", cs.SuccessIcon(), key.Title, opts.KeyID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
210
pkg/cmd/ssh-key/delete/delete_test.go
Normal file
210
pkg/cmd/ssh-key/delete/delete_test.go
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
package delete
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewCmdDelete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tty bool
|
||||
input string
|
||||
output DeleteOptions
|
||||
wantErr bool
|
||||
wantErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "tty",
|
||||
tty: true,
|
||||
input: "123",
|
||||
output: DeleteOptions{KeyID: "123", Confirmed: false},
|
||||
},
|
||||
{
|
||||
name: "confirm flag tty",
|
||||
tty: true,
|
||||
input: "123 --confirm",
|
||||
output: DeleteOptions{KeyID: "123", Confirmed: true},
|
||||
},
|
||||
{
|
||||
name: "shorthand confirm flag tty",
|
||||
tty: true,
|
||||
input: "123 -y",
|
||||
output: DeleteOptions{KeyID: "123", Confirmed: true},
|
||||
},
|
||||
{
|
||||
name: "no tty",
|
||||
input: "123",
|
||||
wantErr: true,
|
||||
wantErrMsg: "--confirm required when not running interactively",
|
||||
},
|
||||
{
|
||||
name: "confirm flag no tty",
|
||||
input: "123 --confirm",
|
||||
output: DeleteOptions{KeyID: "123", Confirmed: true},
|
||||
},
|
||||
{
|
||||
name: "shorthand confirm flag no tty",
|
||||
input: "123 -y",
|
||||
output: DeleteOptions{KeyID: "123", Confirmed: true},
|
||||
},
|
||||
{
|
||||
name: "no args",
|
||||
input: "",
|
||||
wantErr: true,
|
||||
wantErrMsg: "cannot delete: key id required",
|
||||
},
|
||||
{
|
||||
name: "too many args",
|
||||
input: "123 456",
|
||||
wantErr: true,
|
||||
wantErrMsg: "too many arguments",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
ios.SetStdinTTY(tt.tty)
|
||||
ios.SetStdoutTTY(tt.tty)
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
}
|
||||
argv, err := shlex.Split(tt.input)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var cmdOpts *DeleteOptions
|
||||
cmd := NewCmdDelete(f, func(opts *DeleteOptions) error {
|
||||
cmdOpts = opts
|
||||
return nil
|
||||
})
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(&bytes.Buffer{})
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
assert.EqualError(t, err, tt.wantErrMsg)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.output.KeyID, cmdOpts.KeyID)
|
||||
assert.Equal(t, tt.output.Confirmed, cmdOpts.Confirmed)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_deleteRun(t *testing.T) {
|
||||
keyResp := "{\"title\":\"My Key\"}"
|
||||
tests := []struct {
|
||||
name string
|
||||
tty bool
|
||||
opts DeleteOptions
|
||||
httpStubs func(*httpmock.Registry)
|
||||
prompterStubs func(*prompter.PrompterMock)
|
||||
wantStdout string
|
||||
wantErr bool
|
||||
wantErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "delete tty",
|
||||
tty: true,
|
||||
opts: DeleteOptions{KeyID: "123"},
|
||||
prompterStubs: func(pm *prompter.PrompterMock) {
|
||||
pm.ConfirmDeletionFunc = func(_ string) error {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "user/keys/123"), httpmock.StatusStringResponse(200, keyResp))
|
||||
reg.Register(httpmock.REST("DELETE", "user/keys/123"), httpmock.StatusStringResponse(204, ""))
|
||||
},
|
||||
wantStdout: "✓ SSH key \"My Key\" (123) deleted from your account\n",
|
||||
},
|
||||
{
|
||||
name: "delete with confirm flag tty",
|
||||
tty: true,
|
||||
opts: DeleteOptions{KeyID: "123", Confirmed: true},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "user/keys/123"), httpmock.StatusStringResponse(200, keyResp))
|
||||
reg.Register(httpmock.REST("DELETE", "user/keys/123"), httpmock.StatusStringResponse(204, ""))
|
||||
},
|
||||
wantStdout: "✓ SSH key \"My Key\" (123) deleted from your account\n",
|
||||
},
|
||||
{
|
||||
name: "not found tty",
|
||||
tty: true,
|
||||
opts: DeleteOptions{KeyID: "123"},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "user/keys/123"), httpmock.StatusStringResponse(404, ""))
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "HTTP 404 (https://api.github.com/user/keys/123)",
|
||||
},
|
||||
{
|
||||
name: "delete no tty",
|
||||
opts: DeleteOptions{KeyID: "123", Confirmed: true},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "user/keys/123"), httpmock.StatusStringResponse(200, keyResp))
|
||||
reg.Register(httpmock.REST("DELETE", "user/keys/123"), httpmock.StatusStringResponse(204, ""))
|
||||
},
|
||||
wantStdout: "",
|
||||
},
|
||||
{
|
||||
name: "not found no tty",
|
||||
opts: DeleteOptions{KeyID: "123", Confirmed: true},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "user/keys/123"), httpmock.StatusStringResponse(404, ""))
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "HTTP 404 (https://api.github.com/user/keys/123)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
pm := &prompter.PrompterMock{}
|
||||
if tt.prompterStubs != nil {
|
||||
tt.prompterStubs(pm)
|
||||
}
|
||||
tt.opts.Prompter = pm
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
if tt.httpStubs != nil {
|
||||
tt.httpStubs(reg)
|
||||
}
|
||||
|
||||
tt.opts.HttpClient = func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
tt.opts.Config = func() (config.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
}
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdinTTY(tt.tty)
|
||||
ios.SetStdoutTTY(tt.tty)
|
||||
tt.opts.IO = ios
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := deleteRun(&tt.opts)
|
||||
reg.Verify(t)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
assert.EqualError(t, err, tt.wantErrMsg)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantStdout, stdout.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
66
pkg/cmd/ssh-key/delete/http.go
Normal file
66
pkg/cmd/ssh-key/delete/http.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package delete
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
)
|
||||
|
||||
type sshKey struct {
|
||||
Title string
|
||||
}
|
||||
|
||||
func deleteSSHKey(httpClient *http.Client, host string, keyID string) error {
|
||||
url := fmt.Sprintf("%suser/keys/%s", ghinstance.RESTPrefix(host), keyID)
|
||||
req, err := http.NewRequest("DELETE", url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode > 299 {
|
||||
return api.HandleHTTPError(resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSSHKey(httpClient *http.Client, host string, keyID string) (*sshKey, error) {
|
||||
url := fmt.Sprintf("%suser/keys/%s", ghinstance.RESTPrefix(host), keyID)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode > 299 {
|
||||
return nil, api.HandleHTTPError(resp)
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var key sshKey
|
||||
err = json.Unmarshal(b, &key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &key, nil
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package key
|
|||
|
||||
import (
|
||||
cmdAdd "github.com/cli/cli/v2/pkg/cmd/ssh-key/add"
|
||||
cmdDelete "github.com/cli/cli/v2/pkg/cmd/ssh-key/delete"
|
||||
cmdList "github.com/cli/cli/v2/pkg/cmd/ssh-key/list"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -14,8 +15,9 @@ func NewCmdSSHKey(f *cmdutil.Factory) *cobra.Command {
|
|||
Long: "Manage SSH keys registered with your GitHub account.",
|
||||
}
|
||||
|
||||
cmd.AddCommand(cmdList.NewCmdList(f, nil))
|
||||
cmd.AddCommand(cmdAdd.NewCmdAdd(f, nil))
|
||||
cmd.AddCommand(cmdDelete.NewCmdDelete(f, nil))
|
||||
cmd.AddCommand(cmdList.NewCmdList(f, nil))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue