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
34 changes: 30 additions & 4 deletions internal/cli/inv/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"

"github.com/ntwrknrd/nssh/internal/config"
"github.com/ntwrknrd/nssh/internal/connect"
"github.com/ntwrknrd/nssh/internal/inventory"
"github.com/ntwrknrd/nssh/internal/ui"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -47,16 +48,41 @@ func runGet(hostName string) error {
}
meta := metadataForHost(host, cfg, config.DefaultPaths(), index)
auth := effectiveInventoryAuth(cfg, host.Host, meta.Group)
nsshRows := []inventoryDisplayRow{
{Label: "Provider", Value: meta.Owner},
{Label: "Group", Value: valueOrDash(meta.Group)},
}
shadowRows, err := shadowedProviderRows(cfg, config.DefaultPaths(), host.Host)
if err != nil {
return err
}
nsshRows = append(nsshRows, shadowRows...)
printInventoryDisplaySections(inventoryDisplaySections(
inventoryHostSSHDisplayRows(host.Host, host.HostName, valueOrDash(host.User()), host.Port()),
append([]inventoryDisplayRow{
{Label: "Provider", Value: meta.Owner},
{Label: "Group", Value: valueOrDash(meta.Group)},
}, inventoryAuthDisplayRows(auth)...),
append(nsshRows, inventoryAuthDisplayRows(auth)...),
))
return nil
}

// shadowedProviderRows names provider objects that this local host replaces.
func shadowedProviderRows(cfg *config.Config, paths *config.Paths, host string) ([]inventoryDisplayRow, error) {
states, err := inventoryProviderStates(paths)
if err != nil {
return nil, err
}
var rows []inventoryDisplayRow
for _, finding := range connect.ProviderHostFindings(cfg, states) {
if finding.ShadowedBy != host {
continue
}
rows = append(rows, inventoryDisplayRow{
Label: "Shadows",
Value: fmt.Sprintf("%s %s (move local settings to inventory.providers.%s.hosts)", finding.Provider, finding.Host, finding.Provider),
})
}
return rows, nil
}

