/* Copyright © 2022 NAME HERE */ package cmd import ( "fmt" "os" "os/exec" "os/signal" "runtime" "strings" "syscall" "github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/util" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) // runCmd represents the run command var runCmd = &cobra.Command{ Example: ` infisical run --env=dev -- npm run dev infisical run --command "first-command && second-command; more-commands..." `, Use: "run [any infisical run command flags] -- [your application start command]", Short: "Used to inject environments variables into your application process", DisableFlagsInUseLine: true, PreRun: toggleDebug, Args: func(cmd *cobra.Command, args []string) error { // Check if the --command flag has been set commandFlagSet := cmd.Flags().Changed("command") // If the --command flag has been set, check if a value was provided if commandFlagSet { command := cmd.Flag("command").Value.String() if command == "" { return fmt.Errorf("you need to provide a command after the flag --command") } // If the --command flag has been set, args should not be provided if len(args) > 0 { return fmt.Errorf("you cannot set any arguments after --command flag. --command only takes a string command") } } else { // If the --command flag has not been set, at least one arg should be provided if len(args) == 0 { return fmt.Errorf("at least one argument is required after the run command, received %d", len(args)) } } return nil }, Run: func(cmd *cobra.Command, args []string) { envName, err := cmd.Flags().GetString("env") if err != nil { util.HandleError(err, "Unable to parse flag") } if !util.IsSecretEnvironmentValid(envName) { util.PrintMessageAndExit("Invalid environment name passed. Environment names can only be prod, dev, test or staging") } secretOverriding, err := cmd.Flags().GetBool("secret-overriding") 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") } secrets, err := util.GetAllEnvironmentVariables(envName) 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") } if shouldExpandSecrets { secrets = util.SubstituteSecrets(secrets) } if secretOverriding { secrets = util.OverrideWithPersonalSecrets(secrets) } if cmd.Flags().Changed("command") { command := cmd.Flag("command").Value.String() err = executeMultipleCommandWithEnvs(command, secrets) if err != nil { util.HandleError(err, "Unable to execute your chained command") } } else { err = executeSingleCommandWithEnvs(args, secrets) if err != nil { util.HandleError(err, "Unable to execute your single command") } } }, } func init() { rootCmd.AddCommand(runCmd) 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("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 ...\")") } // Will execute a single command and pass in the given secrets into the process func executeSingleCommandWithEnvs(args []string, secrets []models.SingleEnvironmentVariable) error { command := args[0] argsForCommand := args[1:] numberOfSecretsInjected := fmt.Sprintf("\u2713 Injected %v Infisical secrets into your application process successfully", len(secrets)) log.Infof("\x1b[%dm%s\x1b[0m", 32, numberOfSecretsInjected) log.Debugf("executing command: %s %s \n", command, strings.Join(argsForCommand, " ")) log.Debugf("Secrets injected: %v", secrets) cmd := exec.Command(command, argsForCommand...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Env = getAllEnvs(secrets) return execCmd(cmd) } func executeMultipleCommandWithEnvs(fullCommand string, secrets []models.SingleEnvironmentVariable) error { shell := [2]string{"sh", "-c"} if runtime.GOOS == "windows" { shell = [2]string{"cmd", "/C"} } else { shell[0] = os.Getenv("SHELL") } cmd := exec.Command(shell[0], shell[1], fullCommand) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Env = getAllEnvs(secrets) numberOfSecretsInjected := fmt.Sprintf("\u2713 Injected %v Infisical secrets into your application process successfully", len(secrets)) log.Infof("\x1b[%dm%s\x1b[0m", 32, numberOfSecretsInjected) log.Debugf("executing command: %s %s %s \n", shell[0], shell[1], fullCommand) log.Debugf("Secrets injected: %v", secrets) return execCmd(cmd) } // 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 { _ = 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 getAllEnvs(envsToInject []models.SingleEnvironmentVariable) []string { env_map := make(map[string]string) for _, env := range os.Environ() { splitEnv := strings.Split(env, "=") env_map[splitEnv[0]] = splitEnv[1] } for _, env := range envsToInject { env_map[env.Key] = env.Value // overrite any envs with ones to inject if they clash } var allEnvs []string for key, value := range env_map { allEnvs = append(allEnvs, fmt.Sprintf("%s=%s", key, value)) } return allEnvs }