Support listing and removing user Codespaces secrets (#4714)

This commit is contained in:
Josh Gross 2021-11-18 06:53:35 -05:00 committed by GitHub
parent 90313fbf96
commit 577f29ae0d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 191 additions and 33 deletions

View file

@ -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
}

View file

@ -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))

View file

@ -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()

View file

@ -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)
}