diff --git a/go.mod b/go.mod index cf1021ef2..ca455277d 100644 --- a/go.mod +++ b/go.mod @@ -17,9 +17,11 @@ require ( github.com/google/go-cmp v0.5.6 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/gorilla/websocket v1.4.2 + github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-version v1.3.0 github.com/henvic/httpretty v0.0.6 github.com/itchyny/gojq v0.12.6 + github.com/joho/godotenv v1.4.0 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/mattn/go-colorable v0.1.12 github.com/mattn/go-isatty v0.0.14 diff --git a/go.sum b/go.sum index d787de066..f2c325538 100644 --- a/go.sum +++ b/go.sum @@ -243,6 +243,7 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= @@ -254,6 +255,8 @@ github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjh github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= @@ -286,6 +289,8 @@ github.com/itchyny/gojq v0.12.6 h1:VjaFn59Em2wTxDNGcrRkDK9ZHMNa8IksOgL13sLL4d0= github.com/itchyny/gojq v0.12.6/go.mod h1:ZHrkfu7A+RbZLy5J1/JKpS4poEqrzItSTGDItqsfP0A= github.com/itchyny/timefmt-go v0.1.3 h1:7M3LGVDsqcd0VZH2U+x393obrzZisp7C0uEe921iRkU= github.com/itchyny/timefmt-go v0.1.3/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A= +github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= diff --git a/pkg/cmd/secret/set/http.go b/pkg/cmd/secret/set/http.go index 03fdb0d89..f74f757d9 100644 --- a/pkg/cmd/secret/set/http.go +++ b/pkg/cmd/secret/set/http.go @@ -2,7 +2,6 @@ package set import ( "bytes" - "encoding/base64" "encoding/json" "fmt" "sort" @@ -11,14 +10,13 @@ import ( "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/cmd/secret/shared" ) type SecretPayload struct { - EncryptedValue string `json:"encrypted_value"` - Visibility string `json:"visibility,omitempty"` - Repositories []int `json:"selected_repository_ids,omitempty"` - KeyID string `json:"key_id"` + EncryptedValue string `json:"encrypted_value"` + Visibility string `json:"visibility,omitempty"` + Repositories []int64 `json:"selected_repository_ids,omitempty"` + KeyID string `json:"key_id"` } // The Codespaces Secret API currently expects repositories IDs as strings @@ -29,7 +27,6 @@ type CodespacesSecretPayload struct { } type PubKey struct { - Raw [32]byte ID string `json:"key_id"` Key string } @@ -40,17 +37,6 @@ func getPubKey(client *api.Client, host, path string) (*PubKey, error) { if err != nil { return nil, err } - - if pk.Key == "" { - return nil, fmt.Errorf("failed to find public key at %s/%s", host, path) - } - - decoded, err := base64.StdEncoding.DecodeString(pk.Key) - if err != nil { - return nil, fmt.Errorf("failed to decode public key: %w", err) - } - - copy(pk.Raw[:], decoded[0:32]) return &pk, nil } @@ -82,33 +68,7 @@ func putSecret(client *api.Client, host, path string, payload interface{}) error return client.REST(host, "PUT", path, requestBody, nil) } -func putOrgSecret(client *api.Client, host string, pk *PubKey, opts SetOptions, eValue string) error { - secretName := opts.SecretName - orgName := opts.OrgName - visibility := opts.Visibility - - var repositoryIDs []int - var err error - if orgName != "" && visibility == shared.Selected { - repos := make([]ghrepo.Interface, 0, len(opts.RepositoryNames)) - for _, repositoryName := range opts.RepositoryNames { - var repo ghrepo.Interface - if strings.Contains(repositoryName, "/") { - repo, err = ghrepo.FromFullNameWithHost(repositoryName, host) - if err != nil { - return fmt.Errorf("invalid repository name: %w", err) - } - } else { - repo = ghrepo.NewWithHost(opts.OrgName, repositoryName, host) - } - repos = append(repos, repo) - } - repositoryIDs, err = mapRepoToID(client, host, repos) - if err != nil { - return fmt.Errorf("failed to look up IDs for repositories %v: %w", opts.RepositoryNames, err) - } - } - +func putOrgSecret(client *api.Client, host string, pk *PubKey, orgName, visibility, secretName, eValue string, repositoryIDs []int64) error { payload := SecretPayload{ EncryptedValue: eValue, KeyID: pk.ID, @@ -120,35 +80,21 @@ func putOrgSecret(client *api.Client, host string, pk *PubKey, opts SetOptions, return putSecret(client, host, path, payload) } -func putUserSecret(client *api.Client, host string, pk *PubKey, opts SetOptions, eValue string) error { +func putUserSecret(client *api.Client, host string, pk *PubKey, key, eValue string, repositoryIDs []int64) error { payload := CodespacesSecretPayload{ EncryptedValue: eValue, KeyID: pk.ID, } - if len(opts.RepositoryNames) > 0 { - repos := make([]ghrepo.Interface, len(opts.RepositoryNames)) - for i, repo := range opts.RepositoryNames { - // For user secrets, repository names should be fully qualifed (e.g. "owner/repo") - repoNWO, err := ghrepo.FromFullNameWithHost(repo, host) - if err != nil { - return err - } - repos[i] = repoNWO - } - - repositoryIDs, err := mapRepoToID(client, host, repos) - if err != nil { - return fmt.Errorf("failed to look up repository IDs: %w", err) - } + if len(repositoryIDs) > 0 { repositoryStringIDs := make([]string, len(repositoryIDs)) for i, id := range repositoryIDs { - repositoryStringIDs[i] = strconv.Itoa(id) + repositoryStringIDs[i] = strconv.FormatInt(id, 10) } payload.Repositories = repositoryStringIDs } - path := fmt.Sprintf("user/codespaces/secrets/%s", opts.SecretName) + path := fmt.Sprintf("user/codespaces/secrets/%s", key) return putSecret(client, host, path, payload) } @@ -171,7 +117,7 @@ func putRepoSecret(client *api.Client, pk *PubKey, repo ghrepo.Interface, secret } // This does similar logic to `api.RepoNetwork`, but without the overfetching. -func mapRepoToID(client *api.Client, host string, repositories []ghrepo.Interface) ([]int, error) { +func mapRepoToID(client *api.Client, host string, repositories []ghrepo.Interface) ([]int64, error) { queries := make([]string, 0, len(repositories)) for i, repo := range repositories { queries = append(queries, fmt.Sprintf(` @@ -184,7 +130,7 @@ func mapRepoToID(client *api.Client, host string, repositories []ghrepo.Interfac query := fmt.Sprintf(`query MapRepositoryNames { %s }`, strings.Join(queries, "")) graphqlResult := make(map[string]*struct { - DatabaseID int `json:"databaseId"` + DatabaseID int64 `json:"databaseId"` }) if err := client.GraphQL(host, query, nil, &graphqlResult); err != nil { @@ -197,10 +143,9 @@ func mapRepoToID(client *api.Client, host string, repositories []ghrepo.Interfac } sort.Strings(repoKeys) - result := make([]int, len(repositories)) + result := make([]int64, len(repositories)) for i, k := range repoKeys { result[i] = graphqlResult[k].DatabaseID } - return result, nil } diff --git a/pkg/cmd/secret/set/set.go b/pkg/cmd/secret/set/set.go index 4531ab46d..56d13b210 100644 --- a/pkg/cmd/secret/set/set.go +++ b/pkg/cmd/secret/set/set.go @@ -6,6 +6,8 @@ import ( "io" "io/ioutil" "net/http" + "os" + "strings" "github.com/AlecAivazis/survey/v2" "github.com/MakeNowJust/heredoc" @@ -16,6 +18,8 @@ import ( "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/cli/cli/v2/pkg/prompt" + "github.com/hashicorp/go-multierror" + "github.com/joho/godotenv" "github.com/spf13/cobra" "golang.org/x/crypto/nacl/box" ) @@ -26,7 +30,7 @@ type SetOptions struct { Config func() (config.Config, error) BaseRepo func() (ghrepo.Interface, error) - RandomOverride io.Reader + RandomOverride func() io.Reader SecretName string OrgName string @@ -36,6 +40,7 @@ type SetOptions struct { DoNotStore bool Visibility string RepositoryNames []string + EnvFile string } func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command { @@ -81,6 +86,9 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command # Set user-level secret for Codespaces $ gh secret set MYSECRET --user + + # Set multiple secrets imported from the ".env" file + $ gh secret set -f .env `), Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { @@ -91,8 +99,16 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command return err } + if err := cmdutil.MutuallyExclusive("specify only one of `--body` or `--env-file`", opts.Body != "", opts.EnvFile != ""); err != nil { + return err + } + + if err := cmdutil.MutuallyExclusive("specify only one of `--env-file` or `--no-store`", opts.EnvFile != "", opts.DoNotStore); err != nil { + return err + } + if len(args) == 0 { - if !opts.DoNotStore { + if !opts.DoNotStore && opts.EnvFile == "" { return cmdutil.FlagErrorf("must pass name argument") } } else { @@ -136,14 +152,15 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command cmd.Flags().StringSliceVarP(&opts.RepositoryNames, "repos", "r", []string{}, "List of `repositories` that can access an organization or user secret") cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "The value for the secret (reads from standard input if not specified)") cmd.Flags().BoolVar(&opts.DoNotStore, "no-store", false, "Print the encrypted, base64-encoded value instead of storing it on Github") + cmd.Flags().StringVarP(&opts.EnvFile, "env-file", "f", "", "Load secret names and values from a dotenv-formatted `file`") return cmd } func setRun(opts *SetOptions) error { - body, err := getBody(opts) + secrets, err := getSecretsFromOptions(opts) if err != nil { - return fmt.Errorf("did not understand secret body: %w", err) + return err } c, err := opts.HttpClient() @@ -155,23 +172,42 @@ func setRun(opts *SetOptions) error { orgName := opts.OrgName envName := opts.EnvName + var host string var baseRepo ghrepo.Interface if orgName == "" && !opts.UserSecrets { baseRepo, err = opts.BaseRepo() if err != nil { return fmt.Errorf("could not determine base repo: %w", err) } + host = baseRepo.RepoHost() + } else { + cfg, err := opts.Config() + if err != nil { + return err + } + + host, err = cfg.DefaultHost() + if err != nil { + return err + } } - cfg, err := opts.Config() - if err != nil { - return err - } - - host, err := cfg.DefaultHost() - if err != nil { - return err + type repoNamesResult struct { + ids []int64 + err error } + repoNamesC := make(chan repoNamesResult, 1) + go func() { + if len(opts.RepositoryNames) == 0 { + repoNamesC <- repoNamesResult{} + return + } + repositoryIDs, err := mapRepoNamesToIDs(client, host, opts.OrgName, opts.RepositoryNames) + repoNamesC <- repoNamesResult{ + ids: repositoryIDs, + err: err, + } + }() var pk *PubKey if orgName != "" { @@ -187,63 +223,179 @@ func setRun(opts *SetOptions) error { return fmt.Errorf("failed to fetch public key: %w", err) } - eBody, err := box.SealAnonymous(nil, body, &pk.Raw, opts.RandomOverride) - if err != nil { - return fmt.Errorf("failed to encrypt body: %w", err) - } - - encoded := base64.StdEncoding.EncodeToString(eBody) - if opts.DoNotStore { - _, err := fmt.Fprintln(opts.IO.Out, encoded) - return err - } - - if orgName != "" { - err = putOrgSecret(client, host, pk, *opts, encoded) - } else if envName != "" { - err = putEnvSecret(client, pk, baseRepo, envName, opts.SecretName, encoded) - } else if opts.UserSecrets { - err = putUserSecret(client, host, pk, *opts, encoded) + var repositoryIDs []int64 + if result := <-repoNamesC; result.err == nil { + repositoryIDs = result.ids } else { - err = putRepoSecret(client, pk, baseRepo, opts.SecretName, encoded) - } - if err != nil { - return fmt.Errorf("failed to set secret: %w", err) + return result.err } - if opts.IO.IsStdoutTTY() { + setc := make(chan setResult) + for secretKey, secret := range secrets { + key := secretKey + value := secret + go func() { + setc <- setSecret(opts, pk, host, client, baseRepo, key, value, repositoryIDs) + }() + } + + err = nil + cs := opts.IO.ColorScheme() + for i := 0; i < len(secrets); i++ { + result := <-setc + if result.err != nil { + err = multierror.Append(err, result.err) + continue + } + if result.encrypted != "" { + fmt.Fprintln(opts.IO.Out, result.encrypted) + continue + } + if !opts.IO.IsStdoutTTY() { + continue + } target := orgName if opts.UserSecrets { target = "your user" } else if orgName == "" { target = ghrepo.FullName(baseRepo) } - cs := opts.IO.ColorScheme() - fmt.Fprintf(opts.IO.Out, "%s Set secret %s for %s\n", cs.SuccessIconWithColor(cs.Green), opts.SecretName, target) + fmt.Fprintf(opts.IO.Out, "%s Set secret %s for %s\n", cs.SuccessIcon(), result.key, target) + } + return err +} + +type setResult struct { + key string + encrypted string + err error +} + +func setSecret(opts *SetOptions, pk *PubKey, host string, client *api.Client, baseRepo ghrepo.Interface, secretKey string, secret []byte, repositoryIDs []int64) (res setResult) { + orgName := opts.OrgName + envName := opts.EnvName + res.key = secretKey + + decodedPubKey, err := base64.StdEncoding.DecodeString(pk.Key) + if err != nil { + res.err = fmt.Errorf("failed to decode public key: %w", err) + return + } + var peersPubKey [32]byte + copy(peersPubKey[:], decodedPubKey[0:32]) + + var rand io.Reader + if opts.RandomOverride != nil { + rand = opts.RandomOverride() + } + eBody, err := box.SealAnonymous(nil, secret[:], &peersPubKey, rand) + if err != nil { + res.err = fmt.Errorf("failed to encrypt body: %w", err) + return } - return nil + encoded := base64.StdEncoding.EncodeToString(eBody) + if opts.DoNotStore { + res.encrypted = encoded + return + } + + if orgName != "" { + err = putOrgSecret(client, host, pk, opts.OrgName, opts.Visibility, secretKey, encoded, repositoryIDs) + } else if envName != "" { + err = putEnvSecret(client, pk, baseRepo, envName, secretKey, encoded) + } else if opts.UserSecrets { + err = putUserSecret(client, host, pk, secretKey, encoded, repositoryIDs) + } else { + err = putRepoSecret(client, pk, baseRepo, secretKey, encoded) + } + if err != nil { + res.err = fmt.Errorf("failed to set secret %q: %w", secretKey, err) + return + } + return +} + +func getSecretsFromOptions(opts *SetOptions) (map[string][]byte, error) { + secrets := make(map[string][]byte) + + if opts.EnvFile != "" { + var r io.Reader + if opts.EnvFile == "-" { + defer opts.IO.In.Close() + r = opts.IO.In + } else { + f, err := os.Open(opts.EnvFile) + if err != nil { + return nil, fmt.Errorf("failed to open env file: %w", err) + } + defer f.Close() + r = f + } + envs, err := godotenv.Parse(r) + if err != nil { + return nil, fmt.Errorf("error parsing env file: %w", err) + } + if len(envs) == 0 { + return nil, fmt.Errorf("no secrets found in file") + } + for key, value := range envs { + secrets[key] = []byte(value) + } + return secrets, nil + } + + body, err := getBody(opts) + if err != nil { + return nil, fmt.Errorf("did not understand secret body: %w", err) + } + secrets[opts.SecretName] = body + return secrets, nil } func getBody(opts *SetOptions) ([]byte, error) { - if opts.Body == "" { - if opts.IO.CanPrompt() { - err := prompt.SurveyAskOne(&survey.Password{ - Message: "Paste your secret", - }, &opts.Body) - if err != nil { - return nil, err - } - fmt.Fprintln(opts.IO.Out) - } else { - body, err := ioutil.ReadAll(opts.IO.In) - if err != nil { - return nil, fmt.Errorf("failed to read from STDIN: %w", err) - } - - return body, nil - } + if opts.Body != "" { + return []byte(opts.Body), nil } - return []byte(opts.Body), nil + if opts.IO.CanPrompt() { + var bodyInput string + err := prompt.SurveyAskOne(&survey.Password{ + Message: "Paste your secret", + }, &bodyInput) + if err != nil { + return nil, err + } + fmt.Fprintln(opts.IO.Out) + return []byte(bodyInput), nil + } + + body, err := ioutil.ReadAll(opts.IO.In) + if err != nil { + return nil, fmt.Errorf("failed to read from standard input: %w", err) + } + + return body, nil +} + +func mapRepoNamesToIDs(client *api.Client, host, defaultOwner string, repositoryNames []string) ([]int64, error) { + repos := make([]ghrepo.Interface, 0, len(repositoryNames)) + for _, repositoryName := range repositoryNames { + var repo ghrepo.Interface + if strings.Contains(repositoryName, "/") || defaultOwner == "" { + var err error + repo, err = ghrepo.FromFullNameWithHost(repositoryName, host) + if err != nil { + return nil, fmt.Errorf("invalid repository name: %w", err) + } + } else { + repo = ghrepo.NewWithHost(defaultOwner, repositoryName, host) + } + repos = append(repos, repo) + } + repositoryIDs, err := mapRepoToID(client, host, repos) + if err != nil { + return nil, fmt.Errorf("failed to look up IDs for repositories %v: %w", repositoryNames, err) + } + return repositoryIDs, nil } diff --git a/pkg/cmd/secret/set/set_test.go b/pkg/cmd/secret/set/set_test.go index 98ee5b368..d65ed1cae 100644 --- a/pkg/cmd/secret/set/set_test.go +++ b/pkg/cmd/secret/set/set_test.go @@ -4,10 +4,13 @@ import ( "bytes" "encoding/json" "fmt" + "io" "io/ioutil" "net/http" + "os" "testing" + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmd/secret/shared" @@ -200,11 +203,10 @@ func Test_setRun_repo(t *testing.T) { BaseRepo: func() (ghrepo.Interface, error) { return ghrepo.FromFullName("owner/repo") }, - IO: io, - SecretName: "cool_secret", - Body: "a secret", - // Cribbed from https://github.com/golang/crypto/commit/becbf705a91575484002d598f87d74f0002801e7 - RandomOverride: bytes.NewReader([]byte{5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5}), + IO: io, + SecretName: "cool_secret", + Body: "a secret", + RandomOverride: fakeRandom, } err := setRun(opts) @@ -239,12 +241,11 @@ func Test_setRun_env(t *testing.T) { BaseRepo: func() (ghrepo.Interface, error) { return ghrepo.FromFullName("owner/repo") }, - EnvName: "development", - IO: io, - SecretName: "cool_secret", - Body: "a secret", - // Cribbed from https://github.com/golang/crypto/commit/becbf705a91575484002d598f87d74f0002801e7 - RandomOverride: bytes.NewReader([]byte{5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5}), + EnvName: "development", + IO: io, + SecretName: "cool_secret", + Body: "a secret", + RandomOverride: fakeRandom, } err := setRun(opts) @@ -266,7 +267,7 @@ func Test_setRun_org(t *testing.T) { name string opts *SetOptions wantVisibility shared.Visibility - wantRepositories []int + wantRepositories []int64 }{ { name: "all vis", @@ -282,7 +283,7 @@ func Test_setRun_org(t *testing.T) { Visibility: shared.Selected, RepositoryNames: []string{"birkin", "UmbrellaCorporation/wesker"}, }, - wantRepositories: []int{1, 2}, + wantRepositories: []int64{1, 2}, }, } @@ -319,8 +320,7 @@ func Test_setRun_org(t *testing.T) { tt.opts.IO = io tt.opts.SecretName = "cool_secret" tt.opts.Body = "a secret" - // Cribbed from https://github.com/golang/crypto/commit/becbf705a91575484002d598f87d74f0002801e7 - tt.opts.RandomOverride = bytes.NewReader([]byte{5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5}) + tt.opts.RandomOverride = fakeRandom err := setRun(tt.opts) assert.NoError(t, err) @@ -391,8 +391,7 @@ func Test_setRun_user(t *testing.T) { tt.opts.IO = io tt.opts.SecretName = "cool_secret" tt.opts.Body = "a secret" - // Cribbed from https://github.com/golang/crypto/commit/becbf705a91575484002d598f87d74f0002801e7 - tt.opts.RandomOverride = bytes.NewReader([]byte{5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5}) + tt.opts.RandomOverride = fakeRandom err := setRun(tt.opts) assert.NoError(t, err) @@ -430,11 +429,10 @@ func Test_setRun_shouldNotStore(t *testing.T) { BaseRepo: func() (ghrepo.Interface, error) { return ghrepo.FromFullName("owner/repo") }, - IO: io, - Body: "a secret", - DoNotStore: true, - // Cribbed from https://github.com/golang/crypto/commit/becbf705a91575484002d598f87d74f0002801e7 - RandomOverride: bytes.NewReader([]byte{5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5}), + IO: io, + Body: "a secret", + DoNotStore: true, + RandomOverride: fakeRandom, } err := setRun(opts) @@ -500,3 +498,98 @@ func Test_getBodyPrompt(t *testing.T) { assert.NoError(t, err) assert.Equal(t, string(body), "cool secret") } + +func Test_getSecretsFromOptions(t *testing.T) { + genFile := func(s string) string { + f, err := ioutil.TempFile("", "gh-env.*") + if err != nil { + t.Fatal(err) + return "" + } + defer f.Close() + t.Cleanup(func() { + _ = os.Remove(f.Name()) + }) + _, err = f.WriteString(s) + if err != nil { + t.Fatal(err) + } + return f.Name() + } + + tests := []struct { + name string + opts SetOptions + isTTY bool + stdin string + want map[string]string + wantErr bool + }{ + { + name: "secret from arg", + opts: SetOptions{ + SecretName: "FOO", + Body: "bar", + EnvFile: "", + }, + want: map[string]string{"FOO": "bar"}, + }, + { + name: "secrets from stdin", + opts: SetOptions{ + Body: "", + EnvFile: "-", + }, + stdin: `FOO=bar`, + want: map[string]string{"FOO": "bar"}, + }, + { + name: "secrets from file", + opts: SetOptions{ + Body: "", + EnvFile: genFile(heredoc.Doc(` + FOO=bar + QUOTED="my value" + #IGNORED=true + export SHELL=bash + `)), + }, + stdin: `FOO=bar`, + want: map[string]string{ + "FOO": "bar", + "SHELL": "bash", + "QUOTED": "my value", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + io, stdin, _, _ := iostreams.Test() + io.SetStdinTTY(tt.isTTY) + io.SetStdoutTTY(tt.isTTY) + stdin.WriteString(tt.stdin) + opts := tt.opts + opts.IO = io + gotSecrets, err := getSecretsFromOptions(&opts) + if err != nil { + if !tt.wantErr { + t.Fatalf("getSecretsFromOptions() error = %v, wantErr %v", err, tt.wantErr) + } + } else if tt.wantErr { + t.Fatalf("getSecretsFromOptions() error = %v, wantErr %v", err, tt.wantErr) + } + if len(gotSecrets) != len(tt.want) { + t.Fatalf("getSecretsFromOptions() = got %d secrets, want %d", len(gotSecrets), len(tt.want)) + } + for k, v := range gotSecrets { + if tt.want[k] != string(v) { + t.Errorf("getSecretsFromOptions() %s = got %q, want %q", k, string(v), tt.want[k]) + } + } + }) + } +} + +func fakeRandom() io.Reader { + return bytes.NewReader(bytes.Repeat([]byte{5}, 32)) +}