feat(cloudflare/cloudflared): add cloudflared

This commit is contained in:
Kevin Franklin Kim 2024-08-18 11:57:29 +02:00
parent 7f526e987d
commit 6a02b0c6bd
No known key found for this signature in database
16 changed files with 799 additions and 157 deletions

View File

@ -0,0 +1,63 @@
# POSH cloudflared provider
## Usage
### Plugin
```go
package plugin
type Plugin struct {
l log.Logger
cloudflared *cloudflared.Cloudflared
commands command.Commands
}
func New(l log.Logger) (plugin.Plugin, error) {
inst := &Plugin{
l: l,
commands: command.Commands{},
}
// ...
inst.cloudflared, err = cloudflared.New(l)
if err != nil {
return nil, errors.Wrap(err, "failed to create cloudflared")
}
// ...
inst.commands.Add(command.NewCheck(l,
cloudflared.AcccessChecker(inst.cloudflared, ints.cloudflared.Config().GetAccess("my-access")),
))
inst.commands.MustAdd(cloudflared.NewCommand(l, inst.cloudflared))
// ...
return inst, nil
}
```
### Config
```yaml
cloudflared:
path: devops/config/cloudflared
access:
my-access:
type: tcp
port: 1234
hostname: cloudflared.my-domain.com
```
### Ownbrew
```yaml
ownbrew:
packages:
- name: cloudflared
tap: foomo/tap/cloudflare/cloudflared
version: 2024.6.1
```

View File

@ -0,0 +1,7 @@
package cloudflared
type Access struct {
Type string `yaml:"type"`
Hostname string `yaml:"hostname"`
Port int `yaml:"port"`
}

View File

@ -0,0 +1,21 @@
package cloudflared
import (
"context"
"fmt"
"github.com/foomo/posh/pkg/log"
"github.com/foomo/posh/pkg/prompt/check"
)
func AccessChecker(cf *Cloudflared, access Access) check.Checker {
return func(ctx context.Context, l log.Logger) check.Info {
name := "Cloudflare Access"
title := fmt.Sprintf("%s => :%d", access.Hostname, access.Port)
if cf.IsConnected(ctx, access) {
return check.NewSuccessInfo(name, title)
}
return check.NewNoteInfo(name, title)
}
}

View File

@ -0,0 +1,141 @@
package cloudflared
import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"syscall"
"github.com/foomo/posh/pkg/log"
"github.com/pkg/errors"
"github.com/shirou/gopsutil/v3/process"
"github.com/spf13/viper"
)
type (
Cloudflared struct {
l log.Logger
cfg Config
configKey string
}
Option func(*Cloudflared) error
)
// ------------------------------------------------------------------------------------------------
// ~ Options
// ------------------------------------------------------------------------------------------------
func WithConfigKey(v string) Option {
return func(o *Cloudflared) error {
o.configKey = v
return nil
}
}
// ------------------------------------------------------------------------------------------------
// ~ Constructor
// ------------------------------------------------------------------------------------------------
// New command
func New(l log.Logger, opts ...Option) (*Cloudflared, error) {
inst := &Cloudflared{
l: l,
configKey: "cloudflared",
}
for _, opt := range opts {
if opt != nil {
if err := opt(inst); err != nil {
return nil, err
}
}
}
if err := viper.UnmarshalKey(inst.configKey, &inst.cfg); err != nil {
return nil, err
}
return inst, nil
}
// ------------------------------------------------------------------------------------------------
// ~ Public methods
// ------------------------------------------------------------------------------------------------
func (t *Cloudflared) Config() Config {
return t.cfg
}
func (t *Cloudflared) Disonnect(ctx context.Context, access Access) error {
ps, err := process.Processes()
if err != nil {
return err
}
for _, p := range ps {
if value, _ := p.Name(); value == "cloudflared" {
if cmdline, _ := p.Cmdline(); strings.Contains(cmdline, "--hostname "+access.Hostname) {
t.l.Info("closing connection", "hostname", access.Hostname, "pid", p.Pid, "port", access.Port)
return p.Kill()
}
}
}
return nil
}
func (t *Cloudflared) Connect(ctx context.Context, access Access) error {
if t.IsConnected(ctx, access) {
return errors.Errorf("connection already exists: %s", access.Hostname)
}
cmd := exec.CommandContext(ctx, "cloudflared", "access", access.Type)
cmd.Args = append(cmd.Args, "--hostname", access.Hostname, "--url", fmt.Sprintf("127.0.0.1:%d", access.Port))
cmd.Env = append(os.Environ(), "HOME="+t.Config().Path)
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
if err := cmd.Start(); err != nil {
return err
}
if cmd.Process != nil {
t.l.Info("started access", "hostname", access.Hostname, "port", access.Port, "pid", cmd.Process.Pid)
}
return nil
}
func (t *Cloudflared) IsConnected(ctx context.Context, access Access) bool {
list, err := t.List()
if err != nil {
return false
}
for _, p := range list {
if strings.Contains(p.Cmdline, "--hostname "+access.Hostname) {
return true
}
}
return false
}
func (t *Cloudflared) List() ([]Process, error) {
ps, err := process.Processes()
if err != nil {
return nil, err
}
var ret []Process
for _, p := range ps {
if value, _ := p.Name(); value == "cloudflared" {
exe, _ := p.Exe()
cmdline, _ := p.Cmdline()
ret = append(ret, Process{
PID: fmt.Sprintf("%d", p.Pid),
Exe: exe,
Cmdline: cmdline,
})
}
}
return ret, nil
}

