github.com/andrewhsu/cli/v2@v2.0.1-0.20210910131313-d4b4061f5b89/pkg/cmd/secret/set/set.go (about) 1 package set 2 3 import ( 4 "encoding/base64" 5 "errors" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "net/http" 10 "regexp" 11 "strings" 12 13 "github.com/AlecAivazis/survey/v2" 14 "github.com/MakeNowJust/heredoc" 15 "github.com/andrewhsu/cli/v2/api" 16 "github.com/andrewhsu/cli/v2/internal/config" 17 "github.com/andrewhsu/cli/v2/internal/ghrepo" 18 "github.com/andrewhsu/cli/v2/pkg/cmd/secret/shared" 19 "github.com/andrewhsu/cli/v2/pkg/cmdutil" 20 "github.com/andrewhsu/cli/v2/pkg/iostreams" 21 "github.com/andrewhsu/cli/v2/pkg/prompt" 22 "github.com/spf13/cobra" 23 "golang.org/x/crypto/nacl/box" 24 ) 25 26 type SetOptions struct { 27 HttpClient func() (*http.Client, error) 28 IO *iostreams.IOStreams 29 Config func() (config.Config, error) 30 BaseRepo func() (ghrepo.Interface, error) 31 32 RandomOverride io.Reader 33 34 SecretName string 35 OrgName string 36 EnvName string 37 Body string 38 Visibility string 39 RepositoryNames []string 40 } 41 42 func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command { 43 opts := &SetOptions{ 44 IO: f.IOStreams, 45 Config: f.Config, 46 HttpClient: f.HttpClient, 47 } 48 49 cmd := &cobra.Command{ 50 Use: "set <secret-name>", 51 Short: "Create or update secrets", 52 Long: "Locally encrypt a new or updated secret at either the repository, environment, or organization level and send it to GitHub for storage.", 53 Example: heredoc.Doc(` 54 Paste secret in prompt 55 $ gh secret set MYSECRET 56 57 Use environment variable as secret value 58 $ gh secret set MYSECRET -b"${ENV_VALUE}" 59 60 Use file as secret value 61 $ gh secret set MYSECRET < file.json 62 63 Set environment level secret 64 $ gh secret set MYSECRET -bval --env=anEnv 65 66 Set organization level secret visible to entire organization 67 $ gh secret set MYSECRET -bval --org=anOrg --visibility=all 68 69 Set organization level secret visible only to certain repositories 70 $ gh secret set MYSECRET -bval --org=anOrg --repos="repo1,repo2,repo3" 71 `), 72 Args: func(cmd *cobra.Command, args []string) error { 73 if len(args) != 1 { 74 return &cmdutil.FlagError{Err: errors.New("must pass single secret name")} 75 } 76 return nil 77 }, 78 RunE: func(cmd *cobra.Command, args []string) error { 79 // support `-R, --repo` override 80 opts.BaseRepo = f.BaseRepo 81 82 if err := cmdutil.MutuallyExclusive("specify only one of `--org` or `--env`", opts.OrgName != "", opts.EnvName != ""); err != nil { 83 return err 84 } 85 86 opts.SecretName = args[0] 87 88 err := validSecretName(opts.SecretName) 89 if err != nil { 90 return err 91 } 92 93 if cmd.Flags().Changed("visibility") { 94 if opts.OrgName == "" { 95 return &cmdutil.FlagError{Err: errors.New( 96 "--visibility not supported for repository secrets; did you mean to pass --org?")} 97 } 98 99 if opts.Visibility != shared.All && opts.Visibility != shared.Private && opts.Visibility != shared.Selected { 100 return &cmdutil.FlagError{Err: errors.New( 101 "--visibility must be one of `all`, `private`, or `selected`")} 102 } 103 104 if opts.Visibility != shared.Selected && cmd.Flags().Changed("repos") { 105 return &cmdutil.FlagError{Err: errors.New( 106 "--repos only supported when --visibility='selected'")} 107 } 108 109 if opts.Visibility == shared.Selected && !cmd.Flags().Changed("repos") { 110 return &cmdutil.FlagError{Err: errors.New( 111 "--repos flag required when --visibility='selected'")} 112 } 113 } else { 114 if cmd.Flags().Changed("repos") { 115 opts.Visibility = shared.Selected 116 } 117 } 118 119 if runF != nil { 120 return runF(opts) 121 } 122 123 return setRun(opts) 124 }, 125 } 126 cmd.Flags().StringVarP(&opts.OrgName, "org", "o", "", "Set a secret for an organization") 127 cmd.Flags().StringVarP(&opts.EnvName, "env", "e", "", "Set a secret for an environment") 128 cmd.Flags().StringVarP(&opts.Visibility, "visibility", "v", "private", "Set visibility for an organization secret: `all`, `private`, or `selected`") 129 cmd.Flags().StringSliceVarP(&opts.RepositoryNames, "repos", "r", []string{}, "List of repository names for `selected` visibility") 130 cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "A value for the secret. Reads from STDIN if not specified.") 131 132 return cmd 133 } 134 135 func setRun(opts *SetOptions) error { 136 body, err := getBody(opts) 137 if err != nil { 138 return fmt.Errorf("did not understand secret body: %w", err) 139 } 140 141 c, err := opts.HttpClient() 142 if err != nil { 143 return fmt.Errorf("could not create http client: %w", err) 144 } 145 client := api.NewClientFromHTTP(c) 146 147 orgName := opts.OrgName 148 envName := opts.EnvName 149 150 var baseRepo ghrepo.Interface 151 if orgName == "" { 152 baseRepo, err = opts.BaseRepo() 153 if err != nil { 154 return fmt.Errorf("could not determine base repo: %w", err) 155 } 156 } 157 158 cfg, err := opts.Config() 159 if err != nil { 160 return err 161 } 162 163 host, err := cfg.DefaultHost() 164 if err != nil { 165 return err 166 } 167 168 var pk *PubKey 169 if orgName != "" { 170 pk, err = getOrgPublicKey(client, host, orgName) 171 } else if envName != "" { 172 pk, err = getEnvPubKey(client, baseRepo, envName) 173 } else { 174 pk, err = getRepoPubKey(client, baseRepo) 175 } 176 if err != nil { 177 return fmt.Errorf("failed to fetch public key: %w", err) 178 } 179 180 eBody, err := box.SealAnonymous(nil, body, &pk.Raw, opts.RandomOverride) 181 if err != nil { 182 return fmt.Errorf("failed to encrypt body: %w", err) 183 } 184 185 encoded := base64.StdEncoding.EncodeToString(eBody) 186 187 if orgName != "" { 188 err = putOrgSecret(client, host, pk, *opts, encoded) 189 } else if envName != "" { 190 err = putEnvSecret(client, pk, baseRepo, envName, opts.SecretName, encoded) 191 } else { 192 err = putRepoSecret(client, pk, baseRepo, opts.SecretName, encoded) 193 } 194 if err != nil { 195 return fmt.Errorf("failed to set secret: %w", err) 196 } 197 198 if opts.IO.IsStdoutTTY() { 199 target := orgName 200 if orgName == "" { 201 target = ghrepo.FullName(baseRepo) 202 } 203 cs := opts.IO.ColorScheme() 204 fmt.Fprintf(opts.IO.Out, "%s Set secret %s for %s\n", cs.SuccessIconWithColor(cs.Green), opts.SecretName, target) 205 } 206 207 return nil 208 } 209 210 func validSecretName(name string) error { 211 if name == "" { 212 return errors.New("secret name cannot be blank") 213 } 214 215 if strings.HasPrefix(name, "GITHUB_") { 216 return errors.New("secret name cannot begin with GITHUB_") 217 } 218 219 leadingNumber := regexp.MustCompile(`^[0-9]`) 220 if leadingNumber.MatchString(name) { 221 return errors.New("secret name cannot start with a number") 222 } 223 224 validChars := regexp.MustCompile(`^([0-9]|[a-z]|[A-Z]|_)+$`) 225 if !validChars.MatchString(name) { 226 return errors.New("secret name can only contain letters, numbers, and _") 227 } 228 229 return nil 230 } 231 232 func getBody(opts *SetOptions) ([]byte, error) { 233 if opts.Body == "" { 234 if opts.IO.CanPrompt() { 235 err := prompt.SurveyAskOne(&survey.Password{ 236 Message: "Paste your secret", 237 }, &opts.Body) 238 if err != nil { 239 return nil, err 240 } 241 fmt.Fprintln(opts.IO.Out) 242 } else { 243 body, err := ioutil.ReadAll(opts.IO.In) 244 if err != nil { 245 return nil, fmt.Errorf("failed to read from STDIN: %w", err) 246 } 247 248 return body, nil 249 } 250 } 251 252 return []byte(opts.Body), nil 253 }