Merge pull request #4534 from lpessoa/lp-secret-load-env

Adding set secrets from env files

Closes #3610
This commit is contained in:
Mislav Marohnić 2021-12-20 23:43:48 +01:00 committed by GitHub
commit 8272035aac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 341 additions and 144 deletions

2
go.mod
View file

@ -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

5
go.sum
View file

@ -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=

View file

@ -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
}

View file

@ -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
}

View file

@ -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))
}