Skip to content
Open
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
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,68 @@ $ rancher login https://<RANCHER_SERVER_URL> -t my-secret-token

> **Note:** When entering your `<RANCHER_SERVER_URL>`, include the port that was exposed while you installed Rancher Server.

### External Config Helper Support

The Rancher CLI supports external config helpers for enhanced credential management and integration with external systems like credential stores, password managers, or CI/CD pipelines.

#### Using Config Helpers

You can specify a config helper using the `--config-helper` flag or the `RANCHER_CONFIG_HELPER` environment variable:

```bash
# Use an external helper script
$ rancher --config-helper /path/to/my-helper login

# Use environment variable
$ export RANCHER_CONFIG_HELPER=/path/to/my-helper
$ rancher login

# Use built-in file-based config (default)
$ rancher --config-helper built-in login
```

#### Creating Config Helpers

Config helpers are executable scripts or programs that handle loading and storing Rancher CLI configuration. They must support two commands:

**Loading Configuration:**
```bash
helper-script get
```
Should output the complete Rancher CLI configuration as JSON to stdout.

**Storing Configuration:**
```bash
helper-script store '{"Servers":{"server1":{...}},"CurrentServer":"server1"}'
```
Should persist the provided JSON configuration.

**Example Helper Script:**
```bash
#!/bin/bash
case "$1" in
get)
# Load config from your external system
gopass show -o -y rancher-config
;;

store)
# Store config to your external system
echo $2 | gopass insert -f rancher-config
;;
*)
echo "Usage: $0 {get|store}"
exit 1
;;
esac
```

This feature enables integration with:
- Corporate credential management systems
- Cloud provider secret stores (AWS Secrets Manager, Azure Key Vault, etc.)
- CI/CD pipeline secret injection
- Multi-environment configuration management

## Usage

Run `rancher --help` for a list of available commands.
Expand Down
11 changes: 9 additions & 2 deletions cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,15 @@ func GetConfigPath(ctx *cli.Context) string {
}

func loadConfig(ctx *cli.Context) (config.Config, error) {
path := GetConfigPath(ctx)
return config.LoadFromPath(path)
switch ctx.GlobalString("config-helper") {
case "built-in":
path := GetConfigPath(ctx)
return config.LoadFromPath(path)
default:
// allow loading of rancher config by triggering an
// external helper executable
return config.LoadWithHelper(ctx.GlobalString("config-helper"))
}
}

func lookupConfig(ctx *cli.Context) (*config.ServerConfig, error) {
Expand Down
54 changes: 54 additions & 0 deletions cmd/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"testing"
"time"
Expand Down Expand Up @@ -292,3 +294,55 @@ func TestNewHTTPClient(t *testing.T) {
require.Nil(t, proxyURL)
})
}

// TestConfigHelperIntegration tests that the config helper functionality
// integrates properly with the CLI loading mechanism
func TestConfigHelperIntegration(t *testing.T) {
t.Parallel()

t.Run("loadConfig uses built-in helper by default", func(t *testing.T) {
t.Parallel()
assert := assert.New(t)
require := require.New(t)

// Create a temporary config file
dir, err := os.MkdirTemp("", "rancher-cli-test-*")
require.NoError(err)
defer os.RemoveAll(dir)

configPath := filepath.Join(dir, "cli2.json")
configContent := `{"Servers":{"test":{"url":"https://test.com"}},"CurrentServer":"test"}`
err = os.WriteFile(configPath, []byte(configContent), 0600)
require.NoError(err)

// Test that config.LoadFromPath works with built-in helper
conf, err := config.LoadFromPath(configPath)
require.NoError(err)
assert.Equal("built-in", conf.Helper)
assert.Equal("test", conf.CurrentServer)
})

t.Run("external helper integration works", func(t *testing.T) {
t.Parallel()
assert := assert.New(t)
require := require.New(t)

// Create a mock helper script
dir, err := os.MkdirTemp("", "rancher-cli-test-*")
require.NoError(err)
defer os.RemoveAll(dir)

helperScript := `#!/bin/bash
echo '{"Servers":{"helper-test":{"url":"https://helper.com"}},"CurrentServer":"helper-test"}'`
helperPath := filepath.Join(dir, "test-helper")
err = os.WriteFile(helperPath, []byte(helperScript), 0755)
require.NoError(err)

// Test that config.LoadWithHelper works
conf, err := config.LoadWithHelper(helperPath)
require.NoError(err)
assert.Equal(helperPath, conf.Helper)
assert.Equal("helper-test", conf.CurrentServer)
assert.Empty(conf.Path) // Path should be empty for helper-loaded configs
})
}
1 change: 1 addition & 0 deletions cmd/kubectl_token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ func TestCacheCredential(t *testing.T) {
flagSet := flag.NewFlagSet("test", 0)
flagSet.String("server", "rancher.example.com", "doc")
flagSet.String("config", t.TempDir(), "doc")
flagSet.String("config-helper", "built-in", "")
cliCtx := cli.NewContext(nil, flagSet, nil)

serverConfig, err := lookupServerConfig(cliCtx)
Expand Down
1 change: 1 addition & 0 deletions cmd/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,5 +272,6 @@ func newTestConfig() *config.Config {
"server2": {URL: "https://myserver-2.com"},
"server3": {URL: "https://myserver-3.com"},
},
Helper: "built-in",
}
}
48 changes: 48 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
Expand All @@ -24,6 +25,8 @@ type Config struct {
Path string `json:"path,omitempty"`
// CurrentServer the user has in focus
CurrentServer string
// Helper executable to store config
Helper string `json:"helper,omitempty"`
}

// ServerConfig holds the config for each server the user has setup
Expand All @@ -50,6 +53,7 @@ func LoadFromPath(path string) (Config, error) {
cf := Config{
Path: path,
Servers: make(map[string]*ServerConfig),
Helper: "built-in",
}

content, err := os.ReadFile(path)
Expand All @@ -70,6 +74,26 @@ func LoadFromPath(path string) (Config, error) {
return cf, nil
}

// fetch rancher cli config by executing an external helper
func LoadWithHelper(helper string) (Config, error) {
cf := Config{
Path: "",
Servers: make(map[string]*ServerConfig),
Helper: helper,
}

cmd := exec.Command(helper, "get")
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
content, err := cmd.Output()
if err != nil {
return cf, err
}

err = json.Unmarshal(content, &cf)
return cf, err
}

// GetFilePermissionWarnings returns the following warnings based on the file permission:
// - one warning if the file is group-readable
// - one warning if the file is world-readable
Expand Down Expand Up @@ -102,6 +126,17 @@ func GetFilePermissionWarnings(path string) ([]string, error) {
}

func (c Config) Write() error {
switch c.Helper {
case "built-in":
return c.writeNative()
default:
// if rancher config was loaded by external helper
// use the same helper to persist the config
return c.writeWithHelper()
}
}

func (c Config) writeNative() error {
err := os.MkdirAll(filepath.Dir(c.Path), 0700)
if err != nil {
return err
Expand All @@ -118,6 +153,19 @@ func (c Config) Write() error {
return json.NewEncoder(output).Encode(c)
}

func (c Config) writeWithHelper() error {
logrus.Infof("Saving config with helper %s", c.Helper)
jsonConfig, err := json.Marshal(c)
if err != nil {
return err
}
cmd := exec.Command(c.Helper, "store", string(jsonConfig))
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
err = cmd.Run()
return err
}

func (c Config) FocusedServer() (*ServerConfig, error) {
currentServer, found := c.Servers[c.CurrentServer]
if !found || currentServer == nil {
Expand Down
Loading