mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Revert "feat(cli): run watch mode"
This commit is contained in:
@@ -4,14 +4,13 @@ Copyright (c) 2023 Infisical Inc.
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/models"
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
@@ -21,9 +20,6 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var ErrManualSignalInterrupt = errors.New("signal: interrupt")
|
||||
var WaitGroup = new(sync.WaitGroup)
|
||||
|
||||
// runCmd represents the run command
|
||||
var runCmd = &cobra.Command{
|
||||
Example: `
|
||||
@@ -36,8 +32,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 {
|
||||
@@ -57,20 +51,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) {
|
||||
@@ -97,30 +77,11 @@ var runCmd = &cobra.Command{
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
command, err := cmd.Flags().GetString("command")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
secretOverriding, err := cmd.Flags().GetBool("secret-overriding")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
watchMode, err := cmd.Flags().GetBool("watch")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
watchModeInterval, err := cmd.Flags().GetInt("watch-interval")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
@@ -155,16 +116,68 @@ var runCmd = &cobra.Command{
|
||||
Recursive: recursive,
|
||||
}
|
||||
|
||||
injectableEnvironment, err := createInjectableEnvironment(request, projectConfigDir, secretOverriding, shouldExpandSecrets, token)
|
||||
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 {
|
||||
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)
|
||||
if secretOverriding {
|
||||
secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_PERSONAL)
|
||||
} else {
|
||||
secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_SHARED)
|
||||
}
|
||||
|
||||
if shouldExpandSecrets {
|
||||
|
||||
authParams := models.ExpandSecretsAuthentication{}
|
||||
|
||||
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
|
||||
authParams.InfisicalToken = token.Token
|
||||
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
|
||||
authParams.UniversalAuthAccessToken = token.Token
|
||||
}
|
||||
|
||||
secrets = util.ExpandSecrets(secrets, authParams, projectConfigDir)
|
||||
}
|
||||
|
||||
secretsByKey := getSecretsByKeys(secrets)
|
||||
environmentVariables := make(map[string]string)
|
||||
|
||||
// add all existing environment vars
|
||||
for _, s := range os.Environ() {
|
||||
kv := strings.SplitN(s, "=", 2)
|
||||
key := kv[0]
|
||||
value := kv[1]
|
||||
environmentVariables[key] = value
|
||||
}
|
||||
|
||||
// check to see if there are any reserved key words in secrets to inject
|
||||
filterReservedEnvVars(secretsByKey)
|
||||
|
||||
// now add infisical secrets
|
||||
for k, v := range secretsByKey {
|
||||
environmentVariables[k] = v.Value
|
||||
}
|
||||
|
||||
// turn it back into a list of envs
|
||||
var env []string
|
||||
for key, value := range environmentVariables {
|
||||
s := key + "=" + value
|
||||
env = append(env, s)
|
||||
}
|
||||
|
||||
log.Debug().Msgf("injecting the following environment variables into shell: %v", env)
|
||||
|
||||
Telemetry.CaptureEvent("cli-command:run",
|
||||
posthog.NewProperties().
|
||||
Set("secretsCount", injectableEnvironment.SecretsCount).
|
||||
Set("secretsCount", len(secrets)).
|
||||
Set("environment", environmentName).
|
||||
Set("isUsingServiceToken", token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER).
|
||||
Set("isUsingUniversalAuthToken", token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER).
|
||||
@@ -172,160 +185,39 @@ 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 cmd.Flags().Changed("command") {
|
||||
command := cmd.Flag("command").Value.String()
|
||||
|
||||
err = executeMultipleCommandWithEnvs(command, len(secretsByKey), env)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
} else {
|
||||
err = executeSingleCommandWithEnvs(args, len(secretsByKey), env)
|
||||
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) {
|
||||
|
||||
var cmd *exec.Cmd
|
||||
var err error
|
||||
var lastSecretsFetch time.Time
|
||||
var lastUpdateEvent time.Time
|
||||
var watchMutex sync.Mutex
|
||||
var processMutex sync.Mutex
|
||||
var beingTerminated = false
|
||||
var currentETag string
|
||||
|
||||
startProcess := func(environment models.InjectableEnvironmentResult) {
|
||||
currentETag = environment.ETag
|
||||
secretsFetchedAt := time.Now()
|
||||
if secretsFetchedAt.After(lastSecretsFetch) {
|
||||
lastSecretsFetch = secretsFetchedAt
|
||||
}
|
||||
|
||||
shouldRestartProcess := cmd != nil
|
||||
// terminate the old process before starting a new one
|
||||
if shouldRestartProcess {
|
||||
beingTerminated = true
|
||||
|
||||
log.Debug().Msgf(color.HiMagentaString("[HOT RELOAD] Sending SIGTERM to PID %d", cmd.Process.Pid))
|
||||
if e := cmd.Process.Signal(syscall.SIGTERM); e != nil {
|
||||
log.Error().Err(e).Msg(color.HiMagentaString("[HOT RELOAD] Failed to send SIGTERM"))
|
||||
}
|
||||
// wait up to 10 sec for the process to exit
|
||||
for i := 0; i < 10; i++ {
|
||||
if !util.IsProcessRunning(cmd.Process) {
|
||||
// process has been killed so we break out
|
||||
break
|
||||
}
|
||||
if i == 5 {
|
||||
log.Debug().Msg(color.HiMagentaString("[HOT RELOAD] Still waiting for process exit status"))
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
// SIGTERM may not work on Windows so we try SIGKILL
|
||||
if util.IsProcessRunning(cmd.Process) {
|
||||
log.Debug().Msg(color.HiMagentaString("[HOT RELOAD] Process still hasn't fully exited, attempting SIGKILL"))
|
||||
if e := cmd.Process.Kill(); e != nil {
|
||||
log.Error().Err(e).Msg(color.HiMagentaString("[HOT RELOAD] Failed to send SIGKILL"))
|
||||
}
|
||||
}
|
||||
|
||||
cmd = nil
|
||||
}
|
||||
|
||||
processMutex.Lock()
|
||||
|
||||
if lastUpdateEvent.After(secretsFetchedAt) {
|
||||
processMutex.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
beingTerminated = false
|
||||
WaitGroup.Add(1)
|
||||
|
||||
if shouldRestartProcess {
|
||||
log.Info().Msg(color.HiMagentaString("[HOT RELOAD] Environment changes detected. Reloading process..."))
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
defer WaitGroup.Done()
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer processMutex.Unlock()
|
||||
defer WaitGroup.Done()
|
||||
|
||||
exitCode, err := WaitForExitCommand(cmd)
|
||||
|
||||
// ignore errors if we are being terminated
|
||||
if !beingTerminated {
|
||||
if err != nil {
|
||||
if strings.HasPrefix(err.Error(), "exec") || strings.HasPrefix(err.Error(), "fork/exec") {
|
||||
log.Error().Err(err).Msg("Failed to execute command")
|
||||
}
|
||||
if err.Error() != ErrManualSignalInterrupt.Error() {
|
||||
log.Error().Err(err).Msg("Process exited with error")
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
}()
|
||||
var (
|
||||
reservedEnvVars = []string{
|
||||
"HOME", "PATH", "PS1", "PS2",
|
||||
"PWD", "EDITOR", "XAUTHORITY", "USER",
|
||||
"TERM", "TERMINFO", "SHELL", "MAIL",
|
||||
}
|
||||
|
||||
initialEnvironment, err := createInjectableEnvironment(request, projectConfigDir, secretOverriding, expandSecrets, token)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Failed to fetch secrets")
|
||||
reservedEnvVarPrefixes = []string{
|
||||
"XDG_",
|
||||
"LC_",
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
<-recheckSecretsChannel
|
||||
watchMutex.Lock()
|
||||
|
||||
newEnvironmentVariables, err := createInjectableEnvironment(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)
|
||||
} else {
|
||||
log.Debug().Msg("[HOT RELOAD] No changes detected in secrets, not reloading process")
|
||||
}
|
||||
|
||||
watchMutex.Unlock()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
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)
|
||||
@@ -345,94 +237,80 @@ func filterReservedEnvVars(env map[string]models.SingleEnvironmentVariable) {
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(runCmd)
|
||||
runCmd.Flags().String("token", "", "fetch secrets using service token or machine identity access token")
|
||||
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("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().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) {
|
||||
// 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:]
|
||||
|
||||
secrets, err := util.GetAllEnvironmentVariables(request, projectConfigDir)
|
||||
log.Info().Msgf(color.GreenString("Injecting %v Infisical secrets into your application process", secretsCount))
|
||||
|
||||
if err != nil {
|
||||
return models.InjectableEnvironmentResult{}, err
|
||||
}
|
||||
cmd := exec.Command(command, argsForCommand...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = env
|
||||
|
||||
if secretOverriding {
|
||||
secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_PERSONAL)
|
||||
return execCmd(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 {
|
||||
secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_SHARED)
|
||||
}
|
||||
|
||||
if shouldExpandSecrets {
|
||||
|
||||
authParams := models.ExpandSecretsAuthentication{}
|
||||
|
||||
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
|
||||
authParams.InfisicalToken = token.Token
|
||||
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
|
||||
authParams.UniversalAuthAccessToken = token.Token
|
||||
currentShell := os.Getenv("SHELL")
|
||||
if currentShell != "" {
|
||||
shell[0] = currentShell
|
||||
}
|
||||
|
||||
secrets = util.ExpandSecrets(secrets, authParams, projectConfigDir)
|
||||
}
|
||||
|
||||
secretsByKey := getSecretsByKeys(secrets)
|
||||
environmentVariables := make(map[string]string)
|
||||
cmd := exec.Command(shell[0], shell[1], fullCommand)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = env
|
||||
|
||||
// add all existing environment vars
|
||||
for _, s := range os.Environ() {
|
||||
kv := strings.SplitN(s, "=", 2)
|
||||
key := kv[0]
|
||||
value := kv[1]
|
||||
environmentVariables[key] = value
|
||||
}
|
||||
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)
|
||||
|
||||
// check to see if there are any reserved key words in secrets to inject
|
||||
filterReservedEnvVars(secretsByKey)
|
||||
|
||||
// now add infisical secrets
|
||||
for k, v := range secretsByKey {
|
||||
environmentVariables[k] = v.Value
|
||||
}
|
||||
|
||||
env := make([]string, 0, len(environmentVariables))
|
||||
for key, value := range environmentVariables {
|
||||
env = append(env, key+"="+value)
|
||||
}
|
||||
|
||||
return models.InjectableEnvironmentResult{
|
||||
Variables: env,
|
||||
ETag: util.GenerateETagFromSecrets(secrets),
|
||||
SecretsCount: len(secretsByKey),
|
||||
}, nil
|
||||
return execCmd(cmd)
|
||||
}
|
||||
|
||||
func WaitForExitCommand(cmd *exec.Cmd) (int, error) {
|
||||
// Credit: inspired by AWS Valut
|
||||
func execCmd(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 {
|
||||
// ignore errors
|
||||
cmd.Process.Signal(os.Kill) // #nosec G104
|
||||
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
return exitError.ExitCode(), exitError
|
||||
}
|
||||
|
||||
return 2, err
|
||||
_ = cmd.Process.Signal(os.Kill)
|
||||
return fmt.Errorf("failed to wait for command termination: %v", 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
|
||||
waitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus)
|
||||
os.Exit(waitStatus.ExitStatus())
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user