Support listing and removing user Codespaces secrets (#4714)
This commit is contained in:
parent
90313fbf96
commit
577f29ae0d
4 changed files with 191 additions and 33 deletions
|
|
@ -8,6 +8,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
|
|
@ -25,8 +26,9 @@ type ListOptions struct {
|
|||
Config func() (config.Config, error)
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
|
||||
OrgName string
|
||||
EnvName string
|
||||
OrgName string
|
||||
EnvName string
|
||||
UserSecrets bool
|
||||
}
|
||||
|
||||
func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command {
|
||||
|
|
@ -39,13 +41,19 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List secrets",
|
||||
Long: "List secrets for a repository, environment, or organization",
|
||||
Args: cobra.NoArgs,
|
||||
Long: heredoc.Doc(`
|
||||
List secrets on one of the following levels:
|
||||
- repository (default): available to Actions runs in a repository
|
||||
- environment: available to Actions runs for a deployment environment in a repository
|
||||
- organization: available to Actions runs within an organization
|
||||
- user: available to Codespaces for your user
|
||||
`),
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
|
||||
if err := cmdutil.MutuallyExclusive("specify only one of `--org` or `--env`", opts.OrgName != "", opts.EnvName != ""); err != nil {
|
||||
if err := cmdutil.MutuallyExclusive("specify only one of `--org`, `--env`, or `--user`", opts.OrgName != "", opts.EnvName != "", opts.UserSecrets); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -59,6 +67,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
|
||||
cmd.Flags().StringVarP(&opts.OrgName, "org", "o", "", "List secrets for an organization")
|
||||
cmd.Flags().StringVarP(&opts.EnvName, "env", "e", "", "List secrets for an environment")
|
||||
cmd.Flags().BoolVarP(&opts.UserSecrets, "user", "u", false, "List a secret for your user")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -73,7 +82,7 @@ func listRun(opts *ListOptions) error {
|
|||
envName := opts.EnvName
|
||||
|
||||
var baseRepo ghrepo.Interface
|
||||
if orgName == "" {
|
||||
if orgName == "" && !opts.UserSecrets {
|
||||
baseRepo, err = opts.BaseRepo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not determine base repo: %w", err)
|
||||
|
|
@ -81,7 +90,8 @@ func listRun(opts *ListOptions) error {
|
|||
}
|
||||
|
||||
var secrets []*Secret
|
||||
if orgName == "" {
|
||||
showSelectedRepoInfo := opts.IO.IsStdoutTTY()
|
||||
if orgName == "" && !opts.UserSecrets {
|
||||
if envName == "" {
|
||||
secrets, err = getRepoSecrets(client, baseRepo)
|
||||
} else {
|
||||
|
|
@ -101,7 +111,11 @@ func listRun(opts *ListOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
secrets, err = getOrgSecrets(client, host, orgName)
|
||||
if opts.UserSecrets {
|
||||
secrets, err = getUserSecrets(client, host, showSelectedRepoInfo)
|
||||
} else {
|
||||
secrets, err = getOrgSecrets(client, host, orgName, showSelectedRepoInfo)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -117,7 +131,7 @@ func listRun(opts *ListOptions) error {
|
|||
}
|
||||
tp.AddField(updatedAt, nil, nil)
|
||||
if secret.Visibility != "" {
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
if showSelectedRepoInfo {
|
||||
tp.AddField(fmtVisibility(*secret), nil, nil)
|
||||
} else {
|
||||
tp.AddField(strings.ToUpper(string(secret.Visibility)), nil, nil)
|
||||
|
|
@ -158,25 +172,32 @@ func fmtVisibility(s Secret) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func getOrgSecrets(client httpClient, host, orgName string) ([]*Secret, error) {
|
||||
func getOrgSecrets(client httpClient, host, orgName string, showSelectedRepoInfo bool) ([]*Secret, error) {
|
||||
secrets, err := getSecrets(client, host, fmt.Sprintf("orgs/%s/actions/secrets", orgName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type responseData struct {
|
||||
TotalCount int `json:"total_count"`
|
||||
if showSelectedRepoInfo {
|
||||
err = getSelectedRepositoryInformation(client, secrets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return secrets, nil
|
||||
}
|
||||
|
||||
func getUserSecrets(client httpClient, host string, showSelectedRepoInfo bool) ([]*Secret, error) {
|
||||
secrets, err := getSecrets(client, host, "user/codespaces/secrets")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, secret := range secrets {
|
||||
if secret.SelectedReposURL == "" {
|
||||
continue
|
||||
if showSelectedRepoInfo {
|
||||
err = getSelectedRepositoryInformation(client, secrets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result responseData
|
||||
if _, err := apiGet(client, secret.SelectedReposURL, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed determining selected repositories for %s: %w", secret.Name, err)
|
||||
}
|
||||
secret.NumSelectedRepos = result.TotalCount
|
||||
}
|
||||
|
||||
return secrets, nil
|
||||
|
|
@ -256,3 +277,22 @@ func findNextPage(link string) string {
|
|||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getSelectedRepositoryInformation(client httpClient, secrets []*Secret) error {
|
||||
type responseData struct {
|
||||
TotalCount int `json:"total_count"`
|
||||
}
|
||||
|
||||
for _, secret := range secrets {
|
||||
if secret.SelectedReposURL == "" {
|
||||
continue
|
||||
}
|
||||
var result responseData
|
||||
if _, err := apiGet(client, secret.SelectedReposURL, &result); err != nil {
|
||||
return fmt.Errorf("failed determining selected repositories for %s: %w", secret.Name, err)
|
||||
}
|
||||
secret.NumSelectedRepos = result.TotalCount
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,13 @@ func Test_NewCmdList(t *testing.T) {
|
|||
EnvName: "Development",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user",
|
||||
cli: "-u",
|
||||
wants: ListOptions{
|
||||
UserSecrets: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -153,6 +160,30 @@ func Test_listRun(t *testing.T) {
|
|||
"SECRET_THREE\t1975-11-30",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user tty",
|
||||
tty: true,
|
||||
opts: &ListOptions{
|
||||
UserSecrets: true,
|
||||
},
|
||||
wantOut: []string{
|
||||
"SECRET_ONE.*Updated 1988-10-11.*Visible to 1 selected repository",
|
||||
"SECRET_TWO.*Updated 2020-12-04.*Visible to 2 selected repositories",
|
||||
"SECRET_THREE.*Updated 1975-11-30.*Visible to 3 selected repositories",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user not tty",
|
||||
tty: false,
|
||||
opts: &ListOptions{
|
||||
UserSecrets: true,
|
||||
},
|
||||
wantOut: []string{
|
||||
"SECRET_ONE\t1988-10-11\t",
|
||||
"SECRET_TWO\t2020-12-04\t",
|
||||
"SECRET_THREE\t1975-11-30\t",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -203,11 +234,50 @@ func Test_listRun(t *testing.T) {
|
|||
}
|
||||
path = fmt.Sprintf("orgs/%s/actions/secrets", tt.opts.OrgName)
|
||||
|
||||
reg.Register(
|
||||
httpmock.REST("GET", fmt.Sprintf("orgs/%s/actions/secrets/SECRET_THREE/repositories", tt.opts.OrgName)),
|
||||
httpmock.JSONResponse(struct {
|
||||
TotalCount int `json:"total_count"`
|
||||
}{2}))
|
||||
if tt.tty {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", fmt.Sprintf("orgs/%s/actions/secrets/SECRET_THREE/repositories", tt.opts.OrgName)),
|
||||
httpmock.JSONResponse(struct {
|
||||
TotalCount int `json:"total_count"`
|
||||
}{2}))
|
||||
}
|
||||
}
|
||||
|
||||
if tt.opts.UserSecrets {
|
||||
payload.Secrets = []*Secret{
|
||||
{
|
||||
Name: "SECRET_ONE",
|
||||
UpdatedAt: t0,
|
||||
Visibility: shared.Selected,
|
||||
SelectedReposURL: "https://api.github.com/user/codespaces/secrets/SECRET_ONE/repositories",
|
||||
},
|
||||
{
|
||||
Name: "SECRET_TWO",
|
||||
UpdatedAt: t1,
|
||||
Visibility: shared.Selected,
|
||||
SelectedReposURL: "https://api.github.com/user/codespaces/secrets/SECRET_TWO/repositories",
|
||||
},
|
||||
{
|
||||
Name: "SECRET_THREE",
|
||||
UpdatedAt: t2,
|
||||
Visibility: shared.Selected,
|
||||
SelectedReposURL: "https://api.github.com/user/codespaces/secrets/SECRET_THREE/repositories",
|
||||
},
|
||||
}
|
||||
|
||||
path = "user/codespaces/secrets"
|
||||
if tt.tty {
|
||||
for i, secret := range payload.Secrets {
|
||||
hostLen := len("https://api.github.com/")
|
||||
path := secret.SelectedReposURL[hostLen:len(secret.SelectedReposURL)]
|
||||
repositoryCount := i + 1
|
||||
reg.Register(
|
||||
httpmock.REST("GET", path),
|
||||
httpmock.JSONResponse(struct {
|
||||
TotalCount int `json:"total_count"`
|
||||
}{repositoryCount}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reg.Register(httpmock.REST("GET", path), httpmock.JSONResponse(payload))
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
|
|
@ -18,9 +19,10 @@ type RemoveOptions struct {
|
|||
Config func() (config.Config, error)
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
|
||||
SecretName string
|
||||
OrgName string
|
||||
EnvName string
|
||||
SecretName string
|
||||
OrgName string
|
||||
EnvName string
|
||||
UserSecrets bool
|
||||
}
|
||||
|
||||
func NewCmdRemove(f *cmdutil.Factory, runF func(*RemoveOptions) error) *cobra.Command {
|
||||
|
|
@ -33,13 +35,19 @@ func NewCmdRemove(f *cmdutil.Factory, runF func(*RemoveOptions) error) *cobra.Co
|
|||
cmd := &cobra.Command{
|
||||
Use: "remove <secret-name>",
|
||||
Short: "Remove secrets",
|
||||
Long: "Remove a secret for a repository, environment, or organization",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Long: heredoc.Doc(`
|
||||
Remove a secret on one of the following levels:
|
||||
- repository (default): available to Actions runs in a repository
|
||||
- environment: available to Actions runs for a deployment environment in a repository
|
||||
- organization: available to Actions runs within an organization
|
||||
- user: available to Codespaces for your user
|
||||
`),
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
|
||||
if err := cmdutil.MutuallyExclusive("specify only one of `--org` or `--env`", opts.OrgName != "", opts.EnvName != ""); err != nil {
|
||||
if err := cmdutil.MutuallyExclusive("specify only one of `--org`, `--env`, or `--user`", opts.OrgName != "", opts.EnvName != "", opts.UserSecrets); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -54,6 +62,7 @@ func NewCmdRemove(f *cmdutil.Factory, runF func(*RemoveOptions) error) *cobra.Co
|
|||
}
|
||||
cmd.Flags().StringVarP(&opts.OrgName, "org", "o", "", "Remove a secret for an organization")
|
||||
cmd.Flags().StringVarP(&opts.EnvName, "env", "e", "", "Remove a secret for an environment")
|
||||
cmd.Flags().BoolVarP(&opts.UserSecrets, "user", "u", false, "Remove a secret for your user")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -69,7 +78,7 @@ func removeRun(opts *RemoveOptions) error {
|
|||
envName := opts.EnvName
|
||||
|
||||
var baseRepo ghrepo.Interface
|
||||
if orgName == "" {
|
||||
if orgName == "" && !opts.UserSecrets {
|
||||
baseRepo, err = opts.BaseRepo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not determine base repo: %w", err)
|
||||
|
|
@ -81,6 +90,8 @@ func removeRun(opts *RemoveOptions) error {
|
|||
path = fmt.Sprintf("orgs/%s/actions/secrets/%s", orgName, opts.SecretName)
|
||||
} else if envName != "" {
|
||||
path = fmt.Sprintf("repos/%s/environments/%s/secrets/%s", ghrepo.FullName(baseRepo), envName, opts.SecretName)
|
||||
} else if opts.UserSecrets {
|
||||
path = fmt.Sprintf("user/codespaces/secrets/%s", opts.SecretName)
|
||||
} else {
|
||||
path = fmt.Sprintf("repos/%s/actions/secrets/%s", ghrepo.FullName(baseRepo), opts.SecretName)
|
||||
}
|
||||
|
|
@ -102,7 +113,9 @@ func removeRun(opts *RemoveOptions) error {
|
|||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
target := orgName
|
||||
if orgName == "" {
|
||||
if opts.UserSecrets {
|
||||
target = "your user"
|
||||
} else if orgName == "" {
|
||||
target = ghrepo.FullName(baseRepo)
|
||||
}
|
||||
cs := opts.IO.ColorScheme()
|
||||
|
|
|
|||
|
|
@ -49,6 +49,14 @@ func TestNewCmdRemove(t *testing.T) {
|
|||
EnvName: "anEnv",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user",
|
||||
cli: "cool -u",
|
||||
wants: RemoveOptions{
|
||||
SecretName: "cool",
|
||||
UserSecrets: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -201,3 +209,30 @@ func Test_removeRun_org(t *testing.T) {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_removeRun_user(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
|
||||
reg.Register(
|
||||
httpmock.REST("DELETE", "user/codespaces/secrets/cool_secret"),
|
||||
httpmock.StatusStringResponse(204, "No Content"))
|
||||
|
||||
io, _, _, _ := iostreams.Test()
|
||||
|
||||
opts := &RemoveOptions{
|
||||
IO: io,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
},
|
||||
Config: func() (config.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
SecretName: "cool_secret",
|
||||
UserSecrets: true,
|
||||
}
|
||||
|
||||
err := removeRun(opts)
|
||||
assert.NoError(t, err)
|
||||
|
||||
reg.Verify(t)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue