Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: vpn update #126

Merged
merged 4 commits into from
Mar 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions src/cmd/projectDelete.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ package cmd
import (
"context"

"github.com/pkg/errors"

"github.com/zeropsio/zcli/src/cmd/scope"
"github.com/zeropsio/zcli/src/cmdBuilder"
"github.com/zeropsio/zcli/src/i18n"
"github.com/zeropsio/zcli/src/uxHelpers"
"github.com/zeropsio/zerops-go/dto/input/path"

"github.com/zeropsio/zcli/src/i18n"
)

func projectDeleteCmd() *cmdBuilder.Cmd {
Expand All @@ -21,10 +22,17 @@ func projectDeleteCmd() *cmdBuilder.Cmd {
HelpFlag(i18n.T(i18n.ProjectDeleteHelp)).
LoggedUserRunFunc(func(ctx context.Context, cmdData *cmdBuilder.LoggedUserCmdData) error {
if !cmdData.Params.GetBool("confirm") {
err := uxHelpers.YesNoPromptDestructive(ctx, cmdData.UxBlocks, i18n.T(i18n.ProjectDeleteConfirm, cmdData.Project.Name))
confirmed, err := uxHelpers.YesNoPrompt(
ctx,
cmdData.UxBlocks,
i18n.T(i18n.ProjectDeleteConfirm, cmdData.Project.Name),
)
if err != nil {
return err
}
if !confirmed {
return errors.New(i18n.T(i18n.DestructiveOperationConfirmationFailed))
}
}

deleteProjectResponse, err := cmdData.RestApiClient.DeleteProject(
Expand Down
12 changes: 12 additions & 0 deletions src/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package cmd
import (
"context"
"fmt"
"time"

"github.com/zeropsio/zcli/src/cmdBuilder"
"github.com/zeropsio/zcli/src/constants"
"github.com/zeropsio/zcli/src/entity/repository"
"github.com/zeropsio/zcli/src/errorsx"
"github.com/zeropsio/zcli/src/i18n"
"github.com/zeropsio/zcli/src/nettools"
"github.com/zeropsio/zcli/src/uxBlock"
"github.com/zeropsio/zcli/src/uxBlock/styles"
)
Expand All @@ -21,6 +23,7 @@ func rootCmd() *cmdBuilder.Cmd {
return cmdBuilder.NewCmd().
Use("zcli").
SetHelpTemplate(getRootTemplate()).
SilenceError(true).
AddChildrenCmd(loginCmd()).
AddChildrenCmd(versionCmd()).
AddChildrenCmd(scopeCmd()).
Expand Down Expand Up @@ -73,6 +76,15 @@ func rootCmd() *cmdBuilder.Cmd {
body.AddStringsRow(i18n.T(i18n.ScopedProject), fmt.Sprintf("%s [%s]", project.Name.String(), project.ID.Native()))
}

ctx, cancel := context.WithTimeout(ctx, time.Second*5)
defer cancel()

if err := nettools.Ping(ctx, vpnCheckAddress); err != nil {
body.AddStringsRow(i18n.T(i18n.StatusInfoVpnStatus), i18n.T(i18n.VpnCheckingConnectionIsNotActive))
} else {
body.AddStringsRow(i18n.T(i18n.StatusInfoVpnStatus), i18n.T(i18n.VpnCheckingConnectionIsActive))
}

cmdData.UxBlocks.Table(body)

return nil
Expand Down
11 changes: 10 additions & 1 deletion src/cmd/serviceDelete.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package cmd
import (
"context"

"github.com/pkg/errors"

"github.com/zeropsio/zcli/src/cmd/scope"
"github.com/zeropsio/zcli/src/cmdBuilder"
"github.com/zeropsio/zcli/src/i18n"
Expand All @@ -20,10 +22,17 @@ func serviceDeleteCmd() *cmdBuilder.Cmd {
HelpFlag(i18n.T(i18n.ServiceDeleteHelp)).
LoggedUserRunFunc(func(ctx context.Context, cmdData *cmdBuilder.LoggedUserCmdData) error {
if !cmdData.Params.GetBool("confirm") {
err := uxHelpers.YesNoPromptDestructive(ctx, cmdData.UxBlocks, i18n.T(i18n.ServiceDeleteConfirm, cmdData.Service.Name))
confirmed, err := uxHelpers.YesNoPrompt(
ctx,
cmdData.UxBlocks,
i18n.T(i18n.ServiceDeleteConfirm, cmdData.Service.Name),
)
if err != nil {
return err
}
if !confirmed {
return errors.New(i18n.T(i18n.DestructiveOperationConfirmationFailed))
}
}

deleteServiceResponse, err := cmdData.RestApiClient.DeleteServiceStack(
Expand Down
60 changes: 33 additions & 27 deletions src/cmd/vpnDown.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import (
"os"
"os/exec"

"github.com/pkg/errors"

"github.com/zeropsio/zcli/src/cmdBuilder"
"github.com/zeropsio/zcli/src/cmdRunner"
"github.com/zeropsio/zcli/src/constants"
"github.com/zeropsio/zcli/src/i18n"
"github.com/zeropsio/zcli/src/uxBlock"
"github.com/zeropsio/zcli/src/uxBlock/styles"
)

Expand All @@ -18,32 +21,35 @@ func vpnDownCmd() *cmdBuilder.Cmd {
Short(i18n.T(i18n.CmdVpnDown)).
HelpFlag(i18n.T(i18n.VpnDownHelp)).
LoggedUserRunFunc(func(ctx context.Context, cmdData *cmdBuilder.LoggedUserCmdData) error {
uxBlocks := cmdData.UxBlocks

filePath, err := constants.WgConfigFilePath()
if err != nil {
return err
}

// create empty file if not exists, only thing wg-quick needs is a proper file name
f, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
return err
}
defer f.Close()

// TODO - janhajek check if vpn is connected
// TODO - janhajek get somehow a meaningful output
// TODO - janhajek check if wg-quick is installed
// TODO - janhajek a configurable path to wg-quick
c := exec.CommandContext(ctx, "wg-quick", "down", filePath)
_, err = cmdRunner.Run(c)
if err != nil {
return err
}

uxBlocks.PrintInfo(styles.InfoLine(i18n.T(i18n.VpnDown)))

return nil
return disconnectVpn(ctx, cmdData.UxBlocks)
})
}

func disconnectVpn(ctx context.Context, uxBlocks uxBlock.UxBlocks) error {
_, err := exec.LookPath("wg-quick")
if err != nil {
return errors.New(i18n.T(i18n.VpnWgQuickIsNotInstalled))
}

filePath, err := constants.WgConfigFilePath()
if err != nil {
return err
}

// create empty file if not exists, only thing wg-quick needs is a proper file name
f, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
return err
}
defer f.Close()

c := exec.CommandContext(ctx, "wg-quick", "down", filePath)
_, err = cmdRunner.Run(c)
if err != nil {
return err
}

uxBlocks.PrintInfo(styles.InfoLine(i18n.T(i18n.VpnDown)))

return nil
}
102 changes: 92 additions & 10 deletions src/cmd/vpnUp.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,70 @@ import (
"text/template"
"time"

"github.com/pkg/errors"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"

"github.com/zeropsio/zcli/src/cliStorage"
"github.com/zeropsio/zcli/src/cmd/scope"
"github.com/zeropsio/zcli/src/cmdBuilder"
"github.com/zeropsio/zcli/src/cmdRunner"
"github.com/zeropsio/zcli/src/constants"
"github.com/zeropsio/zcli/src/entity"
"github.com/zeropsio/zcli/src/i18n"
"github.com/zeropsio/zcli/src/nettools"
"github.com/zeropsio/zcli/src/uxBlock"
"github.com/zeropsio/zcli/src/uxBlock/styles"
"github.com/zeropsio/zcli/src/uxHelpers"
"github.com/zeropsio/zerops-go/dto/input/body"
"github.com/zeropsio/zerops-go/dto/input/path"
"github.com/zeropsio/zerops-go/types"
"github.com/zeropsio/zerops-go/types/uuid"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"

"github.com/zeropsio/zcli/src/cmdBuilder"
"github.com/zeropsio/zcli/src/i18n"
"github.com/zeropsio/zcli/src/uxBlock/styles"
)

const vpnCheckAddress = "logger.core.zerops"

func vpnUpCmd() *cmdBuilder.Cmd {
return cmdBuilder.NewCmd().
Use("up").
Short(i18n.T(i18n.CmdVpnUp)).
ScopeLevel(scope.Project).
Arg(scope.ProjectArgName, cmdBuilder.OptionalArg()).
BoolFlag("auto-disconnect", false, i18n.T(i18n.VpnAutoDisconnectFlag)).
HelpFlag(i18n.T(i18n.VpnUpHelp)).
LoggedUserRunFunc(func(ctx context.Context, cmdData *cmdBuilder.LoggedUserCmdData) error {
uxBlocks := cmdData.UxBlocks

_, err := exec.LookPath("wg-quick")
if err != nil {
return errors.New(i18n.T(i18n.VpnWgQuickIsNotInstalled))
}

if !isVpnDisconnect(ctx, uxBlocks) {
if cmdData.Params.GetBool("auto-disconnect") {
err := disconnectVpn(ctx, uxBlocks)
if err != nil {
return err
}
} else {
confirmed, err := uxHelpers.YesNoPrompt(
ctx,
cmdData.UxBlocks,
i18n.T(i18n.VpnDisconnectionPrompt),
)
if err != nil {
return err
}
if !confirmed {
return errors.New(i18n.T(i18n.VpnDisconnectionPromptNo))
}

err = disconnectVpn(ctx, uxBlocks)
if err != nil {
return err
}
}
}

privateKey, err := getOrCreatePrivateVpnKey(cmdData)
if err != nil {
return err
Expand Down Expand Up @@ -103,17 +141,15 @@ func vpnUpCmd() *cmdBuilder.Cmd {
return err
}

// TODO - janhajek check if vpn is disconnected
// TODO - janhajek get somehow a meaningful output
// TODO - janhajek check if wg-quick is installed
// TODO - janhajek a configurable path to wg-quick
c := exec.CommandContext(ctx, "wg-quick", "up", filePath)
_, err = cmdRunner.Run(c)
if err != nil {
return err
}

// TODO - janhajek ping {{.Ipv4NetworkGateway}}
if !isVpnConnect(ctx, uxBlocks) {
uxBlocks.PrintWarning(styles.WarningLine(i18n.T(i18n.VpnPingFailed)))
}

uxBlocks.PrintInfo(styles.InfoLine(i18n.T(i18n.VpnUp)))

Expand Down Expand Up @@ -143,6 +179,52 @@ func getOrCreatePrivateVpnKey(cmdData *cmdBuilder.LoggedUserCmdData) (wgtypes.Ke
return vpnKey, nil
}

func isVpnConnect(ctx context.Context, uxBlocks uxBlock.UxBlocks) bool {
p := []uxHelpers.Process{
{
F: func(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, time.Second*5)
defer cancel()

return nettools.Ping(ctx, vpnCheckAddress)
},
RunningMessage: i18n.T(i18n.VpnCheckingConnection),
ErrorMessageMessage: i18n.T(i18n.VpnCheckingConnectionIsNotActive),
SuccessMessage: i18n.T(i18n.VpnCheckingConnectionIsActive),
},
}

err := uxHelpers.ProcessCheckWithSpinner(ctx, uxBlocks, p)

return err == nil
}

func isVpnDisconnect(ctx context.Context, uxBlocks uxBlock.UxBlocks) bool {
p := []uxHelpers.Process{
{
F: func(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, time.Second*5)
defer cancel()

err := nettools.Ping(ctx, vpnCheckAddress)
if err != nil {
//nolint:nilerr // Why: error is good in this case
return nil
} else {
return errors.New("vpn is connected")
}
},
RunningMessage: i18n.T(i18n.VpnCheckingConnection),
ErrorMessageMessage: i18n.T(i18n.VpnCheckingConnectionIsActive),
SuccessMessage: i18n.T(i18n.VpnCheckingConnectionIsNotActive),
},
}

err := uxHelpers.ProcessCheckWithSpinner(ctx, uxBlocks, p)

return err == nil
}

var vpnTmpl = `
[Interface]
PrivateKey = {{.PrivateKey}}
Expand Down
5 changes: 3 additions & 2 deletions src/cmdBuilder/buildCobraCmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ func buildCobraCmd(
cliStorage *cliStorage.Handler,
) (*cobra.Command, error) {
cobraCmd := &cobra.Command{
Short: cmd.short,
SilenceUsage: cmd.silenceUsage,
Short: cmd.short,
SilenceUsage: cmd.silenceUsage,
SilenceErrors: cmd.silenceError,
}

if cmd.helpTemplate != "" {
Expand Down
6 changes: 6 additions & 0 deletions src/cmdBuilder/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Cmd struct {
loggedUserRunFunc loggedUserRunFunc
guestRunFunc guestRunFunc
silenceUsage bool
silenceError bool

scopeLevel ScopeLevel
args []cmdArg
Expand Down Expand Up @@ -90,6 +91,11 @@ func (cmd *Cmd) SilenceUsage(silenceUsage bool) *Cmd {
return cmd
}

func (cmd *Cmd) SilenceError(silenceError bool) *Cmd {
cmd.silenceError = silenceError
return cmd
}

func (cmd *Cmd) ScopeLevel(scopeLevel ScopeLevel) *Cmd {
cmd.scopeLevel = scopeLevel
return cmd
Expand Down
19 changes: 14 additions & 5 deletions src/i18n/en.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ var en = map[string]string{
ConfirmFlag: "If set, zCLI will not ask for confirmation of destructive operations.",
ServiceIdFlag: "If you have access to more than one service, you must specify the service ID for which the\ncommand is to be executed.",
ProjectIdFlag: "If you have access to more than one project, you must specify the project ID for which the\ncommand is to be executed.",
VpnAutoDisconnectFlag: "If set, zCLI will automatically disconnect from the VPN if it is already connected.",

// process
ProcessInvalidState: "last command has finished with error, identifier for communication with our support: %s",
Expand Down Expand Up @@ -159,16 +160,24 @@ var en = map[string]string{
StatusInfoLogFilePath: "Zerops CLI log file path",
StatusInfoWgConfigFilePath: "Zerops CLI wg config file path",
StatusInfoLoggedUser: "Logged user",
StatusInfoVpnStatus: "VPN status",

// debug logs
DebugLogsNotFound: "Debug logs not found",

// vpn
VpnUp: "VPN connected",
VpnDown: "VPN disconnected",
VpnConfigSaved: "VPN config saved",
VpnPrivateKeyCorrupted: "VPN private key corrupted, a new one will be created",
VpnPrivateKeyCreated: "VPN private key created",
VpnUp: "VPN connected",
VpnDown: "VPN disconnected",
VpnConfigSaved: "VPN config saved",
VpnPrivateKeyCorrupted: "VPN private key corrupted, a new one will be created",
VpnPrivateKeyCreated: "VPN private key created",
VpnWgQuickIsNotInstalled: "wg-quick is not installed, please install it and try again",
VpnDisconnectionPrompt: "VPN is active, do you want to disconnect?",
VpnDisconnectionPromptNo: "VPN is active, you can disconnect using the 'zcli vpn down' command",
VpnPingFailed: "VPN ping failed, this could indicate a problem with the VPN connection",
VpnCheckingConnection: "Checking VPN connection",
VpnCheckingConnectionIsActive: "VPN connection is active",
VpnCheckingConnectionIsNotActive: "VPN connection is not active",

////////////
// global //
Expand Down
Loading
Loading