Skip to content
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
147 changes: 147 additions & 0 deletions cmd/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package configcmd

import (
"encoding/json"
"fmt"
"strings"

"github.com/fosrl/cli/internal/config"
"github.com/spf13/cobra"
)

func ConfigCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "config",
Short: "View and edit CLI configuration",
Long: `View and edit persistent CLI configuration without manually editing the config file.`,
}

cmd.AddCommand(configPathCmd())
cmd.AddCommand(configShowCmd())
cmd.AddCommand(configListCmd())
cmd.AddCommand(configGetCmd())
cmd.AddCommand(configSetCmd())

return cmd
}

func configPathCmd() *cobra.Command {
return &cobra.Command{
Use: "path",
Short: "Print the config file path",
RunE: func(cmd *cobra.Command, args []string) error {
path, err := config.ConfigFilePath()
if err != nil {
return err
}
fmt.Println(path)
return nil
},
}
}

func configShowCmd() *cobra.Command {
return &cobra.Command{
Use: "show",
Short: "Print the current config contents",
RunE: func(cmd *cobra.Command, args []string) error {
return dumpConfig(config.ConfigFromContext(cmd.Context()))
},
}
}

func configListCmd() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List settable config keys",
RunE: func(cmd *cobra.Command, args []string) error {
for _, key := range config.ConfigOptions {
fmt.Fprintln(cmd.OutOrStdout(), key)
}
return nil
},
}
}

func configGetCmd() *cobra.Command {
return &cobra.Command{
Use: "get <key>",
Short: "Get a config value",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cfg := config.ConfigFromContext(cmd.Context())

value, err := cfg.GetKey(args[0])
if err != nil {
return err
}
fmt.Println(value)
return nil
},
}
}

func configSetCmd() *cobra.Command {
return &cobra.Command{
Use: "set <key> <value>",
Short: "Set a config value",
Long: `Set a config value and write it to the config file.

Supported keys:
` + strings.Join(config.SupportedConfigKeys(), "\n ") + `

Examples:
pangolin config set up.tunnel_dns true
pangolin config set up.upstream_dns 10.0.0.53
pangolin config set up.upstream_dns 10.0.0.53,10.0.0.54
`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cfg := config.ConfigFromContext(cmd.Context())

if err := cfg.SetKey(args[0], args[1]); err != nil {
return err
}
if err := cfg.Save(); err != nil {
return err
}

value, err := cfg.GetKey(args[0])
if err != nil {
return err
}
fmt.Printf("%s = %s\n", args[0], value)
return nil
},
}
}

func dumpConfig(cfg *config.Config) error {
out := map[string]any{
"log_level": cfg.LogLevel,
"log_file": cfg.LogFile,
"disable_update_check": cfg.DisableUpdateCheck,
"disable_companion_mode": cfg.DisableCompanionMode,
}

up := map[string]any{}
if cfg.IsSet("up.tunnel_dns") {
up["tunnel_dns"] = cfg.GetBool("up.tunnel_dns")
}
if cfg.IsSet("up.override_dns") {
up["override_dns"] = cfg.GetBool("up.override_dns")
}
if cfg.IsSet("up.upstream_dns") {
up["upstream_dns"] = cfg.GetStringSlice("up.upstream_dns")
}
if len(up) > 0 {
out["up"] = up
}

data, err := json.MarshalIndent(out, "", " ")
if err != nil {
return err
}
fmt.Println(string(data))
return nil
}
33 changes: 25 additions & 8 deletions cmd/list/aliases.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const aliasesPageSize = 1000
type aliasesFetchOptions struct {
includeLabels bool
labelFilter []string
status string
}

