Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for monorepo-friendly structuring in doppler.yaml setup file #374

Merged
merged 1 commit into from
Apr 3, 2023
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
4 changes: 2 additions & 2 deletions doppler.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
setup:
project: cli
config: dev
- project: cli
config: dev
218 changes: 129 additions & 89 deletions pkg/cmd/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package cmd
import (
"errors"
"fmt"
"path/filepath"
"strings"

"github.com/DopplerHQ/cli/pkg/configuration"
Expand Down Expand Up @@ -63,106 +64,118 @@ func setup(cmd *cobra.Command, args []string) {
utils.LogDebugError(err.Unwrap())
}

ignoreRepoConfig :=
// ignore when repo config is blank
(repoConfig.Setup.Project == "" && repoConfig.Setup.Config == "") ||
// ignore when project and config are already specified
(localConfig.EnclaveProject.Source == models.FlagSource.String() && localConfig.EnclaveConfig.Source == models.FlagSource.String())

// default to true so repo config is used on --no-interactive
useRepoConfig := true
if !ignoreRepoConfig && canPromptUser {
useRepoConfig = utils.ConfirmationPrompt("Use settings from repo config file (doppler.yaml)?", true)
}

currentProject := localConfig.EnclaveProject.Value
selectedProject := ""

switch localConfig.EnclaveProject.Source {
case models.FlagSource.String():
selectedProject = localConfig.EnclaveProject.Value
case models.EnvironmentSource.String():
utils.Log(valueFromEnvironmentNotice("DOPPLER_PROJECT"))
selectedProject = localConfig.EnclaveProject.Value
default:
if useRepoConfig && repoConfig.Setup.Project != "" {
utils.Print("Auto-selecting project from repo config file")
selectedProject = repoConfig.Setup.Project
break
}

projects, httpErr := http.GetProjects(localConfig.APIHost.Value, utils.GetBool(localConfig.VerifyTLS.Value, true), localConfig.Token.Value, 1, 100)
if !httpErr.IsNil() {
utils.HandleError(httpErr.Unwrap(), httpErr.Message)
}
if len(projects) == 0 {
utils.HandleError(errors.New("you do not have access to any projects"))
}

defaultProject := scopedConfig.EnclaveProject.Value
if repoConfig.Setup.Project != "" {
defaultProject = repoConfig.Setup.Project
// do an initial pass to see if there are errors we want to bail on before attempting to proceed
setupFileErrorCheck(repoConfig.Setup)

for _, repo := range repoConfig.Setup {
watsonian marked this conversation as resolved.
Show resolved Hide resolved
expandedPath, _ := filepath.Abs(repo.Path)
watsonian marked this conversation as resolved.
Show resolved Hide resolved
watsonian marked this conversation as resolved.
Show resolved Hide resolved
scopedConfig = configuration.Get(expandedPath)

ignoreRepoConfig :=
// ignore when repo config is blank
(repo.Project == "" && repo.Config == "") ||
// ignore when project and config are already specified
(localConfig.EnclaveProject.Source == models.FlagSource.String() && localConfig.EnclaveConfig.Source == models.FlagSource.String())

// default to true so repo config is used on --no-interactive
useRepoConfig := true
if !ignoreRepoConfig && canPromptUser {
if len(repoConfig.Setup) > 1 && repo.Path != "" {
useRepoConfig = utils.ConfirmationPrompt(fmt.Sprintf("Use settings from repo config file (doppler.yaml) for %s?", expandedPath), true)
watsonian marked this conversation as resolved.
Show resolved Hide resolved
} else {
useRepoConfig = utils.ConfirmationPrompt("Use settings from repo config file (doppler.yaml)?", true)
}
}

selectedProject = selectProject(projects, defaultProject, canPromptUser)
if selectedProject == "" {
utils.HandleError(errors.New("Invalid project"))
}
}
currentProject := localConfig.EnclaveProject.Value
selectedProject := ""

selectedConfiguredProject := selectedProject == currentProject
selectedConfig := ""

switch localConfig.EnclaveConfig.Source {
case models.FlagSource.String():
selectedConfig = localConfig.EnclaveConfig.Value
case models.EnvironmentSource.String():
utils.Log(valueFromEnvironmentNotice("DOPPLER_CONFIG"))
selectedConfig = localConfig.EnclaveConfig.Value
default:
if useRepoConfig && repoConfig.Setup.Config != "" {
utils.Print("Auto-selecting config from repo config file")
selectedConfig = repoConfig.Setup.Config
break
switch localConfig.EnclaveProject.Source {
case models.FlagSource.String():
selectedProject = localConfig.EnclaveProject.Value
case models.EnvironmentSource.String():
utils.Log(valueFromEnvironmentNotice("DOPPLER_PROJECT"))
selectedProject = localConfig.EnclaveProject.Value
default:
if useRepoConfig && repo.Project != "" {
utils.Print("Auto-selecting project from repo config file")
selectedProject = repo.Project
break
}

projects, httpErr := http.GetProjects(localConfig.APIHost.Value, utils.GetBool(localConfig.VerifyTLS.Value, true), localConfig.Token.Value, 1, 100)
if !httpErr.IsNil() {
utils.HandleError(httpErr.Unwrap(), httpErr.Message)
}
if len(projects) == 0 {
utils.HandleError(errors.New("you do not have access to any projects"))
}

defaultProject := scopedConfig.EnclaveProject.Value
if repo.Project != "" {
defaultProject = repo.Project
}

selectedProject = selectProject(projects, defaultProject, canPromptUser)
if selectedProject == "" {
utils.HandleError(errors.New("Invalid project"))
}
}

configs, apiError := http.GetConfigs(localConfig.APIHost.Value, utils.GetBool(localConfig.VerifyTLS.Value, true), localConfig.Token.Value, selectedProject, "", 1, 100)
if !apiError.IsNil() {
utils.HandleError(apiError.Unwrap(), apiError.Message)
}
if len(configs) == 0 {
utils.Print("You project does not have any configs")
break
}
selectedConfiguredProject := selectedProject == currentProject
selectedConfig := ""

defaultConfig := scopedConfig.EnclaveConfig.Value
if repoConfig.Setup.Config != "" {
defaultConfig = repoConfig.Setup.Config
switch localConfig.EnclaveConfig.Source {
case models.FlagSource.String():
selectedConfig = localConfig.EnclaveConfig.Value
case models.EnvironmentSource.String():
utils.Log(valueFromEnvironmentNotice("DOPPLER_CONFIG"))
selectedConfig = localConfig.EnclaveConfig.Value
default:
if useRepoConfig && repo.Config != "" {
utils.Print("Auto-selecting config from repo config file")
selectedConfig = repo.Config
break
}

configs, apiError := http.GetConfigs(localConfig.APIHost.Value, utils.GetBool(localConfig.VerifyTLS.Value, true), localConfig.Token.Value, selectedProject, "", 1, 100)
if !apiError.IsNil() {
utils.HandleError(apiError.Unwrap(), apiError.Message)
}
if len(configs) == 0 {
utils.Print("You project does not have any configs")
break
}

defaultConfig := scopedConfig.EnclaveConfig.Value
if repo.Config != "" {
defaultConfig = repo.Config
}

selectedConfig = selectConfig(configs, selectedConfiguredProject, defaultConfig, canPromptUser)
if selectedConfig == "" {
utils.HandleError(errors.New("Invalid config"))
}
}

selectedConfig = selectConfig(configs, selectedConfiguredProject, defaultConfig, canPromptUser)
if selectedConfig == "" {
utils.HandleError(errors.New("Invalid config"))
configToSave := map[string]string{
models.ConfigEnclaveProject.String(): selectedProject,
models.ConfigEnclaveConfig.String(): selectedConfig,
}
}

configToSave := map[string]string{
models.ConfigEnclaveProject.String(): selectedProject,
models.ConfigEnclaveConfig.String(): selectedConfig,
}
if saveToken {
configToSave[models.ConfigToken.String()] = localConfig.Token.Value
}
configuration.Set(configuration.Scope, configToSave)

if !utils.Silent {
// do not fetch the LocalConfig since we do not care about env variables or cmd flags
conf := configuration.Get(configuration.Scope)
valuesToPrint := []string{models.ConfigEnclaveConfig.String(), models.ConfigEnclaveProject.String()}
if saveToken {
valuesToPrint = append(valuesToPrint, utils.RedactAuthToken(models.ConfigToken.String()))
configToSave[models.ConfigToken.String()] = localConfig.Token.Value
}
configuration.Set(expandedPath, configToSave)

if !utils.Silent {
// do not fetch the LocalConfig since we do not care about env variables or cmd flags
conf := configuration.Get(expandedPath)
valuesToPrint := []string{models.ConfigEnclaveConfig.String(), models.ConfigEnclaveProject.String()}
if saveToken {
valuesToPrint = append(valuesToPrint, utils.RedactAuthToken(models.ConfigToken.String()))
}
printer.ScopedConfigValues(conf, valuesToPrint, models.ScopedOptionsMap(&conf), utils.OutputJSON, false, false)
}
printer.ScopedConfigValues(conf, valuesToPrint, models.ScopedOptionsMap(&conf), utils.OutputJSON, false, false)
}
}

Expand Down Expand Up @@ -237,6 +250,33 @@ func valueFromEnvironmentNotice(name string) string {
return fmt.Sprintf("Using %s from the environment. To disable this, use --no-read-env.", name)
}

// we're looking for duplicate paths and more than one repo being defined without a path.
func setupFileErrorCheck(repos []models.ProjectConfig) {
// check to see if a repo isn't specifying a path and more than one repo exists
pathCount := make(map[string]int)
watsonian marked this conversation as resolved.
Show resolved Hide resolved
for _, repo := range repos {
if len(repos) > 1 && repo.Path == "" {
utils.HandleError(errors.New("a path must be specified for all repos when more than one exists in the repo config file (doppler.yaml)"))
}
pathCount[repo.Path] += 1
}

// check to see if a path is being used more than once
var badPaths []string
for path, count := range pathCount {
if count > 1 {
badPaths = append(badPaths, path)
}
}
if len(badPaths) > 0 {
errorMessage := []string{"the following path(s) are being used more than once in the repo config file (doppler.yaml):"}
for _, path := range badPaths {
errorMessage = append(errorMessage, fmt.Sprintf(" - %s", path))
}
utils.HandleError(errors.New(strings.Join(errorMessage, "\n")))
}
}

func init() {
setupCmd.Flags().StringP("project", "p", "", "project (e.g. backend)")
setupCmd.Flags().StringP("config", "c", "", "config (e.g. dev)")
Expand Down
23 changes: 15 additions & 8 deletions pkg/controllers/repo_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const repoConfigFileName = "doppler.yaml"
const ymlRepoConfigFileName = "doppler.yml"

// RepoConfig Reads the configuration file (doppler.yaml) if exists and returns the set configuration
func RepoConfig() (models.RepoConfig, Error) {
func RepoConfig() (models.MultiRepoConfig, Error) {

repoConfigFile := filepath.Join("./", repoConfigFileName)
ymlRepoConfigFile := filepath.Join("./", ymlRepoConfigFileName)
Expand All @@ -46,21 +46,28 @@ func RepoConfig() (models.RepoConfig, Error) {
var e Error
e.Err = err
e.Message = "Unable to read doppler repo config file"
return models.RepoConfig{}, e
return models.MultiRepoConfig{}, e
}

var repoConfig models.RepoConfig
var repoConfig models.MultiRepoConfig

if err := yaml.Unmarshal(yamlFile, &repoConfig); err != nil {
var e Error
e.Err = err
e.Message = "Unable to parse doppler repo config file"
return models.RepoConfig{}, e
// Try parsing old repoConfig format (i.e., no slice) for backwards compatibility
var oldRepoConfig models.RepoConfig
if err := yaml.Unmarshal(yamlFile, &oldRepoConfig); err != nil {
var e Error
e.Err = err
e.Message = "Unable to parse doppler repo config file"
return models.MultiRepoConfig{}, e
} else {
repoConfig.Setup = append(repoConfig.Setup, oldRepoConfig.Setup)
return repoConfig, Error{}
}
}

return repoConfig, Error{}
} else if utils.Exists(ymlRepoConfigFile) {
utils.LogWarning(fmt.Sprintf("Found %s file, please rename to %s for repo configuration", ymlRepoConfigFile, repoConfigFileName))
}
return models.RepoConfig{}, Error{}
return models.MultiRepoConfig{}, Error{}
}
21 changes: 16 additions & 5 deletions pkg/models/repo_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,21 @@ limitations under the License.

package models

// RepoConfig holds all repo configuration
// Config struct represents the basic project setup values
type ProjectConfig struct {
Config string `yaml:"config"`
Project string `yaml:"project"`
Path string `yaml:"path"`
}

// RepoConfig struct representing legacy doppler.yaml setup file format
// that only supported a single project and config
type RepoConfig struct {
Setup struct {
Config string `yaml:"config"`
Project string `yaml:"project"`
} `yaml:"setup"`
Setup ProjectConfig `yaml:"setup"`
}

// MultiRepoConfig struct supports doppler.yaml files containing multiple
// project and config combos
type MultiRepoConfig struct {
Setup []ProjectConfig `yaml:"setup"`
}
1 change: 1 addition & 0 deletions tests/e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export DOPPLER_CONFIG="e2e"
"$DIR/e2e/install-sh-update-in-place.sh"
"$DIR/e2e/legacy-commands.sh"
"$DIR/e2e/analytics.sh"
"$DIR/e2e/setup.sh"

echo -e "\nAll tests completed successfully!"
exit 0
Loading