From 838c1af44883b2f9efbf3d532b4cf42d8bec4561 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Mon, 17 Mar 2025 09:51:21 -0300 Subject: [PATCH 1/3] Add file option to secret set CLI command to retrieve secrets from .env or .yaml files --- cli/packages/cmd/secrets.go | 17 +++++-- cli/packages/util/secrets.go | 95 ++++++++++++++++++++++++++++++++++- docs/cli/commands/secrets.mdx | 15 ++++++ 3 files changed, 123 insertions(+), 4 deletions(-) diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index c846db10d..75bd459ca 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -143,7 +143,12 @@ var secretsSetCmd = &cobra.Command{ Short: "Used set secrets", Use: "set [secrets]", DisableFlagsInUseLine: true, - Args: cobra.MinimumNArgs(1), + Args: func(cmd *cobra.Command, args []string) error { + if cmd.Flags().Changed("file") { + return cobra.MinimumNArgs(0)(cmd, args) + } + return cobra.MinimumNArgs(1)(cmd, args) + }, Run: func(cmd *cobra.Command, args []string) { token, err := util.GetInfisicalToken(cmd) if err != nil { @@ -177,13 +182,18 @@ var secretsSetCmd = &cobra.Command{ util.HandleError(err, "Unable to parse secret type") } + file, err := cmd.Flags().GetString("file") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + var secretOperations []models.SecretSetOperation if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { if projectId == "" { util.PrintErrorMessageAndExit("When using service tokens or machine identities, you must set the --projectId flag") } - secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, token) + secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, token, file) } else { if projectId == "" { workspaceFile, err := util.GetWorkSpaceFromFile() @@ -206,7 +216,7 @@ var secretsSetCmd = &cobra.Command{ secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, &models.TokenDetails{ Type: "", Token: loggedInUserDetails.UserCredentials.JTWToken, - }) + }, file) } if err != nil { @@ -691,6 +701,7 @@ func init() { secretsSetCmd.Flags().String("projectId", "", "manually set the project ID to for setting secrets when using machine identity based auth") secretsSetCmd.Flags().String("path", "/", "set secrets within a folder path") secretsSetCmd.Flags().String("type", util.SECRET_TYPE_SHARED, "the type of secret to create: personal or shared") + secretsSetCmd.Flags().String("file", "", "Path to a file with key=value secret pairs (comments with # or // are ignored)") secretsDeleteCmd.Flags().String("type", "personal", "the type of secret to delete: personal or shared (default: personal)") secretsDeleteCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index 0e26e03b0..07f55ce33 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -17,6 +17,7 @@ import ( "github.com/go-resty/resty/v2" "github.com/rs/zerolog/log" "github.com/zalando/go-keyring" + "gopkg.in/yaml.v3" ) func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool, tagSlugs string, expandSecretReferences bool) ([]models.SingleEnvironmentVariable, error) { @@ -563,7 +564,99 @@ func GetPlainTextWorkspaceKey(authenticationToken string, receiverPrivateKey str return crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey), nil } -func SetRawSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string, projectId string, tokenDetails *models.TokenDetails) ([]models.SecretSetOperation, error) { +func parseSecrets(fileName string, content string) (map[string]string, error) { + secrets := make(map[string]string) + + if strings.HasSuffix(fileName, ".yaml") || strings.HasSuffix(fileName, ".yml") { + // Handle YAML secrets + var yamlData map[string]interface{} + if err := yaml.Unmarshal([]byte(content), &yamlData); err != nil { + return nil, fmt.Errorf("failed to parse YAML file: %v", err) + } + + for key, value := range yamlData { + if strValue, ok := value.(string); ok { + secrets[key] = strValue + } else { + return nil, fmt.Errorf("YAML secret '%s' must be a string", key) + } + } + } else { + // Handle .env files + lines := strings.Split(content, "\n") + + for _, line := range lines { + line = strings.TrimSpace(line) + + // Ignore empty lines and comments + if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") { + continue + } + + // Ensure it's a valid key=value pair + splitKeyValue := strings.SplitN(line, "=", 2) + if len(splitKeyValue) != 2 { + return nil, fmt.Errorf("invalid format, expected key=value in line: %s", line) + } + + key, value := strings.TrimSpace(splitKeyValue[0]), strings.TrimSpace(splitKeyValue[1]) + + // Handle quoted values + if (strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`)) || + (strings.HasPrefix(value, `'`) && strings.HasSuffix(value, `'`)) { + value = value[1 : len(value)-1] // Remove surrounding quotes + } + + secrets[key] = value + } + } + + return secrets, nil +} + +func validateSecretKey(key string) error { + if key == "" { + return errors.New("secret keys cannot be empty") + } + if unicode.IsNumber(rune(key[0])) { + return fmt.Errorf("secret key '%s' cannot start with a number", key) + } + if strings.Contains(key, " ") { + return fmt.Errorf("secret key '%s' cannot contain spaces", key) + } + return nil +} + +func SetRawSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string, projectId string, tokenDetails *models.TokenDetails, file string) ([]models.SecretSetOperation, error) { + if file != "" { + content, err := os.ReadFile(file) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + PrintErrorMessageAndExit("File does not exist") + } + return nil, fmt.Errorf("unable to process file [err=%v]", err) + } + + parsedSecrets, err := parseSecrets(file, string(content)) + if err != nil { + PrintErrorMessageAndExit(fmt.Sprintf("error parsing secrets: %v", err)) + } + + // Step 2: Validate secrets + for key, value := range parsedSecrets { + if err := validateSecretKey(key); err != nil { + PrintErrorMessageAndExit(err.Error()) + } + if strings.TrimSpace(value) == "" { + PrintErrorMessageAndExit(fmt.Sprintf("Secret key '%s' has an empty value", key)) + } + secretArgs = append(secretArgs, fmt.Sprintf("%s=%s", key, value)) + } + + if len(secretArgs) == 0 { + PrintErrorMessageAndExit("no valid secrets found in the file") + } + } if tokenDetails == nil { return nil, fmt.Errorf("unable to process set secret operations, token details are missing") diff --git a/docs/cli/commands/secrets.mdx b/docs/cli/commands/secrets.mdx index 2dff5cf7b..d1f4baa9f 100644 --- a/docs/cli/commands/secrets.mdx +++ b/docs/cli/commands/secrets.mdx @@ -219,6 +219,21 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb ``` + + Used to set secrets from a file, supporting both `.env` and `YAML` formats. The file path can be either absolute or relative to the current working directory. + + The file should contain secrets in the following formats: + - `key=value` for `.env` files + - `key: value` for YAML files + + Comments can be written using `# comment` or `// comment`. Empty lines will be ignored during processing. + + + ```bash + # Example + infisical secrets set --file="./.env" + ``` + From d4842dd2736369fbb32dcce859a79a26c850fb69 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Tue, 18 Mar 2025 08:32:31 -0300 Subject: [PATCH 2/3] Improve secret set --file message and make it mutually exclusive with manual secrets args --- cli/packages/cmd/secrets.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index 75bd459ca..1ca53e86d 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -145,7 +145,7 @@ var secretsSetCmd = &cobra.Command{ DisableFlagsInUseLine: true, Args: func(cmd *cobra.Command, args []string) error { if cmd.Flags().Changed("file") { - return cobra.MinimumNArgs(0)(cmd, args) + return cobra.ExactArgs(0)(cmd, args) } return cobra.MinimumNArgs(1)(cmd, args) }, @@ -701,7 +701,7 @@ func init() { secretsSetCmd.Flags().String("projectId", "", "manually set the project ID to for setting secrets when using machine identity based auth") secretsSetCmd.Flags().String("path", "/", "set secrets within a folder path") secretsSetCmd.Flags().String("type", util.SECRET_TYPE_SHARED, "the type of secret to create: personal or shared") - secretsSetCmd.Flags().String("file", "", "Path to a file with key=value secret pairs (comments with # or // are ignored)") + secretsSetCmd.Flags().String("file", "", "Load secrets from the specified file. File format: .env or YAML (comments: # or //). This option is mutually exclusive with command-line secrets arguments.") secretsDeleteCmd.Flags().String("type", "personal", "the type of secret to delete: personal or shared (default: personal)") secretsDeleteCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") From ad6f285b59eb955362fd1ecbef35757f36b6957f Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Tue, 18 Mar 2025 09:58:10 -0300 Subject: [PATCH 3/3] Throw if file and args secrets are used on secret set command --- cli/packages/cmd/secrets.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index 1ca53e86d..8e9f5d63b 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -145,7 +145,10 @@ var secretsSetCmd = &cobra.Command{ DisableFlagsInUseLine: true, Args: func(cmd *cobra.Command, args []string) error { if cmd.Flags().Changed("file") { - return cobra.ExactArgs(0)(cmd, args) + if len(args) > 0 { + return fmt.Errorf("secrets cannot be provided as command-line arguments when the --file option is used. Please choose either file-based or argument-based secret input") + } + return nil } return cobra.MinimumNArgs(1)(cmd, args) },