func valueOrDash(v string) string {
if v == "" {
return "-"
Expand Down
160 changes: 125 additions & 35 deletions internal/cli/inv/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,24 @@ func upsertLocalHostYAML(cfg *config.Config, paths *config.Paths, patch hostPatc
host.SSH.Options["ProxyJump"] = config.NewSSHOptionString(strings.TrimSpace(patch.ProxyJump))
}
host.SSH = config.MergeSSH(host.SSH, config.SSHHostConfig{Options: patch.Identity})
host.Aliases = mergeHostAliases(hostName, host.Aliases, autoLocalHostAliases(hostName), patch.Aliases)
claimed, err := inventoryNameOwners(cfg, paths, hostName)
if err != nil {
return err
}
for _, alias := range patch.Aliases {
if owner := claimed[strings.TrimSpace(alias)]; owner != "" {
return fmt.Errorf("alias %q already identifies host %q", strings.TrimSpace(alias), owner)
}
}
autoAliases := make([]string, 0, 1)
for _, alias := range autoLocalHostAliases(hostName) {
if owner := claimed[alias]; owner != "" {
ui.Warning("Skipped alias %q: it already identifies %s", alias, owner)
continue
}
autoAliases = append(autoAliases, alias)
}
host.Aliases = mergeHostAliases(hostName, host.Aliases, autoAliases, patch.Aliases)
if patch.PortSet {
host.Port = patch.Port
}
Expand Down Expand Up @@ -247,6 +264,29 @@ func autoLocalHostAliases(host string) []string {
return []string{short}
}

// inventoryNameOwners maps every name that resolves to another inventory host
// (its name, aliases, and short name) to that host.
func inventoryNameOwners(cfg *config.Config, paths *config.Paths, exclude string) (map[string]string, error) {
hosts, err := inventoryHosts(cfg, paths)
if err != nil {
return nil, err
}
owners := make(map[string]string)
for _, entry := range hosts {
if entry == nil || entry.Host == exclude {
continue
}
names := append([]string{entry.Host}, entry.Patterns...)
names = append(names, autoLocalHostAliases(entry.Host)...)
for _, name := range names {
if name = strings.TrimSpace(name); name != "" && name != exclude && owners[name] == "" {
owners[name] = entry.Host
}
}
}
return owners, nil
}

func mergeHostAliases(host string, aliasSets ...[]string) []string {
host = strings.TrimSpace(host)
aliases := make([]string, 0)
Expand Down Expand Up @@ -1333,34 +1373,70 @@ func inventoryHosts(cfg *config.Config, paths *config.Paths) ([]*sshconfig.HostE
if err := cfg.Inventory.Validate(); err != nil {
return nil, err
}
states, err := inventoryProviderStates(paths)
if err != nil {
return nil, err
}
// Provider YAML hosts overlay their provider objects; list them on their
// own only when no provider object matches, so each host appears once.
orphaned := make(map[string]map[string]bool)
for _, finding := range connect.ProviderHostFindings(cfg, states) {
if finding.Orphaned {
if orphaned[finding.Provider] == nil {
orphaned[finding.Provider] = make(map[string]bool)
}
orphaned[finding.Provider][finding.Host] = true
}
}
stateProviders := make(map[string]bool, len(states))
for _, state := range states {
stateProviders[state.Provider] = true
}
for providerName, provider := range cfg.Inventory.Providers {
for name, hostCfg := range provider.Hosts {
if provider.Type != config.ProviderLocal && stateProviders[providerName] && !orphaned[providerName][name] {
continue
}
hosts = append(hosts, hostEntryFromInventoryHost(cfg, paths, providerName, name, hostCfg))
}
}
if paths.StateDir != "" && samePath(paths.StateDir, config.DefaultPaths().StateDir) {
stateNames, err := inventory.ListProviderStates()
for _, state := range states {
for _, host := range state.Objects {
if entry := hostEntryFromProviderState(cfg, paths, state, host); entry != nil {
hosts = append(hosts, entry)
}
}
}
sort.SliceStable(hosts, func(i, j int) bool {
if hosts[i].Host != hosts[j].Host {
return hosts[i].Host < hosts[j].Host
}
return hosts[i].SourceFile < hosts[j].SourceFile
})
return hosts, nil
}

// inventoryProviderStates loads cached provider objects for the default state
// directory; other state directories (tests) have no provider cache.
func inventoryProviderStates(paths *config.Paths) ([]*inventory.ProviderState, error) {
if paths.StateDir == "" || !samePath(paths.StateDir, config.DefaultPaths().StateDir) {
return nil, nil
}
names, err := inventory.ListProviderStates()
if err != nil {
return nil, err
}
states := make([]*inventory.ProviderState, 0, len(names))
for _, name := range names {
state, err := inventory.LoadProviderState(name)
if err != nil {
return nil, err
}
for _, providerName := range stateNames {
state, err := inventory.LoadProviderState(providerName)
if err != nil {
return nil, err
}
if state == nil {
continue
}
for _, host := range state.Objects {
entry := hostEntryFromProviderState(paths, state.Provider, host)
if entry != nil {
hosts = append(hosts, entry)
}
}
if state != nil {
states = append(states, state)
}
}
sort.Slice(hosts, func(i, j int) bool { return hosts[i].Host < hosts[j].Host })
return hosts, nil
return states, nil
}

func inventoryProxyHostNames(cfg *config.Config, paths *config.Paths, exclude string) ([]string, error) {
Expand Down Expand Up @@ -1394,7 +1470,15 @@ func findInventoryHost(cfg *config.Config, paths *config.Paths, pattern string)
if err != nil {
return nil, err
}
return sshconfig.FindHostByPattern(hosts, pattern), nil
host := sshconfig.FindHostByPattern(hosts, pattern)
// A host defined in local inventory is local-owned even when a provider
// cache lists the same name; otherwise edits would land in the wrong file.
if host != nil && cfg != nil {
if hostCfg, ok := cfg.Inventory.Providers[config.ProviderLocal].Hosts[host.Host]; ok {
return hostEntryFromInventoryHost(cfg, paths, config.ProviderLocal, host.Host, hostCfg), nil
}
}
return host, nil
}

func hostEntryFromInventoryHost(cfg *config.Config, paths *config.Paths, providerName, name string, hostCfg config.InventoryHostConfig) *sshconfig.HostEntry {
Expand Down Expand Up @@ -1430,24 +1514,20 @@ func inventoryHostSSHHostName(name string, hostCfg config.InventoryHostConfig) s
return strings.TrimSpace(name)
}

func hostEntryFromProviderState(paths *config.Paths, providerName string, host *inventory.ProviderHost) *sshconfig.HostEntry {
if host == nil {
func hostEntryFromProviderState(cfg *config.Config, paths *config.Paths, state *inventory.ProviderState, host *inventory.ProviderHost) *sshconfig.HostEntry {
data := connect.ResolveProviderStateHost(cfg, state, host)
if data == nil {
return nil
}
name := host.HostName
if name == "" {
name = host.Host
}
port := host.Port
port := data.Port
if port == 0 {
port = 22
}
entry := sshconfig.CreateHostEntry(name, host.HostName, host.Username, port, host.AuthMode == config.AuthModePassword, localFilePath(paths, inventory.ProviderIncludeFile(providerName)))
if len(host.Patterns) > 0 {
entry.Patterns = slices.Clone(host.Patterns)
entry.Host = entry.Patterns[0]
}
inventory.SetLocalHostGroup(entry, host.Group)
groupID := inventoryHostGroupID(state.Provider, data.Group)
auth := cfg.ResolveInventoryAuth(config.InventoryAuthContext{Host: data.Hostname, Provider: state.Provider, Group: groupID})
entry := sshconfig.CreateHostEntry(data.Canonical, data.Hostname, data.Username, port, auth.AuthMode == config.AuthModePassword, localFilePath(paths, inventory.ProviderIncludeFile(state.Provider)))
entry.Patterns = mergeHostAliases("", []string{data.Canonical}, data.Aliases)
inventory.SetLocalHostGroup(entry, groupID)
return entry
}

Expand Down Expand Up @@ -1491,7 +1571,17 @@ func saveLocalProviderInventory(cfg *config.Config, paths *config.Paths) error {
if err := os.MkdirAll(filepath.Dir(target), 0700); err != nil {
return fmt.Errorf("create inventory dir: %w", err)
}
return os.WriteFile(target, []byte(text), 0600)
if err := os.WriteFile(target, []byte(text), 0600); err != nil {
return err
}
return config.PruneRootInventoryProviderShadow(localRootConfigPath(paths), cfg, config.ProviderLocal)
}

func localRootConfigPath(paths *config.Paths) string {
if paths == nil {
paths = config.DefaultPaths()
}
return paths.ConfigFile
}

func saveLocalProviderHostInventory(cfg *config.Config, paths *config.Paths, hostName string) error {
Expand All @@ -1509,7 +1599,7 @@ func saveLocalProviderHostInventory(cfg *config.Config, paths *config.Paths, hos
return err
}
if patched {
return nil
return config.PruneRootInventoryProviderShadow(localRootConfigPath(paths), cfg, config.ProviderLocal, "hosts", hostName)
}
return saveLocalProviderInventory(cfg, paths)
}
Expand Down
Loading
Loading