Skip to content

Commit 6a42223

Browse files
committed
feat: simplify work with Database Lab CLI (port forwarding) (platform#91)
1 parent 6bd73b5 commit 6a42223

File tree

13 files changed

+508
-22
lines changed

13 files changed

+508
-22
lines changed

cmd/cli/commands/client.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
package commands
77

88
import (
9+
"net/url"
10+
911
"github.com/urfave/cli/v2"
1012

1113
"gitlab.com/postgres-ai/database-lab/pkg/client/dblabapi"
@@ -17,12 +19,23 @@ const (
1719
URLKey = "url"
1820
TokenKey = "token"
1921
InsecureKey = "insecure"
22+
FwServerURLKey = "forwarding-server-url"
23+
FwLocalPortKey = "forwarding-local-port"
2024
)
2125

2226
// ClientByCLIContext creates a new Database Lab API client.
2327
func ClientByCLIContext(cliCtx *cli.Context) (*dblabapi.Client, error) {
28+
remoteURL, err := url.Parse(cliCtx.String(URLKey))
29+
if err != nil {
30+
return nil, err
31+
}
32+
33+
if cliCtx.String(FwServerURLKey) != "" && cliCtx.String(FwLocalPortKey) != "" {
34+
remoteURL.Host = BuildHostname(remoteURL.Hostname(), cliCtx.String(FwLocalPortKey))
35+
}
36+
2437
options := dblabapi.Options{
25-
Host: cliCtx.String(URLKey),
38+
Host: remoteURL.String(),
2639
VerificationToken: cliCtx.String(TokenKey),
2740
Insecure: cliCtx.Bool(InsecureKey),
2841
}

cmd/cli/commands/clone/actions.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ package clone
88
import (
99
"encoding/json"
1010
"fmt"
11+
"net/url"
1112
"os"
13+
"sync"
1214

1315
"github.com/urfave/cli/v2"
1416

1517
"gitlab.com/postgres-ai/database-lab/cmd/cli/commands"
1618
"gitlab.com/postgres-ai/database-lab/pkg/client/dblabapi/types"
19+
"gitlab.com/postgres-ai/database-lab/pkg/log"
1720
"gitlab.com/postgres-ai/database-lab/pkg/models"
1821
"gitlab.com/postgres-ai/database-lab/pkg/observer"
1922
)
@@ -249,3 +252,83 @@ func observeSummary() func(*cli.Context) error {
249252
return nil
250253
}
251254
}
255+
256+
func forward(cliCtx *cli.Context) error {
257+
remoteURL, err := url.Parse(cliCtx.String(commands.URLKey))
258+
if err != nil {
259+
return err
260+
}
261+
262+
wg := &sync.WaitGroup{}
263+
264+
port, err := retrieveClonePort(cliCtx, wg, remoteURL.Host)
265+
if err != nil {
266+
return err
267+
}
268+
269+
wg.Wait()
270+
271+
log.Dbg(fmt.Sprintf("The clone port has been retrieved: %s", port))
272+
273+
tunnel, err := commands.BuildTunnel(cliCtx, commands.BuildHostname(remoteURL.Hostname(), port))
274+
if err != nil {
275+
return err
276+
}
277+
278+
if err := tunnel.Open(); err != nil {
279+
return err
280+
}
281+
282+
log.Msg(fmt.Sprintf("The clone is available by address: %s", tunnel.Endpoints.Local))
283+
284+
if err := tunnel.Listen(cliCtx.Context); err != nil {
285+
return err
286+
}
287+
288+
return nil
289+
}
290+
291+
func retrieveClonePort(cliCtx *cli.Context, wg *sync.WaitGroup, remoteHost string) (string, error) {
292+
tunnel, err := commands.BuildTunnel(cliCtx, remoteHost)
293+
if err != nil {
294+
return "", err
295+
}
296+
297+
if err := tunnel.Open(); err != nil {
298+
return "", err
299+
}
300+
301+
const goroutineCount = 1
302+
303+
wg.Add(goroutineCount)
304+
305+
go func() {
306+
defer wg.Done()
307+
308+
if err := tunnel.Listen(cliCtx.Context); err != nil {
309+
log.Fatal(err)
310+
}
311+
}()
312+
313+
defer func() {
314+
log.Dbg("Stop tunnel to DBLab")
315+
316+
if err := tunnel.Stop(); err != nil {
317+
log.Err(err)
318+
}
319+
}()
320+
321+
log.Dbg("Retrieving clone port")
322+
323+
dblabClient, err := commands.ClientByCLIContext(cliCtx)
324+
if err != nil {
325+
return "", err
326+
}
327+
328+
clone, err := dblabClient.GetClone(cliCtx.Context, cliCtx.Args().First())
329+
if err != nil {
330+
return "", err
331+
}
332+
333+
return clone.DB.Port, nil
334+
}

cmd/cli/commands/clone/command_list.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,22 @@ func CommandList() []*cli.Command {
155155
Usage: "[EXPERIMENTAL] summarize clone monitoring and check results",
156156
Action: observeSummary(),
157157
},
158+
{
159+
Name: "port-forward",
160+
Usage: "start port forwarding to clone",
161+
Before: func(ctxCli *cli.Context) error {
162+
if err := checkCloneIDBefore(ctxCli); err != nil {
163+
return err
164+
}
165+
166+
if err := commands.CheckForwardingServerURL(ctxCli); err != nil {
167+
return err
168+
}
169+
170+
return nil
171+
},
172+
Action: forward,
173+
},
158174
},
159175
}}
160176
}

