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  }