From 35e476d916373f394f91093515b1b40ae1bdb079 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 29 Aug 2024 22:35:21 +0400 Subject: [PATCH 1/6] Fix: Runtime bugs --- cli/packages/cmd/run.go | 13 ++++++-- cli/packages/util/exec.go | 63 +++++++++++++++++++-------------------- 2 files changed, 41 insertions(+), 35 deletions(-) diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index f4c3188ea8..1ad44a4bae 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -243,7 +243,9 @@ func executeSpecifiedCommand(commandFlag string, args []string, watchMode bool, // start the process log.Info().Msgf(color.GreenString("Injecting %v Infisical secrets into your application process", environment.SecretsCount)) - cmd, err = util.RunCommand(commandFlag, args, environment.Variables) + + shouldWaitForExit := !watchMode + cmd, err = util.RunCommand(commandFlag, args, environment.Variables, shouldWaitForExit) if err != nil { defer WaitGroup.Done() util.HandleError(err) @@ -276,10 +278,11 @@ func executeSpecifiedCommand(commandFlag string, args []string, watchMode bool, util.HandleError(err, "Failed to fetch secrets") } startProcess(initialEnvironment) - recheckSecretsChannel := make(chan bool, 1) // this is the only logic strictly related to watch mode, the rest is shared with non-watch mode if watchMode { + recheckSecretsChannel := make(chan bool, 1) + log.Info().Msg(color.HiMagentaString("[HOT RELOAD] Watching for secret changes...")) // a simple goroutine that triggers the recheckSecretsChan every watch interval (defaults to 10 seconds) @@ -362,6 +365,12 @@ func init() { func createInjectableEnvironment(request models.GetAllSecretsParameters, projectConfigDir string, secretOverriding bool, shouldExpandSecrets bool, token *models.TokenDetails) (models.InjectableEnvironmentResult, error) { + if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { + request.InfisicalToken = token.Token + } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER { + request.UniversalAuthAccessToken = token.Token + } + secrets, err := util.GetAllEnvironmentVariables(request, projectConfigDir) if err != nil { diff --git a/cli/packages/util/exec.go b/cli/packages/util/exec.go index abf4541813..2cdb50f424 100644 --- a/cli/packages/util/exec.go +++ b/cli/packages/util/exec.go @@ -1,24 +1,22 @@ package util import ( + "fmt" "os" "os/exec" "os/signal" "runtime" - "strings" "syscall" - - "github.com/mattn/go-isatty" ) -func RunCommand(singleCommand string, args []string, env []string) (*exec.Cmd, error) { +func RunCommand(singleCommand string, args []string, env []string, waitForExit bool) (*exec.Cmd, error) { var c *exec.Cmd var err error if singleCommand != "" { - c, err = RunCommandFromString(singleCommand, env) + c, err = RunCommandFromString(singleCommand, env, waitForExit) } else { - c, err = RunCommandFromArgs(args, env) + c, err = RunCommandFromArgs(args, env, waitForExit) } return c, err @@ -30,66 +28,65 @@ func IsProcessRunning(p *os.Process) bool { } // For "infisical run -- COMMAND" -func RunCommandFromArgs(command []string, env []string) (*exec.Cmd, error) { - cmd := exec.Command(command[0], command[1:]...) - cmd.Env = env +func RunCommandFromArgs(args []string, env []string, waitForExit bool) (*exec.Cmd, error) { + cmd := exec.Command(args[0], args[1:]...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr + cmd.Env = env - err := execCommand(cmd) + err := execCommand(cmd, waitForExit) return cmd, err } -func execCommand(cmd *exec.Cmd) error { - - shouldForward := !isatty.IsTerminal(os.Stdout.Fd()) - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan) +func execCommand(cmd *exec.Cmd, waitForExit bool) error { + sigChannel := make(chan os.Signal, 1) + signal.Notify(sigChannel) if err := cmd.Start(); err != nil { return err } - // handle all signals go func() { for { - if shouldForward { - // forward to process - sig := <-sigChan - cmd.Process.Signal(sig) - } else { - <-sigChan - } + sig := <-sigChannel + _ = cmd.Process.Signal(sig) // process all sigs } }() + if !waitForExit { + return nil + } + + if err := cmd.Wait(); err != nil { + _ = cmd.Process.Signal(os.Kill) + return fmt.Errorf("failed to wait for command termination: %v", err) + } + + waitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus) + os.Exit(waitStatus.ExitStatus()) return nil } // For "infisical run --command=COMMAND" -func RunCommandFromString(command string, env []string) (*exec.Cmd, error) { +func RunCommandFromString(command string, env []string, waitForExit bool) (*exec.Cmd, error) { shell := [2]string{"sh", "-c"} if runtime.GOOS == "windows" { shell = [2]string{"cmd", "/C"} } else { - // these shells all support the same options we use for sh - shells := []string{"/bash", "/dash", "/fish", "/zsh", "/ksh", "/csh", "/tcsh"} - envShell := os.Getenv("SHELL") - for _, s := range shells { - if strings.HasSuffix(envShell, s) { - shell[0] = envShell - break - } + currentShell := os.Getenv("SHELL") + if currentShell != "" { + shell[0] = currentShell } } + cmd := exec.Command(shell[0], shell[1], command) // #nosec G204 nosemgrep: semgrep_configs.prohibit-exec-command cmd.Env = env cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - err := execCommand(cmd) + err := execCommand(cmd, waitForExit) return cmd, err } From 35a63b8cc67dfc1077c8027dc5f0e53856c2984c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 29 Aug 2024 22:54:49 +0400 Subject: [PATCH 2/6] Fix: Fixed merge related changes --- cli/packages/models/cli.go | 6 +++++ cli/packages/util/helper.go | 14 +++++++++++ docs/cli/commands/run.mdx | 50 +++++++++++++++++++++++++++---------- 3 files changed, 57 insertions(+), 13 deletions(-) diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index c4bbd01755..1bad5e3270 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -104,6 +104,12 @@ type GetAllSecretsParameters struct { Recursive bool } +type InjectableEnvironmentResult struct { + Variables []string + ETag string + SecretsCount int +} + type GetAllFoldersParameters struct { WorkspaceId string Environment string diff --git a/cli/packages/util/helper.go b/cli/packages/util/helper.go index 69a310efae..b758ebd9d7 100644 --- a/cli/packages/util/helper.go +++ b/cli/packages/util/helper.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/sha256" "encoding/base64" + "encoding/hex" "fmt" "math/rand" "os" @@ -298,3 +299,16 @@ func GenerateRandomString(length int) string { } return string(b) } + +func GenerateETagFromSecrets(secrets []models.SingleEnvironmentVariable) string { + sortedSecrets := SortSecretsByKeys(secrets) + content := []byte{} + + for _, secret := range sortedSecrets { + content = append(content, []byte(secret.Key)...) + content = append(content, []byte(secret.Value)...) + } + + hash := sha256.Sum256(content) + return fmt.Sprintf(`"%s"`, hex.EncodeToString(hash[:])) +} diff --git a/docs/cli/commands/run.mdx b/docs/cli/commands/run.mdx index 74aa84947c..bfa4bd6936 100644 --- a/docs/cli/commands/run.mdx +++ b/docs/cli/commands/run.mdx @@ -47,20 +47,20 @@ $ infisical run -- npm run dev Used to fetch secrets via a [machine identity](/documentation/platform/identities/machine-identities) apposed to logged in credentials. Simply, export this variable in the terminal before running this command. ```bash - # Example - export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id= --client-secret= --silent --plain) # --plain flag will output only the token, so it can be fed to an environment variable. --silent will disable any update messages. + # Example + export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id= --client-secret= --silent --plain) # --plain flag will output only the token, so it can be fed to an environment variable. --silent will disable any update messages. ``` Alternatively, you may use service tokens. Please note, however, that service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities). They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys). + ```bash - # Example - export INFISICAL_TOKEN= + # Example + export INFISICAL_TOKEN= ``` - - + @@ -69,22 +69,30 @@ $ infisical run -- npm run dev To use, simply export this variable in the terminal before running this command. ```bash - # Example - export INFISICAL_DISABLE_UPDATE_CHECK=true + # Example + export INFISICAL_DISABLE_UPDATE_CHECK=true ``` - ### Flags - - Explicitly set the directory where the .infisical.json resides. This is useful for some monorepo setups. + + By passing the `watch` flag, you are telling the CLI to watch for changes that happen in your Infisical project. + If secret changes happen, the command you provided will automatically be restarted with the new environment variables attached. ```bash - # Example - infisical run --project-config-dir=/some-dir -- printenv + # Example + infisical run --watch -- printenv ``` + + + + Explicitly set the directory where the .infisical.json resides. This is useful for some monorepo setups. + ```bash + # Example + infisical run --project-config-dir=/some-dir -- printenv + ``` @@ -172,3 +180,19 @@ $ infisical run -- npm run dev + + +## Automatically reload command when secrets change + +To automatically reload your command when secrets change, use the `--watch` flag. + +```bash +infisical run --watch -- npm run dev +``` + +This will watch for changes in your secrets and automatically restart your command with the new secrets. +When your command restarts, it will have the new environment variables injeceted into it. + + + Please note that this feature is intended for development purposes. It is not recommended to use this in production environments. Generally it's not recommended to automatically reload your application in production when remote changes are made. + \ No newline at end of file From 911b62c63a455744475a97a1143aad45067c3218 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 4 Sep 2024 10:05:57 +0400 Subject: [PATCH 3/6] Update run.go --- cli/packages/cmd/run.go | 224 +++++++++++++++++++++++++++------------- 1 file changed, 154 insertions(+), 70 deletions(-) diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index 1ad44a4bae..736f933a65 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -8,6 +8,8 @@ import ( "fmt" "os" "os/exec" + "os/signal" + "runtime" "strings" "sync" "syscall" @@ -172,12 +174,161 @@ var runCmd = &cobra.Command{ Set("multi-command", cmd.Flag("command").Value.String()). Set("version", util.CLI_VERSION)) - executeSpecifiedCommand(command, args, watchMode, watchModeInterval, request, projectConfigDir, shouldExpandSecrets, secretOverriding, token) + if watchMode { + executeCommandWithWatchMode(command, args, watchMode, watchModeInterval, request, projectConfigDir, shouldExpandSecrets, secretOverriding, token) + } else { + if cmd.Flags().Changed("command") { + command := cmd.Flag("command").Value.String() + err = executeMultipleCommandWithEnvs(command, injectableEnvironment.SecretsCount, injectableEnvironment.Variables) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + } else { + err = executeSingleCommandWithEnvs(args, injectableEnvironment.SecretsCount, injectableEnvironment.Variables) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + } + } }, } -func executeSpecifiedCommand(commandFlag string, args []string, watchMode bool, watchModeInterval int, request models.GetAllSecretsParameters, projectConfigDir string, expandSecrets bool, secretOverriding bool, token *models.TokenDetails) { +func filterReservedEnvVars(env map[string]models.SingleEnvironmentVariable) { + var ( + reservedEnvVars = []string{ + "HOME", "PATH", "PS1", "PS2", + "PWD", "EDITOR", "XAUTHORITY", "USER", + "TERM", "TERMINFO", "SHELL", "MAIL", + } + + reservedEnvVarPrefixes = []string{ + "XDG_", + "LC_", + } + ) + + for _, reservedEnvName := range reservedEnvVars { + if _, ok := env[reservedEnvName]; ok { + delete(env, reservedEnvName) + util.PrintWarning(fmt.Sprintf("Infisical secret named [%v] has been removed because it is a reserved secret name", reservedEnvName)) + } + } + + for _, reservedEnvPrefix := range reservedEnvVarPrefixes { + for envName := range env { + if strings.HasPrefix(envName, reservedEnvPrefix) { + delete(env, envName) + util.PrintWarning(fmt.Sprintf("Infisical secret named [%v] has been removed because it contains a reserved prefix", envName)) + } + } + } +} + +func init() { + rootCmd.AddCommand(runCmd) + runCmd.Flags().String("token", "", "fetch secrets using service token or machine identity access token") + runCmd.Flags().String("projectId", "", "manually set the project ID to fetch secrets from when using machine identity based auth") + runCmd.Flags().StringP("env", "e", "dev", "set the environment (dev, prod, etc.) from which your secrets should be pulled from") + runCmd.Flags().Bool("expand", true, "parse shell parameter expansions in your secrets") + runCmd.Flags().Bool("include-imports", true, "import linked secrets ") + runCmd.Flags().Bool("recursive", false, "fetch secrets from all sub-folders") + runCmd.Flags().Bool("secret-overriding", true, "prioritizes personal secrets, if any, with the same name over shared secrets") + runCmd.Flags().Bool("watch", false, "enable reload of application when secrets change") + runCmd.Flags().Int("watch-interval", 10, "interval in seconds to check for secret changes") + runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")") + runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ") + runCmd.Flags().String("path", "/", "get secrets within a folder path") + runCmd.Flags().String("project-config-dir", "", "explicitly set the directory where the .infisical.json resides") +} + +// Will execute a single command and pass in the given secrets into the process +func executeSingleCommandWithEnvs(args []string, secretsCount int, env []string) error { + command := args[0] + argsForCommand := args[1:] + + log.Info().Msgf(color.GreenString("Injecting %v Infisical secrets into your application process", secretsCount)) + + cmd := exec.Command(command, argsForCommand...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = env + + return execBasicCmd(cmd) +} + +func executeMultipleCommandWithEnvs(fullCommand string, secretsCount int, env []string) error { + shell := [2]string{"sh", "-c"} + if runtime.GOOS == "windows" { + shell = [2]string{"cmd", "/C"} + } else { + currentShell := os.Getenv("SHELL") + if currentShell != "" { + shell[0] = currentShell + } + } + + cmd := exec.Command(shell[0], shell[1], fullCommand) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = env + + log.Info().Msgf(color.GreenString("Injecting %v Infisical secrets into your application process", secretsCount)) + log.Debug().Msgf("executing command: %s %s %s \n", shell[0], shell[1], fullCommand) + + return execBasicCmd(cmd) +} + +func execBasicCmd(cmd *exec.Cmd) error { + sigChannel := make(chan os.Signal, 1) + signal.Notify(sigChannel) + + if err := cmd.Start(); err != nil { + return err + } + + go func() { + for { + sig := <-sigChannel + _ = cmd.Process.Signal(sig) // process all sigs + } + }() + + if err := cmd.Wait(); err != nil { + _ = cmd.Process.Signal(os.Kill) + return fmt.Errorf("failed to wait for command termination: %v", err) + } + + waitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus) + os.Exit(waitStatus.ExitStatus()) + return nil +} + +func waitForExitCommand(cmd *exec.Cmd) (int, error) { + if err := cmd.Wait(); err != nil { + // ignore errors + cmd.Process.Signal(os.Kill) // #nosec G104 + + if exitError, ok := err.(*exec.ExitError); ok { + return exitError.ExitCode(), exitError + } + + return 2, err + } + + waitStatus, ok := cmd.ProcessState.Sys().(syscall.WaitStatus) + if !ok { + return 2, fmt.Errorf("unexpected ProcessState type, expected syscall.WaitStatus, got %T", waitStatus) + } + return waitStatus.ExitStatus(), nil +} + +func executeCommandWithWatchMode(commandFlag string, args []string, watchMode bool, watchModeInterval int, request models.GetAllSecretsParameters, projectConfigDir string, expandSecrets bool, secretOverriding bool, token *models.TokenDetails) { var cmd *exec.Cmd var err error @@ -255,7 +406,7 @@ func executeSpecifiedCommand(commandFlag string, args []string, watchMode bool, defer processMutex.Unlock() defer WaitGroup.Done() - exitCode, err := WaitForExitCommand(cmd) + exitCode, err := waitForExitCommand(cmd) // ignore errors if we are being terminated if !beingTerminated { @@ -315,54 +466,6 @@ func executeSpecifiedCommand(commandFlag string, args []string, watchMode bool, } } -func filterReservedEnvVars(env map[string]models.SingleEnvironmentVariable) { - var ( - reservedEnvVars = []string{ - "HOME", "PATH", "PS1", "PS2", - "PWD", "EDITOR", "XAUTHORITY", "USER", - "TERM", "TERMINFO", "SHELL", "MAIL", - } - - reservedEnvVarPrefixes = []string{ - "XDG_", - "LC_", - } - ) - - for _, reservedEnvName := range reservedEnvVars { - if _, ok := env[reservedEnvName]; ok { - delete(env, reservedEnvName) - util.PrintWarning(fmt.Sprintf("Infisical secret named [%v] has been removed because it is a reserved secret name", reservedEnvName)) - } - } - - for _, reservedEnvPrefix := range reservedEnvVarPrefixes { - for envName := range env { - if strings.HasPrefix(envName, reservedEnvPrefix) { - delete(env, envName) - util.PrintWarning(fmt.Sprintf("Infisical secret named [%v] has been removed because it contains a reserved prefix", envName)) - } - } - } -} - -func init() { - rootCmd.AddCommand(runCmd) - runCmd.Flags().String("token", "", "fetch secrets using service token or machine identity access token") - runCmd.Flags().String("projectId", "", "manually set the project ID to fetch secrets from when using machine identity based auth") - runCmd.Flags().StringP("env", "e", "dev", "set the environment (dev, prod, etc.) from which your secrets should be pulled from") - runCmd.Flags().Bool("expand", true, "parse shell parameter expansions in your secrets") - runCmd.Flags().Bool("include-imports", true, "import linked secrets ") - runCmd.Flags().Bool("recursive", false, "fetch secrets from all sub-folders") - runCmd.Flags().Bool("secret-overriding", true, "prioritizes personal secrets, if any, with the same name over shared secrets") - runCmd.Flags().Bool("watch", false, "enable reload of application when secrets change") - runCmd.Flags().Int("watch-interval", 10, "interval in seconds to check for secret changes") - runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")") - runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ") - runCmd.Flags().String("path", "/", "get secrets within a folder path") - runCmd.Flags().String("project-config-dir", "", "explicitly set the directory where the .infisical.json resides") -} - func createInjectableEnvironment(request models.GetAllSecretsParameters, projectConfigDir string, secretOverriding bool, shouldExpandSecrets bool, token *models.TokenDetails) (models.InjectableEnvironmentResult, error) { if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { @@ -426,22 +529,3 @@ func createInjectableEnvironment(request models.GetAllSecretsParameters, project SecretsCount: len(secretsByKey), }, nil } - -func WaitForExitCommand(cmd *exec.Cmd) (int, error) { - if err := cmd.Wait(); err != nil { - // ignore errors - cmd.Process.Signal(os.Kill) // #nosec G104 - - if exitError, ok := err.(*exec.ExitError); ok { - return exitError.ExitCode(), exitError - } - - return 2, err - } - - waitStatus, ok := cmd.ProcessState.Sys().(syscall.WaitStatus) - if !ok { - return 2, fmt.Errorf("unexpected ProcessState type, expected syscall.WaitStatus, got %T", waitStatus) - } - return waitStatus.ExitStatus(), nil -} From 0826b40e2af029e48e8f17aabcb708456061c4b8 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 4 Sep 2024 10:18:17 +0400 Subject: [PATCH 4/6] Fixes and requested changes --- cli/packages/cmd/run.go | 74 ++++++++++++++--------------------------- 1 file changed, 25 insertions(+), 49 deletions(-) diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index 736f933a65..f9126340c9 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -18,13 +18,12 @@ import ( "github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/util" "github.com/fatih/color" - "github.com/posthog/posthog-go" "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) var ErrManualSignalInterrupt = errors.New("signal: interrupt") -var WaitGroup = new(sync.WaitGroup) +var watcherWaitGroup = new(sync.WaitGroup) // runCmd represents the run command var runCmd = &cobra.Command{ @@ -38,8 +37,6 @@ var runCmd = &cobra.Command{ Args: func(cmd *cobra.Command, args []string) error { // Check if the --command flag has been set commandFlagSet := cmd.Flags().Changed("command") - watchIntervalFlagSet := cmd.Flags().Changed("watch-interval") - watchFlagSet := cmd.Flags().Changed("watch") // If the --command flag has been set, check if a value was provided if commandFlagSet { @@ -59,20 +56,6 @@ var runCmd = &cobra.Command{ } } - // If the --watch flag has been set, the --watch-interval flag should also be set - if watchFlagSet && watchIntervalFlagSet { - // Ensure that the --watch-interval flag is set to a positive integer, and is at least 5 seconds - - watchInterval, err := cmd.Flags().GetInt("watch-interval") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - if watchInterval < 5 { - return fmt.Errorf("watch interval must be at least 5 seconds, you passed %d seconds", watchInterval) - } - } - return nil }, Run: func(cmd *cobra.Command, args []string) { @@ -123,6 +106,11 @@ var runCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } + // If the --watch flag has been set, the --watch-interval flag should also be set + if watchMode && watchModeInterval < 5 { + util.HandleError(fmt.Errorf("watch interval must be at least 5 seconds, you passed %d seconds", watchModeInterval)) + } + shouldExpandSecrets, err := cmd.Flags().GetBool("expand") if err != nil { util.HandleError(err, "Unable to parse flag") @@ -157,23 +145,13 @@ var runCmd = &cobra.Command{ Recursive: recursive, } - injectableEnvironment, err := createInjectableEnvironment(request, projectConfigDir, secretOverriding, shouldExpandSecrets, token) + injectableEnvironment, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, shouldExpandSecrets, token) if err != nil { util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid") } log.Debug().Msgf("injecting the following environment variables into shell: %v", injectableEnvironment.Variables) - Telemetry.CaptureEvent("cli-command:run", - posthog.NewProperties(). - Set("secretsCount", injectableEnvironment.SecretsCount). - Set("environment", environmentName). - Set("isUsingServiceToken", token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER). - Set("isUsingUniversalAuthToken", token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER). - Set("single-command", strings.Join(args, " ")). - Set("multi-command", cmd.Flag("command").Value.String()). - Set("version", util.CLI_VERSION)) - if watchMode { executeCommandWithWatchMode(command, args, watchMode, watchModeInterval, request, projectConfigDir, shouldExpandSecrets, secretOverriding, token) } else { @@ -338,9 +316,14 @@ func executeCommandWithWatchMode(commandFlag string, args []string, watchMode bo var processMutex sync.Mutex var beingTerminated = false var currentETag string + environmentVariables, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, expandSecrets, token) - startProcess := func(environment models.InjectableEnvironmentResult) { - currentETag = environment.ETag + if err != nil { + util.HandleError(err, "Failed to fetch secrets") + } + + runCommandWithWatcher := func() { + currentETag = environmentVariables.ETag secretsFetchedAt := time.Now() if secretsFetchedAt.After(lastSecretsFetch) { lastSecretsFetch = secretsFetchedAt @@ -349,6 +332,7 @@ func executeCommandWithWatchMode(commandFlag string, args []string, watchMode bo shouldRestartProcess := cmd != nil // terminate the old process before starting a new one if shouldRestartProcess { + log.Info().Msg(color.HiMagentaString("[HOT RELOAD] Environment changes detected. Reloading process...")) beingTerminated = true log.Debug().Msgf(color.HiMagentaString("[HOT RELOAD] Sending SIGTERM to PID %d", cmd.Process.Pid)) @@ -386,25 +370,21 @@ func executeCommandWithWatchMode(commandFlag string, args []string, watchMode bo } beingTerminated = false - WaitGroup.Add(1) - - if shouldRestartProcess { - log.Info().Msg(color.HiMagentaString("[HOT RELOAD] Environment changes detected. Reloading process...")) - } + watcherWaitGroup.Add(1) // start the process - log.Info().Msgf(color.GreenString("Injecting %v Infisical secrets into your application process", environment.SecretsCount)) + log.Info().Msgf(color.GreenString("Injecting %v Infisical secrets into your application process", environmentVariables.SecretsCount)) shouldWaitForExit := !watchMode - cmd, err = util.RunCommand(commandFlag, args, environment.Variables, shouldWaitForExit) + cmd, err = util.RunCommand(commandFlag, args, environmentVariables.Variables, shouldWaitForExit) if err != nil { - defer WaitGroup.Done() + defer watcherWaitGroup.Done() util.HandleError(err) } go func() { defer processMutex.Unlock() - defer WaitGroup.Done() + defer watcherWaitGroup.Done() exitCode, err := waitForExitCommand(cmd) @@ -424,11 +404,7 @@ func executeCommandWithWatchMode(commandFlag string, args []string, watchMode bo }() } - initialEnvironment, err := createInjectableEnvironment(request, projectConfigDir, secretOverriding, expandSecrets, token) - if err != nil { - util.HandleError(err, "Failed to fetch secrets") - } - startProcess(initialEnvironment) + runCommandWithWatcher() // this is the only logic strictly related to watch mode, the rest is shared with non-watch mode if watchMode { @@ -448,14 +424,14 @@ func executeCommandWithWatchMode(commandFlag string, args []string, watchMode bo <-recheckSecretsChannel watchMutex.Lock() - newEnvironmentVariables, err := createInjectableEnvironment(request, projectConfigDir, secretOverriding, expandSecrets, token) + environmentVariables, err = fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, expandSecrets, token) if err != nil { log.Error().Err(err).Msg("[HOT RELOAD] Failed to fetch secrets") continue } - if newEnvironmentVariables.ETag != currentETag { - startProcess(newEnvironmentVariables) + if environmentVariables.ETag != currentETag { + runCommandWithWatcher() } else { log.Debug().Msg("[HOT RELOAD] No changes detected in secrets, not reloading process") } @@ -466,7 +442,7 @@ func executeCommandWithWatchMode(commandFlag string, args []string, watchMode bo } } -func createInjectableEnvironment(request models.GetAllSecretsParameters, projectConfigDir string, secretOverriding bool, shouldExpandSecrets bool, token *models.TokenDetails) (models.InjectableEnvironmentResult, error) { +func fetchAndFormatSecretsForShell(request models.GetAllSecretsParameters, projectConfigDir string, secretOverriding bool, shouldExpandSecrets bool, token *models.TokenDetails) (models.InjectableEnvironmentResult, error) { if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { request.InfisicalToken = token.Token From 91ebcca0fd5445e20c92b4b959c683518e869c20 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 4 Sep 2024 10:44:39 +0400 Subject: [PATCH 5/6] Update run.go --- cli/packages/cmd/run.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index f9126340c9..a28a48cd6f 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -316,13 +316,12 @@ func executeCommandWithWatchMode(commandFlag string, args []string, watchMode bo var processMutex sync.Mutex var beingTerminated = false var currentETag string - environmentVariables, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, expandSecrets, token) if err != nil { util.HandleError(err, "Failed to fetch secrets") } - runCommandWithWatcher := func() { + runCommandWithWatcher := func(environmentVariables models.InjectableEnvironmentResult) { currentETag = environmentVariables.ETag secretsFetchedAt := time.Now() if secretsFetchedAt.After(lastSecretsFetch) { @@ -404,7 +403,12 @@ func executeCommandWithWatchMode(commandFlag string, args []string, watchMode bo }() } - runCommandWithWatcher() + initialEnvironmentVariables, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, expandSecrets, token) + if err != nil { + util.HandleError(err, "Failed to fetch secrets") + } + + runCommandWithWatcher(initialEnvironmentVariables) // this is the only logic strictly related to watch mode, the rest is shared with non-watch mode if watchMode { @@ -424,14 +428,14 @@ func executeCommandWithWatchMode(commandFlag string, args []string, watchMode bo <-recheckSecretsChannel watchMutex.Lock() - environmentVariables, err = fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, expandSecrets, token) + newEnvironmentVariables, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, expandSecrets, token) if err != nil { log.Error().Err(err).Msg("[HOT RELOAD] Failed to fetch secrets") continue } - if environmentVariables.ETag != currentETag { - runCommandWithWatcher() + if newEnvironmentVariables.ETag != currentETag { + runCommandWithWatcher(newEnvironmentVariables) } else { log.Debug().Msg("[HOT RELOAD] No changes detected in secrets, not reloading process") } From 5e0b78b10470ae07e0e46449a4949e6525c82309 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 4 Sep 2024 19:34:51 +0400 Subject: [PATCH 6/6] Requested changes --- cli/packages/cmd/run.go | 67 ++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 38 deletions(-) diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index a28a48cd6f..fa5176d89a 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -153,7 +153,7 @@ var runCmd = &cobra.Command{ log.Debug().Msgf("injecting the following environment variables into shell: %v", injectableEnvironment.Variables) if watchMode { - executeCommandWithWatchMode(command, args, watchMode, watchModeInterval, request, projectConfigDir, shouldExpandSecrets, secretOverriding, token) + executeCommandWithWatchMode(command, args, watchModeInterval, request, projectConfigDir, shouldExpandSecrets, secretOverriding, token) } else { if cmd.Flags().Changed("command") { command := cmd.Flag("command").Value.String() @@ -306,7 +306,7 @@ func waitForExitCommand(cmd *exec.Cmd) (int, error) { return waitStatus.ExitStatus(), nil } -func executeCommandWithWatchMode(commandFlag string, args []string, watchMode bool, watchModeInterval int, request models.GetAllSecretsParameters, projectConfigDir string, expandSecrets bool, secretOverriding bool, token *models.TokenDetails) { +func executeCommandWithWatchMode(commandFlag string, args []string, watchModeInterval int, request models.GetAllSecretsParameters, projectConfigDir string, expandSecrets bool, secretOverriding bool, token *models.TokenDetails) { var cmd *exec.Cmd var err error @@ -359,6 +359,9 @@ func executeCommandWithWatchMode(commandFlag string, args []string, watchMode bo } cmd = nil + } else { + // If `cmd` is nil, we know this is the first time we are starting the process + log.Info().Msg(color.HiMagentaString("[HOT RELOAD] Watching for secret changes...")) } processMutex.Lock() @@ -374,8 +377,7 @@ func executeCommandWithWatchMode(commandFlag string, args []string, watchMode bo // start the process log.Info().Msgf(color.GreenString("Injecting %v Infisical secrets into your application process", environmentVariables.SecretsCount)) - shouldWaitForExit := !watchMode - cmd, err = util.RunCommand(commandFlag, args, environmentVariables.Variables, shouldWaitForExit) + cmd, err = util.RunCommand(commandFlag, args, environmentVariables.Variables, false) if err != nil { defer watcherWaitGroup.Done() util.HandleError(err) @@ -403,46 +405,35 @@ func executeCommandWithWatchMode(commandFlag string, args []string, watchMode bo }() } - initialEnvironmentVariables, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, expandSecrets, token) - if err != nil { - util.HandleError(err, "Failed to fetch secrets") - } - - runCommandWithWatcher(initialEnvironmentVariables) - - // this is the only logic strictly related to watch mode, the rest is shared with non-watch mode - if watchMode { - recheckSecretsChannel := make(chan bool, 1) - - log.Info().Msg(color.HiMagentaString("[HOT RELOAD] Watching for secret changes...")) - - // a simple goroutine that triggers the recheckSecretsChan every watch interval (defaults to 10 seconds) - go func() { - for { - time.Sleep(time.Duration(watchModeInterval) * time.Second) - recheckSecretsChannel <- true - } - }() + recheckSecretsChannel := make(chan bool, 1) + recheckSecretsChannel <- true + // a simple goroutine that triggers the recheckSecretsChan every watch interval (defaults to 10 seconds) + go func() { for { - <-recheckSecretsChannel - watchMutex.Lock() - - newEnvironmentVariables, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, expandSecrets, token) - if err != nil { - log.Error().Err(err).Msg("[HOT RELOAD] Failed to fetch secrets") - continue - } + time.Sleep(time.Duration(watchModeInterval) * time.Second) + recheckSecretsChannel <- true + } + }() - if newEnvironmentVariables.ETag != currentETag { - runCommandWithWatcher(newEnvironmentVariables) - } else { - log.Debug().Msg("[HOT RELOAD] No changes detected in secrets, not reloading process") - } + for { + <-recheckSecretsChannel + watchMutex.Lock() - watchMutex.Unlock() + newEnvironmentVariables, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, expandSecrets, token) + if err != nil { + log.Error().Err(err).Msg("[HOT RELOAD] Failed to fetch secrets") + continue + } + if newEnvironmentVariables.ETag != currentETag { + runCommandWithWatcher(newEnvironmentVariables) + } else { + log.Debug().Msg("[HOT RELOAD] No changes detected in secrets, not reloading process") } + + watchMutex.Unlock() + } }