cmd/cli/commands/config/actions.go

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ import (
1616
"gitlab.com/postgres-ai/database-lab/cmd/cli/commands"
1717
)
1818

19+
// headers of a config list.
20+
const (
21+
envHeader = "ENV "
22+
urlHeader = "URL"
23+
fwServerHeader = "Forwarding server URL"
24+
fwPortHeader = "Forwarding local port"
25+
)
26+
1927
// createEnvironment creates a new CLI environment.
2028
func createEnvironment() func(*cli.Context) error {
2129
return func(cliCtx *cli.Context) (err error) {
@@ -119,32 +127,54 @@ func list() func(*cli.Context) error {
119127
}
120128

121129
environmentNames := make([]string, 0, len(cfg.Environments))
122-
maxNameLength := 0
130+
maxNameLen := 0
131+
maxURLLen := len(urlHeader)
132+
maxFwServerLen := len(fwServerHeader)
123133

124134
for environmentName := range cfg.Environments {
125135
environmentNames = append(environmentNames, environmentName)
126136

127137
nameLength := len(environmentName)
128-
if maxNameLength < nameLength {
129-
maxNameLength = nameLength
138+
if maxNameLen < nameLength {
139+
maxNameLen = nameLength
140+
}
141+
142+
urlLength := len(cfg.Environments[environmentName].URL)
143+
if maxURLLen < urlLength {
144+
maxURLLen = urlLength
145+
}
146+
147+
urlFwLength := len(cfg.Environments[environmentName].Forwarding.ServerURL)
148+
if maxFwServerLen < urlFwLength {
149+
maxFwServerLen = urlFwLength
130150
}
131151
}
132152

133153
sort.Strings(environmentNames)
134154

135-
listOutput := buildListOutput(cfg, environmentNames, maxNameLength)
155+
listOutput := buildListOutput(cfg, environmentNames, maxNameLen, maxURLLen, maxFwServerLen)
136156

137157
_, err = fmt.Fprintf(cliCtx.App.Writer, "Available CLI environments:\n%s", listOutput)
138158

139159
return commands.ToActionError(err)
140160
}
141161
}
142162

143-
func buildListOutput(cfg *CLIConfig, environmentNames []string, maxNameLength int) string {
163+
func buildListOutput(cfg *CLIConfig, environmentNames []string, maxNameLen, maxURLLen, maxFwLen int) string {
164+
// TODO(akartasov): Draw as a table.
144165
const outputAlign = 2
145166

146167
s := strings.Builder{}
147168

169+
s.WriteString(envHeader)
170+
s.WriteString(strings.Repeat(" ", maxNameLen+outputAlign))
171+
s.WriteString(urlHeader)
172+
s.WriteString(strings.Repeat(" ", maxURLLen-len(urlHeader)+outputAlign))
173+
s.WriteString(fwServerHeader)
174+
s.WriteString(strings.Repeat(" ", maxFwLen-len(fwServerHeader)+outputAlign))
175+
s.WriteString(fwPortHeader)
176+
s.WriteString("\n")
177+
148178
for _, environmentName := range environmentNames {
149179
if environmentName == cfg.CurrentEnvironment {
150180
s.WriteString("[*] ")
@@ -153,8 +183,12 @@ func buildListOutput(cfg *CLIConfig, environmentNames []string, maxNameLength in
153183
}
154184

155185
s.WriteString(environmentName)
156-
s.WriteString(strings.Repeat(" ", maxNameLength-len(environmentName)+outputAlign))
186+
s.WriteString(strings.Repeat(" ", maxNameLen-len(environmentName)+outputAlign))
157187
s.WriteString(cfg.Environments[environmentName].URL)
188+
s.WriteString(strings.Repeat(" ", maxURLLen-len(cfg.Environments[environmentName].URL)+outputAlign))
189+
s.WriteString(cfg.Environments[environmentName].Forwarding.ServerURL)
190+
s.WriteString(strings.Repeat(" ", maxFwLen-len(cfg.Environments[environmentName].Forwarding.ServerURL)+outputAlign))
191+
s.WriteString(cfg.Environments[environmentName].Forwarding.LocalPort)
158192
s.WriteString("\n")
159193
}
160194

cmd/cli/commands/config/command_list.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ func CommandList() []*cli.Command {
3737
Name: "insecure",
3838
Usage: "allow insecure server connections when using SSL",
3939
},
40+
&cli.StringFlag{
41+
Name: "forwarding-server-url",
42+
Usage: "forwarding server URL of Database Lab instance",
43+
},
44+
&cli.StringFlag{
45+
Name: "forwarding-local-port",
46+
Usage: "local port for forwarding to the Database Lab instance",
47+
},
4048
},
4149
},
4250
{
@@ -57,6 +65,14 @@ func CommandList() []*cli.Command {
5765
Name: "insecure",
5866
Usage: "allow insecure server connections when using SSL",
5967
},
68+
&cli.StringFlag{
69+
Name: "forwarding-server-url",
70+
Usage: "forwarding server URL of Database Lab instance",
71+
},
72+
&cli.StringFlag{
73+
Name: "forwarding-local-port",
74+
Usage: "local port for forwarding to the Database Lab instance",
75+
},
6076
},
6177
},
6278
{

cmd/cli/commands/config/environment.go

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,23 @@ import (
1313

1414
// CLIConfig defines a format of CLI configuration.
1515
type CLIConfig struct {
16-
Version string `yaml:"version" json:"version"`
1716
CurrentEnvironment string `yaml:"current_environment" json:"current_environment"`
1817
Environments map[string]Environment `yaml:"environments" json:"environments"`
1918
}
2019

2120
// Environment defines a format of environment configuration.
2221
type Environment struct {
23-
EnvironmentID string `yaml:"-" json:"environment_id"`
24-
URL string `yaml:"url" json:"url"`
25-
Token string `yaml:"token" json:"token"`
26-
Insecure bool `yaml:"insecure" json:"insecure"`
22+
EnvironmentID string `yaml:"-" json:"environment_id"`
23+
URL string `yaml:"url" json:"url"`
24+
Token string `yaml:"token" json:"token"`
25+
Insecure bool `yaml:"insecure" json:"insecure"`
26+
Forwarding Forwarding `yaml:"forwarding" json:"forwarding"`
27+
}
28+
29+
// Forwarding defines configuration for port forwarding.
30+
type Forwarding struct {
31+
ServerURL string `yaml:"server_url" json:"server_url"`
32+
LocalPort string `yaml:"local_port" json:"local_port"`
2733
}
2834

2935
// AddEnvironmentToConfig adds a new environment to CLIConfig.
@@ -40,6 +46,10 @@ func AddEnvironmentToConfig(c *cli.Context, cfg *CLIConfig, environmentID string
4046
URL: c.String(commands.URLKey),
4147
Token: c.String(commands.TokenKey),
4248
Insecure: c.Bool(commands.InsecureKey),
49+
Forwarding: Forwarding{
50+
ServerURL: c.String(commands.FwServerURLKey),
51+
LocalPort: c.String(commands.FwLocalPortKey),
52+
},
4353
}
4454

4555
if cfg.Environments == nil {
@@ -81,6 +91,14 @@ func updateEnvironmentInConfig(c *cli.Context, cfg *CLIConfig, environmentID str
8191
newEnvironment.Insecure = c.Bool(commands.InsecureKey)
8292
}
8393

94+
if c.IsSet(commands.FwServerURLKey) {
95+
newEnvironment.Forwarding.ServerURL = c.String(commands.FwServerURLKey)
96+
}
97+
98+
if c.IsSet(commands.FwLocalPortKey) {
99+
newEnvironment.Forwarding.LocalPort = c.String(commands.FwLocalPortKey)
100+
}
101+
84102
if newEnvironment == environment {
85103
return errors.New("config unchanged. Set different option values to update.") // nolint
86104
}

cmd/cli/commands/initialization/actions.go renamed to cmd/cli/commands/global/actions.go

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@
22
2020 © Postgres.ai
33
*/
44

5-
// Package initialization provides commands for a CLI initialization.
6-
package initialization
5+
// Package global provides general commands for CLI usage.
6+
package global
77

88
import (
99
"fmt"
10+
"net/url"
1011
"os"
1112

1213
"github.com/pkg/errors"
1314
"github.com/urfave/cli/v2"
1415

1516
"gitlab.com/postgres-ai/database-lab/cmd/cli/commands"
1617
"gitlab.com/postgres-ai/database-lab/cmd/cli/commands/config"
18+
"gitlab.com/postgres-ai/database-lab/pkg/log"
1719
)
1820

1921
func initCLI(c *cli.Context) error {
@@ -37,8 +39,6 @@ func initCLI(c *cli.Context) error {
3739
}
3840
}
3941

40-
cfg.Version = c.App.Version
41-
4242
environmentID := c.String(commands.EnvironmentIDKey)
4343
if err := config.AddEnvironmentToConfig(c, cfg, environmentID); err != nil {
4444
return err
@@ -53,3 +53,33 @@ func initCLI(c *cli.Context) error {
5353

5454
return err
5555
}
56+
57+
func forward(cliCtx *cli.Context) error {
58+
remoteURL, err := url.Parse(cliCtx.String(commands.URLKey))
59+
if err != nil {
60+
return err
61+
}
62+
63+
tunnel, err := commands.BuildTunnel(cliCtx, remoteURL.Host)
64+
if err != nil {
65+
return err
66+
}
67+
68+
if err := tunnel.Open(); err != nil {
69+
return err
70+
}
71+
72+
defer func() {
73+
if stopErr := tunnel.Stop(); err == nil {
74+
err = stopErr
75+
}
76+
}()
77+
78+
log.Msg(fmt.Sprintf("The connection is available by address: %s", tunnel.Endpoints.Local))
79+
80+
if err := tunnel.Listen(cliCtx.Context); err != nil {
81+
return err
82+
}
83+
84+
return nil
85+
}

0 commit comments

Comments
 (0)