diff --git a/packages/cmd/agent_proxy.go b/packages/cmd/agent_proxy.go index d880573a..7d500a8e 100644 --- a/packages/cmd/agent_proxy.go +++ b/packages/cmd/agent_proxy.go @@ -58,7 +58,7 @@ infisical secrets agent-proxy connect -- claude`, agentProxyConnectCmd.Flags().String("proxy", "", "address of the agent proxy as host:port (falls back to INFISICAL_AGENT_PROXY_ADDRESS)") agentProxyConnectCmd.Flags().StringP("env", "e", "", "environment slug to fetch proxied services and secrets from (falls back to INFISICAL_ENVIRONMENT, then .infisical.yaml or a legacy .infisical.json)") - agentProxyConnectCmd.Flags().String("path", "/", "secret path (folder) scope (falls back to INFISICAL_SECRET_PATH, then secrets-management.default-secret-path in .infisical.yaml or defaultSecretPath in a legacy .infisical.json)") + agentProxyConnectCmd.Flags().String("path", "/", "secret path (folder) scope (falls back to INFISICAL_SECRET_PATH, then secrets-management.secret-path in .infisical.yaml or defaultSecretPath in a legacy .infisical.json)") agentProxyConnectCmd.Flags().String("projectId", "", "project id (falls back to INFISICAL_PROJECT_ID, then .infisical.yaml or a legacy .infisical.json)") agentProxyConnectCmd.Flags().String("client-id", "", "universal auth client id for the agent machine identity") agentProxyConnectCmd.Flags().String("client-secret", "", "universal auth client secret for the agent machine identity") @@ -169,7 +169,7 @@ func runAgentProxyConnect(cmd *cobra.Command, args []string) { environment := util.ResolveEnvironmentName(cmd) if environment == "" { - util.HandleError(fmt.Errorf("the environment is required; pass --env, set INFISICAL_ENVIRONMENT, or set secrets-management.default-environment in .infisical.yaml (or defaultEnvironment in a legacy .infisical.json)")) + util.HandleError(fmt.Errorf("the environment is required; pass --env, set INFISICAL_ENVIRONMENT, or set secrets-management.environment in .infisical.yaml (or defaultEnvironment in a legacy .infisical.json)")) } secretPath := util.ResolveSecretPath(cmd) diff --git a/packages/cmd/agent_proxy_run.go b/packages/cmd/agent_proxy_run.go index f1c24662..3090ec71 100644 --- a/packages/cmd/agent_proxy_run.go +++ b/packages/cmd/agent_proxy_run.go @@ -44,7 +44,7 @@ func buildAgentProxyRunCmd() *cobra.Command { } agentProxyRunCmd.Flags().StringP("env", "e", "", "environment slug to fetch proxied services and secrets from (falls back to INFISICAL_ENVIRONMENT, then .infisical.yaml or a legacy .infisical.json)") - agentProxyRunCmd.Flags().String("path", "/", "secret path (folder) to fetch from (falls back to INFISICAL_SECRET_PATH, then secrets-management.default-secret-path in .infisical.yaml or defaultSecretPath in a legacy .infisical.json)") + agentProxyRunCmd.Flags().String("path", "/", "secret path (folder) to fetch from (falls back to INFISICAL_SECRET_PATH, then secrets-management.secret-path in .infisical.yaml or defaultSecretPath in a legacy .infisical.json)") agentProxyRunCmd.Flags().String("projectId", "", "project id (falls back to INFISICAL_PROJECT_ID, then .infisical.yaml or a legacy .infisical.json)") agentProxyRunCmd.Flags().String("token", "", "run using this token instead of your logged-in session") agentProxyRunCmd.Flags().Bool("sandbox", true, "run the agent inside the OS sandbox") @@ -78,7 +78,7 @@ func runAgentProxyRun(cmd *cobra.Command, args []string) { // Same resolution order as `connect`: flag, then env var, then .infisical.yaml. environment := util.ResolveEnvironmentName(cmd) if environment == "" { - util.HandleError(fmt.Errorf("the environment is required; pass --env, set INFISICAL_ENVIRONMENT, or set secrets-management.default-environment in .infisical.yaml (or defaultEnvironment in a legacy .infisical.json)")) + util.HandleError(fmt.Errorf("the environment is required; pass --env, set INFISICAL_ENVIRONMENT, or set secrets-management.environment in .infisical.yaml (or defaultEnvironment in a legacy .infisical.json)")) } secretPath := util.ResolveSecretPath(cmd) diff --git a/packages/cmd/init.go b/packages/cmd/init.go index 851422e0..784eca47 100644 --- a/packages/cmd/init.go +++ b/packages/cmd/init.go @@ -36,6 +36,8 @@ infisical init --project-id `, }, Run: func(cmd *cobra.Command, args []string) { projectID, _ := cmd.Flags().GetString("project-id") + silent, _ := cmd.Flags().GetBool("silent") + showMessages := !silent && !isStructuredOutputRequested(cmd) if util.WorkspaceConfigFileExistsInCurrentPath() { if !isatty.IsTerminal(os.Stdin.Fd()) { @@ -77,7 +79,7 @@ infisical init --project-id `, if userCreds.OrganizationID != "" { rejectTransientOrgOverride(userCreds) } - if err := writeWorkspaceFile(models.Workspace{ID: projectID}); err != nil { + if err := writeWorkspaceFile(models.Workspace{ID: projectID}, showMessages); err != nil { util.HandleError(err) } hintDirectoryProfileBinding(userCreds) @@ -167,7 +169,7 @@ infisical init --project-id `, util.HandleError(err) } - err = writeWorkspaceFile(filteredWorkspaces[index]) + err = writeWorkspaceFile(filteredWorkspaces[index], showMessages) if err != nil { util.HandleError(err) } @@ -335,7 +337,7 @@ func pickOrganization(httpClient *resty.Client, label string, username string) ( return subItems[subIndex].ID, &selectedSubOrgName, nil } -func writeWorkspaceFile(selectedWorkspace models.Workspace) error { +func writeWorkspaceFile(selectedWorkspace models.Workspace, showMessages bool) error { workspaceFileToSave := models.WorkspaceConfigFileYaml{ Version: util.INFISICAL_WORKSPACE_CONFIG_YAML_VERSION, SecretsManagement: models.SecretsManagementConfigSection{ @@ -348,13 +350,15 @@ func writeWorkspaceFile(selectedWorkspace models.Workspace) error { return err } - err = util.WriteFileAtomic(util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, marshalledWorkspaceFile, 0o600) + err = util.WriteFileAtomic(util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME, marshalledWorkspaceFile, 0o600) if err != nil { return err } // the legacy file is left in place; .infisical.yaml takes precedence over it from now on - util.RemindToRemoveLegacyWorkspaceConfig(".") + if _, err := os.Stat(util.LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME); err == nil && showMessages { + util.PrintWarning(fmt.Sprintf("The legacy %s in this directory is no longer used because %s takes precedence. You can delete it.", util.LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME, util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME)) + } return nil } diff --git a/packages/cmd/init_test.go b/packages/cmd/init_test.go index a5a95682..0cf1c0af 100644 --- a/packages/cmd/init_test.go +++ b/packages/cmd/init_test.go @@ -1,9 +1,12 @@ package cmd import ( + "bytes" + "io" "os" "path/filepath" "runtime" + "strings" "testing" "github.com/Infisical/infisical-merge/packages/models" @@ -23,11 +26,11 @@ func TestWriteWorkspaceFile(t *testing.T) { t.Run("fresh directory gets only a yaml config", func(t *testing.T) { dir := isolateWorkspaceDir(t) - if err := writeWorkspaceFile(models.Workspace{ID: "proj-new"}); err != nil { + if err := writeWorkspaceFile(models.Workspace{ID: "proj-new"}, true); err != nil { t.Fatalf("writeWorkspaceFile: %v", err) } - yamlPath := filepath.Join(dir, util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) + yamlPath := filepath.Join(dir, util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME) raw, err := os.ReadFile(yamlPath) if err != nil { t.Fatalf("read %s: %v", yamlPath, err) @@ -64,19 +67,19 @@ func TestWriteWorkspaceFile(t *testing.T) { if err != nil { t.Fatalf("read dir: %v", err) } - if len(entries) != 1 || entries[0].Name() != util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME { - t.Errorf("expected only %s in dir, got %v", util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, entries) + if len(entries) != 1 || entries[0].Name() != util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME { + t.Errorf("expected only %s in dir, got %v", util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME, entries) } }) t.Run("leaves a legacy json config in place", func(t *testing.T) { dir := isolateWorkspaceDir(t) - jsonPath := filepath.Join(dir, util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + jsonPath := filepath.Join(dir, util.LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME) if err := os.WriteFile(jsonPath, []byte(`{"workspaceId":"proj-old"}`), 0o600); err != nil { t.Fatalf("write json: %v", err) } - if err := writeWorkspaceFile(models.Workspace{ID: "proj-new"}); err != nil { + if err := writeWorkspaceFile(models.Workspace{ID: "proj-new"}, true); err != nil { t.Fatalf("writeWorkspaceFile: %v", err) } @@ -97,4 +100,28 @@ func TestWriteWorkspaceFile(t *testing.T) { t.Errorf("WorkspaceId = %q, want proj-new", cfg.WorkspaceId) } }) + + t.Run("legacy json warning is shown only when messages are enabled", func(t *testing.T) { + var stderr bytes.Buffer + originalStderrWriter := util.GetStderrWriter + util.GetStderrWriter = func() io.Writer { return &stderr } + t.Cleanup(func() { util.GetStderrWriter = originalStderrWriter }) + + for _, showMessages := range []bool{false, true} { + stderr.Reset() + dir := isolateWorkspaceDir(t) + if err := os.WriteFile(filepath.Join(dir, util.LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME), []byte(`{"workspaceId":"proj-old"}`), 0o600); err != nil { + t.Fatalf("write json: %v", err) + } + + if err := writeWorkspaceFile(models.Workspace{ID: "proj-new"}, showMessages); err != nil { + t.Fatalf("writeWorkspaceFile: %v", err) + } + + warned := strings.Contains(stderr.String(), util.LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + if warned != showMessages { + t.Errorf("showMessages=%t: warned = %t, stderr:\n%s", showMessages, warned, stderr.String()) + } + } + }) } diff --git a/packages/cmd/root.go b/packages/cmd/root.go index a14d4476..8695c070 100644 --- a/packages/cmd/root.go +++ b/packages/cmd/root.go @@ -162,9 +162,6 @@ func init() { if err != nil { util.HandleError(err) } - if !silent && !isStructuredOutputRequested(cmd) { - util.EnableWorkspaceConfigMessages() - } profileFlag, err := cmd.Flags().GetString("profile") if err != nil { @@ -197,11 +194,6 @@ func init() { if !util.IsRunningInDocker() && !silent && !isStructuredOutputRequested(cmd) { util.CheckForUpdateWithWriter(cmd.ErrOrStderr()) - projectConfigDir := "" - if f := cmd.Flags().Lookup("project-config-dir"); f != nil { - projectConfigDir = f.Value.String() - } - util.DisplayWorkspaceConfigMigrationNoticeWithWriter(silent, projectConfigDir, cmd.ErrOrStderr()) } loggedInDetails, err := util.GetCurrentLoggedInUserDetails(false) diff --git a/packages/models/cli.go b/packages/models/cli.go index a393fc6c..d48b5e40 100644 --- a/packages/models/cli.go +++ b/packages/models/cli.go @@ -186,13 +186,29 @@ type GeneralConfigSection struct { Domain string `yaml:"domain,omitempty"` } +// GitBranchEnvironmentOverride is one entry of the secrets-management.overrides.git-branch list. +type GitBranchEnvironmentOverride struct { + Branch string `yaml:"branch"` + Environment string `yaml:"environment"` +} + +type OverridesConfigSection struct { + GitBranch []GitBranchEnvironmentOverride `yaml:"git-branch,omitempty"` +} + type SecretsManagementConfigSection struct { - ProjectID string `yaml:"project-id"` - DefaultEnvironment string `yaml:"default-environment"` - DefaultSecretPath string `yaml:"default-secret-path,omitempty"` - Mappings struct { - GitBranchToEnvironment map[string]string `yaml:"git-branch-to-environment"` - } `yaml:"mappings"` + ProjectID string `yaml:"project-id"` + Environment string `yaml:"environment,omitempty"` + SecretPath string `yaml:"secret-path,omitempty"` + Overrides OverridesConfigSection `yaml:"overrides,omitempty"` + // Deprecated: superseded by Environment; still read until support for it is removed. + DefaultEnvironment string `yaml:"default-environment,omitempty"` + // Deprecated: superseded by SecretPath; still read until support for it is removed. + DefaultSecretPath string `yaml:"default-secret-path,omitempty"` + // Deprecated: superseded by Overrides.GitBranch; still read until support for it is removed. + Mappings struct { + GitBranchToEnvironment map[string]string `yaml:"git-branch-to-environment,omitempty"` + } `yaml:"mappings,omitempty"` } type WorkspaceConfigFileYaml struct { diff --git a/packages/util/config.go b/packages/util/config.go index 6bd7693f..ffaa4814 100644 --- a/packages/util/config.go +++ b/packages/util/config.go @@ -30,7 +30,7 @@ func ConfigFileExists() bool { } func WorkspaceConfigFileExistsInCurrentPath() bool { - for _, fileName := range []string{INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, INFISICAL_WORKSPACE_CONFIG_FILE_NAME} { + for _, fileName := range []string{INFISICAL_WORKSPACE_CONFIG_FILE_NAME, LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME} { if _, err := os.Stat(fileName); err == nil { return true } else { @@ -65,66 +65,19 @@ func GetDomainFromFile() (domain string, valid bool) { } func GetWorkSpaceFromFilePath(configFileDir string) (models.WorkspaceConfigFile, error) { - yamlConfigFilePath := filepath.Join(configFileDir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) - jsonConfigFilePath := filepath.Join(configFileDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + yamlConfigFilePath := filepath.Join(configFileDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + jsonConfigFilePath := filepath.Join(configFileDir, LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME) // the yaml file takes precedence over the legacy json file when both exist if workspaceConfigFile, exists, err := readWorkspaceConfigYamlIfExists(yamlConfigFilePath); exists { - if err == nil { - RemindToRemoveLegacyWorkspaceConfig(configFileDir) - } return workspaceConfigFile, err } if _, err := os.Stat(jsonConfigFilePath); os.IsNotExist(err) { - return models.WorkspaceConfigFile{}, fmt.Errorf("no %s or %s found in %s", INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, INFISICAL_WORKSPACE_CONFIG_FILE_NAME, configFileDir) - } - - workspaceConfigFile, err := GetWorkspaceConfigByPath(jsonConfigFilePath) - if err != nil { - return models.WorkspaceConfigFile{}, err - } - - createWorkspaceConfigYamlFromLegacy(jsonConfigFilePath, yamlConfigFilePath) - - return workspaceConfigFile, nil -} - -func createWorkspaceConfigYamlFromLegacy(jsonConfigFilePath string, yamlConfigFilePath string) bool { - if err := convertLegacyWorkspaceConfigToYaml(jsonConfigFilePath, yamlConfigFilePath); err != nil { - log.Debug().Err(err).Msgf("createWorkspaceConfigYamlFromLegacy: unable to create [path=%s], using the legacy [path=%s]", yamlConfigFilePath, jsonConfigFilePath) - return false - } - - if workspaceConfigMessagesEnabled { - PrintlnStderr(fmt.Sprintf("Created %s from the legacy %s (commit it if this project is under version control).", yamlConfigFilePath, jsonConfigFilePath)) - } - RemindToRemoveLegacyWorkspaceConfig(filepath.Dir(jsonConfigFilePath)) - return true -} - -var legacyWorkspaceConfigReminderShown bool - -var workspaceConfigMessagesEnabled bool - -func EnableWorkspaceConfigMessages() { - workspaceConfigMessagesEnabled = true -} - -func RemindToRemoveLegacyWorkspaceConfig(configFileDir string) { - if legacyWorkspaceConfigReminderShown || !workspaceConfigMessagesEnabled { - return - } - if absDir, err := filepath.Abs(configFileDir); err == nil { - configFileDir = absDir - } - jsonConfigFilePath := filepath.Join(configFileDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - if _, err := os.Stat(jsonConfigFilePath); err != nil { - return + return models.WorkspaceConfigFile{}, fmt.Errorf("no %s or %s found in %s", INFISICAL_WORKSPACE_CONFIG_FILE_NAME, LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME, configFileDir) } - legacyWorkspaceConfigReminderShown = true - PrintWarning(fmt.Sprintf("Both %s and %s exist in %s. %s takes precedence, so the legacy %s is no longer used and you can remove it.", INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, INFISICAL_WORKSPACE_CONFIG_FILE_NAME, configFileDir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, jsonConfigFilePath)) + return GetWorkspaceConfigByPath(jsonConfigFilePath) } func readWorkspaceConfigYamlIfExists(path string) (workspaceConfigFile models.WorkspaceConfigFile, exists bool, err error) { @@ -154,61 +107,59 @@ func readWorkspaceConfigYaml(path string) (models.WorkspaceConfigFile, error) { return workspaceConfigFromYaml(workspaceConfigFileYaml), nil } -func convertLegacyWorkspaceConfigToYaml(jsonConfigFilePath string, yamlConfigFilePath string) error { - if _, err := os.Stat(yamlConfigFilePath); err == nil { - return fmt.Errorf("convertLegacyWorkspaceConfigToYaml: %s already exists", yamlConfigFilePath) - } else if !os.IsNotExist(err) { - return fmt.Errorf("convertLegacyWorkspaceConfigToYaml: unable to check %s [err=%s]", yamlConfigFilePath, err) +// workspaceConfigFromYaml accepts both the current keys and the deprecated default-environment / +// default-secret-path / mappings ones. Each field is resolved on its own: environment wins over +// default-environment and secret-path wins over default-secret-path when set, and overrides.git-branch replaces +// the mappings when present, so a partial migration keeps the deprecated value for whichever field has not been +// moved yet. +func workspaceConfigFromYaml(workspaceConfigYaml models.WorkspaceConfigFileYaml) models.WorkspaceConfigFile { + secretsManagement := workspaceConfigYaml.SecretsManagement + workspaceConfigFile := models.WorkspaceConfigFile{ + WorkspaceId: secretsManagement.ProjectID, + DefaultEnvironment: secretsManagement.DefaultEnvironment, + GitBranchToEnvironmentMapping: secretsManagement.Mappings.GitBranchToEnvironment, + DefaultSecretPath: secretsManagement.DefaultSecretPath, + Domain: workspaceConfigYaml.General.Domain, } - legacyWorkspaceConfig, err := GetWorkspaceConfigByPath(jsonConfigFilePath) - if err != nil { - return err + if secretsManagement.Environment != "" { + workspaceConfigFile.DefaultEnvironment = secretsManagement.Environment } - yamlConfigFileAsBytes, err := yaml.Marshal(workspaceConfigToYaml(legacyWorkspaceConfig)) - if err != nil { - return fmt.Errorf("convertLegacyWorkspaceConfigToYaml: unable to marshal yaml [err=%s]", err) + if secretsManagement.SecretPath != "" { + workspaceConfigFile.DefaultSecretPath = secretsManagement.SecretPath } - // written atomically so a partially written .infisical.yaml is never left behind - if err := WriteFileAtomic(yamlConfigFilePath, yamlConfigFileAsBytes, 0600); err != nil { - return fmt.Errorf("convertLegacyWorkspaceConfigToYaml: unable to create %s [err=%s]", yamlConfigFilePath, err) + if len(secretsManagement.Overrides.GitBranch) > 0 { + workspaceConfigFile.GitBranchToEnvironmentMapping = gitBranchMapFromOverrides(secretsManagement.Overrides.GitBranch) } - if _, err := readWorkspaceConfigYaml(yamlConfigFilePath); err != nil { - os.Remove(yamlConfigFilePath) - return err - } - - return nil + return workspaceConfigFile } -func workspaceConfigToYaml(workspaceConfig models.WorkspaceConfigFile) models.WorkspaceConfigFileYaml { - var workspaceConfigYaml models.WorkspaceConfigFileYaml - workspaceConfigYaml.Version = INFISICAL_WORKSPACE_CONFIG_YAML_VERSION - workspaceConfigYaml.General.Domain = workspaceConfig.Domain - workspaceConfigYaml.SecretsManagement.ProjectID = workspaceConfig.WorkspaceId - workspaceConfigYaml.SecretsManagement.DefaultEnvironment = workspaceConfig.DefaultEnvironment - workspaceConfigYaml.SecretsManagement.DefaultSecretPath = workspaceConfig.DefaultSecretPath - workspaceConfigYaml.SecretsManagement.Mappings.GitBranchToEnvironment = workspaceConfig.GitBranchToEnvironmentMapping - return workspaceConfigYaml -} - -func workspaceConfigFromYaml(workspaceConfigYaml models.WorkspaceConfigFileYaml) models.WorkspaceConfigFile { - return models.WorkspaceConfigFile{ - WorkspaceId: workspaceConfigYaml.SecretsManagement.ProjectID, - DefaultEnvironment: workspaceConfigYaml.SecretsManagement.DefaultEnvironment, - GitBranchToEnvironmentMapping: workspaceConfigYaml.SecretsManagement.Mappings.GitBranchToEnvironment, - DefaultSecretPath: workspaceConfigYaml.SecretsManagement.DefaultSecretPath, - Domain: workspaceConfigYaml.General.Domain, +// gitBranchMapFromOverrides builds the in-memory branch map from the git-branch overrides list. Entries +// without a branch are skipped, and the first entry for a branch wins. +func gitBranchMapFromOverrides(gitBranchOverrides []models.GitBranchEnvironmentOverride) map[string]string { + if len(gitBranchOverrides) == 0 { + return nil + } + branchToEnvironment := make(map[string]string, len(gitBranchOverrides)) + for _, override := range gitBranchOverrides { + if override.Branch == "" { + continue + } + if _, exists := branchToEnvironment[override.Branch]; exists { + continue + } + branchToEnvironment[override.Branch] = override.Environment } + return branchToEnvironment } // findWorkspaceConfigFileInDir looks for a .infisical.yaml (or legacy .infisical.json) in exactly // the given directory, without walking up. The YAML file takes precedence. func findWorkspaceConfigFileInDir(dir string) (string, bool) { - for _, fileName := range []string{INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, INFISICAL_WORKSPACE_CONFIG_FILE_NAME} { + for _, fileName := range []string{INFISICAL_WORKSPACE_CONFIG_FILE_NAME, LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME} { path := filepath.Join(dir, fileName) if _, err := os.Stat(path); err == nil { return path, true @@ -217,25 +168,6 @@ func findWorkspaceConfigFileInDir(dir string) (string, bool) { return "", false } -// ResolveWorkspaceConfigFile returns the path of the workspace config file a command will read. -// With an explicit projectConfigDir (the --project-config-dir flag) only that directory is -// considered, matching GetWorkSpaceFromFilePath. Otherwise it walks up from the current directory. -func ResolveWorkspaceConfigFile(projectConfigDir string) (string, error) { - if projectConfigDir == "" { - return FindWorkspaceConfigFile() - } - - path, found := findWorkspaceConfigFileInDir(projectConfigDir) - if !found { - return "", fmt.Errorf("no %s or %s found in %s", INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, INFISICAL_WORKSPACE_CONFIG_FILE_NAME, projectConfigDir) - } - - if absPath, err := filepath.Abs(path); err == nil { - path = absPath - } - return path, nil -} - // FindWorkspaceConfigFile searches for a .infisical.yaml (or legacy .infisical.json) file in the current directory // and all parent directories. In each directory the YAML file takes precedence. func FindWorkspaceConfigFile() (string, error) { @@ -260,7 +192,7 @@ func FindWorkspaceConfigFile() (string, error) { } // file not found - return "", fmt.Errorf("file not found: %s or %s", INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + return "", fmt.Errorf("file not found: %s or %s", INFISICAL_WORKSPACE_CONFIG_FILE_NAME, LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME) } diff --git a/packages/util/config_test.go b/packages/util/config_test.go index a7f8fcc4..28e5ab20 100644 --- a/packages/util/config_test.go +++ b/packages/util/config_test.go @@ -1,12 +1,9 @@ package util import ( - "bytes" - "io" + "maps" "os" "path/filepath" - "runtime" - "strings" "testing" "github.com/Infisical/infisical-merge/packages/models" @@ -174,129 +171,10 @@ func fileExists(path string) bool { return err == nil } -// makeDirReadOnly blocks creating files in dir for the rest of the test, so a .infisical.yaml can't be written. -func makeDirReadOnly(t *testing.T, dir string) { - t.Helper() - if runtime.GOOS == "windows" || os.Geteuid() == 0 { - t.Skip("directory permissions don't block writes here") - } - if err := os.Chmod(dir, 0o500); err != nil { - t.Fatalf("chmod %s: %v", dir, err) - } - t.Cleanup(func() { _ = os.Chmod(dir, 0o700) }) -} - -// captureStderr sends what the CLI prints to stderr into the returned buffer for the rest of the test. -func captureStderr(t *testing.T) *bytes.Buffer { - t.Helper() - var buf bytes.Buffer - original := GetStderrWriter - GetStderrWriter = func() io.Writer { return &buf } - t.Cleanup(func() { GetStderrWriter = original }) - return &buf -} - -// enableWorkspaceConfigMessages turns the yaml messages on for the rest of the test, as the root command does -// outside --silent and structured output. -func enableWorkspaceConfigMessages(t *testing.T) { - t.Helper() - EnableWorkspaceConfigMessages() - t.Cleanup(func() { workspaceConfigMessagesEnabled = false }) -} - -// startNewRun forgets that the reminder to remove the legacy json was shown, as if the CLI was run again. -func startNewRun() { - legacyWorkspaceConfigReminderShown = false -} - -func TestLegacyWorkspaceConfigRemovalMessage(t *testing.T) { - dir := t.TempDir() - jsonPath := filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - yamlPath := filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) - writeTestFile(t, jsonPath, legacyWorkspaceJSON) - stderr := captureStderr(t) - enableWorkspaceConfigMessages(t) - startNewRun() - - readWorkspaceConfig := func(t *testing.T) string { - t.Helper() - stderr.Reset() - cfg, err := GetWorkSpaceFromFilePath(dir) - if err != nil { - t.Fatalf("GetWorkSpaceFromFilePath: %v", err) - } - assertFullWorkspaceConfig(t, cfg, "proj-123") - return stderr.String() - } - assertContains := func(t *testing.T, out string, wants ...string) { - t.Helper() - for _, want := range wants { - if !strings.Contains(out, want) { - t.Errorf("output missing %q, got:\n%s", want, out) - } - } - } - - bothFilesExist := []string{"Both .infisical.yaml and .infisical.json exist in " + dir, "the legacy " + jsonPath + " is no longer used and you can remove it"} - - // the run that creates the yaml says both files exist - out := readWorkspaceConfig(t) - assertContains(t, out, append([]string{"Created " + yamlPath}, bothFilesExist...)...) - if out := readWorkspaceConfig(t); out != "" { - t.Errorf("expected the message once per run, got on the second read:\n%s", out) - } - - // later runs keep saying it while both files exist, without creating the yaml again - for run := 2; run <= 3; run++ { - startNewRun() - out := readWorkspaceConfig(t) - assertContains(t, out, bothFilesExist...) - if strings.Contains(out, "Created ") { - t.Errorf("run %d: the yaml already exists, so it should not be created again, got:\n%s", run, out) - } - if out := readWorkspaceConfig(t); out != "" { - t.Errorf("run %d: expected the message once per run, got on the second read:\n%s", run, out) - } - } - - // nothing to say once the json is gone - if err := os.Remove(jsonPath); err != nil { - t.Fatalf("remove %s: %v", jsonPath, err) - } - startNewRun() - if out := readWorkspaceConfig(t); out != "" { - t.Errorf("expected no message once the legacy json is removed, got:\n%s", out) - } -} - -func TestLegacyWorkspaceConfigMessagesSilenced(t *testing.T) { - dir := t.TempDir() - jsonPath := filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - writeTestFile(t, jsonPath, legacyWorkspaceJSON) - // --silent and structured output leave the messages off - stderr := captureStderr(t) - - // the run that creates the yaml, then a later run where both files exist - for run := 1; run <= 2; run++ { - startNewRun() - cfg, err := GetWorkSpaceFromFilePath(dir) - if err != nil { - t.Fatalf("run %d: GetWorkSpaceFromFilePath: %v", run, err) - } - assertFullWorkspaceConfig(t, cfg, "proj-123") - if !fileExists(filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) { - t.Fatalf("run %d: the yaml should still be created in silent mode", run) - } - if out := stderr.String(); out != "" { - t.Errorf("run %d: expected no output in silent mode, got:\n%s", run, out) - } - } -} - func TestGetWorkSpaceFromFilePath(t *testing.T) { t.Run("reads yaml when only yaml exists", func(t *testing.T) { dir := t.TempDir() - writeTestFile(t, filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME), workspaceYAML) + writeTestFile(t, filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME), workspaceYAML) cfg, err := GetWorkSpaceFromFilePath(dir) if err != nil { @@ -305,10 +183,9 @@ func TestGetWorkSpaceFromFilePath(t *testing.T) { assertFullWorkspaceConfig(t, cfg, "proj-123") }) - t.Run("reads legacy json and creates a yaml beside it", func(t *testing.T) { + t.Run("reads legacy json when only json exists", func(t *testing.T) { dir := t.TempDir() - jsonPath := filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - yamlPath := filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) + jsonPath := filepath.Join(dir, LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME) writeTestFile(t, jsonPath, legacyWorkspaceJSON) cfg, err := GetWorkSpaceFromFilePath(dir) @@ -317,52 +194,19 @@ func TestGetWorkSpaceFromFilePath(t *testing.T) { } assertFullWorkspaceConfig(t, cfg, "proj-123") - yamlCfg, err := readWorkspaceConfigYaml(yamlPath) - if err != nil { - t.Fatalf("yaml should be created when reading the legacy json: %v", err) - } - assertFullWorkspaceConfig(t, yamlCfg, "proj-123") - - raw, err := os.ReadFile(jsonPath) - if err != nil { - t.Fatalf("legacy %s should not be removed: %v", jsonPath, err) - } - if string(raw) != legacyWorkspaceJSON { - t.Errorf("legacy json was modified:\n%s", raw) - } - - // later reads come from the yaml - cfg, err = GetWorkSpaceFromFilePath(dir) - if err != nil { - t.Fatalf("unexpected error on the second read: %v", err) - } - assertFullWorkspaceConfig(t, cfg, "proj-123") - }) - - t.Run("falls back to the legacy json when the yaml can't be created", func(t *testing.T) { - dir := t.TempDir() - jsonPath := filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - writeTestFile(t, jsonPath, legacyWorkspaceJSON) - makeDirReadOnly(t, dir) - - cfg, err := GetWorkSpaceFromFilePath(dir) - if err != nil { - t.Fatalf("a failed yaml creation should not fail the read: %v", err) - } - assertFullWorkspaceConfig(t, cfg, "proj-123") - if fileExists(filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) { - t.Error("yaml should not exist in a read-only directory") - } if !fileExists(jsonPath) { t.Errorf("legacy %s should not be removed", jsonPath) } + if fileExists(filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME)) { + t.Error("yaml should not be created when reading the legacy json") + } }) t.Run("prefers yaml and leaves json untouched when both exist", func(t *testing.T) { dir := t.TempDir() - jsonPath := filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + jsonPath := filepath.Join(dir, LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME) writeTestFile(t, jsonPath, `{"workspaceId":"from-json"}`) - writeTestFile(t, filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME), workspaceYAML) + writeTestFile(t, filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME), workspaceYAML) cfg, err := GetWorkSpaceFromFilePath(dir) if err != nil { @@ -376,7 +220,7 @@ func TestGetWorkSpaceFromFilePath(t *testing.T) { t.Run("malformed json returns an error and keeps the json", func(t *testing.T) { dir := t.TempDir() - jsonPath := filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + jsonPath := filepath.Join(dir, LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME) writeTestFile(t, jsonPath, `{not json`) if _, err := GetWorkSpaceFromFilePath(dir); err == nil { @@ -385,7 +229,7 @@ func TestGetWorkSpaceFromFilePath(t *testing.T) { if !fileExists(jsonPath) { t.Errorf("legacy %s should not be removed", jsonPath) } - if fileExists(filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) { + if fileExists(filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME)) { t.Error("yaml should not be created from malformed json") } }) @@ -397,167 +241,191 @@ func TestGetWorkSpaceFromFilePath(t *testing.T) { }) } -func TestGetWorkSpaceFromFileReadsParentLegacyConfig(t *testing.T) { - root := t.TempDir() - jsonPath := filepath.Join(root, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - writeTestFile(t, jsonPath, legacyWorkspaceJSON) - nested := filepath.Join(root, "a", "b") - if err := os.MkdirAll(nested, 0o700); err != nil { - t.Fatalf("mkdir: %v", err) - } - t.Chdir(nested) - - cfg, err := GetWorkSpaceFromFile() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertFullWorkspaceConfig(t, cfg, "proj-123") - if !fileExists(jsonPath) { - t.Errorf("legacy %s should not be removed", jsonPath) - } - yamlPath := filepath.Join(root, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) - if !fileExists(yamlPath) { - t.Error("yaml should be created next to the legacy json, not in the current directory") - } - if fileExists(filepath.Join(nested, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) { - t.Error("yaml should not be created in the current directory") - } - - found, err := FindWorkspaceConfigFile() - if err != nil { - t.Fatalf("FindWorkspaceConfigFile: %v", err) - } - if filepath.Base(found) != INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME { - t.Errorf("FindWorkspaceConfigFile = %q, want the created yaml file", found) - } -} +const workspaceYAMLCurrent = `general: + domain: https://eu.infisical.com +secrets-management: + project-id: proj-123 + environment: dev + secret-path: /backend + overrides: + git-branch: + - branch: main + environment: prod + - branch: staging + environment: staging +` -func TestConvertLegacyWorkspaceConfigToYaml(t *testing.T) { - t.Run("carries every field over and leaves the json in place", func(t *testing.T) { +func TestWorkspaceConfigYamlSchemas(t *testing.T) { + readYaml := func(t *testing.T, contents string) (models.WorkspaceConfigFile, error) { + t.Helper() dir := t.TempDir() - jsonPath := filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - yamlPath := filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) - writeTestFile(t, jsonPath, legacyWorkspaceJSON) + yamlPath := filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + writeTestFile(t, yamlPath, contents) + cfg, err := GetWorkSpaceFromFilePath(dir) - if err := convertLegacyWorkspaceConfigToYaml(jsonPath, yamlPath); err != nil { - t.Fatalf("convertLegacyWorkspaceConfigToYaml: %v", err) + raw, readErr := os.ReadFile(yamlPath) + if readErr != nil { + t.Fatalf("read %s: %v", yamlPath, readErr) } + if string(raw) != contents { + t.Errorf("%s should not be modified when read, got:\n%s", yamlPath, raw) + } + return cfg, err + } - cfg, err := readWorkspaceConfigYaml(yamlPath) + t.Run("reads the current keys", func(t *testing.T) { + cfg, err := readYaml(t, workspaceYAMLCurrent) if err != nil { - t.Fatalf("readWorkspaceConfigYaml: %v", err) + t.Fatalf("unexpected error: %v", err) } assertFullWorkspaceConfig(t, cfg, "proj-123") + if got := cfg.GitBranchToEnvironmentMapping["staging"]; got != "staging" { + t.Errorf("GitBranchToEnvironmentMapping[staging] = %q, want staging", got) + } + }) - raw, err := os.ReadFile(yamlPath) + t.Run("reads the deprecated default-environment, default-secret-path and mappings", func(t *testing.T) { + cfg, err := readYaml(t, workspaceYAML) if err != nil { - t.Fatalf("read %s: %v", yamlPath, err) - } - if !strings.HasPrefix(string(raw), "version: 1\n") { - t.Errorf("yaml should start with the config version, got:\n%s", raw) + t.Fatalf("unexpected error: %v", err) } + assertFullWorkspaceConfig(t, cfg, "proj-123") + }) - info, err := os.Stat(yamlPath) + t.Run("the current keys win over the deprecated ones", func(t *testing.T) { + cfg, err := readYaml(t, `secrets-management: + project-id: proj-123 + default-environment: old-default + default-secret-path: /old + mappings: + git-branch-to-environment: + main: old-main + feature: old-feature + environment: dev + secret-path: /backend + overrides: + git-branch: + - branch: main + environment: prod +`) if err != nil { - t.Fatalf("stat %s: %v", yamlPath, err) + t.Fatalf("unexpected error: %v", err) } - if runtime.GOOS != "windows" && info.Mode().Perm() != 0o600 { - t.Errorf("yaml perm = %o, want 600", info.Mode().Perm()) + if cfg.DefaultEnvironment != "dev" { + t.Errorf("DefaultEnvironment = %q, want dev", cfg.DefaultEnvironment) } - if !fileExists(jsonPath) { - t.Error("the legacy json should be left in place") + if cfg.DefaultSecretPath != "/backend" { + t.Errorf("DefaultSecretPath = %q, want /backend", cfg.DefaultSecretPath) + } + // the deprecated mappings are ignored, not merged with the overrides + want := map[string]string{"main": "prod"} + if !maps.Equal(cfg.GitBranchToEnvironmentMapping, want) { + t.Errorf("GitBranchToEnvironmentMapping = %v, want %v", cfg.GitBranchToEnvironmentMapping, want) } }) - t.Run("refuses to overwrite an existing yaml", func(t *testing.T) { - dir := t.TempDir() - jsonPath := filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - yamlPath := filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) - writeTestFile(t, jsonPath, legacyWorkspaceJSON) - const existingYAML = "secrets-management:\n project-id: proj-existing\n" - writeTestFile(t, yamlPath, existingYAML) - - if err := convertLegacyWorkspaceConfigToYaml(jsonPath, yamlPath); err == nil { - t.Fatal("expected an error when the yaml already exists") - } - raw, err := os.ReadFile(yamlPath) + t.Run("overrides alone keep the deprecated default-environment and default-secret-path", func(t *testing.T) { + cfg, err := readYaml(t, `secrets-management: + project-id: proj-123 + default-environment: prod + default-secret-path: /old + mappings: + git-branch-to-environment: + main: old-main + overrides: + git-branch: + - branch: feature/x + environment: staging +`) if err != nil { - t.Fatalf("read %s: %v", yamlPath, err) + t.Fatalf("unexpected error: %v", err) } - if string(raw) != existingYAML { - t.Errorf("existing yaml was modified:\n%s", raw) + if cfg.DefaultEnvironment != "prod" { + t.Errorf("DefaultEnvironment = %q, want prod", cfg.DefaultEnvironment) } - }) - - t.Run("invalid json leaves no yaml behind", func(t *testing.T) { - dir := t.TempDir() - jsonPath := filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - yamlPath := filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) - writeTestFile(t, jsonPath, "{not json") - - if err := convertLegacyWorkspaceConfigToYaml(jsonPath, yamlPath); err == nil { - t.Fatal("expected an error for invalid json") + if cfg.DefaultSecretPath != "/old" { + t.Errorf("DefaultSecretPath = %q, want /old", cfg.DefaultSecretPath) } - if fileExists(yamlPath) { - t.Error("no yaml should be written when the json cannot be read") + want := map[string]string{"feature/x": "staging"} + if !maps.Equal(cfg.GitBranchToEnvironmentMapping, want) { + t.Errorf("GitBranchToEnvironmentMapping = %v, want %v", cfg.GitBranchToEnvironmentMapping, want) } }) -} - -func TestResolveWorkspaceConfigFile(t *testing.T) { - t.Run("empty dir walks up from cwd", func(t *testing.T) { - root := t.TempDir() - jsonPath := filepath.Join(root, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - writeTestFile(t, jsonPath, legacyWorkspaceJSON) - nested := filepath.Join(root, "a", "b") - if err := os.MkdirAll(nested, 0o700); err != nil { - t.Fatalf("mkdir: %v", err) - } - t.Chdir(nested) - got, err := ResolveWorkspaceConfigFile("") + t.Run("environment and secret-path alone keep the deprecated mappings", func(t *testing.T) { + cfg, err := readYaml(t, `secrets-management: + project-id: proj-123 + default-environment: old-default + default-secret-path: /old + mappings: + git-branch-to-environment: + main: prod + environment: dev + secret-path: /backend +`) if err != nil { t.Fatalf("unexpected error: %v", err) } - if got != jsonPath { - t.Errorf("got %q, want %q", got, jsonPath) + if cfg.DefaultEnvironment != "dev" { + t.Errorf("DefaultEnvironment = %q, want dev", cfg.DefaultEnvironment) } - }) - - t.Run("explicit dir does not walk up", func(t *testing.T) { - root := t.TempDir() - writeTestFile(t, filepath.Join(root, INFISICAL_WORKSPACE_CONFIG_FILE_NAME), legacyWorkspaceJSON) - empty := filepath.Join(root, "empty") - if err := os.MkdirAll(empty, 0o700); err != nil { - t.Fatalf("mkdir: %v", err) + if cfg.DefaultSecretPath != "/backend" { + t.Errorf("DefaultSecretPath = %q, want /backend", cfg.DefaultSecretPath) } - t.Chdir(root) - - if _, err := ResolveWorkspaceConfigFile("empty"); err == nil { - t.Error("expected an error when the explicit dir has no config") + want := map[string]string{"main": "prod"} + if !maps.Equal(cfg.GitBranchToEnvironmentMapping, want) { + t.Errorf("GitBranchToEnvironmentMapping = %v, want %v", cfg.GitBranchToEnvironmentMapping, want) } }) - t.Run("explicit dir prefers yaml and returns an absolute path", func(t *testing.T) { - root := t.TempDir() - cfgDir := filepath.Join(root, "cfg") - if err := os.MkdirAll(cfgDir, 0o700); err != nil { - t.Fatalf("mkdir: %v", err) - } - writeTestFile(t, filepath.Join(cfgDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME), legacyWorkspaceJSON) - yamlPath := filepath.Join(cfgDir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) - writeTestFile(t, yamlPath, workspaceYAML) - t.Chdir(root) - - got, err := ResolveWorkspaceConfigFile("cfg") + t.Run("first override for a branch wins and entries without a branch are skipped", func(t *testing.T) { + cfg, err := readYaml(t, `secrets-management: + project-id: proj-123 + overrides: + git-branch: + - environment: orphan + - branch: main + environment: prod + - branch: main + environment: staging +`) if err != nil { t.Fatalf("unexpected error: %v", err) } - if !filepath.IsAbs(got) { - t.Errorf("expected an absolute path, got %q", got) - } - if filepath.Base(got) != INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME { - t.Errorf("expected the yaml to win, got %q", got) + want := map[string]string{"main": "prod"} + if !maps.Equal(cfg.GitBranchToEnvironmentMapping, want) { + t.Errorf("GitBranchToEnvironmentMapping = %v, want %v", cfg.GitBranchToEnvironmentMapping, want) } }) } + +func TestGetWorkSpaceFromFileReadsParentLegacyConfig(t *testing.T) { + root := t.TempDir() + jsonPath := filepath.Join(root, LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + writeTestFile(t, jsonPath, legacyWorkspaceJSON) + nested := filepath.Join(root, "a", "b") + if err := os.MkdirAll(nested, 0o700); err != nil { + t.Fatalf("mkdir: %v", err) + } + t.Chdir(nested) + + cfg, err := GetWorkSpaceFromFile() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + assertFullWorkspaceConfig(t, cfg, "proj-123") + if !fileExists(jsonPath) { + t.Errorf("legacy %s should not be removed", jsonPath) + } + if fileExists(filepath.Join(root, INFISICAL_WORKSPACE_CONFIG_FILE_NAME)) { + t.Error("yaml should not be created when reading the legacy json") + } + + found, err := FindWorkspaceConfigFile() + if err != nil { + t.Fatalf("FindWorkspaceConfigFile: %v", err) + } + if filepath.Base(found) != LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME { + t.Errorf("FindWorkspaceConfigFile = %q, want the legacy json file", found) + } +} diff --git a/packages/util/constants.go b/packages/util/constants.go index c94a0eda..59bd4eee 100644 --- a/packages/util/constants.go +++ b/packages/util/constants.go @@ -7,12 +7,15 @@ import ( ) const ( - CONFIG_FILE_NAME = "infisical-config.json" - CONFIG_FOLDER_NAME = ".infisical" - INFISICAL_DEFAULT_US_URL = "https://app.infisical.com" - INFISICAL_DEFAULT_EU_URL = "https://eu.infisical.com" - INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json" - INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME = ".infisical.yaml" + CONFIG_FILE_NAME = "infisical-config.json" + CONFIG_FOLDER_NAME = ".infisical" + INFISICAL_DEFAULT_US_URL = "https://app.infisical.com" + INFISICAL_DEFAULT_EU_URL = "https://eu.infisical.com" + + // Legacy workspace config file name, still supported for backwards compatibility. + LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json" + INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.yaml" + INFISICAL_WORKSPACE_CONFIG_YAML_VERSION = 1 INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN" INFISICAL_PROJECT_ID_NAME = "INFISICAL_PROJECT_ID" @@ -92,8 +95,7 @@ const ( KUBERNETES_SERVICE_ACCOUNT_CA_CERT_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token" - UPDATE_CHECK_CACHE_FILE_NAME = "update-check.json" - MIGRATION_NOTICE_CACHE_FILE_NAME = "migration-notices.json" + UPDATE_CHECK_CACHE_FILE_NAME = "update-check.json" ) var ( diff --git a/packages/util/helper.go b/packages/util/helper.go index b4c61e48..d6a87bed 100644 --- a/packages/util/helper.go +++ b/packages/util/helper.go @@ -557,7 +557,7 @@ func GetCmdFlagOrEnvWithDefaultValue(cmd *cobra.Command, flag string, envNames [ // ResolveEnvironmentName resolves the environment slug for a command, in order: // the --env flag (if explicitly set) > INFISICAL_ENVIRONMENT > .infisical.yaml -// (git-branch mapping, then default-environment) > the flag's own default value. +// (git-branch override or mapping, then the default environment) > the flag's own default value. // It keys off cmd.Flags().Changed rather than an empty-value check so env and workspace // are still consulted when --env is left at its default. func ResolveEnvironmentName(cmd *cobra.Command) string { @@ -579,7 +579,7 @@ func ResolveEnvironmentName(cmd *cobra.Command) string { // ResolveSecretPath resolves the secret path (folder) for a command, in order: // the --path flag (if explicitly set) > INFISICAL_SECRET_PATH > .infisical.yaml -// default-secret-path > the flag's own default value ("/"). +// secret-path (or the deprecated default-secret-path) > the flag's own default value ("/"). func ResolveSecretPath(cmd *cobra.Command) string { if cmd.Flags().Changed("path") { value, _ := cmd.Flags().GetString("path") @@ -597,7 +597,7 @@ func ResolveSecretPath(cmd *cobra.Command) string { // ResolveSecretPaths is ResolveSecretPath for commands whose --path flag can be repeated // (`run`). Explicit flags are returned as given; otherwise the single path from -// INFISICAL_SECRET_PATH or .infisical.json defaultSecretPath, then the flag's default. +// INFISICAL_SECRET_PATH or .infisical.yaml secret-path, then the flag's default. func ResolveSecretPaths(cmd *cobra.Command) []string { if cmd.Flags().Changed("path") { values, _ := cmd.Flags().GetStringArray("path") diff --git a/packages/util/migration_notice.go b/packages/util/migration_notice.go deleted file mode 100644 index 12823a04..00000000 --- a/packages/util/migration_notice.go +++ /dev/null @@ -1,114 +0,0 @@ -package util - -import ( - "encoding/json" - "fmt" - "io" - "maps" - "os" - "path/filepath" - "time" - - "github.com/fatih/color" -) - -const migrationNoticeCacheTTL = 24 * time.Hour - -type MigrationNotice struct { - ID string - Title string - Message string - DocsURL string -} - -type migrationNoticeCache struct { - LastShownTimes map[string]time.Time `json:"lastShownTimes"` -} - -// migrationNoticeCacheSingleton is the one cache map for this process. -var migrationNoticeCacheSingleton = migrationNoticeCache{LastShownTimes: map[string]time.Time{}} - -var migrationNoticeCachePath string - -func getMigrationNoticeCachePath() (string, error) { - homeDir, err := GetHomeDir() - if err != nil { - return "", err - } - return filepath.Join(homeDir, CONFIG_FOLDER_NAME, MIGRATION_NOTICE_CACHE_FILE_NAME), nil -} - -// readMigrationNoticeCache returns the process-wide cache. The LastShownTimes map is -// allocated once; a later read for the same path returns that map. A different cache -// path clears it and loads that file. A missing or unreadable file leaves the map empty. -func readMigrationNoticeCache() migrationNoticeCache { - path, err := getMigrationNoticeCachePath() - if err == nil && path == migrationNoticeCachePath { - return migrationNoticeCacheSingleton - } - - clear(migrationNoticeCacheSingleton.LastShownTimes) - migrationNoticeCachePath = path - if err != nil { - return migrationNoticeCacheSingleton - } - - data, readErr := os.ReadFile(path) - if readErr != nil { - return migrationNoticeCacheSingleton - } - - var decoded migrationNoticeCache - if json.Unmarshal(data, &decoded) != nil || decoded.LastShownTimes == nil { - return migrationNoticeCacheSingleton - } - maps.Copy(migrationNoticeCacheSingleton.LastShownTimes, decoded.LastShownTimes) - return migrationNoticeCacheSingleton -} - -// migrationNoticeRecentlyShown returns true if the notice with this ID was shown within the TTL. -func migrationNoticeRecentlyShown(id string) bool { - lastShownTime, ok := readMigrationNoticeCache().LastShownTimes[id] - return ok && time.Since(lastShownTime) < migrationNoticeCacheTTL -} - -// recordMigrationNoticeShown stamps the cache so the notice with this ID is throttled. -func recordMigrationNoticeShown(id string) { - path, err := getMigrationNoticeCachePath() - if err != nil { - return - } - if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { - return - } - cache := readMigrationNoticeCache() - cache.LastShownTimes[id] = time.Now() - data, err := json.Marshal(cache) - if err != nil { - return - } - // Best-effort write; a failure just means the notice may show again next run. - _ = WriteFileAtomic(path, data, 0o600) -} - -func DisplayMigrationNoticeWithWriter(notice MigrationNotice, w io.Writer) { - if os.Getenv("INFISICAL_DISABLE_MIGRATION_NOTICES") != "" { - return - } - if migrationNoticeRecentlyShown(notice.ID) { - return - } - - yellow := color.New(color.FgYellow).SprintFunc() - bold := color.New(color.FgYellow, color.Bold).SprintFunc() - - body := notice.Message - if notice.DocsURL != "" { - body += "\n\nYou can read more here: " + notice.DocsURL - } - - fmt.Fprintln(w, bold(notice.Title)) - fmt.Fprintln(w, yellow(body+"\n")) - - recordMigrationNoticeShown(notice.ID) -} diff --git a/packages/util/migration_notice_test.go b/packages/util/migration_notice_test.go deleted file mode 100644 index 3c3519c6..00000000 --- a/packages/util/migration_notice_test.go +++ /dev/null @@ -1,153 +0,0 @@ -package util - -import ( - "bytes" - "encoding/json" - "os" - "path/filepath" - "strings" - "testing" - "time" -) - -// setupMigrationNoticeTest points the home dir at a temp dir so the notice cache is isolated. -func setupMigrationNoticeTest(t *testing.T) { - t.Helper() - isolateHomeDir(t) - t.Setenv("INFISICAL_DISABLE_MIGRATION_NOTICES", "") -} - -func TestDisplayMigrationNotice(t *testing.T) { - t.Run("prints the title, message and docs link", func(t *testing.T) { - setupMigrationNoticeTest(t) - - var buf bytes.Buffer - DisplayMigrationNoticeWithWriter(MigrationNotice{ - ID: "test-notice", - Title: "Important: X is deprecated.", - Message: "What's happening: X goes away.\nWhat to do: use Y.", - DocsURL: "https://infisical.com/docs/x-to-y", - }, &buf) - - want := "Important: X is deprecated.\n" + - "What's happening: X goes away.\nWhat to do: use Y.\n\n" + - "You can read more here: https://infisical.com/docs/x-to-y\n\n" - if buf.String() != want { - t.Errorf("notice = %q, want %q", buf.String(), want) - } - if !fileExists(migrationNoticeCachePathForTest(t)) { - t.Error("expected the notice cache file to be written") - } - }) - - t.Run("omits the docs line without a docs link", func(t *testing.T) { - setupMigrationNoticeTest(t) - - var buf bytes.Buffer - DisplayMigrationNoticeWithWriter(MigrationNotice{ID: "test-notice", Title: "Important: X is deprecated.", Message: "What to do: use Y."}, &buf) - - if strings.Contains(buf.String(), "You can read more here:") { - t.Errorf("expected no docs line, got:\n%s", buf.String()) - } - }) - - t.Run("shown once per day per notice ID", func(t *testing.T) { - setupMigrationNoticeTest(t) - notice := MigrationNotice{ID: "first", Title: "first notice", Message: "first"} - - var buf bytes.Buffer - DisplayMigrationNoticeWithWriter(notice, &buf) - if buf.Len() == 0 { - t.Fatal("expected the notice on the first call") - } - - buf.Reset() - DisplayMigrationNoticeWithWriter(notice, &buf) - if buf.Len() != 0 { - t.Errorf("expected no notice within the TTL, got:\n%s", buf.String()) - } - - buf.Reset() - DisplayMigrationNoticeWithWriter(MigrationNotice{ID: "second", Title: "second notice", Message: "second"}, &buf) - if !strings.Contains(buf.String(), "second notice") { - t.Errorf("a different notice ID should not be throttled, got:\n%s", buf.String()) - } - - cache := readMigrationNoticeCache() - if _, ok := cache.LastShownTimes["first"]; !ok { - t.Error("recording the second notice should keep the first one's entry") - } - }) - - t.Run("keeps both notices in the same cache map", func(t *testing.T) { - setupMigrationNoticeTest(t) - - var buf bytes.Buffer - DisplayMigrationNoticeWithWriter(MigrationNotice{ID: "first", Title: "first notice", Message: "first"}, &buf) - DisplayMigrationNoticeWithWriter(MigrationNotice{ID: "second", Title: "second notice", Message: "second"}, &buf) - - cache := readMigrationNoticeCache() - if _, ok := cache.LastShownTimes["first"]; !ok { - t.Error("expected the first notice in the cache map") - } - if _, ok := cache.LastShownTimes["second"]; !ok { - t.Error("expected the second notice in the cache map") - } - if len(cache.LastShownTimes) != 2 { - t.Errorf("cache map length = %d, want 2", len(cache.LastShownTimes)) - } - }) - - t.Run("shown again after the TTL", func(t *testing.T) { - setupMigrationNoticeTest(t) - - cachePath := migrationNoticeCachePathForTest(t) - if err := os.MkdirAll(filepath.Dir(cachePath), 0o700); err != nil { - t.Fatalf("mkdir: %v", err) - } - data, err := json.Marshal(migrationNoticeCache{LastShownTimes: map[string]time.Time{"test-notice": time.Now().Add(-25 * time.Hour)}}) - if err != nil { - t.Fatalf("marshal: %v", err) - } - writeTestFile(t, cachePath, string(data)) - - var buf bytes.Buffer - DisplayMigrationNoticeWithWriter(MigrationNotice{ID: "test-notice", Title: "Important: X is deprecated.", Message: "use Y"}, &buf) - if !strings.Contains(buf.String(), "Important:") { - t.Errorf("expected the notice after the TTL expired, got:\n%s", buf.String()) - } - }) - - t.Run("a corrupt cache doesn't block the notice", func(t *testing.T) { - setupMigrationNoticeTest(t) - - cachePath := migrationNoticeCachePathForTest(t) - if err := os.MkdirAll(filepath.Dir(cachePath), 0o700); err != nil { - t.Fatalf("mkdir: %v", err) - } - writeTestFile(t, cachePath, "{not json") - - var buf bytes.Buffer - DisplayMigrationNoticeWithWriter(MigrationNotice{ID: "test-notice", Title: "Important: X is deprecated.", Message: "use Y"}, &buf) - if buf.Len() == 0 { - t.Error("expected the notice when the cache can't be parsed") - } - if !migrationNoticeRecentlyShown("test-notice") { - t.Error("expected the cache to be rewritten after showing the notice") - } - }) - - t.Run("disabled by INFISICAL_DISABLE_MIGRATION_NOTICES", func(t *testing.T) { - setupMigrationNoticeTest(t) - t.Setenv("INFISICAL_DISABLE_MIGRATION_NOTICES", "1") - - var buf bytes.Buffer - DisplayMigrationNoticeWithWriter(MigrationNotice{ID: "test-notice", Title: "Important: X is deprecated.", Message: "use Y"}, &buf) - if buf.Len() != 0 { - t.Errorf("expected no notice, got:\n%s", buf.String()) - } - if fileExists(migrationNoticeCachePathForTest(t)) { - t.Error("expected no notice cache file to be written") - } - }) -} diff --git a/packages/util/resolve_test.go b/packages/util/resolve_test.go index 94eae2bb..1d28c8e5 100644 --- a/packages/util/resolve_test.go +++ b/packages/util/resolve_test.go @@ -37,7 +37,7 @@ func writeWorkspaceYaml(t *testing.T, contents string) { t.Helper() dir := t.TempDir() t.Chdir(dir) - if err := os.WriteFile(filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME), []byte(contents), 0o600); err != nil { + if err := os.WriteFile(filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME), []byte(contents), 0o600); err != nil { t.Fatalf("write workspace yaml: %v", err) } } @@ -142,19 +142,118 @@ func TestResolveEnvironmentName(t *testing.T) { } }) - t.Run("legacy json is read and a yaml is created beside it", func(t *testing.T) { + t.Run("yaml environment used when flag and env unset", func(t *testing.T) { + writeWorkspaceYaml(t, "secrets-management:\n environment: fromyaml\n") + t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") + if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "fromyaml" { + t.Fatalf("got %q, want fromyaml", got) + } + }) + + t.Run("yaml environment wins over the deprecated default-environment", func(t *testing.T) { + writeWorkspaceYaml(t, "secrets-management:\n default-environment: fromdeprecated\n environment: fromyaml\n") + t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") + if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "fromyaml" { + t.Fatalf("got %q, want fromyaml", got) + } + }) + + t.Run("yaml git-branch override wins over environment", func(t *testing.T) { + mockCurrentBranch(t, "main") + writeWorkspaceYaml(t, `secrets-management: + environment: fromyaml + overrides: + git-branch: + - branch: main + environment: frombranch +`) + t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") + if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "frombranch" { + t.Fatalf("got %q, want frombranch", got) + } + }) + + t.Run("yaml environment used when the branch has no override", func(t *testing.T) { + mockCurrentBranch(t, "feature") + writeWorkspaceYaml(t, `secrets-management: + environment: fromyaml + overrides: + git-branch: + - branch: main + environment: frombranch +`) + t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") + if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "fromyaml" { + t.Fatalf("got %q, want fromyaml", got) + } + }) + + t.Run("yaml git-branch override matches the last segment of the branch", func(t *testing.T) { + mockCurrentBranch(t, "feature/login") + writeWorkspaceYaml(t, `secrets-management: + environment: fromyaml + overrides: + git-branch: + - branch: login + environment: frombranch +`) + t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") + if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "frombranch" { + t.Fatalf("got %q, want frombranch", got) + } + }) + + t.Run("yaml deprecated default-environment used when only overrides are set and the branch has no override", func(t *testing.T) { + mockCurrentBranch(t, "main") + writeWorkspaceYaml(t, `secrets-management: + default-environment: fromyaml + overrides: + git-branch: + - branch: feature/x + environment: frombranch +`) + t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") + if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "fromyaml" { + t.Fatalf("got %q, want fromyaml", got) + } + }) + + t.Run("deprecated yaml mapping still matches the last segment of the branch", func(t *testing.T) { + mockCurrentBranch(t, "feature/login") + writeWorkspaceYaml(t, `secrets-management: + default-environment: fromyaml + mappings: + git-branch-to-environment: + login: frombranch +`) + t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") + if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "frombranch" { + t.Fatalf("got %q, want frombranch", got) + } + }) + + t.Run("legacy json mapping still matches the last segment of the branch", func(t *testing.T) { + mockCurrentBranch(t, "feature/login") + writeWorkspace(t, `{"defaultEnvironment":"fromfile","gitBranchToEnvironmentMapping":{"login":"frombranch"}}`) + t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") + if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "frombranch" { + t.Fatalf("got %q, want frombranch", got) + } + }) + + t.Run("legacy json is read without being migrated", func(t *testing.T) { writeWorkspace(t, `{"defaultEnvironment":"fromfile"}`) t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "fromfile" { t.Fatalf("got %q, want fromfile", got) } - if !fileExists(INFISICAL_WORKSPACE_CONFIG_FILE_NAME) { - t.Fatalf("legacy %s should not be removed", INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + if !fileExists(LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME) { + t.Fatalf("legacy %s should not be removed", LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME) } - if !fileExists(INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) { - t.Fatalf("%s should be created", INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) + if fileExists(INFISICAL_WORKSPACE_CONFIG_FILE_NAME) { + t.Fatalf("%s should not be created", INFISICAL_WORKSPACE_CONFIG_FILE_NAME) } - // the second resolve reads the created yaml + // the second resolve still reads the legacy json if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "fromfile" { t.Fatalf("got %q on the second resolve, want fromfile", got) } @@ -162,7 +261,7 @@ func TestResolveEnvironmentName(t *testing.T) { t.Run("yaml file wins over legacy json in the same directory", func(t *testing.T) { writeWorkspaceYaml(t, "secrets-management:\n default-environment: fromyaml\n") - writeTestFile(t, INFISICAL_WORKSPACE_CONFIG_FILE_NAME, `{"defaultEnvironment":"fromjson"}`) + writeTestFile(t, LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME, `{"defaultEnvironment":"fromjson"}`) t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "fromyaml" { t.Fatalf("got %q, want fromyaml", got) @@ -231,7 +330,23 @@ func TestResolveSecretPath(t *testing.T) { } }) - t.Run("defaults to / when yaml file has no default-secret-path", func(t *testing.T) { + t.Run("yaml secret-path when flag and env unset", func(t *testing.T) { + writeWorkspaceYaml(t, "secrets-management:\n secret-path: /fromyaml\n") + t.Setenv(INFISICAL_SECRET_PATH_NAME, "") + if got := ResolveSecretPath(newResolveTestCmd(t)); got != "/fromyaml" { + t.Fatalf("got %q, want /fromyaml", got) + } + }) + + t.Run("yaml secret-path wins over the deprecated default-secret-path", func(t *testing.T) { + writeWorkspaceYaml(t, "secrets-management:\n default-secret-path: /fromdeprecated\n secret-path: /fromyaml\n") + t.Setenv(INFISICAL_SECRET_PATH_NAME, "") + if got := ResolveSecretPath(newResolveTestCmd(t)); got != "/fromyaml" { + t.Fatalf("got %q, want /fromyaml", got) + } + }) + + t.Run("defaults to / when yaml file has no secret path", func(t *testing.T) { writeWorkspaceYaml(t, "secrets-management:\n project-id: proj-123\n") t.Setenv(INFISICAL_SECRET_PATH_NAME, "") if got := ResolveSecretPath(newResolveTestCmd(t)); got != "/" { diff --git a/packages/util/workspace_config_migration_notice.go b/packages/util/workspace_config_migration_notice.go deleted file mode 100644 index 760f2eb6..00000000 --- a/packages/util/workspace_config_migration_notice.go +++ /dev/null @@ -1,59 +0,0 @@ -package util - -import ( - "fmt" - "io" - "os" - "path/filepath" -) - -const workspaceConfigMigrationNoticeID = "workspace-config-yaml" - -const workspaceConfigMigrationGuideURL = "https://infisical.com/docs/cli/project-config-migration" - -// DisplayWorkspaceConfigMigrationNoticeWithWriter prints a once-per-day notice when the -// workspace config in use is a legacy .infisical.json and the CLI can't create a .infisical.yaml -// next to it. It first tries to create the yaml itself, so the notice only shows when that fails -// (read-only directory, invalid json, ...). -// Stays quiet in --silent mode, and can be disabled with INFISICAL_DISABLE_WORKSPACE_CONFIG_MIGRATION_NOTICE. -// -// projectConfigDir is the value of --project-config-dir when the command defines it; when set, -// only that directory is considered so the notice names the same file the command reads. -func DisplayWorkspaceConfigMigrationNoticeWithWriter(isSilent bool, projectConfigDir string, w io.Writer) { - if isSilent { - return - } - if os.Getenv("INFISICAL_DISABLE_WORKSPACE_CONFIG_MIGRATION_NOTICE") != "" { - return - } - - // if the workspace config is not a legacy .infisical.json, don't print the notice - legacyConfigFilePath, err := ResolveWorkspaceConfigFile(projectConfigDir) - if err != nil || filepath.Base(legacyConfigFilePath) != INFISICAL_WORKSPACE_CONFIG_FILE_NAME { - return - } - - // nothing to tell the user once the yaml exists, whether created here or by another command - configFileDir := filepath.Dir(legacyConfigFilePath) - yamlConfigFilePath := filepath.Join(configFileDir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) - if createWorkspaceConfigYamlFromLegacy(legacyConfigFilePath, yamlConfigFilePath) { - return - } - if _, err := os.Stat(yamlConfigFilePath); err == nil { - return - } - - DisplayMigrationNoticeWithWriter(MigrationNotice{ - ID: workspaceConfigMigrationNoticeID, - Title: fmt.Sprintf("Important: unable to create %s. This project still uses the deprecated %s.", INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, INFISICAL_WORKSPACE_CONFIG_FILE_NAME), - Message: fmt.Sprintf( - "\nThe Infisical CLI is reading the legacy %s config file, and support for it will be removed in the near future.\n"+ - "Falling back to the legacy config file \"%s\".\n\n"+ - "The CLI attempts to create the %s config file next to it automatically, but could not write it to \"%s\".\n\n"+ - "What to do: make sure that the directory is writable and your \"%s\" file is valid JSON, then run any Infisical command again, or create \"%s\" there yourself.", - INFISICAL_WORKSPACE_CONFIG_FILE_NAME, legacyConfigFilePath, - INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, configFileDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, - ), - DocsURL: workspaceConfigMigrationGuideURL, - }, w) -} diff --git a/packages/util/workspace_config_migration_notice_test.go b/packages/util/workspace_config_migration_notice_test.go deleted file mode 100644 index dabc9ffe..00000000 --- a/packages/util/workspace_config_migration_notice_test.go +++ /dev/null @@ -1,250 +0,0 @@ -package util - -import ( - "bytes" - "os" - "path/filepath" - "strings" - "testing" -) - -// isolateHomeDir points the home dir at a temp dir. os.UserHomeDir reads HOME on Unix -// and USERPROFILE on Windows, so both are set. -func isolateHomeDir(t *testing.T) { - t.Helper() - home := t.TempDir() - t.Setenv("HOME", home) - t.Setenv("USERPROFILE", home) -} - -// setupWorkspaceConfigMigrationNoticeTest points the home dir at a temp dir so the notice cache is isolated, -// chdirs into a fresh project dir, and returns the project dir. -func setupWorkspaceConfigMigrationNoticeTest(t *testing.T) string { - t.Helper() - isolateHomeDir(t) - t.Setenv("INFISICAL_DISABLE_WORKSPACE_CONFIG_MIGRATION_NOTICE", "") - t.Setenv("INFISICAL_DISABLE_MIGRATION_NOTICES", "") - projectDir := t.TempDir() - t.Chdir(projectDir) - return projectDir -} - -func migrationNoticeCachePathForTest(t *testing.T) string { - t.Helper() - path, err := getMigrationNoticeCachePath() - if err != nil { - t.Fatalf("cache path: %v", err) - } - return path -} - -func TestWorkspaceConfigMigrationNoticeShownForLegacyJSON(t *testing.T) { - projectDir := setupWorkspaceConfigMigrationNoticeTest(t) - jsonPath := filepath.Join(projectDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - writeTestFile(t, jsonPath, legacyWorkspaceJSON) - makeDirReadOnly(t, projectDir) - - var buf bytes.Buffer - DisplayWorkspaceConfigMigrationNoticeWithWriter(false, "", &buf) - - out := buf.String() - for _, want := range []string{ - "Important: unable to create .infisical.yaml", - jsonPath, - "could not write it in " + projectDir, - workspaceConfigMigrationGuideURL, - } { - if !strings.Contains(out, want) { - t.Errorf("notice missing %q, got:\n%s", want, out) - } - } - if !fileExists(migrationNoticeCachePathForTest(t)) { - t.Error("expected the notice cache file to be written") - } - - buf.Reset() - DisplayWorkspaceConfigMigrationNoticeWithWriter(false, "", &buf) - if buf.Len() != 0 { - t.Errorf("expected no notice within the TTL, got:\n%s", buf.String()) - } -} - -func TestWorkspaceConfigMigrationNoticeShownForLegacyJSONInParentDir(t *testing.T) { - projectDir := setupWorkspaceConfigMigrationNoticeTest(t) - writeTestFile(t, filepath.Join(projectDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME), legacyWorkspaceJSON) - nested := filepath.Join(projectDir, "a", "b") - if err := os.MkdirAll(nested, 0o700); err != nil { - t.Fatalf("mkdir: %v", err) - } - t.Chdir(nested) - makeDirReadOnly(t, projectDir) - - var buf bytes.Buffer - DisplayWorkspaceConfigMigrationNoticeWithWriter(false, "", &buf) - if !strings.Contains(buf.String(), "Important:") { - t.Errorf("expected the notice for a legacy json in a parent dir, got:\n%s", buf.String()) - } -} - -func TestWorkspaceConfigMigrationNoticeCreatesYamlInsteadOfWarning(t *testing.T) { - projectDir := setupWorkspaceConfigMigrationNoticeTest(t) - jsonPath := filepath.Join(projectDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - writeTestFile(t, jsonPath, legacyWorkspaceJSON) - stderr := captureStderr(t) - enableWorkspaceConfigMessages(t) - startNewRun() - - var buf bytes.Buffer - DisplayWorkspaceConfigMigrationNoticeWithWriter(false, "", &buf) - if buf.Len() != 0 { - t.Errorf("expected no notice once the yaml is created, got:\n%s", buf.String()) - } - - // the command reading the new yaml in the same run must not repeat that the json can be removed - if _, err := GetWorkSpaceFromFile(); err != nil { - t.Fatalf("GetWorkSpaceFromFile: %v", err) - } - if got := strings.Count(stderr.String(), "you can remove"); got != 1 { - t.Errorf("expected the removal message once, got it %d times:\n%s", got, stderr.String()) - } - if fileExists(migrationNoticeCachePathForTest(t)) { - t.Error("expected no notice cache file to be written") - } - if !fileExists(filepath.Join(projectDir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) { - t.Error("expected the yaml to be created next to the legacy json") - } - if !fileExists(jsonPath) { - t.Errorf("legacy %s should not be removed", jsonPath) - } -} - -func TestWorkspaceConfigMigrationNoticeNotShown(t *testing.T) { - cases := []struct { - name string - setup func(t *testing.T, projectDir string) - isSilent bool - }{ - { - name: "yaml only", - setup: func(t *testing.T, projectDir string) { - writeTestFile(t, filepath.Join(projectDir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME), workspaceYAML) - }, - }, - { - name: "yaml takes precedence over json", - setup: func(t *testing.T, projectDir string) { - writeTestFile(t, filepath.Join(projectDir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME), workspaceYAML) - writeTestFile(t, filepath.Join(projectDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME), legacyWorkspaceJSON) - }, - }, - { - name: "no workspace config", - setup: func(t *testing.T, projectDir string) {}, - }, - { - name: "silent", - setup: func(t *testing.T, projectDir string) { - writeTestFile(t, filepath.Join(projectDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME), legacyWorkspaceJSON) - }, - isSilent: true, - }, - { - name: "disabled by env var", - setup: func(t *testing.T, projectDir string) { - writeTestFile(t, filepath.Join(projectDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME), legacyWorkspaceJSON) - t.Setenv("INFISICAL_DISABLE_WORKSPACE_CONFIG_MIGRATION_NOTICE", "1") - }, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - projectDir := setupWorkspaceConfigMigrationNoticeTest(t) - tc.setup(t, projectDir) - - var buf bytes.Buffer - DisplayWorkspaceConfigMigrationNoticeWithWriter(tc.isSilent, "", &buf) - if buf.Len() != 0 { - t.Errorf("expected no notice, got:\n%s", buf.String()) - } - if fileExists(migrationNoticeCachePathForTest(t)) { - t.Error("expected no notice cache file to be written") - } - }) - } -} - -// Reproduces: cwd apps/web, --project-config-dir ../../services/api, legacy json at the repo root. -// The notice must describe the file the run reads (services/api), not the one found walking up from cwd. -func TestWorkspaceConfigMigrationNoticeHonorsProjectConfigDir(t *testing.T) { - layout := func(t *testing.T) (repoDir string, apiDir string) { - t.Helper() - repoDir = setupWorkspaceConfigMigrationNoticeTest(t) - apiDir = filepath.Join(repoDir, "services", "api") - webDir := filepath.Join(repoDir, "apps", "web") - for _, dir := range []string{apiDir, webDir} { - if err := os.MkdirAll(dir, 0o700); err != nil { - t.Fatalf("mkdir: %v", err) - } - } - t.Chdir(webDir) - return repoDir, apiDir - } - projectConfigDir := filepath.Join("..", "..", "services", "api") - - t.Run("yaml in project config dir, legacy json above cwd", func(t *testing.T) { - repoDir, apiDir := layout(t) - writeTestFile(t, filepath.Join(repoDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME), legacyWorkspaceJSON) - writeTestFile(t, filepath.Join(apiDir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME), workspaceYAML) - - var buf bytes.Buffer - DisplayWorkspaceConfigMigrationNoticeWithWriter(false, projectConfigDir, &buf) - if buf.Len() != 0 { - t.Errorf("expected no notice when the run uses a yaml, got:\n%s", buf.String()) - } - if fileExists(migrationNoticeCachePathForTest(t)) { - t.Error("expected no notice cache file to be written") - } - if fileExists(filepath.Join(repoDir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) { - t.Error("the repo root json is not the file the run reads, so no yaml should be created for it") - } - }) - - t.Run("legacy json in project config dir, yaml above cwd", func(t *testing.T) { - repoDir, apiDir := layout(t) - writeTestFile(t, filepath.Join(repoDir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME), workspaceYAML) - apiJSONPath := filepath.Join(apiDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - writeTestFile(t, apiJSONPath, legacyWorkspaceJSON) - makeDirReadOnly(t, apiDir) - - var buf bytes.Buffer - DisplayWorkspaceConfigMigrationNoticeWithWriter(false, projectConfigDir, &buf) - out := buf.String() - for _, want := range []string{ - "Important: unable to create .infisical.yaml", - "File in use: " + apiJSONPath, - "could not write it in " + apiDir, - } { - if !strings.Contains(out, want) { - t.Errorf("notice missing %q, got:\n%s", want, out) - } - } - if strings.Contains(out, filepath.Join(repoDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME)) { - t.Errorf("notice must not name the repo root json, got:\n%s", out) - } - }) - - t.Run("empty project config dir, legacy json above cwd", func(t *testing.T) { - repoDir, _ := layout(t) - writeTestFile(t, filepath.Join(repoDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME), legacyWorkspaceJSON) - - var buf bytes.Buffer - DisplayWorkspaceConfigMigrationNoticeWithWriter(false, projectConfigDir, &buf) - if buf.Len() != 0 { - t.Errorf("expected no notice when the project config dir has no config, got:\n%s", buf.String()) - } - if fileExists(filepath.Join(repoDir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) { - t.Error("the repo root json is not the file the run reads, so no yaml should be created for it") - } - }) -}