github.com/andrewhsu/cli/v2@v2.0.1-0.20210910131313-d4b4061f5b89/pkg/cmd/secret/list/list.go (about) 1 package list 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "net/http" 7 "regexp" 8 "strings" 9 "time" 10 11 "github.com/andrewhsu/cli/v2/api" 12 "github.com/andrewhsu/cli/v2/internal/config" 13 "github.com/andrewhsu/cli/v2/internal/ghinstance" 14 "github.com/andrewhsu/cli/v2/internal/ghrepo" 15 "github.com/andrewhsu/cli/v2/pkg/cmd/secret/shared" 16 "github.com/andrewhsu/cli/v2/pkg/cmdutil" 17 "github.com/andrewhsu/cli/v2/pkg/iostreams" 18 "github.com/andrewhsu/cli/v2/utils" 19 "github.com/spf13/cobra" 20 ) 21 22 type ListOptions struct { 23 HttpClient func() (*http.Client, error) 24 IO *iostreams.IOStreams 25 Config func() (config.Config, error) 26 BaseRepo func() (ghrepo.Interface, error) 27 28 OrgName string 29 EnvName string 30 } 31 32 func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command { 33 opts := &ListOptions{ 34 IO: f.IOStreams, 35 Config: f.Config, 36 HttpClient: f.HttpClient, 37 } 38 39 cmd := &cobra.Command{ 40 Use: "list", 41 Short: "List secrets", 42 Long: "List secrets for a repository, environment, or organization", 43 Args: cobra.NoArgs, 44 RunE: func(cmd *cobra.Command, args []string) error { 45 // support `-R, --repo` override 46 opts.BaseRepo = f.BaseRepo 47 48 if err := cmdutil.MutuallyExclusive("specify only one of `--org` or `--env`", opts.OrgName != "", opts.EnvName != ""); err != nil { 49 return err 50 } 51 52 if runF != nil { 53 return runF(opts) 54 } 55 56 return listRun(opts) 57 }, 58 } 59 60 cmd.Flags().StringVarP(&opts.OrgName, "org", "o", "", "List secrets for an organization") 61 cmd.Flags().StringVarP(&opts.EnvName, "env", "e", "", "List secrets for an environment") 62 63 return cmd 64 } 65 66 func listRun(opts *ListOptions) error { 67 client, err := opts.HttpClient() 68 if err != nil { 69 return fmt.Errorf("could not create http client: %w", err) 70 } 71 72 orgName := opts.OrgName 73 envName := opts.EnvName 74 75 var baseRepo ghrepo.Interface 76 if orgName == "" { 77 baseRepo, err = opts.BaseRepo() 78 if err != nil { 79 return fmt.Errorf("could not determine base repo: %w", err) 80 } 81 } 82 83 var secrets []*Secret 84 if orgName == "" { 85 if envName == "" { 86 secrets, err = getRepoSecrets(client, baseRepo) 87 } else { 88 secrets, err = getEnvSecrets(client, baseRepo, envName) 89 } 90 } else { 91 var cfg config.Config 92 var host string 93 94 cfg, err = opts.Config() 95 if err != nil { 96 return err 97 } 98 99 host, err = cfg.DefaultHost() 100 if err != nil { 101 return err 102 } 103 104 secrets, err = getOrgSecrets(client, host, orgName) 105 } 106 107 if err != nil { 108 return fmt.Errorf("failed to get secrets: %w", err) 109 } 110 111 tp := utils.NewTablePrinter(opts.IO) 112 for _, secret := range secrets { 113 tp.AddField(secret.Name, nil, nil) 114 updatedAt := secret.UpdatedAt.Format("2006-01-02") 115 if opts.IO.IsStdoutTTY() { 116 updatedAt = fmt.Sprintf("Updated %s", updatedAt) 117 } 118 tp.AddField(updatedAt, nil, nil) 119 if secret.Visibility != "" { 120 if opts.IO.IsStdoutTTY() { 121 tp.AddField(fmtVisibility(*secret), nil, nil) 122 } else { 123 tp.AddField(strings.ToUpper(string(secret.Visibility)), nil, nil) 124 } 125 } 126 tp.EndRow() 127 } 128 129 err = tp.Render() 130 if err != nil { 131 return err 132 } 133 134 return nil 135 } 136 137 type Secret struct { 138 Name string 139 UpdatedAt time.Time `json:"updated_at"` 140 Visibility shared.Visibility 141 SelectedReposURL string `json:"selected_repositories_url"` 142 NumSelectedRepos int 143 } 144 145 func fmtVisibility(s Secret) string { 146 switch s.Visibility { 147 case shared.All: 148 return "Visible to all repositories" 149 case shared.Private: 150 return "Visible to private repositories" 151 case shared.Selected: 152 if s.NumSelectedRepos == 1 { 153 return "Visible to 1 selected repository" 154 } else { 155 return fmt.Sprintf("Visible to %d selected repositories", s.NumSelectedRepos) 156 } 157 } 158 return "" 159 } 160 161 func getOrgSecrets(client httpClient, host, orgName string) ([]*Secret, error) { 162 secrets, err := getSecrets(client, host, fmt.Sprintf("orgs/%s/actions/secrets", orgName)) 163 if err != nil { 164 return nil, err 165 } 166 167 type responseData struct { 168 TotalCount int `json:"total_count"` 169 } 170 171 for _, secret := range secrets { 172 if secret.SelectedReposURL == "" { 173 continue 174 } 175 var result responseData 176 if _, err := apiGet(client, secret.SelectedReposURL, &result); err != nil { 177 return nil, fmt.Errorf("failed determining selected repositories for %s: %w", secret.Name, err) 178 } 179 secret.NumSelectedRepos = result.TotalCount 180 } 181 182 return secrets, nil 183 } 184 185 func getEnvSecrets(client httpClient, repo ghrepo.Interface, envName string) ([]*Secret, error) { 186 path := fmt.Sprintf("repos/%s/environments/%s/secrets", ghrepo.FullName(repo), envName) 187 return getSecrets(client, repo.RepoHost(), path) 188 } 189 190 func getRepoSecrets(client httpClient, repo ghrepo.Interface) ([]*Secret, error) { 191 return getSecrets(client, repo.RepoHost(), fmt.Sprintf("repos/%s/actions/secrets", 192 ghrepo.FullName(repo))) 193 } 194 195 type secretsPayload struct { 196 Secrets []*Secret 197 } 198 199 type httpClient interface { 200 Do(*http.Request) (*http.Response, error) 201 } 202 203 func getSecrets(client httpClient, host, path string) ([]*Secret, error) { 204 var results []*Secret 205 url := fmt.Sprintf("%s%s?per_page=100", ghinstance.RESTPrefix(host), path) 206 207 for { 208 var payload secretsPayload 209 nextURL, err := apiGet(client, url, &payload) 210 if err != nil { 211 return nil, err 212 } 213 results = append(results, payload.Secrets...) 214 215 if nextURL == "" { 216 break 217 } 218 url = nextURL 219 } 220 221 return results, nil 222 } 223 224 func apiGet(client httpClient, url string, data interface{}) (string, error) { 225 req, err := http.NewRequest("GET", url, nil) 226 if err != nil { 227 return "", err 228 } 229 req.Header.Set("Content-Type", "application/json; charset=utf-8") 230 231 resp, err := client.Do(req) 232 if err != nil { 233 return "", err 234 } 235 defer resp.Body.Close() 236 237 if resp.StatusCode > 299 { 238 return "", api.HandleHTTPError(resp) 239 } 240 241 dec := json.NewDecoder(resp.Body) 242 if err := dec.Decode(data); err != nil { 243 return "", err 244 } 245 246 return findNextPage(resp.Header.Get("Link")), nil 247 } 248 249 var linkRE = regexp.MustCompile(`<([^>]+)>;\s*rel="([^"]+)"`) 250 251 func findNextPage(link string) string { 252 for _, m := range linkRE.FindAllStringSubmatch(link, -1) { 253 if len(m) > 2 && m[2] == "next" { 254 return m[1] 255 } 256 } 257 return "" 258 }