Skip to content

Commit 5309a20

Browse files
committed
implement gh secret create and gh secret list
1 parent f415245 commit 5309a20

File tree

9 files changed

+1077
-2
lines changed

9 files changed

+1077
-2
lines changed

pkg/cmd/gist/create/create.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,8 @@ func createGist(client *http.Client, hostname, description string, public bool,
227227
}
228228
requestBody := bytes.NewReader(requestByte)
229229

230-
apliClient := api.NewClientFromHTTP(client)
231-
err = apliClient.REST(hostname, "POST", path, requestBody, &result)
230+
apiClient := api.NewClientFromHTTP(client)
231+
err = apiClient.REST(hostname, "POST", path, requestBody, &result)
232232
if err != nil {
233233
return nil, err
234234
}

pkg/cmd/root/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
releaseCmd "github.com/cli/cli/pkg/cmd/release"
2020
repoCmd "github.com/cli/cli/pkg/cmd/repo"
2121
creditsCmd "github.com/cli/cli/pkg/cmd/repo/credits"
22+
secretCmd "github.com/cli/cli/pkg/cmd/secret"
2223
versionCmd "github.com/cli/cli/pkg/cmd/version"
2324
"github.com/cli/cli/pkg/cmdutil"
2425
"github.com/spf13/cobra"
@@ -74,6 +75,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command {
7475
cmd.AddCommand(creditsCmd.NewCmdCredits(f, nil))
7576
cmd.AddCommand(gistCmd.NewCmdGist(f))
7677
cmd.AddCommand(completionCmd.NewCmdCompletion(f.IOStreams))
78+
cmd.AddCommand(secretCmd.NewCmdSecret(f))
7779

7880
// the `api` command should not inherit any extra HTTP headers
7981
bareHTTPCmdFactory := *f

pkg/cmd/secret/create/create.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package create
2+
3+
import (
4+
"encoding/base64"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"io/ioutil"
9+
"net/http"
10+
"strings"
11+
12+
"github.com/MakeNowJust/heredoc"
13+
"github.com/cli/cli/api"
14+
"github.com/cli/cli/internal/ghinstance"
15+
"github.com/cli/cli/internal/ghrepo"
16+
"github.com/cli/cli/pkg/cmd/secret/shared"
17+
"github.com/cli/cli/pkg/cmdutil"
18+
"github.com/cli/cli/pkg/iostreams"
19+
"github.com/spf13/cobra"
20+
"golang.org/x/crypto/nacl/box"
21+
)
22+
23+
type CreateOptions struct {
24+
HttpClient func() (*http.Client, error)
25+
IO *iostreams.IOStreams
26+
BaseRepo func() (ghrepo.Interface, error)
27+
28+
RandomOverride io.Reader
29+
30+
SecretName string
31+
OrgName string
32+
Body string
33+
Visibility string
34+
RepositoryNames []string
35+
}
36+
37+
func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command {
38+
opts := &CreateOptions{
39+
IO: f.IOStreams,
40+
HttpClient: f.HttpClient,
41+
}
42+
43+
cmd := &cobra.Command{
44+
Use: "create <secret name>",
45+
Short: "Create secrets",
46+
Long: "Locally encrypt a new secret and send it to GitHub for storage.",
47+
Example: heredoc.Doc(`
48+
$ cat SECRET.txt | gh secret create NEW_SECRET
49+
$ gh secret create NEW_SECRET -b"some literal value"
50+
$ gh secret create NEW_SECRET -b"@file.json"
51+
$ gh secret create ORG_SECRET --org
52+
$ gh secret create ORG_SECRET --org=anotherOrg --visibility=selected -r="repo1,repo2,repo3"
53+
$ gh secret create ORG_SECRET --org=anotherOrg --visibility="all"
54+
`),
55+
Args: func(cmd *cobra.Command, args []string) error {
56+
if len(args) != 1 {
57+
return &cmdutil.FlagError{Err: errors.New("must pass single secret name")}
58+
}
59+
if !cmd.Flags().Changed("body") && opts.IO.IsStdinTTY() {
60+
return &cmdutil.FlagError{Err: errors.New("no --body specified but nothing on STIDN")}
61+
}
62+
return nil
63+
},
64+
RunE: func(cmd *cobra.Command, args []string) error {
65+
// support `-R, --repo` override
66+
opts.BaseRepo = f.BaseRepo
67+
68+
opts.SecretName = args[0]
69+
70+
if cmd.Flags().Changed("visibility") {
71+
if opts.OrgName == "" {
72+
return &cmdutil.FlagError{Err: errors.New(
73+
"--visibility not supported for repository secrets; did you mean to pass --org?")}
74+
}
75+
76+
if opts.Visibility != shared.VisAll && opts.Visibility != shared.VisPrivate && opts.Visibility != shared.VisSelected {
77+
return &cmdutil.FlagError{Err: errors.New(
78+
"--visibility must be one of `all`, `private`, or `selected`")}
79+
}
80+
}
81+
82+
if cmd.Flags().Changed("repos") && opts.Visibility != shared.VisSelected {
83+
return &cmdutil.FlagError{Err: errors.New(
84+
"--repos only supported when --visibility='selected'")}
85+
}
86+
87+
if opts.Visibility == shared.VisSelected && len(opts.RepositoryNames) == 0 {
88+
return &cmdutil.FlagError{Err: errors.New(
89+
"--repos flag required when --visibility='selected'")}
90+
}
91+
92+
if runF != nil {
93+
return runF(opts)
94+
}
95+
96+
return createRun(opts)
97+
},
98+
}
99+
cmd.Flags().StringVar(&opts.OrgName, "org", "", "List secrets for an organization")
100+
cmd.Flags().Lookup("org").NoOptDefVal = "@owner"
101+
cmd.Flags().StringVarP(&opts.Visibility, "visibility", "v", "private", "Set visibility for an organization secret: `all`, `private`, or `selected`")
102+
cmd.Flags().StringSliceVarP(&opts.RepositoryNames, "repos", "r", []string{}, "List of repository names for `selected` visibility")
103+
cmd.Flags().StringVarP(&opts.Body, "body", "b", "-", "Provide either a literal string or a file path; prepend file paths with an @. Reads from STDIN if not provided.")
104+
105+
return cmd
106+
}
107+
108+
func createRun(opts *CreateOptions) error {
109+
body, err := getBody(opts)
110+
if err != nil {
111+
return fmt.Errorf("did not understand secret body: %w", err)
112+
}
113+
114+
c, err := opts.HttpClient()
115+
if err != nil {
116+
return fmt.Errorf("could not create http client: %w", err)
117+
}
118+
client := api.NewClientFromHTTP(c)
119+
120+
var baseRepo ghrepo.Interface
121+
if opts.OrgName == "" || opts.OrgName == "@owner" {
122+
baseRepo, err = opts.BaseRepo()
123+
if err != nil {
124+
return fmt.Errorf("could not determine base repo: %w", err)
125+
}
126+
}
127+
128+
host := ghinstance.OverridableDefault()
129+
if opts.OrgName == "@owner" {
130+
opts.OrgName = baseRepo.RepoOwner()
131+
host = baseRepo.RepoHost()
132+
}
133+
134+
var pk *PubKey
135+
if opts.OrgName != "" {
136+
pk, err = getOrgPublicKey(client, host, opts.OrgName)
137+
} else {
138+
pk, err = getRepoPubKey(client, baseRepo)
139+
}
140+
if err != nil {
141+
return fmt.Errorf("failed to fetch public key: %w", err)
142+
}
143+
144+
eBody, err := box.SealAnonymous(nil, body, &pk.Raw, opts.RandomOverride)
145+
if err != nil {
146+
return fmt.Errorf("failed to encrypt body: %w", err)
147+
}
148+
149+
encoded := base64.StdEncoding.EncodeToString(eBody)
150+
151+
if opts.OrgName != "" {
152+
err = putOrgSecret(client, pk, host, *opts, encoded)
153+
} else {
154+
err = putRepoSecret(client, pk, baseRepo, opts.SecretName, encoded)
155+
}
156+
if err != nil {
157+
return fmt.Errorf("failed to create secret: %w", err)
158+
}
159+
160+
return nil
161+
}
162+
163+
func getBody(opts *CreateOptions) (body []byte, err error) {
164+
if opts.Body == "-" {
165+
body, err = ioutil.ReadAll(opts.IO.In)
166+
if err != nil {
167+
return nil, fmt.Errorf("failed to read from STDIN: %w", err)
168+
}
169+
170+
return
171+
}
172+
173+
if strings.HasPrefix(opts.Body, "@") {
174+
body, err = opts.IO.ReadUserFile(opts.Body[1:])
175+
if err != nil {
176+
return nil, fmt.Errorf("failed to read file %s: %w", opts.Body[1:], err)
177+
}
178+
179+
return
180+
}
181+
182+
return []byte(opts.Body), nil
183+
}

0 commit comments

Comments
 (0)