func aliasesCmd() *cobra.Command {
Expand All @@ -43,6 +44,7 @@ func aliasesCmd() *cobra.Command {
requested := aliasesFetchOptions{
includeLabels: withLabels,
labelFilter: labelFilter,
status: "approved",
}

data, err := fetchAllAliases(apiClient, orgID, requested)
Expand Down Expand Up @@ -71,10 +73,7 @@ func fetchAllAliases(apiClient *api.Client, orgID string, requested aliasesFetch

var combined api.ListUserResourceAliasesData
for page := 1; ; page++ {
pageData, err := apiClient.ListUserResourceAliases(orgID, page, aliasesPageSize, api.ListUserResourceAliasesOptions{
IncludeLabels: effective.includeLabels,
LabelFilter: effective.labelFilter,
})
pageData, err := apiClient.ListUserResourceAliases(orgID, page, aliasesPageSize, listAliasesAPIOptions(effective))
if err != nil {
return nil, err
}
Expand All @@ -99,6 +98,18 @@ func fetchAllAliases(apiClient *api.Client, orgID string, requested aliasesFetch
}

func resolveAliasesFetchOptions(apiClient *api.Client, orgID string, requested aliasesFetchOptions) (aliasesFetchOptions, bool, error) {
effective, clientSideFilter, err := resolveAliasesFetchOptionsWithStatus(apiClient, orgID, requested)
if err == nil || !isBadRequest(err) || requested.status == "" {
return effective, clientSideFilter, err
}

// Older servers reject unknown query params such as status; retry without it.
withoutStatus := requested
withoutStatus.status = ""
return resolveAliasesFetchOptionsWithStatus(apiClient, orgID, withoutStatus)
}

func resolveAliasesFetchOptionsWithStatus(apiClient *api.Client, orgID string, requested aliasesFetchOptions) (aliasesFetchOptions, bool, error) {
effective := requested

if err := probeAliasesPage(apiClient, orgID, effective); err == nil {
Expand All @@ -111,6 +122,7 @@ func resolveAliasesFetchOptions(apiClient *api.Client, orgID string, requested a
effective = aliasesFetchOptions{
includeLabels: true,
labelFilter: nil,
status: requested.status,
}
if err := probeAliasesPage(apiClient, orgID, effective); err == nil {
return effective, true, nil
Expand All @@ -119,19 +131,24 @@ func resolveAliasesFetchOptions(apiClient *api.Client, orgID string, requested a
}
}

effective = aliasesFetchOptions{}
effective = aliasesFetchOptions{status: requested.status}
if err := probeAliasesPage(apiClient, orgID, effective); err != nil {
return effective, false, err
}

return effective, false, nil
}

func probeAliasesPage(apiClient *api.Client, orgID string, opts aliasesFetchOptions) error {
_, err := apiClient.ListUserResourceAliases(orgID, 1, 1, api.ListUserResourceAliasesOptions{
func listAliasesAPIOptions(opts aliasesFetchOptions) api.ListUserResourceAliasesOptions {
return api.ListUserResourceAliasesOptions{
IncludeLabels: opts.includeLabels,
LabelFilter: opts.labelFilter,
})
Status: opts.status,
}
}

func probeAliasesPage(apiClient *api.Client, orgID string, opts aliasesFetchOptions) error {
_, err := apiClient.ListUserResourceAliases(orgID, 1, 1, listAliasesAPIOptions(opts))
return err
}

Expand Down
4 changes: 3 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/fosrl/cli/cmd/auth/logout"
"github.com/fosrl/cli/cmd/authdaemon"
companioncmd "github.com/fosrl/cli/cmd/companion"
configcmd "github.com/fosrl/cli/cmd/config"
"github.com/fosrl/cli/cmd/down"
"github.com/fosrl/cli/cmd/list"
"github.com/fosrl/cli/cmd/logs"
Expand Down Expand Up @@ -60,6 +61,7 @@ func RootCommand(initResources bool) (*cobra.Command, error) {
}
cmd.AddCommand(selectcmd.SelectCmd())
cmd.AddCommand(list.ListCmd())
cmd.AddCommand(configcmd.ConfigCmd())

// Platform-specific commands - nil on unsupported platforms
if upCmd := up.UpCmd(); upCmd != nil {
Expand Down Expand Up @@ -112,7 +114,7 @@ func RootCommand(initResources bool) (*cobra.Command, error) {

func commandNeedsAuthInit(cmd *cobra.Command) bool {
for c := cmd; c != nil; c = c.Parent() {
if c.Name() == "companion" {
if c.Name() == "companion" || c.Name() == "config" {
return false
}
}
Expand Down
22 changes: 21 additions & 1 deletion cmd/up/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func ClientUpCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "client",
Short: "Start a client connection",
Long: "Bring up a client tunneled connection",
Long: `Bring up a client tunneled connection.`,
PreRunE: func(cmd *cobra.Command, args []string) error {
// `--id` and `--secret` must be specified together
if (opts.ID == "") != (opts.Secret == "") {
Expand All @@ -93,6 +93,9 @@ func ClientUpCmd() *cobra.Command {
return errors.New("--silent and --attached options conflict")
}

cfg := config.ConfigFromContext(cmd.Context())
applyUpDefaults(cmd, &opts, cfg)

if err := validateDNSIP(opts.DNS, "netstack-dns"); err != nil {
return err
}
Expand Down Expand Up @@ -136,6 +139,23 @@ func ClientUpCmd() *cobra.Command {
return cmd
}

// Precedence: flags > env/config > built-in defaults.
func applyUpDefaults(cmd *cobra.Command, opts *ClientUpCmdOpts, cfg *config.Config) {
if cfg == nil {
return
}

if !cmd.Flags().Changed("tunnel-dns") && cfg.IsSet("up.tunnel_dns") {
opts.TunnelDNS = cfg.GetBool("up.tunnel_dns")
}
if !cmd.Flags().Changed("override-dns") && cfg.IsSet("up.override_dns") {
opts.OverrideDNS = cfg.GetBool("up.override_dns")
}
if !cmd.Flags().Changed("upstream-dns") && cfg.IsSet("up.upstream_dns") {
opts.UpstreamDNS = cfg.GetStringSlice("up.upstream_dns")
}
}

func clientUpMain(cmd *cobra.Command, opts *ClientUpCmdOpts, extraArgs []string) error {
apiClient := api.FromContext(cmd.Context())
accountStore := config.AccountStoreFromContext(cmd.Context())
Expand Down
2 changes: 1 addition & 1 deletion cmd/up/up_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ If ran with no subcommand, 'client' is passed.
cmd.AddCommand(client.ClientUpCmd())

return cmd
}
}
1 change: 1 addition & 0 deletions docs/pangolin.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Pangolin CLI

* [pangolin apply](pangolin_apply.md) - Apply commands
* [pangolin auth](pangolin_auth.md) - Authentication commands
* [pangolin config](pangolin_config.md) - View and edit CLI configuration
* [pangolin down](pangolin_down.md) - Stop a connection
* [pangolin list](pangolin_list.md) - List resources and other items from the server
* [pangolin login](pangolin_login.md) - Login to Pangolin
Expand Down
23 changes: 23 additions & 0 deletions docs/pangolin_config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## pangolin config

View and edit CLI configuration

### Synopsis

View and edit persistent CLI configuration without manually editing the config file.

### Options

```
-h, --help help for config
```

### SEE ALSO

* [pangolin](pangolin.md) - Pangolin CLI
* [pangolin config get](pangolin_config_get.md) - Get a config value
* [pangolin config list](pangolin_config_list.md) - List settable config keys
* [pangolin config path](pangolin_config_path.md) - Print the config file path
* [pangolin config set](pangolin_config_set.md) - Set a config value
* [pangolin config show](pangolin_config_show.md) - Print the current config contents

18 changes: 18 additions & 0 deletions docs/pangolin_config_get.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
## pangolin config get

Get a config value

```
pangolin config get <key> [flags]
```

### Options

```
-h, --help help for get
```

### SEE ALSO

* [pangolin config](pangolin_config.md) - View and edit CLI configuration

18 changes: 18 additions & 0 deletions docs/pangolin_config_list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
## pangolin config list

List settable config keys

```
pangolin config list [flags]
```

### Options

```
-h, --help help for list
```

### SEE ALSO

* [pangolin config](pangolin_config.md) - View and edit CLI configuration

18 changes: 18 additions & 0 deletions docs/pangolin_config_path.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
## pangolin config path

Print the config file path

```
pangolin config path [flags]
```

### Options

```
-h, --help help for path
```

### SEE ALSO

* [pangolin config](pangolin_config.md) - View and edit CLI configuration

Loading
Loading