Skip to content

Commit

Permalink
Merge pull request #468 from DopplerHQ/andre/inheritance
Browse files Browse the repository at this point in the history
Add support for config inheritance
  • Loading branch information
apazzolini authored Dec 10, 2024
2 parents ace4cfe + a6e430d commit d67deaf
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 28 deletions.
83 changes: 69 additions & 14 deletions pkg/cmd/configs.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,22 @@ var configsDeleteCmd = &cobra.Command{
var configsUpdateCmd = &cobra.Command{
Use: "update [config]",
Short: "Update a config",
Long: "Update properties about a config, such as its name, inheritability flag, and inheritances",
Args: cobra.MaximumNArgs(1),
ValidArgsFunction: configNamesValidArgs,
Run: updateConfigs,
Example: `Updating a config's name
$ doppler configs update --project proj --config dev_branch --name dev_branch2
Enabling a config to be inherited
$ doppler configs update --project proj --config dev --inheritable=true
Configuring which configs the given config inherits
Note: The inherits flag accepts a comma separated list of PROJ_NAME.CONF_NAME
$ doppler configs update --project proj --config dev --inherits="shared-db.dev,shared-api.dev"
To clear the inheritance list, pass in an empty value:
$ doppler configs update --project proj --config dev --inherits=`,
}

var configsLockCmd = &cobra.Command{
Expand Down Expand Up @@ -196,34 +209,77 @@ func deleteConfigs(cmd *cobra.Command, args []string) {

func updateConfigs(cmd *cobra.Command, args []string) {
jsonFlag := utils.OutputJSON

nameSet := cmd.Flags().Changed("name")
inheritableSet := cmd.Flags().Changed("inheritable")
inheritsSet := cmd.Flags().Changed("inherits")

varsChanged := 0
for _, v := range []bool{nameSet, inheritableSet, inheritsSet} {
if v {
varsChanged++
}
}

if varsChanged != 1 {
utils.HandleError(fmt.Errorf("Exactly one of name, inheritable, and inherits must be specified"))
}

name := cmd.Flag("name").Value.String()
inheritable := utils.GetBoolFlag(cmd, "inheritable")
inherits := cmd.Flag("inherits").Value.String()
yes := utils.GetBoolFlag(cmd, "yes")
localConfig := configuration.LocalConfig(cmd)

utils.RequireValue("token", localConfig.Token.Value)
utils.RequireValue("name", name)

config := localConfig.EnclaveConfig.Value
if len(args) > 0 {
config = args[0]
}

if !yes {
utils.PrintWarning("Renaming this config may break your current deploys.")
if !utils.ConfirmationPrompt("Continue?", false) {
utils.Log("Aborting")
return
if nameSet {
if !yes {
utils.PrintWarning("Renaming this config may break your current deploys.")
if !utils.ConfirmationPrompt("Continue?", false) {
utils.Log("Aborting")
return
}
}

info, err := http.UpdateConfig(localConfig.APIHost.Value, utils.GetBool(localConfig.VerifyTLS.Value, true), localConfig.Token.Value, localConfig.EnclaveProject.Value, config, name)
if !err.IsNil() {
utils.HandleError(err.Unwrap(), err.Message)
}

if !utils.Silent {
printer.ConfigInfo(info, jsonFlag)
}

}

info, err := http.UpdateConfig(localConfig.APIHost.Value, utils.GetBool(localConfig.VerifyTLS.Value, true), localConfig.Token.Value, localConfig.EnclaveProject.Value, config, name)
if !err.IsNil() {
utils.HandleError(err.Unwrap(), err.Message)
if inheritableSet {
info, err := http.UpdateConfigInheritable(localConfig.APIHost.Value, utils.GetBool(localConfig.VerifyTLS.Value, true), localConfig.Token.Value, localConfig.EnclaveProject.Value, config, inheritable)
if !err.IsNil() {
utils.HandleError(err.Unwrap(), err.Message)
}

if !utils.Silent {
printer.ConfigInfo(info, jsonFlag)
}
}

if !utils.Silent {
printer.ConfigInfo(info, jsonFlag)
if inheritsSet {
info, err := http.UpdateConfigInherits(localConfig.APIHost.Value, utils.GetBool(localConfig.VerifyTLS.Value, true), localConfig.Token.Value, localConfig.EnclaveProject.Value, config, inherits)
if !err.IsNil() {
utils.HandleError(err.Unwrap(), err.Message)
}

if !utils.Silent {
printer.ConfigInfo(info, jsonFlag)
}
}

}

func lockConfigs(cmd *cobra.Command, args []string) {
Expand Down Expand Up @@ -395,9 +451,8 @@ func init() {
utils.HandleError(err)
}
configsUpdateCmd.Flags().String("name", "", "config name")
if err := configsUpdateCmd.MarkFlagRequired("name"); err != nil {
utils.HandleError(err)
}
configsUpdateCmd.Flags().Bool("inheritable", false, "toggle config inheritability")
configsUpdateCmd.Flags().String("inherits", "", "configs to inherit (e.g. \"proj2.prd,shared.prd\")")
configsUpdateCmd.Flags().BoolP("yes", "y", false, "proceed without confirmation")
configsCmd.AddCommand(configsUpdateCmd)

Expand Down
84 changes: 84 additions & 0 deletions pkg/http/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,90 @@ func UpdateConfig(host string, verifyTLS bool, apiKey string, project string, co
return info, Error{}
}

func UpdateConfigInheritable(host string, verifyTLS bool, apiKey string, project string, config string, inheritable bool) (models.ConfigInfo, Error) {
postBody := map[string]interface{}{"inheritable": inheritable}
body, err := json.Marshal(postBody)
if err != nil {
return models.ConfigInfo{}, Error{Err: err, Message: "Invalid config info"}
}

var params []queryParam
params = append(params, queryParam{Key: "project", Value: project})
params = append(params, queryParam{Key: "config", Value: config})

url, err := generateURL(host, "/v3/configs/config/inheritable", params)
if err != nil {
return models.ConfigInfo{}, Error{Err: err, Message: "Unable to generate url"}
}

statusCode, _, response, err := PostRequest(url, verifyTLS, apiKeyHeader(apiKey), body)
if err != nil {
return models.ConfigInfo{}, Error{Err: err, Message: "Unable to update config", Code: statusCode}
}

var result map[string]interface{}
err = json.Unmarshal(response, &result)
if err != nil {
return models.ConfigInfo{}, Error{Err: err, Message: "Unable to parse API response", Code: statusCode}
}

configInfo, ok := result["config"].(map[string]interface{})
if !ok {
return models.ConfigInfo{}, Error{Err: fmt.Errorf("Unexpected type parsing config info, expected map[string]interface{}, got %T", result["config"]), Message: "Unable to parse API response", Code: statusCode}
}
info := models.ParseConfigInfo(configInfo)
return info, Error{}
}

func UpdateConfigInherits(host string, verifyTLS bool, apiKey string, project string, config string, inherits string) (models.ConfigInfo, Error) {
inheritsObj := []models.ConfigDescriptor{}

if len(inherits) > 0 {
configDescriptors := strings.Split(inherits, ",")
for _, cd := range configDescriptors {
parts := strings.Split(cd, ".")
if len(parts) != 2 {
return models.ConfigInfo{}, Error{Message: "Config descriptors must match the format \"projectSlug.configName\""}
}
inheritsObj = append(inheritsObj, models.ConfigDescriptor{ProjectSlug: parts[0], ConfigName: parts[1]})
}
}

postBody := map[string]interface{}{"inherits": inheritsObj}
body, err := json.Marshal(postBody)

if err != nil {
return models.ConfigInfo{}, Error{Err: err, Message: "Invalid config info"}
}

var params []queryParam
params = append(params, queryParam{Key: "project", Value: project})
params = append(params, queryParam{Key: "config", Value: config})

url, err := generateURL(host, "/v3/configs/config/inherits", params)
if err != nil {
return models.ConfigInfo{}, Error{Err: err, Message: "Unable to generate url"}
}

statusCode, _, response, err := PostRequest(url, verifyTLS, apiKeyHeader(apiKey), body)
if err != nil {
return models.ConfigInfo{}, Error{Err: err, Message: "Unable to update config", Code: statusCode}
}

var result map[string]interface{}
err = json.Unmarshal(response, &result)
if err != nil {
return models.ConfigInfo{}, Error{Err: err, Message: "Unable to parse API response", Code: statusCode}
}

configInfo, ok := result["config"].(map[string]interface{})
if !ok {
return models.ConfigInfo{}, Error{Err: fmt.Errorf("Unexpected type parsing config info, expected map[string]interface{}, got %T", result["config"]), Message: "Unable to parse API response", Code: statusCode}
}
info := models.ParseConfigInfo(configInfo)
return info, Error{}
}

// GetActivityLogs get activity logs
func GetActivityLogs(host string, verifyTLS bool, apiKey string, page int, number int) ([]models.ActivityLog, Error) {
var params []queryParam
Expand Down
24 changes: 16 additions & 8 deletions pkg/models/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,17 @@ type EnvironmentInfo struct {

// ConfigInfo project info
type ConfigInfo struct {
Name string `json:"name"`
Root bool `json:"root"`
Locked bool `json:"locked"`
Environment string `json:"environment"`
Project string `json:"project"`
CreatedAt string `json:"created_at"`
InitialFetchAt string `json:"initial_fetch_at"`
LastFetchAt string `json:"last_fetch_at"`
Name string `json:"name"`
Root bool `json:"root"`
Locked bool `json:"locked"`
Environment string `json:"environment"`
Project string `json:"project"`
CreatedAt string `json:"created_at"`
InitialFetchAt string `json:"initial_fetch_at"`
LastFetchAt string `json:"last_fetch_at"`
Inheritable bool `json:"inheritable"`
Inherits []ConfigDescriptor `json:"inherits"`
InheritedBy []ConfigDescriptor `json:"inheritedBy"`
}

// ConfigLog a log
Expand Down Expand Up @@ -175,3 +178,8 @@ type ActorWorkplaceInfo struct {
type WatchSecrets struct {
Type string `json:"type"`
}

type ConfigDescriptor struct {
ProjectSlug string `json:"projectSlug"`
ConfigName string `json:"configName"`
}
19 changes: 19 additions & 0 deletions pkg/models/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,25 @@ func ParseConfigInfo(info map[string]interface{}) ConfigInfo {
if info["last_fetch_at"] != nil {
configInfo.LastFetchAt = info["last_fetch_at"].(string)
}
if info["inheritable"] != nil {
configInfo.Inheritable = info["inheritable"].(bool)
}
if info["inherits"] != nil {
configInfo.Inherits = []ConfigDescriptor{}
inherits := info["inherits"].([]interface{})
for _, i := range inherits {
descriptorMap := i.(map[string]interface{})
configInfo.Inherits = append(configInfo.Inherits, ConfigDescriptor{ProjectSlug: descriptorMap["projectSlug"].(string), ConfigName: descriptorMap["configName"].(string)})
}
}
if info["inheritedBy"] != nil {
configInfo.InheritedBy = []ConfigDescriptor{}
inheritedBy := info["inheritedBy"].([]interface{})
for _, i := range inheritedBy {
descriptorMap := i.(map[string]interface{})
configInfo.InheritedBy = append(configInfo.InheritedBy, ConfigDescriptor{ProjectSlug: descriptorMap["projectSlug"].(string), ConfigName: descriptorMap["configName"].(string)})
}
}

return configInfo
}
Expand Down
64 changes: 58 additions & 6 deletions pkg/printer/enclave.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"fmt"
"math"
"sort"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -131,8 +132,38 @@ func ConfigInfo(info models.ConfigInfo, jsonFlag bool) {
return
}

rows := [][]string{{info.Name, info.InitialFetchAt, info.LastFetchAt, info.CreatedAt, info.Environment, info.Project}}
Table([]string{"name", "initial fetch", "last fetch", "created at", "environment", "project"}, rows, TableOptions())
var inheritsStrings []string
var inheritsHeader string
if info.Inheritable {
inheritsHeader = "inherited by"
for _, inheritedBy := range info.InheritedBy {
inheritsStrings = append(inheritsStrings, fmt.Sprintf("%s.%s", inheritedBy.ProjectSlug, inheritedBy.ConfigName))
}
} else {
inheritsHeader = "inherits"
for _, inherits := range info.Inherits {
inheritsStrings = append(inheritsStrings, fmt.Sprintf("%s.%s", inherits.ProjectSlug, inherits.ConfigName))
}
}

var inheritsString string
if len(inheritsStrings) > 0 {
inheritsString = strings.Join(inheritsStrings, ", ")
} else {
inheritsString = "<NONE>"
}

rows := [][]string{{
info.Name,
info.InitialFetchAt,
info.LastFetchAt,
info.CreatedAt,
info.Environment,
info.Project,
strings.ToUpper(strconv.FormatBool(info.Inheritable)),
inheritsString,
}}
Table([]string{"name", "initial fetch", "last fetch", "created at", "environment", "project", "inheritable", inheritsHeader}, rows, TableOptions())
}

// ConfigsInfo print configs
Expand All @@ -142,12 +173,33 @@ func ConfigsInfo(info []models.ConfigInfo, jsonFlag bool) {
return
}

header := []string{"name", "initial fetch", "last fetch", "created at", "environment", "project", "inheritable", "inherits"}
var rows [][]string
for _, configInfo := range info {
rows = append(rows, []string{configInfo.Name, configInfo.InitialFetchAt, configInfo.LastFetchAt, configInfo.CreatedAt,
configInfo.Environment, configInfo.Project})
}
Table([]string{"name", "initial fetch", "last fetch", "created at", "environment", "project"}, rows, TableOptions())
var inheritsStrings []string
for _, inherits := range configInfo.Inherits {
inheritsStrings = append(inheritsStrings, fmt.Sprintf("%s.%s", inherits.ProjectSlug, inherits.ConfigName))
}

var inheritsString string
if len(inheritsStrings) > 0 {
inheritsString = strings.Join(inheritsStrings, ", ")
} else {
inheritsString = "<NONE>"
}

rows = append(rows, []string{
configInfo.Name,
configInfo.InitialFetchAt,
configInfo.LastFetchAt,
configInfo.CreatedAt,
configInfo.Environment,
configInfo.Project,
strings.ToUpper(strconv.FormatBool(configInfo.Inheritable)),
inheritsString,
})
}
Table(header, rows, TableOptions())
}

// EnvironmentsInfo print environments
Expand Down

0 comments on commit d67deaf

Please sign in to comment.