cli/azd/pkg/config/config.go (232 lines of code) (raw):
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
// Package config provides functionality related to storing application-wide configuration data.
//
// Configuration data stored should not be specific to a given repository/project.
package config
import (
"encoding/base64"
"encoding/json"
"fmt"
"path/filepath"
"regexp"
"strings"
"github.com/google/uuid"
)
//
//nolint:lll
var vaultPattern = regexp.MustCompile(
`^vault://[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$`,
)
// Azd configuration for the current user
// Configuration data is stored in user's home directory @ ~/.azd/config.json
type Config interface {
Raw() map[string]any
// similar to Raw() but it will resolve any vault references
ResolvedRaw() map[string]any
// Get retrieves the value stored at the specified path
Get(path string) (any, bool)
// GetString retrieves the value stored at the specified path as a string
GetString(path string) (string, bool)
// GetSection retrieves the value stored at the specified path and unmarshals it into the provided section
GetSection(path string, section any) (bool, error)
// GetMap retrieves the map stored at the specified path
GetMap(path string) (map[string]any, bool)
// GetSlice retrieves the slice stored at the specified path
GetSlice(path string) ([]any, bool)
// Set stores the value at the specified path
Set(path string, value any) error
// SetSecret stores the secrets at the specified path within a local user vault
SetSecret(path string, value string) error
// Unset removes the value stored at the specified path
Unset(path string) error
// IsEmpty returns a value indicating whether the configuration is empty
IsEmpty() bool
}
// NewEmptyConfig creates a empty configuration object.
func NewEmptyConfig() Config {
return NewConfig(nil)
}
// NewConfig creates a configuration object, populated with an initial set of keys and values. If [data] is nil or an
// empty map, and empty configuration object is returned, but [NewEmptyConfig] might better express your intention.
func NewConfig(data map[string]any) Config {
if data == nil {
data = map[string]any{}
}
return &config{
data: data,
}
}
// Top level AZD configuration
type config struct {
vaultId string
vault Config
data map[string]any
}
// Returns a value indicating whether the configuration is empty
func (c *config) IsEmpty() bool {
return len(c.data) == 0
}
// Gets the raw values stored in the configuration as a Go map
func (c *config) Raw() map[string]any {
return c.data
}
const vaultKeyName = "vault"
// Gets the raw values stored in the configuration and resolve any vault references
func (c *config) ResolvedRaw() map[string]any {
resolvedRaw := &config{
data: map[string]any{},
}
paths := paths(c.data)
for _, path := range paths {
if path == vaultKeyName {
// a resolved raw should not include a reference a vault, as all secrets should be resolved
// when a config file contains a vault reference and the vault is not found, azd returns os.ErrNotExist
// and to the eyes of components using a Config, that means the config does not exists.
continue
}
// get will always return true (no need to check) because the path was gotten from the raw config
value, _ := c.Get(path)
if err := resolvedRaw.Set(path, value); err != nil {
panic(fmt.Errorf("failed setting resolved raw value: %w", err))
}
}
return resolvedRaw.data
}
// paths recursively traverses a map and returns a list of all the paths to the leaf nodes.
// The start parameter is the initial map to start traversing from.
// It returns a slice of strings representing the paths to the leaf nodes.
func paths(start map[string]any) []string {
var all []string
for path, value := range start {
if node, isNode := value.(map[string]any); isNode {
for _, child := range paths(node) {
all = append(all, fmt.Sprintf("%s.%s", path, child))
}
} else {
all = append(all, path)
}
}
return all
}
// SetSecret stores the secrets at the specified path within a local user vault
func (c *config) SetSecret(path string, value string) error {
if c.vaultId == "" {
c.vault = NewConfig(nil)
c.vaultId = uuid.New().String()
if err := c.Set(vaultKeyName, c.vaultId); err != nil {
return fmt.Errorf("failed setting vault id: %w", err)
}
}
pathId := uuid.New().String()
vaultRef := fmt.Sprintf("vault://%s/%s", c.vaultId, pathId)
if err := c.vault.Set(pathId, base64.StdEncoding.EncodeToString([]byte(value))); err != nil {
return fmt.Errorf("failed setting secret value: %w", err)
}
return c.Set(path, vaultRef)
}
// Sets a value at the specified location
func (c *config) Set(path string, value any) error {
depth := 1
currentNode := c.data
parts := strings.Split(path, ".")
for _, part := range parts {
if depth == len(parts) {
currentNode[part] = value
return nil
}
var node map[string]any
value, ok := currentNode[part]
if !ok || value == nil {
node = map[string]any{}
}
if value != nil {
node, ok = value.(map[string]any)
if !ok {
return fmt.Errorf("failed converting node at path '%s' to map", part)
}
}
currentNode[part] = node
currentNode = node
depth++
}
return nil
}
// Removes any values stored at the specified path
// When the path location is an object will remove the whole node
// When the path does not exist, will return a `nil` value
func (c *config) Unset(path string) error {
depth := 1
currentNode := c.data
parts := strings.Split(path, ".")
for _, part := range parts {
if depth == len(parts) {
delete(currentNode, part)
return nil
}
var node map[string]any
value, ok := currentNode[part]
// Path already doesn't exist, NOOP
if !ok || value == nil {
return nil
}
node, ok = value.(map[string]any)
if !ok {
return fmt.Errorf("failed converting node at path '%s' to map", part)
}
currentNode[part] = node
currentNode = node
depth++
}
return nil
}
// Gets the value stored at the specified location
// Returns the value if exists, otherwise returns nil & a value indicating if the value existing
func (c *config) Get(path string) (any, bool) {
depth := 1
currentNode := c.data
parts := strings.Split(path, ".")
for _, part := range parts {
// When the depth is equal to the number of parts, we have reached the desired node path
// At this point we can perform any final processing on the node and return the result
if depth == len(parts) {
value, ok := currentNode[part]
if !ok {
return value, ok
}
return c.interpolateNodeValue(value)
}
value, ok := currentNode[part]
if !ok {
return value, ok
}
node, ok := value.(map[string]any)
if !ok {
return nil, false
}
currentNode = node
depth++
}
return nil, false
}
// GetMap retrieves the map stored at the specified path
func (c *config) GetMap(path string) (map[string]any, bool) {
value, ok := c.Get(path)
if !ok {
return nil, false
}
node, ok := value.(map[string]any)
return node, ok
}
// GetSlice retrieves the slice stored at the specified path
func (c *config) GetSlice(path string) ([]any, bool) {
value, ok := c.Get(path)
if !ok {
return nil, false
}
node, ok := value.([]any)
return node, ok
}
// Gets the value stored at the specified location as a string
func (c *config) GetString(path string) (string, bool) {
value, ok := c.Get(path)
if !ok {
return "", false
}
str, ok := value.(string)
return str, ok
}
func (c *config) GetSection(path string, section any) (bool, error) {
sectionConfig, ok := c.Get(path)
if !ok {
return false, nil
}
jsonBytes, err := json.Marshal(sectionConfig)
if err != nil {
return true, fmt.Errorf("marshalling section config: %w", err)
}
if err := json.Unmarshal(jsonBytes, section); err != nil {
return true, fmt.Errorf("unmarshalling section config: %w", err)
}
return true, nil
}
// getSecret retrieves the secret stored at the specified path from a local user vault
func (c *config) getSecret(vaultRef string) (string, bool) {
encodedValue, ok := c.vault.GetString(filepath.Base(vaultRef))
if !ok {
return "", false
}
bytes, err := base64.StdEncoding.DecodeString(encodedValue)
if err != nil {
return "", false
}
return string(bytes), true
}
// interpolateNodeValue processes the node, iterates on any nested nodes and interpolates any vault references
func (c *config) interpolateNodeValue(value any) (any, bool) {
// Check if the value is a vault reference
// If it is, retrieve the secret from the vault
if vaultRef, isString := value.(string); isString && vaultPattern.MatchString(vaultRef) {
return c.getSecret(vaultRef)
}
// If the value is a map, recursively iterate over the map and interpolate the values
if node, isMap := value.(map[string]any); isMap {
// We want to ensure we return a cloned map so that we don't modify the original data
// stored within the config map data structure
cloneMap := map[string]any{}
for key, val := range node {
if nodeValue, ok := c.interpolateNodeValue(val); ok {
cloneMap[key] = nodeValue
}
}
return cloneMap, true
}
// Finally, if the value is not handled above we can return the value as is
return value, true
}