Skip to content

Commit

Permalink
Adding Cobra Cli + fixes (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
mmiranda authored Nov 29, 2021
1 parent baeaefc commit ba1b21b
Show file tree
Hide file tree
Showing 10 changed files with 1,009 additions and 294 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Binaries for programs and plugins
cfdtunnel
./cfdtunnel

# Test binary, built with `go test -c`
*.test
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ You can use any command on top of *cfdtunnel*:

### Kubectl
```bash
cfdtunnel --profile my-profile1 kubectl get namespaces
cfdtunnel --profile my-profile1 -- kubectl get namespaces
```
### K9S
```bash
cfdtunnel --profile my-profile1 k9s
cfdtunnel --profile my-profile1 -- k9s
```

### Configuration
Expand All @@ -48,7 +48,7 @@ Example:
[my-profile1]
host = https://kubernetes.foo.bar.com
port = 1234
env = HTTPS_PROXY=127.0.0.1:1234
env = HTTPS_PROXY=socks5://127.0.0.1:1234
# env = OTHER=value

[my-profile2]
Expand Down
200 changes: 200 additions & 0 deletions cfdtunnel/cfdtunnel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package cfdtunnel

import (
"fmt"
"os"
"os/exec"
"strings"
"time"

log "github.com/sirupsen/logrus"
"gopkg.in/ini.v1"
)

const (
iniConfigFile = ".cfdtunnel/config"
localClientDefaultPort = "5555"
)

var (
// LogLevel sets the level of each log
LogLevel = log.WarnLevel
)

// TunnelConfig struct stores data to launch cloudflared process such as hostname and port.
// It also stores preset Environment Variables needed to use together with the tunnel consumer.
type TunnelConfig struct {
host string
port string
envVars []string
}

type config struct {
ini *ini.File
}

// Arguments struct stores the arguments passed to cfdtunel such as the profile to use, the command to run and the arguments for that command
type Arguments struct {
Profile string
Command string
Args []string
}

func init() {
log.SetOutput(os.Stdout)
log.SetLevel(LogLevel)
}

// Execute runs the entire flow of cfdtunnel tool
func (args Arguments) Execute() {
log.SetLevel(LogLevel)

config, err := readIniConfigFile(getHomePathIniFile(iniConfigFile))

if err != nil {
log.Fatalf("An error occurred reading your INI file: %v", err.Error())
}

tunnelConfig, err := config.readConfigSection(args.Profile)

if err != nil {
log.Fatalf("An error occurred reading your INI file: %v", err.Error())
}
tunnelConfig.setupEnvironmentVariables()
cmd := tunnelConfig.startProxyTunnel()
args.runSubCommand()

// Kill it:
commandKill(cmd)
}

// NewTunnel returns a new instance of the tunnel arguments to be executed
func NewTunnel(profile string, cmdArguments []string) *Arguments {

return &Arguments{
Profile: profile,
Command: cmdArguments[0],
Args: cmdArguments[1:],
}
}

// commandKill Kills an specific *exec.Cmd command
func commandKill(cmd *exec.Cmd) {
log.Debugf("Trying to kill PID: %v", cmd.Process.Pid)

if err := cmd.Process.Kill(); err != nil {
log.Fatal("failed to kill process: ", err)
}
}

// runSubCommand Runs the SubCommand and its arguments passed to cfdtunnel
func (args Arguments) runSubCommand() {
log.Debugf("Running subcommand: %v", args.Command)
if !checkSubCommandExists(args.Command) {
os.Exit(1)
}

output, err := exec.Command(args.Command, args.Args...).CombinedOutput()

fmt.Println(string(output))

if err != nil {
log.Fatalf("An error occurred trying to run the command %v: %v", args.Command, err)
}

}

// readIniConfigFile reads and load the config file.
func readIniConfigFile(configFile string) (config, error) {

cfg, err := ini.ShadowLoad(configFile)

if err != nil {
return config{}, err
}

return config{
ini: cfg,
}, err
}

// setupEnvironmentVariables Sets every environment variables that are expected and informed on the config file
func (tunnelConfig TunnelConfig) setupEnvironmentVariables() {
for _, env := range tunnelConfig.envVars {
if !strings.Contains(env, "=") {
continue
}
iniEnv := strings.Split(env, "=")
log.Debugf("Exporting Environment variable: %v", env)
os.Setenv(iniEnv[0], iniEnv[1])
}
}

// startProxyTunnel Starts the proxy tunnel (cloudflared process) and return its command instance
func (tunnelConfig TunnelConfig) startProxyTunnel() *exec.Cmd {
log.Debugf("Starting proxy tunnel for %v on port: %v", tunnelConfig.host, tunnelConfig.port)

cmd := exec.Command("cloudflared", "access", "tcp", "--hostname", tunnelConfig.host, "--url", "127.0.0.1:"+tunnelConfig.port)

err := cmd.Start()

// Hacky thing to wait for the first process start correctly
time.Sleep(1 * time.Second)

if err != nil {
log.Fatalf("Could not start cloudflared: %v", err.Error())
}

log.Debugf("cloudflared process running on PID: %v", cmd.Process.Pid)

return cmd
}

// readConfigSection reads an specific section from a config file.
// It returns a tunnelConfig struct containing the hostname, port and any environment variable needed
func (cfg config) readConfigSection(section string) (TunnelConfig, error) {

secs, err := cfg.ini.GetSection(section)

if err != nil {
log.Debugf("An error occurred: %v", err.Error())
return TunnelConfig{}, err
}

host, _ := secs.GetKey("host")
port := secs.Key("port").Validate(func(port string) string {
if len(port) == 0 {
return localClientDefaultPort
}
return port
})

envVars := []string{}
if secs.Key("env").ValueWithShadows()[0] != "" {
envVars = secs.Key("env").ValueWithShadows()
}

return TunnelConfig{
host: host.String(),
port: port,
envVars: envVars,
}, nil
}

// getHomePathIniFile Returns the full path of config file based on users home directory
func getHomePathIniFile(file string) string {
home, _ := os.UserHomeDir()

return home + "/" + file
}

// checkSubCommandExists simple check if an specific binary exists in the OS
func checkSubCommandExists(command string) bool {
_, err := exec.LookPath(command)
if err != nil {
log.Errorf("An error occurred: %v", err.Error())
return false
}

return true
}
Loading

0 comments on commit ba1b21b

Please sign in to comment.