View File

@ -0,0 +1,285 @@
package cloudflared
import (
"context"
"encoding/base64"
"os"
"os/exec"
"github.com/foomo/posh/pkg/command/tree"
"github.com/foomo/posh/pkg/log"
"github.com/foomo/posh/pkg/prompt/goprompt"
"github.com/foomo/posh/pkg/readline"
"github.com/foomo/posh/pkg/shell"
"github.com/foomo/posh/pkg/util/suggests"
"github.com/pkg/errors"
"github.com/pterm/pterm"
)
type (
Command struct {
l log.Logger
name string
cloudflared *Cloudflared
commandTree tree.Root
}
CommandOption func(*Command)
)
// ------------------------------------------------------------------------------------------------
// ~ Options
// ------------------------------------------------------------------------------------------------
func CommandWithName(v string) CommandOption {
return func(o *Command) {
o.name = v
}
}
// ------------------------------------------------------------------------------------------------
// ~ Constructor
// ------------------------------------------------------------------------------------------------
func NewCommand(l log.Logger, cloudflared *Cloudflared, opts ...CommandOption) (*Command, error) {
inst := &Command{
l: l.Named("cloudflared"),
name: "cloudflared",
cloudflared: cloudflared,
}
for _, opt := range opts {
if opt != nil {
opt(inst)
}
}
if err := os.MkdirAll(inst.cloudflared.Config().Path, 0700); err != nil {
return nil, err
}
inst.commandTree = tree.New(&tree.Node{
Name: inst.name,
Description: "Run cloudflared",
Nodes: tree.Nodes{
{
Name: "access",
Description: "Forward access",
Nodes: tree.Nodes{
{
Name: "list",
Description: "list forward access",
Execute: inst.accessList,
},
{
Name: "connect",
Description: "open access by name ",
Args: tree.Args{
{
Name: "name",
Description: "Name of the access",
Suggest: func(ctx context.Context, t tree.Root, r *readline.Readline) []goprompt.Suggest {
return suggests.List(inst.cloudflared.Config().AccessNames())
},
},
},
Execute: inst.accessConnect,
},
{
Name: "disconect",
Description: "close access by name ",
Args: tree.Args{
{
Name: "name",
Description: "Name of the access",
Suggest: func(ctx context.Context, t tree.Root, r *readline.Readline) []goprompt.Suggest {
return suggests.List(inst.cloudflared.Config().AccessNames())
},
},
},
Execute: inst.accessDisconnect,
},
},
Execute: inst.execute,
},
{
Name: "tunnel",
Description: "manage tunnels",
Nodes: tree.Nodes{
{
Name: "login",
Description: "Generate a configuration file with your login details",
Execute: inst.execute,
},
{
Name: "create",
Description: "Create a new tunnel with given name",
Args: tree.Args{
{
Name: "tunnel",
Description: "UUID or name",
},
},
Execute: inst.tunnelCreate,
},
{
Name: "delete",
Description: "Delete existing tunnel by UUID or name",
Args: tree.Args{
{
Name: "tunnel",
Description: "UUID or name",
},
},
Execute: inst.execute,
},
{
Name: "route",
Description: "Define which traffic routed from Cloudflare edge to this tunnel",
Nodes: tree.Nodes{
{
Name: "dns",
Description: "HostnameRoute a hostname by creating a DNS CNAME record to a tunnel",
Args: tree.Args{
{
Name: "tunnel",
Description: "UUID or name",
},
{
Name: "hostname",
Description: "Hostname for the dns enty",
},
},
Execute: inst.execute,
},
},
Execute: inst.execute,
},
{
Name: "token",
Description: "Create a new tunnel",
Args: tree.Args{
{
Name: "tunnel",
Description: "UUID or name",
},
},
Execute: inst.execute,
},
{
Name: "list",
Description: "List existing tunnels",
Execute: inst.execute,
},
{
Name: "info",
Description: "List details about the active connectors for a tunnel",
Execute: inst.execute,
},
},
Execute: inst.execute,
},
},
Execute: inst.execute,
})
return inst, nil
}
// ------------------------------------------------------------------------------------------------
// ~ Public methods
// ------------------------------------------------------------------------------------------------
func (c *Command) Name() string {
return c.commandTree.Node().Name
}
func (c *Command) Description() string {
return c.commandTree.Node().Description
}
func (c *Command) Complete(ctx context.Context, r *readline.Readline) []goprompt.Suggest {
return c.commandTree.Complete(ctx, r)
}
func (c *Command) Validate(ctx context.Context, r *readline.Readline) error {
if _, err := exec.LookPath("cloudflared"); err != nil {
c.l.Print()
return errors.New("missing cloudflared executable")
}
return nil
}
func (c *Command) Execute(ctx context.Context, r *readline.Readline) error {
return c.commandTree.Execute(ctx, r)
}
func (c *Command) Help(ctx context.Context, r *readline.Readline) string {
return c.commandTree.Help(ctx, r)
}
// ------------------------------------------------------------------------------------------------
// ~ Private methods
// ------------------------------------------------------------------------------------------------
func (c *Command) execute(ctx context.Context, r *readline.Readline) error {
return shell.New(ctx, c.l, "cloudflared").
Args(r.Args()...).
Args(r.Flags()...).
Args(r.AdditionalArgs()...).
Env("HOME=" + c.cloudflared.Config().Path).
Run()
}
func (c *Command) accessList(ctx context.Context, r *readline.Readline) error {
list, err := c.cloudflared.List()
if err != nil {
return err
}
data := pterm.TableData{
{"pid", "cmdline"},
}
for _, p := range list {
data = append(data, []string{p.PID, p.Cmdline})
}
return pterm.DefaultTable.WithHasHeader(true).WithData(data).Render()
}
func (c *Command) accessConnect(ctx context.Context, r *readline.Readline) error {
access := c.cloudflared.Config().GetAccesss(r.Args().At(2))
return c.cloudflared.Connect(ctx, access)
}
func (c *Command) accessDisconnect(ctx context.Context, r *readline.Readline) error {
access := c.cloudflared.Config().GetAccesss(r.Args().At(2))
return c.cloudflared.Disonnect(ctx, access)
}
func (c *Command) tunnelCreate(ctx context.Context, r *readline.Readline) error {
if err := shell.New(ctx, c.l, "cloudflared", "tunnel", "create", r.Args().At(2)).
Args(r.Flags()...).
Args(r.AdditionalArgs()...).
Env("HOME=" + c.cloudflared.Config().Path).
Run(); err != nil {
return err
}
out, err := shell.New(ctx, c.l, "cloudflared", "tunnel", "token", r.Args().At(2)).
Args(r.Flags()...).
Args(r.AdditionalArgs()...).
Env("HOME=" + c.cloudflared.Config().Path).
Output()
if err != nil {
return err
}
var outDec []byte
if _, err := base64.StdEncoding.Decode(outDec, out); err != nil {
return err
}
c.l.Info(string(outDec))
return nil
}

