318 lines
7 KiB
Go
318 lines
7 KiB
Go
package config
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/cli/cli/internal/ghinstance"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// This type implements a Config interface and represents a config file on disk.
|
|
type fileConfig struct {
|
|
ConfigMap
|
|
documentRoot *yaml.Node
|
|
}
|
|
|
|
type HostConfig struct {
|
|
ConfigMap
|
|
Host string
|
|
}
|
|
|
|
func (c *fileConfig) Root() *yaml.Node {
|
|
return c.ConfigMap.Root
|
|
}
|
|
|
|
func (c *fileConfig) Get(hostname, key string) (string, error) {
|
|
val, _, err := c.GetWithSource(hostname, key)
|
|
return val, err
|
|
}
|
|
|
|
func (c *fileConfig) GetWithSource(hostname, key string) (string, string, error) {
|
|
if hostname != "" {
|
|
var notFound *NotFoundError
|
|
|
|
hostCfg, err := c.configForHost(hostname)
|
|
if err != nil && !errors.As(err, ¬Found) {
|
|
return "", "", err
|
|
}
|
|
|
|
var hostValue string
|
|
if hostCfg != nil {
|
|
hostValue, err = hostCfg.GetStringValue(key)
|
|
if err != nil && !errors.As(err, ¬Found) {
|
|
return "", "", err
|
|
}
|
|
}
|
|
|
|
if hostValue != "" {
|
|
return hostValue, HostsConfigFile(), nil
|
|
}
|
|
}
|
|
|
|
defaultSource := ConfigFile()
|
|
|
|
value, err := c.GetStringValue(key)
|
|
|
|
var notFound *NotFoundError
|
|
|
|
if err != nil && errors.As(err, ¬Found) {
|
|
return defaultFor(key), defaultSource, nil
|
|
} else if err != nil {
|
|
return "", defaultSource, err
|
|
}
|
|
|
|
if value == "" {
|
|
return defaultFor(key), defaultSource, nil
|
|
}
|
|
|
|
return value, defaultSource, nil
|
|
}
|
|
|
|
func (c *fileConfig) Set(hostname, key, value string) error {
|
|
if hostname == "" {
|
|
return c.SetStringValue(key, value)
|
|
} else {
|
|
hostCfg, err := c.configForHost(hostname)
|
|
var notFound *NotFoundError
|
|
if errors.As(err, ¬Found) {
|
|
hostCfg = c.makeConfigForHost(hostname)
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
return hostCfg.SetStringValue(key, value)
|
|
}
|
|
}
|
|
|
|
func (c *fileConfig) UnsetHost(hostname string) {
|
|
if hostname == "" {
|
|
return
|
|
}
|
|
|
|
hostsEntry, err := c.FindEntry("hosts")
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
cm := ConfigMap{hostsEntry.ValueNode}
|
|
cm.RemoveEntry(hostname)
|
|
}
|
|
|
|
func (c *fileConfig) configForHost(hostname string) (*HostConfig, error) {
|
|
hosts, err := c.hostEntries()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse hosts config: %w", err)
|
|
}
|
|
|
|
for _, hc := range hosts {
|
|
if strings.EqualFold(hc.Host, hostname) {
|
|
return hc, nil
|
|
}
|
|
}
|
|
return nil, &NotFoundError{fmt.Errorf("could not find config entry for %q", hostname)}
|
|
}
|
|
|
|
func (c *fileConfig) CheckWriteable(hostname, key string) error {
|
|
// TODO: check filesystem permissions
|
|
return nil
|
|
}
|
|
|
|
func (c *fileConfig) Write() error {
|
|
mainData := yaml.Node{Kind: yaml.MappingNode}
|
|
hostsData := yaml.Node{Kind: yaml.MappingNode}
|
|
|
|
nodes := c.documentRoot.Content[0].Content
|
|
for i := 0; i < len(nodes)-1; i += 2 {
|
|
if nodes[i].Value == "hosts" {
|
|
hostsData.Content = append(hostsData.Content, nodes[i+1].Content...)
|
|
} else {
|
|
mainData.Content = append(mainData.Content, nodes[i], nodes[i+1])
|
|
}
|
|
}
|
|
|
|
mainBytes, err := yaml.Marshal(&mainData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
filename := ConfigFile()
|
|
err = WriteConfigFile(filename, yamlNormalize(mainBytes))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
hostsBytes, err := yaml.Marshal(&hostsData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return WriteConfigFile(HostsConfigFile(), yamlNormalize(hostsBytes))
|
|
}
|
|
|
|
func (c *fileConfig) Aliases() (*AliasConfig, error) {
|
|
// The complexity here is for dealing with either a missing or empty aliases key. It's something
|
|
// we'll likely want for other config sections at some point.
|
|
entry, err := c.FindEntry("aliases")
|
|
var nfe *NotFoundError
|
|
notFound := errors.As(err, &nfe)
|
|
if err != nil && !notFound {
|
|
return nil, err
|
|
}
|
|
|
|
toInsert := []*yaml.Node{}
|
|
|
|
keyNode := entry.KeyNode
|
|
valueNode := entry.ValueNode
|
|
|
|
if keyNode == nil {
|
|
keyNode = &yaml.Node{
|
|
Kind: yaml.ScalarNode,
|
|
Value: "aliases",
|
|
}
|
|
toInsert = append(toInsert, keyNode)
|
|
}
|
|
|
|
if valueNode == nil || valueNode.Kind != yaml.MappingNode {
|
|
valueNode = &yaml.Node{
|
|
Kind: yaml.MappingNode,
|
|
Value: "",
|
|
}
|
|
toInsert = append(toInsert, valueNode)
|
|
}
|
|
|
|
if len(toInsert) > 0 {
|
|
newContent := []*yaml.Node{}
|
|
if notFound {
|
|
newContent = append(c.Root().Content, keyNode, valueNode)
|
|
} else {
|
|
for i := 0; i < len(c.Root().Content); i++ {
|
|
if i == entry.Index {
|
|
newContent = append(newContent, keyNode, valueNode)
|
|
i++
|
|
} else {
|
|
newContent = append(newContent, c.Root().Content[i])
|
|
}
|
|
}
|
|
}
|
|
c.Root().Content = newContent
|
|
}
|
|
|
|
return &AliasConfig{
|
|
Parent: c,
|
|
ConfigMap: ConfigMap{Root: valueNode},
|
|
}, nil
|
|
}
|
|
|
|
func (c *fileConfig) hostEntries() ([]*HostConfig, error) {
|
|
entry, err := c.FindEntry("hosts")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not find hosts config: %w", err)
|
|
}
|
|
|
|
hostConfigs, err := c.parseHosts(entry.ValueNode)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not parse hosts config: %w", err)
|
|
}
|
|
|
|
return hostConfigs, nil
|
|
}
|
|
|
|
// Hosts returns a list of all known hostnames configured in hosts.yml
|
|
func (c *fileConfig) Hosts() ([]string, error) {
|
|
entries, err := c.hostEntries()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hostnames := []string{}
|
|
for _, entry := range entries {
|
|
hostnames = append(hostnames, entry.Host)
|
|
}
|
|
|
|
sort.SliceStable(hostnames, func(i, j int) bool { return hostnames[i] == ghinstance.Default() })
|
|
|
|
return hostnames, nil
|
|
}
|
|
|
|
func (c *fileConfig) DefaultHost() (string, error) {
|
|
val, _, err := c.DefaultHostWithSource()
|
|
return val, err
|
|
}
|
|
|
|
func (c *fileConfig) DefaultHostWithSource() (string, string, error) {
|
|
hosts, err := c.Hosts()
|
|
if err == nil && len(hosts) == 1 {
|
|
return hosts[0], HostsConfigFile(), nil
|
|
}
|
|
|
|
return ghinstance.Default(), "", nil
|
|
}
|
|
|
|
func (c *fileConfig) makeConfigForHost(hostname string) *HostConfig {
|
|
hostRoot := &yaml.Node{Kind: yaml.MappingNode}
|
|
hostCfg := &HostConfig{
|
|
Host: hostname,
|
|
ConfigMap: ConfigMap{Root: hostRoot},
|
|
}
|
|
|
|
var notFound *NotFoundError
|
|
hostsEntry, err := c.FindEntry("hosts")
|
|
if errors.As(err, ¬Found) {
|
|
hostsEntry.KeyNode = &yaml.Node{
|
|
Kind: yaml.ScalarNode,
|
|
Value: "hosts",
|
|
}
|
|
hostsEntry.ValueNode = &yaml.Node{Kind: yaml.MappingNode}
|
|
root := c.Root()
|
|
root.Content = append(root.Content, hostsEntry.KeyNode, hostsEntry.ValueNode)
|
|
} else if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
hostsEntry.ValueNode.Content = append(hostsEntry.ValueNode.Content,
|
|
&yaml.Node{
|
|
Kind: yaml.ScalarNode,
|
|
Value: hostname,
|
|
}, hostRoot)
|
|
|
|
return hostCfg
|
|
}
|
|
|
|
func (c *fileConfig) parseHosts(hostsEntry *yaml.Node) ([]*HostConfig, error) {
|
|
hostConfigs := []*HostConfig{}
|
|
|
|
for i := 0; i < len(hostsEntry.Content)-1; i = i + 2 {
|
|
hostname := hostsEntry.Content[i].Value
|
|
hostRoot := hostsEntry.Content[i+1]
|
|
hostConfig := HostConfig{
|
|
ConfigMap: ConfigMap{Root: hostRoot},
|
|
Host: hostname,
|
|
}
|
|
hostConfigs = append(hostConfigs, &hostConfig)
|
|
}
|
|
|
|
if len(hostConfigs) == 0 {
|
|
return nil, errors.New("could not find any host configurations")
|
|
}
|
|
|
|
return hostConfigs, nil
|
|
}
|
|
|
|
func yamlNormalize(b []byte) []byte {
|
|
if bytes.Equal(b, []byte("{}\n")) {
|
|
return []byte{}
|
|
}
|
|
return b
|
|
}
|
|
|
|
func defaultFor(key string) string {
|
|
for _, co := range configOptions {
|
|
if co.Key == key {
|
|
return co.DefaultValue
|
|
}
|
|
}
|
|
return ""
|
|
}
|