github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/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/MakeNowJust/heredoc" 12 "github.com/ungtb10d/cli/v2/api" 13 "github.com/ungtb10d/cli/v2/internal/config" 14 "github.com/ungtb10d/cli/v2/internal/ghinstance" 15 "github.com/ungtb10d/cli/v2/internal/ghrepo" 16 "github.com/ungtb10d/cli/v2/pkg/cmd/secret/shared" 17 "github.com/ungtb10d/cli/v2/pkg/cmdutil" 18 "github.com/ungtb10d/cli/v2/pkg/iostreams" 19 "github.com/ungtb10d/cli/v2/utils" 20 "github.com/spf13/cobra" 21 ) 22 23 type ListOptions struct { 24 HttpClient func() (*http.Client, error) 25 IO *iostreams.IOStreams 26 Config func() (config.Config, error) 27 BaseRepo func() (ghrepo.Interface, error) 28 29 OrgName string 30 EnvName string 31 UserSecrets bool 32 Application string 33 } 34 35 func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command { 36 opts := &ListOptions{ 37 IO: f.IOStreams, 38 Config: f.Config, 39 HttpClient: f.HttpClient, 40 } 41 42 cmd := &cobra.Command{ 43 Use: "list", 44 Short: "List secrets", 45 Long: heredoc.Doc(` 46 List secrets on one of the following levels: 47 - repository (default): available to Actions runs or Dependabot in a repository 48 - environment: available to Actions runs for a deployment environment in a repository 49 - organization: available to Actions runs, Dependabot, or Codespaces within an organization 50 - user: available to Codespaces for your user 51 `), 52 Aliases: []string{"ls"}, 53 Args: cobra.NoArgs, 54 RunE: func(cmd *cobra.Command, args []string) error { 55 // support `-R, --repo` override 56 opts.BaseRepo = f.BaseRepo 57 58 if err := cmdutil.MutuallyExclusive("specify only one of `--org`, `--env`, or `--user`", opts.OrgName != "", opts.EnvName != "", opts.UserSecrets); err != nil { 59 return err 60 } 61 62 if runF != nil { 63 return runF(opts) 64 } 65 66 return listRun(opts) 67 }, 68 } 69 70 cmd.Flags().StringVarP(&opts.OrgName, "org", "o", "", "List secrets for an organization") 71 cmd.Flags().StringVarP(&opts.EnvName, "env", "e", "", "List secrets for an environment") 72 cmd.Flags().BoolVarP(&opts.UserSecrets, "user", "u", false, "List a secret for your user") 73 cmdutil.StringEnumFlag(cmd, &opts.Application, "app", "a", "", []string{shared.Actions, shared.Codespaces, shared.Dependabot}, "List secrets for a specific application") 74 75 return cmd 76 } 77 78 func listRun(opts *ListOptions) error { 79 client, err := opts.HttpClient() 80 if err != nil { 81 return fmt.Errorf("could not create http client: %w", err) 82 } 83 84 orgName := opts.OrgName 85 envName := opts.EnvName 86 87 var baseRepo ghrepo.Interface 88 if orgName == "" && !opts.UserSecrets { 89 baseRepo, err = opts.BaseRepo() 90 if err != nil { 91 return fmt.Errorf("could not determine base repo: %w", err) 92 } 93 } 94 95 secretEntity, err := shared.GetSecretEntity(orgName, envName, opts.UserSecrets) 96 if err != nil { 97 return err 98 } 99 100 secretApp, err := shared.GetSecretApp(opts.Application, secretEntity) 101 if err != nil { 102 return err 103 } 104 105 if !shared.IsSupportedSecretEntity(secretApp, secretEntity) { 106 return fmt.Errorf("%s secrets are not supported for %s", secretEntity, secretApp) 107 } 108 109 var secrets []*Secret 110 showSelectedRepoInfo := opts.IO.IsStdoutTTY() 111 112 switch secretEntity { 113 case shared.Repository: 114 secrets, err = getRepoSecrets(client, baseRepo, secretApp) 115 case shared.Environment: 116 secrets, err = getEnvSecrets(client, baseRepo, envName) 117 case shared.Organization, shared.User: 118 var cfg config.Config 119 var host string 120 121 cfg, err = opts.Config() 122 if err != nil { 123 return err 124 } 125 126 host, _ = cfg.DefaultHost() 127 128 if secretEntity == shared.User { 129 secrets, err = getUserSecrets(client, host, showSelectedRepoInfo) 130 } else { 131 secrets, err = getOrgSecrets(client, host, orgName, showSelectedRepoInfo, secretApp) 132 } 133 } 134 135 if err != nil { 136 return fmt.Errorf("failed to get secrets: %w", err) 137 } 138 139 if len(secrets) == 0 { 140 return cmdutil.NewNoResultsError("no secrets found") 141 } 142 143 if err := opts.IO.StartPager(); err == nil { 144 defer opts.IO.StopPager() 145 } else { 146 fmt.Fprintf(opts.IO.ErrOut, "failed to start pager: %v\n", err) 147 } 148 149 //nolint:staticcheck // SA1019: utils.NewTablePrinter is deprecated: use internal/tableprinter 150 tp := utils.NewTablePrinter(opts.IO) 151 for _, secret := range secrets { 152 tp.AddField(secret.Name, nil, nil) 153 updatedAt := secret.UpdatedAt.Format("2006-01-02") 154 if opts.IO.IsStdoutTTY() { 155 updatedAt = fmt.Sprintf("Updated %s", updatedAt) 156 } 157 tp.AddField(updatedAt, nil, nil) 158 if secret.Visibility != "" { 159 if showSelectedRepoInfo { 160 tp.AddField(fmtVisibility(*secret), nil, nil) 161 } else { 162 tp.AddField(strings.ToUpper(string(secret.Visibility)), nil, nil) 163 } 164 } 165 tp.EndRow() 166 } 167 168 err = tp.Render() 169 if err != nil { 170 return err 171 } 172 173 return nil 174 } 175 176 type Secret struct { 177 Name string 178 UpdatedAt time.Time `json:"updated_at"` 179 Visibility shared.Visibility 180 SelectedReposURL string `json:"selected_repositories_url"` 181 NumSelectedRepos int 182 } 183 184 func fmtVisibility(s Secret) string { 185 switch s.Visibility { 186 case shared.All: 187 return "Visible to all repositories" 188 case shared.Private: 189 return "Visible to private repositories" 190 case shared.Selected: 191 if s.NumSelectedRepos == 1 { 192 return "Visible to 1 selected repository" 193 } else { 194 return fmt.Sprintf("Visible to %d selected repositories", s.NumSelectedRepos) 195 } 196 } 197 return "" 198 } 199 200 func getOrgSecrets(client httpClient, host, orgName string, showSelectedRepoInfo bool, app shared.App) ([]*Secret, error) { 201 secrets, err := getSecrets(client, host, fmt.Sprintf("orgs/%s/%s/secrets", orgName, app)) 202 if err != nil { 203 return nil, err 204 } 205 206 if showSelectedRepoInfo { 207 err = getSelectedRepositoryInformation(client, secrets) 208 if err != nil { 209 return nil, err 210 } 211 } 212 return secrets, nil 213 } 214 215 func getUserSecrets(client httpClient, host string, showSelectedRepoInfo bool) ([]*Secret, error) { 216 secrets, err := getSecrets(client, host, "user/codespaces/secrets") 217 if err != nil { 218 return nil, err 219 } 220 221 if showSelectedRepoInfo { 222 err = getSelectedRepositoryInformation(client, secrets) 223 if err != nil { 224 return nil, err 225 } 226 } 227 228 return secrets, nil 229 } 230 231 func getEnvSecrets(client httpClient, repo ghrepo.Interface, envName string) ([]*Secret, error) { 232 path := fmt.Sprintf("repos/%s/environments/%s/secrets", ghrepo.FullName(repo), envName) 233 return getSecrets(client, repo.RepoHost(), path) 234 } 235 236 func getRepoSecrets(client httpClient, repo ghrepo.Interface, app shared.App) ([]*Secret, error) { 237 return getSecrets(client, repo.RepoHost(), fmt.Sprintf("repos/%s/%s/secrets", 238 ghrepo.FullName(repo), app)) 239 } 240 241 type secretsPayload struct { 242 Secrets []*Secret 243 } 244 245 type httpClient interface { 246 Do(*http.Request) (*http.Response, error) 247 } 248 249 func getSecrets(client httpClient, host, path string) ([]*Secret, error) { 250 var results []*Secret 251 url := fmt.Sprintf("%s%s?per_page=100", ghinstance.RESTPrefix(host), path) 252 253 for { 254 var payload secretsPayload 255 nextURL, err := apiGet(client, url, &payload) 256 if err != nil { 257 return nil, err 258 } 259 results = append(results, payload.Secrets...) 260 261 if nextURL == "" { 262 break 263 } 264 url = nextURL 265 } 266 267 return results, nil 268 } 269 270 func apiGet(client httpClient, url string, data interface{}) (string, error) { 271 req, err := http.NewRequest("GET", url, nil) 272 if err != nil { 273 return "", err 274 } 275 req.Header.Set("Content-Type", "application/json; charset=utf-8") 276 277 resp, err := client.Do(req) 278 if err != nil { 279 return "", err 280 } 281 defer resp.Body.Close() 282 283 if resp.StatusCode > 299 { 284 return "", api.HandleHTTPError(resp) 285 } 286 287 dec := json.NewDecoder(resp.Body) 288 if err := dec.Decode(data); err != nil { 289 return "", err 290 } 291 292 return findNextPage(resp.Header.Get("Link")), nil 293 } 294 295 var linkRE = regexp.MustCompile(`<([^>]+)>;\s*rel="([^"]+)"`) 296 297 func findNextPage(link string) string { 298 for _, m := range linkRE.FindAllStringSubmatch(link, -1) { 299 if len(m) > 2 && m[2] == "next" { 300 return m[1] 301 } 302 } 303 return "" 304 } 305 306 func getSelectedRepositoryInformation(client httpClient, secrets []*Secret) error { 307 type responseData struct { 308 TotalCount int `json:"total_count"` 309 } 310 311 for _, secret := range secrets { 312 if secret.SelectedReposURL == "" { 313 continue 314 } 315 var result responseData 316 if _, err := apiGet(client, secret.SelectedReposURL, &result); err != nil { 317 return fmt.Errorf("failed determining selected repositories for %s: %w", secret.Name, err) 318 } 319 secret.NumSelectedRepos = result.TotalCount 320 } 321 322 return nil 323 }