View File

@ -0,0 +1,22 @@
package cloudflared
import (
"sort"
"github.com/samber/lo"
)
type Config struct {
Path string `yaml:"path"`
Access map[string]Access `yaml:"access"`
}
func (c Config) AccessNames() []string {
ret := lo.Keys(c.Access)
sort.Strings(ret)
return ret
}
func (c Config) GetAccesss(name string) Access {
return c.Access[name]
}

View File

@ -0,0 +1,7 @@
package cloudflared
type Process struct {
PID string
Exe string
Cmdline string
}

View File

@ -8,10 +8,11 @@
package plugin
type Plugin struct {
l log.Logger
beam *beam.Beam
cache cache.Cache
commands command.Commands
l log.Logger
beam *beam.Beam
cloudflared *cloudflared.cloudflared
cache cache.Cache
commands command.Commands
}
func New(l log.Logger) (plugin.Plugin, error) {
@ -33,6 +34,11 @@ func New(l log.Logger) (plugin.Plugin, error) {
return nil, errors.Wrap(err, "failed to create kubectl")
}
inst.cloudflared, err = cloudflared.New(l)
if err != nil {
return nil, errors.Wrap(err, "failed to create cloudflared")
}
inst.beam, err = beam.NewBeam(l, inst.op)
if err != nil {
return nil, errors.Wrap(err, "failed to create beam")
@ -40,10 +46,11 @@ func New(l log.Logger) (plugin.Plugin, error) {
// ...
inst.commands.Add(command.NewCheck(l,
beam.TunnelChecker(inst.beam, "my-env", "my-cluster"),
beam.ClusterChecker(inst.cloudflared, inst.beam.Config().GetCluster("my-cluster")),
beam.DatabaseChecker(inst.cloudflared, inst.beam.Config().GetDatabase("my-database")),
))
inst.commands.MustAdd(beam.NewCommand(l, inst.beam, inst.kubectl))
inst.commands.MustAdd(beam.NewCommand(l, inst.beam, inst.kubectl, inst.cloudflared))
// ...
@ -55,23 +62,16 @@ func New(l log.Logger) (plugin.Plugin, error) {
```yaml
beam:
my-env:
clusters:
my-cluster:
port: 1234
hostname: beam.my-domain.com
credentials:
item: <name|uuid>
vault: <name|uuid>
account: <account>
```
### Ownbrew
```yaml
ownbrew:
packages:
- name: cloudflared
tap: foomo/tap/cloudflare/cloudflared
version: 2024.6.1
clusters:
my-cluster:
port: 12200
hostname: "my-concierge.domain.com"
kubeconfig:
item: <document>
vault: <vault>
account: <account>
databases:
my-database:
port: 12202
hostname: "my-database.domain.com"
```

View File

@ -1,11 +1,6 @@
package beam
import (
"fmt"
"net"
"os/exec"
"time"
"github.com/foomo/posh-providers/onepassword"
"github.com/foomo/posh/pkg/log"
"github.com/spf13/viper"
@ -25,7 +20,7 @@ type (
// ~ Options
// ------------------------------------------------------------------------------------------------
func CommandWithConfigKey(v string) Option {
func WithConfigKey(v string) Option {
return func(o *Beam) error {
o.configKey = v
return nil
@ -36,8 +31,8 @@ func CommandWithConfigKey(v string) Option {
// ~ Constructor
// ------------------------------------------------------------------------------------------------
// NewBeam command
func NewBeam(l log.Logger, op *onepassword.OnePassword, opts ...Option) (*Beam, error) {
// New command
func New(l log.Logger, op *onepassword.OnePassword, opts ...Option) (*Beam, error) {
inst := &Beam{
l: l,
op: op,
@ -64,35 +59,3 @@ func NewBeam(l log.Logger, op *onepassword.OnePassword, opts ...Option) (*Beam,
func (t *Beam) Config() Config {
return t.cfg
}
func (t *Beam) Start() {
t.l.Info("Starting beam tunnels")
for _, tunnel := range t.cfg {
for _, cluster := range tunnel.Clusters {
go t.tunnel(cluster.Hostname, cluster.Port)
}
}
}
// ------------------------------------------------------------------------------------------------
// ~ Private methods
// ------------------------------------------------------------------------------------------------
func (t *Beam) tunnel(hostname string, port int) {
for {
addr := fmt.Sprintf("127.0.0.1:%d", port)
if _, err := net.DialTimeout("tcp", addr, time.Second); err == nil {
t.l.Debug("tunnel/port already exists", "addr", addr, "err", err)
time.Sleep(10 * time.Second)
continue
}
cmd := exec.Command("cloudflared", "access", "tcp", "--hostname", hostname, "--url", fmt.Sprintf("127.0.0.1:%d", port))
t.l.Info("started tunnel", "addr", addr)
if err := cmd.Run(); err != nil {
t.l.Warn("failed to start tunnel", "error", err)
time.Sleep(time.Second)
continue
}
t.l.Info("done?", "addr", addr)
}
}

View File

@ -3,22 +3,42 @@ package beam
import (
"context"
"fmt"
"net"
"time"
"github.com/foomo/posh-providers/cloudflare/cloudflared"
"github.com/foomo/posh/pkg/log"
"github.com/foomo/posh/pkg/prompt/check"
)
func TunnelChecker(p *Beam, tunnel, cluster string) check.Checker {
func ClusterChecker(cf *cloudflared.Cloudflared, cluster Cluster) check.Checker {
return func(ctx context.Context, l log.Logger) check.Info {
name := "Beam"
c := p.Config().GetTunnel(tunnel).GetCluster(cluster)
addr := fmt.Sprintf("127.0.0.1:%d", c.Port)
if _, err := net.DialTimeout("tcp", addr, time.Second); err != nil {
return check.NewNoteInfo(name, fmt.Sprintf("Tunnel `%s` to cluster `%s` is closed", tunnel, cluster))
} else {
return check.NewSuccessInfo(name, fmt.Sprintf("Tunnel `%s` to cluster `%s` is open", tunnel, cluster))
name := "Beam Cluster"
title := fmt.Sprintf("%s => :%d", cluster.Hostname, cluster.Port)
if cf.IsConnected(ctx, cloudflared.Access{
Type: "tcp",
Hostname: cluster.Hostname,
Port: cluster.Port,
}) {
return check.NewSuccessInfo(name, title)
}
return check.NewNoteInfo(name, title)
}
}
func DatabaseChecker(cf *cloudflared.Cloudflared, database Database) check.Checker {
return func(ctx context.Context, l log.Logger) check.Info {
name := "Beam Database"
title := fmt.Sprintf("%s => :%d", database.Hostname, database.Port)
if cf.IsConnected(ctx, cloudflared.Access{
Type: "tcp",
Hostname: database.Hostname,
Port: database.Port,
}) {
return check.NewSuccessInfo(name, title)
}
return check.NewNoteInfo(name, title)
}
}

View File

@ -5,7 +5,7 @@ import (
)
type Cluster struct {
Port int `json:"port" yaml:"port"`
Hostname string `json:"hostname" yaml:"hostname"`
Credentials onepassword.Secret `json:"credentials" yaml:"credentials"`
Port int `yaml:"port"`
Hostname string `yaml:"hostname"`
Kubeconfig onepassword.Secret `yaml:"kubeconfig"`
}

View File

@ -3,21 +3,17 @@ package beam
import (
"context"
"fmt"
"net"
"os"
"os/exec"
"path"
"strings"
"time"
"github.com/foomo/posh-providers/cloudflare/cloudflared"
"github.com/foomo/posh-providers/kubernets/kubectl"
"github.com/foomo/posh/pkg/command/tree"
"github.com/foomo/posh/pkg/log"
"github.com/foomo/posh/pkg/prompt/goprompt"
"github.com/foomo/posh/pkg/readline"
"github.com/foomo/posh/pkg/shell"
"github.com/foomo/posh/pkg/util/suggests"
"github.com/pkg/errors"
"github.com/pterm/pterm"
)
type (
@ -26,6 +22,7 @@ type (
beam *Beam
name string
kubectl *kubectl.Kubectl
cloudflared *cloudflared.Cloudflared
commandTree tree.Root
}
CommandOption func(*Command)
@ -45,12 +42,13 @@ func CommandWithName(v string) CommandOption {
// ~ Constructor
// ------------------------------------------------------------------------------------------------
func NewCommand(l log.Logger, beam *Beam, kubectl *kubectl.Kubectl, opts ...CommandOption) (*Command, error) {
func NewCommand(l log.Logger, beam *Beam, kubectl *kubectl.Kubectl, cloudflared *cloudflared.Cloudflared, opts ...CommandOption) (*Command, error) {
inst := &Command{
l: l.Named("beam"),
name: "beam",
beam: beam,
kubectl: kubectl,
l: l.Named("beam"),
name: "beam",
beam: beam,
kubectl: kubectl,
cloudflared: cloudflared,
}
for _, opt := range opts {
if opt != nil {
@ -63,39 +61,64 @@ func NewCommand(l log.Logger, beam *Beam, kubectl *kubectl.Kubectl, opts ...Comm
Description: "Run beam",
Nodes: tree.Nodes{
{
Name: "tunnel",
Values: func(ctx context.Context, r *readline.Readline) []goprompt.Suggest {
return suggests.List(inst.beam.cfg.GetTunnelNames())
},
Description: "Tunnel",
Name: "status",
Description: "Show connection status",
Execute: inst.status,
},
{
Name: "cluster",
Description: "Manage cluster connection",
Nodes: tree.Nodes{
{
Name: "tunnel",
Description: "Start cloudflared tunnel",
Args: tree.Args{
Name: "name",
Description: "Cluster name",
Values: func(ctx context.Context, r *readline.Readline) []goprompt.Suggest {
return suggests.List(inst.beam.cfg.ClusterNames())
},
Nodes: tree.Nodes{
{
Name: "cluster",
Description: "Cluster name",
Suggest: func(ctx context.Context, t tree.Root, r *readline.Readline) []goprompt.Suggest {
return suggests.List(inst.beam.Config().GetTunnel(r.Args().At(0)).GetClusterNames())
},
Name: "connect",
Description: "Connect to cluster",
Execute: inst.clusterConnect,
},
{
Name: "kubeconfig",
Description: "Download kubeconfig",
Execute: inst.clusterKubeconfig,
},
{
Name: "disconnect",
Description: "Disconnect to cluster",
Execute: inst.clusterDisconnect,
},
},
Execute: inst.tunnel,
Execute: nil,
},
},
},
{
Name: "database",
Description: "Manage cluster connection",
Nodes: tree.Nodes{
{
Name: "kubeconfig",
Description: "Download kubeconfig",
Args: tree.Args{
Name: "name",
Description: "Cluster name",
Values: func(ctx context.Context, r *readline.Readline) []goprompt.Suggest {
return suggests.List(inst.beam.cfg.DatabaseNames())
},
Nodes: tree.Nodes{
{
Name: "cluster",
Description: "Cluster name",
Suggest: func(ctx context.Context, t tree.Root, r *readline.Readline) []goprompt.Suggest {
return suggests.List(inst.beam.Config().GetTunnel(r.Args().At(0)).GetClusterNames())
},
Name: "connect",
Description: "Connect to database",
Execute: inst.databaseConnect,
},
{
Name: "disconnect",
Description: "Disconnect to database",
Execute: inst.databaseDisconnect,
},
},
Execute: inst.kubeconfig,
Execute: nil,
},
},
},
@ -133,51 +156,79 @@ func (c *Command) Help(ctx context.Context, r *readline.Readline) string {
// ~ Private methods
// ------------------------------------------------------------------------------------------------
func (c *Command) tunnel(ctx context.Context, r *readline.Readline) error {
tunnel := c.beam.Config().GetTunnel(r.Args().At(0))
cluster := tunnel.GetCluster(r.Args().At(2))
addr := fmt.Sprintf("127.0.0.1:%d", cluster.Port)
if _, err := net.DialTimeout("tcp", addr, time.Second); err == nil {
out, _ := shell.New(ctx, c.l, "ps", "-a", "-x", "|", "grep", addr, "|", "grep", "-v", "grep").CombinedOutput()
c.l.Infof(`Process list:
%s
To manually stop it, run:
$ kill -1 <PID>
`, string(out))
return errors.Errorf("tunnel/port already exists: %s", addr)
}
cmd := exec.CommandContext(ctx, "cloudflared", "access", "tcp", "--hostname", cluster.Hostname, "--url", addr)
if err := cmd.Start(); err != nil {
return err
}
if cmd.Process != nil {
c.l.Info("started tunnel", "pid", cmd.Process.Pid)
}
return nil
}
func (c *Command) kubeconfig(ctx context.Context, r *readline.Readline) error {
tunnel := c.beam.Config().GetTunnel(r.Args().At(0))
cluster := tunnel.GetCluster(r.Args().At(2))
kubeconfig, err := c.beam.op.GetDocument(ctx, cluster.Credentials)
func (c *Command) status(ctx context.Context, r *readline.Readline) error {
list, err := c.cloudflared.List()
if err != nil {
return err
}
filename := path.Join(c.kubectl.Config().ConfigPath, r.Args().At(2)+".yaml")
c.l.Info("Retrieving kubeconfig", "tunnel", r.Args().At(0), "cluster", r.Args().At(2), "filename", filename)
data := pterm.TableData{
{"pid", "cmdline"},
}
kubeconfig = strings.ReplaceAll(kubeconfig, "$PORT", fmt.Sprintf("%d", cluster.Port))
for _, p := range list {
data = append(data, []string{p.PID, p.Cmdline})
}
if err := os.WriteFile(filename, []byte(kubeconfig), 0600); err != nil {
return pterm.DefaultTable.WithHasHeader(true).WithData(data).Render()
}
func (c *Command) clusterKubeconfig(ctx context.Context, r *readline.Readline) error {
clusterName := r.Args().At(1)
clusterConfig := c.beam.Config().GetCluster(clusterName)
kubectlCluster := c.kubectl.Cluster(clusterName)
c.l.Info("Retrieving kubeconfig", "cluster", clusterName, "filename", kubectlCluster.Config(""))
kubeconfig, err := c.beam.op.GetDocument(ctx, clusterConfig.Kubeconfig)
if err != nil {
return err
}
return nil
kubeconfig = strings.ReplaceAll(kubeconfig, "$PORT", fmt.Sprintf("%d", clusterConfig.Port))
return os.WriteFile(kubectlCluster.Config(""), []byte(kubeconfig), 0600)
}
func (c *Command) clusterConnect(ctx context.Context, r *readline.Readline) error {
clusterName := r.Args().At(1)
clusterConfig := c.beam.Config().GetCluster(clusterName)
return c.cloudflared.Connect(ctx, cloudflared.Access{
Type: "tcp",
Hostname: clusterConfig.Hostname,
Port: clusterConfig.Port,
})
}
func (c *Command) clusterDisconnect(ctx context.Context, r *readline.Readline) error {
clusterName := r.Args().At(1)
clusterConfig := c.beam.Config().GetCluster(clusterName)
return c.cloudflared.Disonnect(ctx, cloudflared.Access{
Type: "tcp",
Hostname: clusterConfig.Hostname,
Port: clusterConfig.Port,
})
}
func (c *Command) databaseConnect(ctx context.Context, r *readline.Readline) error {
databaseName := r.Args().At(1)
databaseConfig := c.beam.Config().GetCluster(databaseName)
return c.cloudflared.Connect(ctx, cloudflared.Access{
Type: "tcp",
Hostname: databaseConfig.Hostname,
Port: databaseConfig.Port,
})
}
func (c *Command) databaseDisconnect(ctx context.Context, r *readline.Readline) error {
databaseName := r.Args().At(1)
databaseConfig := c.beam.Config().GetCluster(databaseName)
return c.cloudflared.Disonnect(ctx, cloudflared.Access{
Type: "tcp",
Hostname: databaseConfig.Hostname,
Port: databaseConfig.Port,
})
}

View File

@ -6,14 +6,37 @@ import (
"github.com/samber/lo"
)
type Config map[string]Tunnel
func (c Config) GetTunnel(name string) Tunnel {
return c[name]
type Config struct {
Clusters map[string]Cluster `yaml:"clusters"`
Databases map[string]Database `yaml:"databases"`
}
func (c Config) GetTunnelNames() []string {
ret := lo.Keys(c)
func (c Config) GetDatabase(name string) Database {
return c.Databases[name]
}
func (c Config) DatabaseNames() []string {
ret := lo.Keys(c.Databases)
sort.Strings(ret)
return ret
}
func (c Config) DatabaseExists(name string) bool {
_, ok := c.Databases[name]
return ok
}
func (c Config) GetCluster(name string) Cluster {
return c.Clusters[name]
}
func (c Config) ClusterNames() []string {
ret := lo.Keys(c.Clusters)
sort.Strings(ret)
return ret
}
func (c Config) ClusterExists(name string) bool {
_, ok := c.Clusters[name]
return ok
}

6
foomo/beam/database.go Normal file
View File

@ -0,0 +1,6 @@
package beam
type Database struct {
Port int `yaml:"port"`
Hostname string `yaml:"hostname"`
}

9
go.mod
View File

@ -9,6 +9,7 @@ require (
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/c-bata/go-prompt v0.2.6
github.com/cloudrecipes/packagejson v1.0.0
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/digitalocean/godo v1.119.0
github.com/foomo/posh v0.5.10
github.com/google/go-github/v47 v47.1.0
@ -16,6 +17,7 @@ require (
github.com/pkg/errors v0.9.1
github.com/pterm/pterm v0.12.79
github.com/samber/lo v1.46.0
github.com/shirou/gopsutil/v3 v3.23.7
github.com/slack-go/slack v0.13.1
github.com/spf13/viper v1.19.0
go.uber.org/zap v1.27.0
@ -34,6 +36,7 @@ require (
github.com/containerd/console v1.0.3 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-test/deep v1.1.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/gookit/color v1.5.4 // indirect
@ -42,6 +45,7 @@ require (
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/lithammer/fuzzysearch v1.1.8 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@ -51,17 +55,22 @@ require (
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pkg/term v1.1.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.6.0 // indirect
github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/crypto v0.23.0 // indirect

24
go.sum
View File

@ -48,11 +48,15 @@ github.com/franklinkim/go-prompt v0.2.7-0.20210427061716-a8f4995d7aa5 h1:kXNtle4
github.com/franklinkim/go-prompt v0.2.7-0.20210427061716-a8f4995d7aa5/go.mod h1:+syUfnvYJUO5A+6QMQYXAyzkxHMNlj9dH2LIeQfBSjc=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v47 v47.1.0 h1:Cacm/WxQBOa9lF0FT0EMjZ2BWMetQ1TQfyurn4yF1z8=
@ -92,6 +96,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@ -124,6 +130,8 @@ github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=
github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=
@ -147,6 +155,12 @@ github.com/samber/lo v1.46.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shirou/gopsutil/v3 v3.23.7 h1:C+fHO8hfIppoJ1WdsVm1RoI0RwXoNdfTK7yWXV0wVj4=
github.com/shirou/gopsutil/v3 v3.23.7/go.mod h1:c4gnmoRC0hQuaLqvxnx1//VXQ0Ms/X9UnJF8pddY5z4=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/slack-go/slack v0.13.1 h1:6UkM3U1OnbhPsYeb1IMkQ6HSNOSikWluwOncJt4Tz/o=
github.com/slack-go/slack v0.13.1/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
@ -176,6 +190,10 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
@ -184,6 +202,8 @@ github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1z
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@ -214,6 +234,7 @@ golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -221,6 +242,7 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -229,8 +251,10 @@ golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=