From e81c750f23cbc445e02711d4ad4b0c05897831cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=A9rence=20Chateign=C3=A9?= Date: Sun, 11 Jun 2023 19:23:13 +0200 Subject: [PATCH] omg you should always bind your flags in prerun else it overwrites between commands --- .github/workflows/e2e.yaml | 20 +++--- .vscode/launch.json | 23 +++++++ cmd/connect.go | 93 ++++---------------------- cmd/disconnect.go | 67 +++---------------- cmd/docs.go | 13 ---- cmd/init.go | 33 +++++---- cmd/root.go | 7 +- cmd/run.go | 31 ++++----- cmd/status.go | 10 ++- cmd/stop.go | 37 +++++++--- common/{tailscale-api.go => common.go} | 83 +++++++++++++++++++++++ main.go | 3 - 12 files changed, 207 insertions(+), 213 deletions(-) create mode 100644 .vscode/launch.json rename common/{tailscale-api.go => common.go} (83%) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index c1fa6a6..52f8b85 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -8,11 +8,15 @@ on: branches: [main] env: - AWS_REGION: eu-west-3 BINARY_NAME: xit - TS_TAILNET: ${{ secrets.TS_TAILNET }} - TS_API_KEY: ${{ secrets.TS_API_KEY }} - TS_AUTH_KEY: ${{ secrets.TS_AUTH_KEY }} + # xit environment variables + XIT_REGION: eu-west-3 + XIT_NON_INTERACTIVE: "true" + XIT_CONNECT: "true" + XIT_SHUTDOWN: 5m + XIT_TS_TAILNET: ${{ secrets.TS_TAILNET }} + XIT_TS_API_KEY: ${{ secrets.TS_API_KEY }} + XIT_TS_AUTH_KEY: ${{ secrets.TS_AUTH_KEY }} permissions: id-token: write # This is required for requesting the JWT @@ -35,15 +39,15 @@ jobs: - name: Setup environment uses: ./.github/actions/setup with: - region: ${{ env.AWS_REGION }} + region: ${{ env.XIT_REGION }} role_arn: ${{ secrets.AWS_GITHUB_ACTIONS_XIT_ROLE_ARN }} tailscale_authkey: ${{ secrets.TS_GITHUB_ACTIONS_AUTH_KEY }} - name: Download xit uses: ./.github/actions/download with: binary_name: ${{ env.BINARY_NAME }} - # - name: Run "xit run" - # run: ./xit run --region ${{ env.AWS_REGION }} --non-interactive --connect --shutdown 5m - # TODO: check if the public IP address matches the one from the new instance + - name: Run "xit run" + run: ./xit run - name: Run xit status run: ./xit status + # TODO: check if the public IP address matches the one from the new instance diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..929f412 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + + { + "name": "Launch file", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${file}" + }, + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${fileDirname}" + } + ] +} diff --git a/cmd/connect.go b/cmd/connect.go index 3fbe2de..b926db7 100644 --- a/cmd/connect.go +++ b/cmd/connect.go @@ -1,13 +1,7 @@ -/* -Copyright © 2023 NAME HERE -*/ package cmd import ( "fmt" - "os/exec" - "regexp" - "strings" "github.com/cterence/xit/common" "github.com/manifoldco/promptui" @@ -25,18 +19,22 @@ var connectCmd = &cobra.Command{ This command will run tailscale up and choose the exit node with the machine name provided. Example : xit connect xit-eu-west-3-i-048afd4880f66c596`, + PreRun: func(cmd *cobra.Command, args []string) { + viper.BindPFlag("ts_api_key", cmd.PersistentFlags().Lookup("ts-api-key")) + viper.BindPFlag("ts_tailnet", cmd.PersistentFlags().Lookup("ts-tailnet")) + viper.BindPFlag("non_interactive", cmd.PersistentFlags().Lookup("non-interactive")) + }, Run: func(cmd *cobra.Command, args []string) { // Using the CLI on the host, run tailscale up and choose the exit node with the machine name provided tsApiKey := viper.GetString("ts_api_key") tailnet := viper.GetString("ts_tailnet") nonInteractive := viper.GetBool("non_interactive") - // Create ternary operator to choose between sudo and sudo -n var machineConnect string if len(args) != 0 { machineConnect = args[0] - } else { + } else if !nonInteractive { xitDevices, err := common.FindActiveXitDevices(tsApiKey, tailnet) if err != nil { fmt.Println("Failed to find active xit devices:", err) @@ -68,76 +66,15 @@ var connectCmd = &cobra.Command{ } machineConnect = xitDevices[idx].Hostname + } else { + fmt.Println("No machine name provided") + return } - fmt.Printf("Will run the command:\nsudo tailscale up --exit-node=%s\n", machineConnect) - - // Create a confirmation prompt - var result string - - // Use promptui for the confirmation prompt - if !nonInteractive { - prompt := promptui.Select{ - Label: "Are you sure you want to connect to this machine?", - Items: []string{"yes", "no"}, - } - - _, result, err := prompt.Run() - if err != nil { - fmt.Println("Failed to read input:", err) - return - } - - if result != "yes" { - fmt.Println("Aborting...") - return - } - } - - sudoNonInteractive := "" - if nonInteractive { - sudoNonInteractive = "-n" - } - - // FIXME: Have correct errors when sudo is not available - // Run the command and parse the output - out, err := exec.Command("sudo", sudoNonInteractive, "tailscale", "up", "--exit-node="+machineConnect).CombinedOutput() - // If the command was unsuccessful, extract tailscale up command from error message with a regex and run it + err := common.RunTailscaleUpCommand("tailscale up --exit-node="+machineConnect, nonInteractive) if err != nil { - // extract latest "tailscale up" command from output with a regex and run it - regexp := regexp.MustCompile(`tailscale up .*`) - tailscaleUpCommand := regexp.FindString(string(out)) - - fmt.Printf("\nExisting configuration found, will run updated tailscale up command:\nsudo %s\n\n", tailscaleUpCommand) - - // Use promptui for the confirmation prompt - if !nonInteractive { - prompt := promptui.Select{ - Label: "Are you sure you want to connect to this machine?", - Items: []string{"yes", "no"}, - } - - _, result, err = prompt.Run() - if err != nil { - fmt.Println("Failed to read input:", err) - return - } - - if result != "yes" { - fmt.Println("Aborting...") - return - } - } - - tailscaleUpCommandSplitted := strings.Split(tailscaleUpCommand, " ") - tailscaleUpCommandSplitted = append([]string{sudoNonInteractive}, tailscaleUpCommandSplitted...) - - _, err = exec.Command("sudo", tailscaleUpCommandSplitted...).CombinedOutput() - if err != nil { - fmt.Println("Failed to run command:", err) - } - - fmt.Println("Connected.") + fmt.Println("Failed to run tailscale up command:", err) + return } fmt.Println("Connected.") @@ -149,9 +86,5 @@ func init() { connectCmd.PersistentFlags().StringP("ts-api-key", "", "", "TailScale API Key") connectCmd.PersistentFlags().StringP("ts-tailnet", "", "", "TailScale Tailnet") - connectCmd.PersistentFlags().BoolP("non-interactive", "n", false, "Do not prompt for confirmation") - - viper.BindPFlag("ts_api_key", connectCmd.PersistentFlags().Lookup("ts-api-key")) - viper.BindPFlag("ts_tailnet", connectCmd.PersistentFlags().Lookup("ts-tailnet")) - viper.BindPFlag("non_interactive", runCmd.PersistentFlags().Lookup("non-interactive")) + connectCmd.PersistentFlags().BoolP("non-interactive", "", false, "Do not prompt for confirmation") } diff --git a/cmd/disconnect.go b/cmd/disconnect.go index f7a4515..472c3ee 100644 --- a/cmd/disconnect.go +++ b/cmd/disconnect.go @@ -1,18 +1,13 @@ -/* -Copyright © 2023 NAME HERE -*/ package cmd import ( "encoding/json" "fmt" "os/exec" - "regexp" - "strings" "github.com/cterence/xit/common" - "github.com/manifoldco/promptui" "github.com/spf13/cobra" + "github.com/spf13/viper" ) // disconnectCmd represents the disconnect command @@ -20,7 +15,12 @@ var disconnectCmd = &cobra.Command{ Use: "disconnect", Short: "Disconnect from the current exit node", Long: ``, + PreRun: func(cmd *cobra.Command, args []string) { + viper.BindPFlag("non_interactive", cmd.PersistentFlags().Lookup("non-interactive")) + }, Run: func(cmd *cobra.Command, args []string) { + nonInteractive := viper.GetBool("non_interactive") + var status common.TailscaleStatus out, err := exec.Command("tailscale", "debug", "prefs").CombinedOutput() @@ -36,65 +36,18 @@ var disconnectCmd = &cobra.Command{ return } - command := "tailscale up --exit-node=" - - // Use promptui for the confirmation prompt - prompt := promptui.Select{ - Label: "Are you sure you want to disconnect from this machine?", - Items: []string{"yes", "no"}, - } - - _, result, err := prompt.Run() + err = common.RunTailscaleUpCommand("tailscale up --exit-node=", nonInteractive) if err != nil { - fmt.Println("Failed to read input:", err) + fmt.Println("Failed to run tailscale up command:", err) return } - if result != "yes" { - fmt.Println("Aborting...") - return - } - - // Run the command and parse the output - - out, err = exec.Command("sudo", strings.Split(command, " ")...).CombinedOutput() - // If the command was unsuccessful, extract tailscale up command from error message with a regex and run it - if err != nil { - // extract latest "tailscale up" command from output with a regex and run it - regexp := regexp.MustCompile(`tailscale up .*`) - command = regexp.FindString(string(out)) - - fmt.Printf("\nExisting configuration found, will run updated tailscale up command:\nsudo %s\n", command) - - // Use promptui for the confirmation prompt - prompt = promptui.Select{ - Label: "Are you sure you want to disconnect from this machine?", - Items: []string{"yes", "no"}, - } - - _, result, err = prompt.Run() - if err != nil { - fmt.Println("Failed to read input:", err) - return - } - - if result != "yes" { - fmt.Println("Aborting...") - return - } - - _, err = exec.Command("sudo", strings.Split(command, " ")...).CombinedOutput() - if err != nil { - fmt.Println("Failed to run command:", err) - } - - fmt.Println("Disconnected.") - } - fmt.Println("Disconnected.") }, } func init() { rootCmd.AddCommand(disconnectCmd) + + disconnectCmd.PersistentFlags().BoolP("non-interactive", "n", false, "Do not prompt for confirmation") } diff --git a/cmd/docs.go b/cmd/docs.go index 7666fa2..559c472 100644 --- a/cmd/docs.go +++ b/cmd/docs.go @@ -1,6 +1,3 @@ -/* -Copyright © 2023 NAME HERE -*/ package cmd import ( @@ -29,14 +26,4 @@ var docsCmd = &cobra.Command{ func init() { rootCmd.AddCommand(docsCmd) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // docsCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // docsCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } diff --git a/cmd/init.go b/cmd/init.go index b156c34..c9fe789 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -1,6 +1,3 @@ -/* -Copyright © 2023 NAME HERE -*/ package cmd import ( @@ -21,10 +18,17 @@ In details it will: - add a tag:xit to your policy - update autoapprovers to allow exit nodes to be created - add a ssh configuration allowing users to ssh into tagged xit machines`, + PreRun: func(cmd *cobra.Command, args []string) { + viper.BindPFlag("ts_api_key", cmd.PersistentFlags().Lookup("ts-api-key")) + viper.BindPFlag("ts_tailnet", cmd.PersistentFlags().Lookup("ts-tailnet")) + viper.BindPFlag("non_interactive", cmd.PersistentFlags().Lookup("non-interactive")) + viper.BindPFlag("dry_run", cmd.PersistentFlags().Lookup("dry-run")) + }, Run: func(cmd *cobra.Command, args []string) { tsApiKey := viper.GetString("ts_api_key") tailnet := viper.GetString("ts_tailnet") dryRun := viper.GetBool("dry_run") + nonInteractive := viper.GetBool("non_interactive") // Get the policy configuration policy, err := common.GetPolicy(tsApiKey, tailnet) @@ -79,15 +83,19 @@ Add a ssh configuration allowing users to ssh into tagged xit machines Your new policy document will look like this: %s - -Do you want to continue? [y/N] `, policyJSON) - var answer string - fmt.Scanln(&answer) - if answer != "y" { - fmt.Println("Aborting") - return + if !nonInteractive { + result, err := common.PromptYesNo("Do you want to continue?") + if err != nil { + fmt.Println("Failed to prompt for confirmation:", err) + return + } + + if !result { + fmt.Println("Aborting...") + return + } } err = common.UpdatePolicy(tsApiKey, tailnet, policy) @@ -106,7 +114,6 @@ func init() { initCmd.PersistentFlags().StringP("ts-api-key", "", "", "TailScale API Key") initCmd.PersistentFlags().StringP("ts-tailnet", "", "", "TailScale Tailnet") - - viper.BindPFlag("ts_api_key", initCmd.PersistentFlags().Lookup("ts-api-key")) - viper.BindPFlag("ts_tailnet", initCmd.PersistentFlags().Lookup("ts-tailnet")) + initCmd.PersistentFlags().BoolP("non-interactive", "n", false, "Do not prompt for confirmation") + initCmd.PersistentFlags().BoolP("dry-run", "d", false, "Do not actually terminate instances") } diff --git a/cmd/root.go b/cmd/root.go index 3161c11..b160b06 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,6 +1,3 @@ -/* -Copyright © 2023 NAME HERE -*/ package cmd import ( @@ -35,9 +32,6 @@ func Execute() { func init() { cobra.OnInitialize(InitConfig) rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.xit.yaml)") - rootCmd.PersistentFlags().BoolP("dry-run", "", false, "Dry run the command") - - viper.BindPFlag("dry_run", rootCmd.PersistentFlags().Lookup("dry-run")) } func InitConfig() { @@ -53,6 +47,7 @@ func InitConfig() { viper.AddConfigPath(home) viper.SetConfigType("yaml") viper.SetConfigName(".xit") + viper.SetEnvPrefix("xit") } viper.AutomaticEnv() diff --git a/cmd/run.go b/cmd/run.go index 9b929e2..a5ea45c 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -1,6 +1,3 @@ -/* -Copyright © 2023 NAME HERE -*/ package cmd import ( @@ -31,6 +28,14 @@ This command will create an EC2 instance in the targeted region with the followi - SSH access enabled - Tagged with App=xit - The instance will be created as a spot instance in the default VPC`, + PreRun: func(cmd *cobra.Command, args []string) { + viper.BindPFlag("ts_auth_key", cmd.PersistentFlags().Lookup("ts-auth-key")) + viper.BindPFlag("region", cmd.PersistentFlags().Lookup("region")) + viper.BindPFlag("shutdown", cmd.PersistentFlags().Lookup("shutdown")) + viper.BindPFlag("connect", cmd.PersistentFlags().Lookup("connect")) + viper.BindPFlag("dry_run", cmd.PersistentFlags().Lookup("dry-run")) + viper.BindPFlag("non_interactive", cmd.PersistentFlags().Lookup("non-interactive")) + }, Run: func(cmd *cobra.Command, args []string) { // Set up AWS session in the desired region tsAuthKey := viper.GetString("ts_auth_key") @@ -173,6 +178,7 @@ sudo echo "sudo shutdown" | at now + ` + fmt.Sprint(durationMinutes) + ` minutes return } + // Initialize loading spinner var wg sync.WaitGroup var s *chin.Chin @@ -223,6 +229,7 @@ sudo echo "sudo shutdown" | at now + ` + fmt.Sprint(durationMinutes) + ` minutes } found: + // Stop the loading spinner if !nonInteractive { s.Stop() wg.Wait() @@ -271,26 +278,12 @@ sudo echo "sudo shutdown" | at now + ` + fmt.Sprint(durationMinutes) + ` minutes } func init() { - cobra.OnInitialize(InitConfig) rootCmd.AddCommand(runCmd) runCmd.PersistentFlags().StringP("ts-auth-key", "", "", "TailScale Auth Key") runCmd.PersistentFlags().StringP("region", "r", "", "AWS Region to create the instance into") runCmd.PersistentFlags().StringP("shutdown", "s", "2h", "Terminate the instance after the specified duration (e.g. 2h, 1h30m, 30m)") - runCmd.PersistentFlags().BoolP("non-interactive", "n", false, "Do not prompt for confirmation") runCmd.PersistentFlags().BoolP("connect", "c", false, "Connect to the instance after it is created") - - viper.BindPFlag("ts_auth_key", runCmd.PersistentFlags().Lookup("ts-auth-key")) - viper.BindPFlag("region", runCmd.PersistentFlags().Lookup("region")) - viper.BindPFlag("shutdown", runCmd.PersistentFlags().Lookup("shutdown")) - viper.BindPFlag("non_interactive", runCmd.PersistentFlags().Lookup("non-interactive")) - viper.BindPFlag("connect", runCmd.PersistentFlags().Lookup("connect")) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: + runCmd.PersistentFlags().BoolP("dry-run", "d", false, "Do not launch the instance") + runCmd.PersistentFlags().BoolP("non-interactive", "", false, "Do not prompt for confirmation") } diff --git a/cmd/status.go b/cmd/status.go index b4564a7..869334f 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -1,6 +1,3 @@ -/* -Copyright © 2023 NAME HERE -*/ package cmd import ( @@ -23,6 +20,10 @@ var statusCmd = &cobra.Command{ This command will show the status of xit devices, including the device name and whether it is connected or not. Example : xit status`, + PreRun: func(cmd *cobra.Command, args []string) { + viper.BindPFlag("ts_api_key", cmd.PersistentFlags().Lookup("ts-api-key")) + viper.BindPFlag("ts_tailnet", cmd.PersistentFlags().Lookup("ts-tailnet")) + }, Run: func(cmd *cobra.Command, args []string) { tsApiKey := viper.GetString("ts_api_key") tailnet := viper.GetString("ts_tailnet") @@ -101,7 +102,4 @@ func init() { statusCmd.PersistentFlags().StringP("ts-api-key", "", "", "TailScale API Key") statusCmd.PersistentFlags().StringP("ts-tailnet", "", "", "TailScale Tailnet") - - viper.BindPFlag("ts_api_key", statusCmd.PersistentFlags().Lookup("ts-api-key")) - viper.BindPFlag("ts_tailnet", statusCmd.PersistentFlags().Lookup("ts-tailnet")) } diff --git a/cmd/stop.go b/cmd/stop.go index 3f07767..b74c58e 100644 --- a/cmd/stop.go +++ b/cmd/stop.go @@ -1,6 +1,3 @@ -/* -Copyright © 2023 NAME HERE -*/ package cmd import ( @@ -26,10 +23,17 @@ var stopCmd = &cobra.Command{ If one or more machine names are specified, only those instances will be terminated. Example : xit stop xit-eu-west-3-i-048afd4880f66c596`, + PreRun: func(cmd *cobra.Command, args []string) { + viper.BindPFlag("ts_api_key", cmd.PersistentFlags().Lookup("ts-api-key")) + viper.BindPFlag("ts_tailnet", cmd.PersistentFlags().Lookup("ts-tailnet")) + viper.BindPFlag("non_interactive", cmd.PersistentFlags().Lookup("non-interactive")) + viper.BindPFlag("dry_run", cmd.PersistentFlags().Lookup("dry-run")) + }, Run: func(cmd *cobra.Command, args []string) { - dryRun := viper.GetBool("dry_run") tsApiKey := viper.GetString("ts_api_key") tailnet := viper.GetString("ts_tailnet") + dryRun := viper.GetBool("dry_run") + nonInteractive := viper.GetBool("non_interactive") var devicesToStop []common.Device @@ -44,7 +48,7 @@ Example : xit stop xit-eu-west-3-i-048afd4880f66c596`, return } - if len(args) == 0 { + if len(args) == 0 && !nonInteractive { // Create a fuzzy finder selector with the xit devices idx, err := fuzzyfinder.FindMulti(xitDevices, func(i int) string { return xitDevices[i].Hostname @@ -68,6 +72,24 @@ Example : xit stop xit-eu-west-3-i-048afd4880f66c596`, } } + if !nonInteractive { + fmt.Println("The following devices will be stopped:") + for _, device := range devicesToStop { + fmt.Println("-", device.Hostname) + } + + result, err := common.PromptYesNo("Are you sure you want to stop these machines?") + if err != nil { + fmt.Println("Failed to prompt for confirmation:", err) + return + } + + if !result { + fmt.Println("Aborting...") + return + } + } + // TODO: cleanup xit instances that were not last seen recently // TODO: warning when stopping a deice to which you are connected, propose to disconnect before for _, machine := range devicesToStop { @@ -118,7 +140,6 @@ func init() { stopCmd.PersistentFlags().StringP("ts-api-key", "", "", "TailScale API Key") stopCmd.PersistentFlags().StringP("ts-tailnet", "", "", "TailScale Tailnet") - - viper.BindPFlag("ts_api_key", stopCmd.PersistentFlags().Lookup("ts-api-key")) - viper.BindPFlag("ts_tailnet", stopCmd.PersistentFlags().Lookup("ts-tailnet")) + stopCmd.PersistentFlags().BoolP("non-interactive", "n", false, "Do not prompt for confirmation") + stopCmd.PersistentFlags().BoolP("dry-run", "", false, "Do not actually terminate instances") } diff --git a/common/tailscale-api.go b/common/common.go similarity index 83% rename from common/tailscale-api.go rename to common/common.go index e54543b..681cdf2 100644 --- a/common/tailscale-api.go +++ b/common/common.go @@ -6,7 +6,10 @@ import ( "fmt" "io" "net/http" + "os/exec" + "regexp" "sort" + "strings" "time" "github.com/aws/aws-sdk-go/aws" @@ -368,3 +371,83 @@ func sendRequest(tsApiKey, tailnet, method, url string, body []byte) ([]byte, er return body, nil } + +// Function that uses promptui to return a boolean value +func PromptYesNo(question string) (bool, error) { + prompt := promptui.Select{ + Label: question, + Items: []string{"Yes", "No"}, + } + + _, result, err := prompt.Run() + if err != nil { + return false, fmt.Errorf("failed to prompt for yes/no: %w", err) + } + + if result == "Yes" { + return true, nil + } + + return false, nil +} + +func RunTailscaleUpCommand(command string, nonInteractive bool) error { + tailscaleCommand := strings.Split(command, " ") + + if nonInteractive { + tailscaleCommand = append([]string{"-n"}, tailscaleCommand...) + } + + fmt.Println("Running command:\nsudo", strings.Join(tailscaleCommand, " ")) + + if !nonInteractive { + result, err := PromptYesNo("Are you sure you want to run this command?") + if err != nil { + return fmt.Errorf("failed to prompt for confirmation: %w", err) + } + + if !result { + fmt.Println("Aborting...") + return nil + } + } + + out, err := exec.Command("sudo", tailscaleCommand...).CombinedOutput() + // If the command was unsuccessful, extract tailscale up command from error message with a regex and run it + if err != nil { + // extract latest "tailscale up" command from output with a regex and run it + regexp := regexp.MustCompile(`tailscale up .*`) + loggedTailscaleCommand := regexp.FindString(string(out)) + + if loggedTailscaleCommand == "" { + return fmt.Errorf("failed to find tailscale up command in output: %s", string(out)) + } + + fmt.Printf("Existing Tailscale configuration found, will run updated tailscale up command:\nsudo %s\n", loggedTailscaleCommand) + + // Use promptui for the confirmation prompt + if !nonInteractive { + result, err := PromptYesNo("Are you sure you want to run this command?") + if err != nil { + return fmt.Errorf("failed to prompt for confirmation: %w", err) + } + + if !result { + fmt.Println("Aborting...") + return nil + } + } + + tailscaleCommand = strings.Split(loggedTailscaleCommand, " ") + + if nonInteractive { + tailscaleCommand = append([]string{"-n"}, tailscaleCommand...) + } + + _, err = exec.Command("sudo", tailscaleCommand...).CombinedOutput() + if err != nil { + return fmt.Errorf("failed to run command: %w", err) + } + } + return nil +} diff --git a/main.go b/main.go index 1422200..c54052d 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,3 @@ -/* -Copyright © 2023 NAME HERE -*/ package main import "github.com/cterence/xit/cmd"