Skip to content

Commit f433aa8

Browse files
authored
Merge pull request #295 from gravitational/fred/env-loader/bugfixes-1
env-loader: bug fixes
2 parents cb392a5 + 7c3ac76 commit f433aa8

File tree

5 files changed

+198
-21
lines changed

5 files changed

+198
-21
lines changed

tools/env-loader/cmd/env-loader.go

+31-12
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ package main
1818

1919
import (
2020
"fmt"
21+
"io"
2122
"log"
2223
"maps"
24+
"os"
2325
"slices"
2426

2527
"github.com/alecthomas/kingpin/v2"
@@ -30,6 +32,9 @@ import (
3032

3133
const EnvVarPrefix = "ENV_LOADER_"
3234

35+
// This is a package-level var to assist with capturing stdout in tests
36+
var outputWriter io.Writer = os.Stdout
37+
3338
type config struct {
3439
EnvironmentsDirectory string
3540
Environment string
@@ -38,12 +43,12 @@ type config struct {
3843
Writer string
3944
}
4045

41-
func parseCLI() *config {
46+
func parseCLI(args []string) *config {
4247
c := &config{}
4348

4449
kingpin.Flag("environments-directory", "Path to the directory containing all environments, defaulting to the repo root").
4550
Short('d').
46-
Envar(EnvVarPrefix + "ENVIRONMENT").
51+
Envar(EnvVarPrefix + "ENVIRONMENTS_DIRECTORY").
4752
StringVar(&c.EnvironmentsDirectory)
4853

4954
kingpin.Flag("environment", "Name of the environment containing the values to load").
@@ -68,12 +73,11 @@ func parseCLI() *config {
6873
Default("dotenv").
6974
EnumVar(&c.Writer, slices.Collect(maps.Keys(writers.FromName))...)
7075

71-
kingpin.Parse()
72-
76+
kingpin.MustParse(kingpin.CommandLine.Parse(args))
7377
return c
7478
}
7579

76-
func run(c *config) error {
80+
func getRequestedEnvValues(c *config) (map[string]string, error) {
7781
// Load in values
7882
var envValues map[string]string
7983
var err error
@@ -84,28 +88,43 @@ func run(c *config) error {
8488
}
8589

8690
if err != nil {
87-
return trace.Wrap(err, "failed to load all environment values")
91+
return nil, trace.Wrap(err, "failed to load all environment values")
8892
}
8993

9094
// Filter out values not requested
91-
maps.DeleteFunc(envValues, func(key, _ string) bool {
92-
return !slices.Contains(c.Values, key)
93-
})
95+
if len(c.Values) > 0 {
96+
maps.DeleteFunc(envValues, func(key, _ string) bool {
97+
return !slices.Contains(c.Values, key)
98+
})
99+
}
100+
101+
return envValues, nil
102+
}
103+
104+
func run(c *config) error {
105+
envValues, err := getRequestedEnvValues(c)
106+
if err != nil {
107+
return trace.Wrap(err, "failed to get requested environment values")
108+
}
94109

95110
// Build the output string
96111
writer := writers.FromName[c.Writer]
97-
envValueOutput, err := writer.FormatEnvironmentValues(map[string]string{})
112+
envValueOutput, err := writer.FormatEnvironmentValues(envValues)
98113
if err != nil {
99114
return trace.Wrap(err, "failed to format output values with writer %q", c.Writer)
100115
}
101116

102117
// Write it to stdout
103-
fmt.Print(envValueOutput)
118+
_, err = fmt.Fprint(outputWriter, envValueOutput)
119+
if err != nil {
120+
return trace.Wrap(err, "failed to print output %q", envValueOutput)
121+
}
122+
104123
return nil
105124
}
106125

107126
func main() {
108-
c := parseCLI()
127+
c := parseCLI(os.Args[1:])
109128

110129
err := run(c)
111130
if err != nil {
+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"path/filepath"
6+
"slices"
7+
"testing"
8+
9+
"github.com/alecthomas/kingpin/v2"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestParseCli(t *testing.T) {
14+
parseCLI([]string{})
15+
16+
flags := kingpin.CommandLine.Model().Flags
17+
flagEnvVars := make([]string, 0, len(flags))
18+
for _, flag := range flags {
19+
if flag.Envar != "" {
20+
flagEnvVars = append(flagEnvVars, flag.Envar)
21+
}
22+
}
23+
24+
uniqueFlagEnvVars := slices.Compact(slices.Clone(flagEnvVars))
25+
26+
require.ElementsMatch(t, flagEnvVars, uniqueFlagEnvVars, "not all flag env vars are unique")
27+
}
28+
29+
func TestGetRequestedEnvValues(t *testing.T) {
30+
tests := []struct {
31+
desc string
32+
c *config
33+
expectedValues map[string]string
34+
}{
35+
{
36+
desc: "specific values",
37+
c: &config{
38+
EnvironmentsDirectory: filepath.Join("..", "pkg", "testdata", "repos", "basic repo", ".environments"),
39+
Environment: "env1",
40+
ValueSets: []string{
41+
"testing1",
42+
},
43+
Values: []string{
44+
"setLevel",
45+
"envLevelCommon1",
46+
},
47+
},
48+
expectedValues: map[string]string{
49+
"setLevel": "set level",
50+
"envLevelCommon1": "env level",
51+
},
52+
},
53+
{
54+
desc: "full value set",
55+
c: &config{
56+
EnvironmentsDirectory: filepath.Join("..", "pkg", "testdata", "repos", "basic repo", ".environments"),
57+
Environment: "env1",
58+
ValueSets: []string{
59+
"testing1",
60+
},
61+
},
62+
expectedValues: map[string]string{
63+
"setLevel": "set level",
64+
"setLevelCommon": "testing1 level",
65+
"envLevelCommon1": "env level",
66+
"envLevelCommon2": "set level",
67+
"topLevelCommon1": "top level",
68+
"topLevelCommon2": "env level",
69+
},
70+
},
71+
{
72+
desc: "specific env",
73+
c: &config{
74+
EnvironmentsDirectory: filepath.Join("..", "pkg", "testdata", "repos", "basic repo", ".environments"),
75+
Environment: "env1",
76+
},
77+
expectedValues: map[string]string{
78+
"envLevelCommon1": "env level",
79+
"envLevelCommon2": "env level",
80+
"topLevelCommon1": "top level",
81+
"topLevelCommon2": "env level",
82+
},
83+
},
84+
}
85+
86+
for _, test := range tests {
87+
actualValues, err := getRequestedEnvValues(test.c)
88+
require.NoError(t, err)
89+
require.EqualValues(t, test.expectedValues, actualValues)
90+
}
91+
}
92+
93+
func TestRun(t *testing.T) {
94+
tests := []struct {
95+
desc string
96+
c *config
97+
expectedOutput string
98+
}{
99+
{
100+
desc: "specific values",
101+
c: &config{
102+
EnvironmentsDirectory: filepath.Join("..", "pkg", "testdata", "repos", "basic repo", ".environments"),
103+
Environment: "env1",
104+
ValueSets: []string{
105+
"testing1",
106+
},
107+
Values: []string{
108+
"setLevel",
109+
"envLevelCommon1",
110+
},
111+
Writer: "dotenv",
112+
},
113+
expectedOutput: "envLevelCommon1=env level\nsetLevel=set level\n",
114+
},
115+
}
116+
117+
for _, test := range tests {
118+
// Setup to capture stdout
119+
var outputBytes bytes.Buffer
120+
outputWriter = &outputBytes
121+
122+
err := run(test.c)
123+
124+
output := outputBytes.String()
125+
126+
require.NoError(t, err)
127+
require.Equal(t, test.expectedOutput, output)
128+
}
129+
}

tools/env-loader/pkg/envloader.go

+27-9
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ const EnvironmentNameDirectorySeparator = "/"
3838
// value files.
3939
const CommonFileGlob = "common.*"
4040

41+
const gitFakeLinkFileIdentifier = "gitdir: "
42+
4143
func findGitRepoRoot() (string, error) {
4244
cwd, err := os.Getwd()
4345
if err != nil {
@@ -47,20 +49,37 @@ func findGitRepoRoot() (string, error) {
4749
// Walk upwards until a '.git' directory is found, or root is reached
4850
path := filepath.Clean(cwd)
4951
for {
50-
fileInfo, err := os.Lstat(filepath.Join(path, ".git"))
52+
gitFsObjectPath := filepath.Join(path, ".git")
53+
fileInfo, err := os.Lstat(gitFsObjectPath)
5154
// If failed to stat the fs object and it exists
5255
if err != nil && !os.IsNotExist(err) {
53-
return "", trace.Wrap(err, "failed to read file information for %q", path)
56+
return "", trace.Wrap(err, "failed to read file information for %q", gitFsObjectPath)
5457
}
5558

5659
// If the .git fs object was found and it is a directory
57-
if err == nil && fileInfo.IsDir() {
58-
absPath, err := filepath.Abs(path)
59-
if err != nil {
60-
return "", trace.Wrap(err, "failed to get absolute path for git repo at %q", path)
60+
if err == nil {
61+
isCurrentPathAGitDirectory := fileInfo.IsDir()
62+
63+
// Perform some rudimentary checking to see if the .git directory
64+
// exists elsewhere, as is the case with submodules:
65+
// https://git-scm.com/docs/git-init#Documentation/git-init.txt-code--separate-git-dircodeemltgit-dirgtem
66+
if fileInfo.Mode().IsRegular() {
67+
fileContents, err := os.ReadFile(gitFsObjectPath)
68+
if err != nil {
69+
return "", trace.Wrap(err, "failed to read .git file at %q", gitFsObjectPath)
70+
}
71+
72+
isCurrentPathAGitDirectory = strings.HasPrefix(string(fileContents), gitFakeLinkFileIdentifier)
6173
}
6274

63-
return absPath, nil
75+
if isCurrentPathAGitDirectory {
76+
absPath, err := filepath.Abs(path)
77+
if err != nil {
78+
return "", trace.Wrap(err, "failed to get absolute path for git repo at %q", path)
79+
}
80+
81+
return absPath, nil
82+
}
6483
}
6584

6685
// If the .git fs object was found and is not a directory, or it wasn't
@@ -90,8 +109,7 @@ func findCommonFilesInPath(basePath, relativeSubdirectoryPath string) ([]string,
90109
var commonFilePaths []string
91110
currentDirectoryPath := basePath
92111
for _, directoryNameToCheck := range subdirectoryNames {
93-
currentDirectoryPath := filepath.Join(currentDirectoryPath, directoryNameToCheck)
94-
112+
currentDirectoryPath = filepath.Join(currentDirectoryPath, directoryNameToCheck)
95113
fileInfo, err := os.Lstat(currentDirectoryPath)
96114
if err != nil {
97115
return nil, trace.Wrap(err, "failed to lstat %q", currentDirectoryPath)

tools/env-loader/pkg/envloader_test.go

+11
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ func ensureGitFSObjectsExist(t *testing.T) {
4545
return os.WriteFile(gitPath, nil, 0400)
4646
})
4747

48+
ensureGitFSObjectExist(t, "git repo with submodule", "directory", createGitFile)
49+
ensureGitFSObjectExist(t, filepath.Join("git repo with submodule", "submodule"), "file", func(gitPath string) error {
50+
// This path doesn't (currently) actually need to be created
51+
return os.WriteFile(gitPath, []byte("gitdir: ../.git/modules/submodule\n"), 0400)
52+
})
53+
4854
ensureGitFSObjectExist(t, "nested repos", "directory", createGitFile)
4955
ensureGitFSObjectExist(t, filepath.Join("nested repos", "subdirectory"), "directory", createGitFile)
5056
}
@@ -101,6 +107,11 @@ func TestFindGitRepoRoot(t *testing.T) {
101107
workingDirectory: filepath.Join("nested repos", "subdirectory"),
102108
expectedRoot: filepath.Join("nested repos", "subdirectory"),
103109
},
110+
{
111+
desc: "from submodule",
112+
workingDirectory: filepath.Join("git repo with submodule", "submodule"),
113+
expectedRoot: filepath.Join("git repo with submodule", "submodule"),
114+
},
104115
}
105116

106117
reposDirectory := getTestDataDir(t, "repos")

tools/env-loader/pkg/testdata/repos/git repo with submodule/submodule/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)