package credentials import ( "fmt" "io/ioutil" "os" "gopkg.in/yaml.v3" ) // ChainConfig represents a single chain configuration type ChainConfig struct { Name string `yaml:"name"` PublicId string `yaml:"publicId"` AuthKeyId string `yaml:"authKeyId"` AuthKey string `yaml:"authKey"` Endpoint string `yaml:"endpoint"` } // Config represents the entire configuration file structure type Config struct { Default string `yaml:"default"` Chains []ChainConfig `yaml:"chains"` node *yaml.Node // Store the original node for preserving formatting } // LoadConfig reads and parses a YAML configuration file func LoadConfig(filePath string) (*Config, error) { // Expand the file path (handles ~/, ./, and environment variables) expandedPath, err := expandPath(filePath) if err != nil { return nil, fmt.Errorf("failed to expand path %s: %w", filePath, err) } // Read the file data, err := ioutil.ReadFile(expandedPath) if err != nil { return nil, fmt.Errorf("failed to read config file %s: %w", expandedPath, err) } // Parse YAML with Node to preserve formatting var node yaml.Node if err := yaml.Unmarshal(data, &node); err != nil { return nil, fmt.Errorf("failed to parse YAML config: %w", err) } var config Config if err := node.Decode(&config); err != nil { return nil, fmt.Errorf("failed to decode YAML config: %w", err) } // Store the node for later use when saving config.node = &node return &config, nil } // LoadConfigFromString parses YAML configuration from a string func LoadConfigFromString(yamlContent string) (*Config, error) { var node yaml.Node if err := yaml.Unmarshal([]byte(yamlContent), &node); err != nil { return nil, fmt.Errorf("failed to parse YAML config: %w", err) } var config Config if err := node.Decode(&config); err != nil { return nil, fmt.Errorf("failed to decode YAML config: %w", err) } config.node = &node return &config, nil } // GetDefaultChain returns the chain configuration for the default publicId func (c *Config) GetDefaultChain() *ChainConfig { for i := range c.Chains { if c.Chains[i].PublicId == c.Default { return &c.Chains[i] } } return nil } // GetChainByPublicId returns the chain configuration for the specified publicId func (c *Config) GetChainByPublicId(publicId string) *ChainConfig { for i := range c.Chains { if c.Chains[i].PublicId == publicId { return &c.Chains[i] } } return nil } // ListChains returns all chain names func (c *Config) ListChains() []string { names := make([]string, len(c.Chains)) for i, chain := range c.Chains { names[i] = chain.Name } return names } // AddChain adds a chain configuration to the configuration func (c *Config) AddChain(chain *ChainConfig) { c.Chains = append(c.Chains, *chain) } // SetDefault sets the default chain publicId func (c *Config) SetDefault(publicId string) { c.Default = publicId } // DeleteChain deletes a chain configuration from the configuration func (c *Config) DeleteChain(publicId string) error { if publicId == c.Default { return fmt.Errorf("cannot delete default chain") } for i, chain := range c.Chains { if chain.PublicId == publicId { c.Chains = append(c.Chains[:i], c.Chains[i+1:]...) return nil } } return nil } // SaveConfig writes the configuration to a YAML file func (c *Config) SaveConfig(filePath string) error { expandedPath, err := expandPath(filePath) if err != nil { return fmt.Errorf("failed to expand path %s: %w", filePath, err) } var data []byte if c.node != nil { // Update the node with current config values while preserving formatting if err := c.node.Encode(c); err != nil { return fmt.Errorf("failed to encode config to node: %w", err) } // Ensure newlines between chain entries c.ensureChainSeparation() // Marshal the node to preserve comments and formatting data, err = yaml.Marshal(c.node) if err != nil { return fmt.Errorf("failed to marshal config to YAML: %w", err) } } else { // Fallback to regular marshaling if no node is available data, err = yaml.Marshal(c) if err != nil { return fmt.Errorf("failed to marshal config to YAML: %w", err) } } if err := ioutil.WriteFile(expandedPath, data, 0644); err != nil { return fmt.Errorf("failed to write config file %s: %w", expandedPath, err) } return nil } // ensureChainSeparation adds blank lines between chain entries in the YAML node func (c *Config) ensureChainSeparation() { if c.node == nil || len(c.node.Content) == 0 { return } // Navigate to the document node, then the mapping node docNode := c.node if docNode.Kind == yaml.DocumentNode && len(docNode.Content) > 0 { docNode = docNode.Content[0] } if docNode.Kind != yaml.MappingNode { return } // Find the "chains" key in the mapping for i := 0; i < len(docNode.Content); i += 2 { if i+1 >= len(docNode.Content) { break } keyNode := docNode.Content[i] valueNode := docNode.Content[i+1] if keyNode.Value == "chains" && valueNode.Kind == yaml.SequenceNode { // Add HeadComment (newline before) to each chain entry except the first for j := 1; j < len(valueNode.Content); j++ { chainNode := valueNode.Content[j] if chainNode.HeadComment == "" { chainNode.HeadComment = "\n" } } break } } } // expandPath is a simple path expansion function (you can replace this with your utils.ExpandPath) func expandPath(path string) (string, error) { // Expand environment variables path = os.ExpandEnv(path) // Handle home directory if len(path) > 0 && path[0] == '~' { home, err := os.UserHomeDir() if err != nil { return "", err } if len(path) == 1 { path = home } else if path[1] == '/' { path = home + path[1:] } } return path, nil }