Files
warmbly/internal/cli/config/resolve.go
T

150 lines
4.3 KiB
Go

package config
import (
"fmt"
"net/url"
"os"
"strings"
)
// Resolved is the answer to "who am I and where am I pointed", worked out once
// per invocation from flags, environment and the two files, in that order.
type Resolved struct {
Host string
APIURL string
Token string
// Source says where the token came from, because "it worked yesterday" is
// almost always an environment variable nobody remembers exporting.
Source string
Entry *Host
}
// ErrNoToken is returned when nothing is signed in. Callers turn it into the
// one message worth printing: how to sign in.
type ErrNoToken struct{ Host string }
func (e *ErrNoToken) Error() string {
return fmt.Sprintf("not signed in to %s.\nRun `warmbly auth login` to sign in, or set WARMBLY_TOKEN to an API key (wmbly_...).", e.Host)
}
// Resolve works out the active host and token. hostFlag is --host, empty when
// not given.
func Resolve(cfg *Config, hosts Hosts, hostFlag string) (*Resolved, error) {
host := strings.TrimSpace(hostFlag)
if host == "" {
host = strings.TrimSpace(os.Getenv(HostEnv))
}
if host == "" {
host = strings.TrimSpace(cfg.ActiveHost)
}
if host == "" {
// One signed-in host and no preference is not ambiguous.
if names := hosts.Names(); len(names) == 1 {
host = names[0]
}
}
if host == "" {
host = DefaultHost
}
host = NormalizeHost(host)
r := &Resolved{Host: host, Entry: hosts[host]}
if r.Entry != nil {
r.APIURL = r.Entry.APIURL
r.Token = r.Entry.Token
r.Source = HostsPath()
}
// The environment wins, and says so, so a surprising result is traceable.
if v := strings.TrimSpace(os.Getenv(TokenEnv)); v != "" {
r.Token, r.Source = v, TokenEnv
} else if v := strings.TrimSpace(os.Getenv(APIKeyEnv)); v != "" {
r.Token, r.Source = v, APIKeyEnv
}
if v := strings.TrimSpace(os.Getenv(APIURLEnv)); v != "" {
r.APIURL = strings.TrimRight(v, "/")
}
if r.APIURL == "" {
r.APIURL = DefaultAPIURL(host)
}
if r.Token == "" {
return r, &ErrNoToken{Host: host}
}
return r, nil
}
// NormalizeHost turns whatever someone typed into the key hosts.yml uses: a
// bare host, no scheme, no path, no trailing slash.
func NormalizeHost(raw string) string {
h := strings.TrimSpace(strings.ToLower(raw))
h = strings.TrimSuffix(h, "/")
if h == "" {
return DefaultHost
}
if strings.Contains(h, "://") {
if u, err := url.Parse(h); err == nil && u.Host != "" {
h = u.Host
}
}
// A bare "app.warmbly.com" or "api.warmbly.com" means the same account as
// "warmbly.com"; storing three entries for one instance helps nobody. The
// dot count keeps a real two-label host like "api.dev" intact.
for _, prefix := range []string{"app.", "api.", "www."} {
if strings.HasPrefix(h, prefix) && strings.Count(h, ".") > 1 {
return strings.TrimPrefix(h, prefix)
}
}
return h
}
// isLoopback reports whether a host is this machine. That is the only case
// where plaintext HTTP is acceptable, because every request carries a bearer
// token: a remote host that merely names a port must still get https.
func isLoopback(host string) bool {
name, _, found := strings.Cut(host, ":")
if !found {
name = host
}
return name == "localhost" || name == "127.0.0.1" || name == "[::1]" || name == "::1"
}
// DefaultAPIURL is the base URL to try first for a host. The hosted service is
// known; a self-hosted host follows the layout the installer writes.
func DefaultAPIURL(host string) string {
host = NormalizeHost(host)
if host == DefaultHost {
return "https://api." + DefaultHost
}
if isLoopback(host) {
return "http://" + host
}
// A host that names a port is already an exact address, so no subdomain is
// invented for it; the scheme stays https.
if strings.Contains(host, ":") {
return "https://" + host
}
return "https://api." + host
}
// CandidateAPIURLs are the bases `auth login` probes for a self-hosted host,
// most likely first. The installer's three shapes produce the first three.
func CandidateAPIURLs(host string) []string {
host = NormalizeHost(host)
if strings.Contains(host, "://") {
return []string{strings.TrimRight(host, "/")}
}
scheme := "https"
if isLoopback(host) {
scheme = "http"
}
// A host with a port is already an exact address; do not invent subdomains.
if strings.Contains(host, ":") {
return []string{scheme + "://" + host}
}
return []string{
scheme + "://api." + host,
scheme + "://" + host,
scheme + "://" + host + "/api",
}
}