From 5589583e4d0ae97b9b76f0bb2380939382dda57d Mon Sep 17 00:00:00 2001 From: lpessoa Date: Fri, 15 Oct 2021 13:38:41 +0000 Subject: [PATCH] Adding set secrets from env files Allows to set multiple secrets from an env file. gh secret set -f secrets.env The env file follows a simple format as defined in https://github.com/joho/godotenv SPAM=eggs FOO="bar" --- go.mod | 1 + go.sum | 2 + pkg/cmd/secret/set/http.go | 3 +- pkg/cmd/secret/set/set.go | 117 +++++++++++++++++++++++++------------ 4 files changed, 85 insertions(+), 38 deletions(-) diff --git a/go.mod b/go.mod index cf1021ef2..bb9a0a649 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( 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..b194a19fc 100644 --- a/go.sum +++ b/go.sum @@ -286,6 +286,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..61239e4e3 100644 --- a/pkg/cmd/secret/set/http.go +++ b/pkg/cmd/secret/set/http.go @@ -82,8 +82,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 +func putOrgSecret(client *api.Client, host string, pk *PubKey, opts SetOptions, secretName string, eValue string) error { orgName := opts.OrgName visibility := opts.Visibility diff --git a/pkg/cmd/secret/set/set.go b/pkg/cmd/secret/set/set.go index 4531ab46d..ac0f11df1 100644 --- a/pkg/cmd/secret/set/set.go +++ b/pkg/cmd/secret/set/set.go @@ -16,6 +16,7 @@ import ( "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/cli/cli/v2/pkg/prompt" + "github.com/joho/godotenv" "github.com/spf13/cobra" "golang.org/x/crypto/nacl/box" ) @@ -36,6 +37,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 +83,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 +96,12 @@ 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 len(args) == 0 { - if !opts.DoNotStore { + if !opts.DoNotStore && opts.EnvFile == "" { return cmdutil.FlagErrorf("must pass name argument") } } else { @@ -136,14 +145,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() @@ -187,42 +197,77 @@ 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) - } else { - err = putRepoSecret(client, pk, baseRepo, opts.SecretName, encoded) - } - if err != nil { - return fmt.Errorf("failed to set secret: %w", err) - } - - if opts.IO.IsStdoutTTY() { - target := orgName - if opts.UserSecrets { - target = "your user" - } else if orgName == "" { - target = ghrepo.FullName(baseRepo) + for secretKey, secret := range secrets { + // for env files the randomoverride used in tests needs to be reset on every iteration + if len(opts.EnvFile) > 0 { + if rd, ok := opts.RandomOverride.(io.Seeker); ok { + if _, err := rd.Seek(0, 0); err != nil { + return fmt.Errorf("internal error: %w", err) + } + } } - cs := opts.IO.ColorScheme() - fmt.Fprintf(opts.IO.Out, "%s Set secret %s for %s\n", cs.SuccessIconWithColor(cs.Green), opts.SecretName, target) + eBody, err := box.SealAnonymous(nil, secret[:], &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, secretKey, encoded) + } else if envName != "" { + err = putEnvSecret(client, pk, baseRepo, envName, secretKey, encoded) + } else if opts.UserSecrets { + err = putUserSecret(client, host, pk, *opts, encoded) + } else { + err = putRepoSecret(client, pk, baseRepo, secretKey, encoded) + } + if err != nil { + return fmt.Errorf("failed to set secret: %w", err) + } + + if opts.IO.IsStdoutTTY() { + 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), secretKey, target) + } + } + return nil +} + +func getSecretsFromOptions(opts *SetOptions) (map[string][]byte, error) { + secrets := make(map[string][]byte) + + if len(opts.EnvFile) > 0 { + envs, err := godotenv.Read(opts.EnvFile) + if err != nil { + return nil, fmt.Errorf("could no open env file: %w", err) + } + for key, value := range envs { + secrets[key] = []byte(value) + } + + } else { + body, err := getBody(opts) + if err != nil { + return nil, fmt.Errorf("did not understand secret body: %w", err) + } + secrets[opts.SecretName] = body } - return nil + if len(secrets) == 0 { + return nil, fmt.Errorf("no secrets defined") + } + return secrets, nil } func getBody(opts *SetOptions) ([]byte, error) {