Skip to content

Commit

Permalink
feat: hints (#196)
Browse files Browse the repository at this point in the history
Hints can be used to "hint" options into endpoints discovered though
other means, for instance, zeroconf.

Wishlist will match the `hint.match` (as a glob) against the discovered
endpoints' names, and will apply the given settings to said endpoints.

It'll only apply to discovered endpoints, and will not affect other
endpoints.

This can only be set via YAML configuration.

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
  • Loading branch information
caarlos0 authored Jun 30, 2023
1 parent 08bf3b7 commit e707484
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 5 deletions.
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,21 +201,31 @@ Run `wishlist --help` to see all the options.

### SRV records

Wishlist can also find nodes from DNS SRV records, on one or more domains.
Wishlist can also find nodes from DNS `SRV` records, on one or more domains.

Run `wishlist --srv.domain {your domain}` to get started. You can repeat the
flag for multiple domains.

By default, Wishlist will set the name of the endpoint to the SRV target.
You can, however, customize that with a TXT record in the following format:
By default, Wishlist will set the name of the endpoint to the `SRV` target.
You can, however, customize that with a `TXT` record in the following format:

```
wishlist.name full.address:22=thename
```

So, in this case, a SRV record pointing to `full.address` on port `22` will get
So, in this case, a `SRV` record pointing to `full.address` on port `22` will get
the name `thename`.

Run `wishlist --help` to see all the options.

### Hints

You can use the `hints` key in the YAML configuration file to hint settings into
discovered endpoints.

Check the [example configuration file](/_example/config.yaml) to learn
what options are available.

### Using the binary

```sh
Expand Down
62 changes: 62 additions & 0 deletions _example/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,68 @@ endpoints:
- LANG
- SOME_ENV

# Hints can be used to hint settings into discovered endpoints.
#
# You can use it to change the user, port, set remote commands, etc.
#
# A host can match multiple hints, in which all matched hints will be applied,
# the last one having the priority.
hints:
- #
# Glob to be used to match the discovered names.
match: "*.local"

# SSH port to use.
port: 23234

# URL to be printed in the list.
link:
name: Optional link name
url: https://github.com/charmbracelet/wishlist

# Descripton of the item.
description: "A description of this endpoint.\nCan have multiple lines."

# User to use to connect.
# Defaults to the current remote user.
user: notme

# Command to run against the remote address.
# Defaults to asking for a shell.
remote_command: uptime -a

# Whether to forward the SSH agent.
# Will cause the connection to fail if no agent is available.
forward_agent: true # forwards the ssh agent

# IdentityFiles to try to use to authorize.
# Only used in local mode.
identity_files:
- ~/.ssh/id_rsa
- ~/.ssh/id_ed25519

# Requests a TTY.
# Defaults to true if remote_command is empty.
request_tty: true

# Connection timeout.
connect_timeout: 10s

# Set environment variables into the connection.
# Analogous to SSH's SetEnv.
set_env:
- FOO=bar
- BAR=baz

# Environments from the environment that match these keys will also be set
# into the connection.
# Analogous to SSH's SendEnv.
# Defaults to ["LC_*", "LANG"].
send_env:
- LC_*
- LANG
- SOME_ENV

# Users to allow access to the list
users:
- # User login
Expand Down
51 changes: 50 additions & 1 deletion cmd/wishlist/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package main
import (
"errors"
"fmt"
"net"
"os"
"path/filepath"
"reflect"
"runtime/debug"
"sort"
"strings"
Expand All @@ -21,6 +23,7 @@ import (
"github.com/charmbracelet/wishlist/srv"
"github.com/charmbracelet/wishlist/sshconfig"
"github.com/charmbracelet/wishlist/zeroconf"
"github.com/gobwas/glob"
"github.com/hashicorp/go-multierror"
mcobra "github.com/muesli/mango-cobra"
"github.com/muesli/roff"
Expand Down Expand Up @@ -217,6 +220,52 @@ func userConfigPaths() []string {
return append(paths, "/etc/ssh/ssh_config")
}

func applyHints(seed []*wishlist.Endpoint, hints []wishlist.EndpointHint) []*wishlist.Endpoint {
for _, hint := range hints {
glob, err := glob.Compile(hint.Match)
if err != nil {
log.Error("invalid hint match", "match", hint.Match, "error", err)
continue
}
for i, end := range seed {
if !glob.Match(end.Name) {
continue
}
if hint.Port != "" {
host, _, _ := net.SplitHostPort(end.Address)
end.Address = net.JoinHostPort(host, hint.Port)
}
if s := hint.User; s != "" {
end.User = s
}
if s := hint.ForwardAgent; s != nil {
end.ForwardAgent = *s
}
if s := hint.RequestTTY; s != nil {
end.RequestTTY = *s
}
if s := hint.RemoteCommand; s != "" {
end.RemoteCommand = s
}
if s := hint.Desc; s != "" {
end.Desc = s
}
if s := hint.Link; !reflect.DeepEqual(s, wishlist.Link{}) {
end.Link = s
}
end.SendEnv = append(end.SendEnv, hint.SendEnv...)
end.SetEnv = append(end.SetEnv, hint.SetEnv...)
end.PreferredAuthentications = append(end.PreferredAuthentications, hint.PreferredAuthentications...)
end.IdentityFiles = append(end.IdentityFiles, hint.IdentityFiles...)
if s := hint.Timeout; s != 0 {
end.Timeout = s
}
seed[i] = end
}
}
return seed
}

func getConfig(configFile string, seed []*wishlist.Endpoint) (wishlist.Config, string, error) {
var allErrs error
for _, path := range append([]string{configFile}, userConfigPaths()...) {
Expand Down Expand Up @@ -283,7 +332,7 @@ func getYAMLConfig(path string, seed []*wishlist.Endpoint) (wishlist.Config, err
return config, fmt.Errorf("failed to parse config: %w", err)
}

config.Endpoints = append(config.Endpoints, seed...)
config.Endpoints = append(config.Endpoints, applyHints(seed, config.Hints)...)
return config, nil
}

Expand Down
61 changes: 61 additions & 0 deletions cmd/wishlist/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,64 @@ func TestUserConfigPaths(t *testing.T) {
}, paths)
})
}

func TestApplyHints(t *testing.T) {
boolPtr := func(b bool) *bool {
return &b
}
result := applyHints([]*wishlist.Endpoint{
{
Name: "foo.bar.local",
Address: "foo.bar.local:22",
},
}, []wishlist.EndpointHint{
{
Match: "match nothing",
User: "nope",
},
{
Match: "*.local",
Port: "2345",
User: "carlos",
ForwardAgent: boolPtr(true),
RequestTTY: boolPtr(true),
RemoteCommand: "tmux a",
Desc: "The descriptions",
Link: wishlist.Link{
Name: "foo.bar",
URL: "https://github.com/charmbracelet/wishlist",
},
SendEnv: []string{"FOO_*"},
SetEnv: []string{"FOO_TEST=bar"},
PreferredAuthentications: []string{"publickey"},
IdentityFiles: []string{"~/.ssh/charm_id_ed25519"},
Timeout: time.Minute,
},
{
Match: "invalid.*******$$$\a\a\a",
},
{
Match: "foo.*.local",
Port: "22234",
},
})
require.Len(t, result, 1)
require.Equal(t, wishlist.Endpoint{
Name: "foo.bar.local",
Address: "foo.bar.local:22234",
User: "carlos",
ForwardAgent: true,
RequestTTY: true,
RemoteCommand: "tmux a",
Desc: "The descriptions",
Link: wishlist.Link{
Name: "foo.bar",
URL: "https://github.com/charmbracelet/wishlist",
},
SendEnv: []string{"FOO_*"},
SetEnv: []string{"FOO_TEST=bar"},
PreferredAuthentications: []string{"publickey"},
IdentityFiles: []string{"~/.ssh/charm_id_ed25519"},
Timeout: time.Minute,
}, *result[0])
}
19 changes: 19 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,24 @@ type Endpoint struct {
Middlewares []wish.Middleware `yaml:"-"` // wish middlewares you can use in the factory method.
}

// EndpointHint can be used to match a discovered endpoint (through zeroconf
// for example) and set additional options into it.
type EndpointHint struct {
Match string `yaml:"match"`
Port string `yaml:"port"`
User string `yaml:"user"`
ForwardAgent *bool `yaml:"forward_agent"`
RequestTTY *bool `yaml:"request_tty"`
RemoteCommand string `yaml:"remote_command"`
Desc string `yaml:"description"`
Link Link `yaml:"link"`
SendEnv []string `yaml:"send_env"`
SetEnv []string `yaml:"set_env"`
PreferredAuthentications []string `yaml:"preferred_authentications"`
IdentityFiles []string `yaml:"identity_files"`
Timeout time.Duration `yaml:"connect_timeout"`
}

// Authentications returns either the client preferred authentications or the
// default publickey,keyboard-interactive
func (e Endpoint) Authentications() []string {
Expand Down Expand Up @@ -128,6 +146,7 @@ type Config struct {
Listen string `yaml:"listen"` // Address to listen on.
Port int64 `yaml:"port"` // Port to start the first server on.
Endpoints []*Endpoint `yaml:"endpoints"` // Endpoints to list.
Hints []EndpointHint `yaml:"hints"` // Endpoints hints to apply to discovered hosts.
Factory func(Endpoint) (*ssh.Server, error) `yaml:"-"` // Factory used to create the SSH server for the given endpoint.
Users []User `yaml:"users"` // Users allowed to access the list.
Metrics Metrics `yaml:"metrics"` // Metrics configuration.
Expand Down

0 comments on commit e707484

Please sign in to comment.