Skip to content

Commit 684383f

Browse files
authored
Merge pull request #364 from mbland/project-name-uniform-enforcement
2 parents 882507f + c47b568 commit 684383f

File tree

10 files changed

+153
-25
lines changed

10 files changed

+153
-25
lines changed

cli/options.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package cli
1818

1919
import (
2020
"bytes"
21-
"fmt"
2221
"io"
2322
"os"
2423
"path/filepath"
@@ -64,9 +63,9 @@ func NewProjectOptions(configs []string, opts ...ProjectOptionsFn) (*ProjectOpti
6463
// WithName defines ProjectOptions' name
6564
func WithName(name string) ProjectOptionsFn {
6665
return func(o *ProjectOptions) error {
67-
if name != loader.NormalizeProjectName(name) {
68-
return fmt.Errorf("%q is not a valid project name: it must contain "+
69-
"only characters from [a-z0-9_-] and start with [a-z0-9]", name)
66+
normalized := loader.NormalizeProjectName(name)
67+
if err := loader.CheckOriginalProjectNameIsNormalized(name, normalized); err != nil {
68+
return err
7069
}
7170
o.Name = name
7271
return nil

cli/options_test.go

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,23 @@ func TestProjectName(t *testing.T) {
5252
assert.Equal(t, p.Name, "42my_project_env")
5353
})
5454

55+
t.Run("by name must not be empty", func(t *testing.T) {
56+
_, err := NewProjectOptions([]string{"testdata/simple/compose.yaml"}, WithName(""))
57+
assert.ErrorContains(t, err, `project name must not be empty`)
58+
})
59+
60+
t.Run("by name must not come from root directory", func(t *testing.T) {
61+
opts, err := NewProjectOptions([]string{"testdata/simple/compose.yaml"},
62+
WithWorkingDirectory("/"))
63+
assert.NilError(t, err)
64+
p, err := ProjectFromOptions(opts)
65+
66+
// On macOS and Linux, the message will start with "/". On Windows, it will
67+
// start with "\\\\". So we leave that part of the error off here.
68+
assert.ErrorContains(t, err, `is not a valid project name`)
69+
assert.Assert(t, p == nil)
70+
})
71+
5572
t.Run("by name start with invalid char '-'", func(t *testing.T) {
5673
_, err := NewProjectOptions([]string{"testdata/simple/compose.yaml"}, WithName("-my_project"))
5774
assert.ErrorContains(t, err, `"-my_project" is not a valid project name`)
@@ -61,8 +78,8 @@ func TestProjectName(t *testing.T) {
6178
}))
6279
assert.NilError(t, err)
6380
p, err := ProjectFromOptions(opts)
64-
assert.NilError(t, err)
65-
assert.Equal(t, p.Name, "my_project")
81+
assert.ErrorContains(t, err, `"-my_project" is not a valid project name`)
82+
assert.Assert(t, p == nil)
6683
})
6784

6885
t.Run("by name start with invalid char '_'", func(t *testing.T) {
@@ -74,8 +91,8 @@ func TestProjectName(t *testing.T) {
7491
}))
7592
assert.NilError(t, err)
7693
p, err := ProjectFromOptions(opts)
77-
assert.NilError(t, err)
78-
assert.Equal(t, p.Name, "my_project")
94+
assert.ErrorContains(t, err, `"_my_project" is not a valid project name`)
95+
assert.Assert(t, p == nil)
7996
})
8097

8198
t.Run("by name contains dots", func(t *testing.T) {
@@ -87,21 +104,21 @@ func TestProjectName(t *testing.T) {
87104
}))
88105
assert.NilError(t, err)
89106
p, err := ProjectFromOptions(opts)
90-
assert.NilError(t, err)
91-
assert.Equal(t, p.Name, "wwwmyproject")
107+
assert.ErrorContains(t, err, `"www.my.project" is not a valid project name`)
108+
assert.Assert(t, p == nil)
92109
})
93110

94111
t.Run("by name uppercase", func(t *testing.T) {
95112
_, err := NewProjectOptions([]string{"testdata/simple/compose.yaml"}, WithName("MY_PROJECT"))
96113
assert.ErrorContains(t, err, `"MY_PROJECT" is not a valid project name`)
97114

98115
opts, err := NewProjectOptions([]string{"testdata/simple/compose.yaml"}, WithEnv([]string{
99-
fmt.Sprintf("%s=%s", consts.ComposeProjectName, "_my_project"),
116+
fmt.Sprintf("%s=%s", consts.ComposeProjectName, "MY_PROJECT"),
100117
}))
101118
assert.NilError(t, err)
102119
p, err := ProjectFromOptions(opts)
103-
assert.NilError(t, err)
104-
assert.Equal(t, p.Name, "my_project")
120+
assert.ErrorContains(t, err, `"MY_PROJECT" is not a valid project name`)
121+
assert.Assert(t, p == nil)
105122
})
106123

107124
t.Run("by working dir", func(t *testing.T) {

cli/options_windows_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@ func TestConvertWithEnvVar(t *testing.T) {
2828
defer os.Unsetenv("COMPOSE_CONVERT_WINDOWS_PATHS")
2929
opts, _ := NewProjectOptions([]string{"testdata/simple/compose-with-paths.yaml"},
3030
WithOsEnv,
31-
WithWorkingDirectory("C:\\\\"))
31+
WithWorkingDirectory("C:\\project-dir\\"))
3232

3333
p, err := ProjectFromOptions(opts)
3434

3535
assert.NilError(t, err)
3636
assert.Equal(t, len(p.Services[0].Volumes), 3)
3737
assert.Equal(t, p.Services[0].Volumes[0].Source, "/c/docker/project")
38-
assert.Equal(t, p.Services[0].Volumes[1].Source, "/c/relative")
39-
assert.Equal(t, p.Services[0].Volumes[2].Source, "/c/relative2")
38+
assert.Equal(t, p.Services[0].Volumes[1].Source, "/c/project-dir/relative")
39+
assert.Equal(t, p.Services[0].Volumes[2].Source, "/c/project-dir/relative2")
4040
}

loader/full-struct_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,7 @@ func fullExampleJSON(workingDir, homeDir string) string {
10501050
"external": false
10511051
}
10521052
},
1053+
"name": "full_example_project_name",
10531054
"networks": {
10541055
"external-network": {
10551056
"name": "external-network",

loader/loader.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,16 @@ type Options struct {
6464
projectName string
6565
// Indicates when the projectName was imperatively set or guessed from path
6666
projectNameImperativelySet bool
67+
// The value of projectName before normalization
68+
projectNameBeforeNormalization string
6769
// Profiles set profiles to enable
6870
Profiles []string
6971
}
7072

7173
func (o *Options) SetProjectName(name string, imperativelySet bool) {
7274
o.projectName = NormalizeProjectName(name)
7375
o.projectNameImperativelySet = imperativelySet
76+
o.projectNameBeforeNormalization = name
7477
}
7578

7679
func (o Options) GetProjectName() (string, bool) {
@@ -264,8 +267,20 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
264267
return project, err
265268
}
266269

270+
func CheckOriginalProjectNameIsNormalized(original, normalized string) error {
271+
if original != normalized {
272+
return fmt.Errorf("%q is not a valid project name: it must contain only "+
273+
"characters from [a-z0-9_-] and start with [a-z0-9]", original)
274+
} else if normalized == "" {
275+
return fmt.Errorf("project name must not be empty")
276+
}
277+
return nil
278+
}
279+
267280
func projectName(details types.ConfigDetails, opts *Options) (string, error) {
268281
projectName, projectNameImperativelySet := opts.GetProjectName()
282+
projectNameBeforeNormalization := opts.projectNameBeforeNormalization
283+
269284
var pjNameFromConfigFile string
270285

271286
for _, configFile := range details.ConfigFiles {
@@ -284,9 +299,15 @@ func projectName(details types.ConfigDetails, opts *Options) (string, error) {
284299
}
285300
pjNameFromConfigFile = interpolated["name"].(string)
286301
}
287-
pjNameFromConfigFile = NormalizeProjectName(pjNameFromConfigFile)
288-
if !projectNameImperativelySet && pjNameFromConfigFile != "" {
289-
projectName = pjNameFromConfigFile
302+
pjNameFromConfigFileNormalized := NormalizeProjectName(pjNameFromConfigFile)
303+
if !projectNameImperativelySet && pjNameFromConfigFileNormalized != "" {
304+
projectName = pjNameFromConfigFileNormalized
305+
projectNameBeforeNormalization = pjNameFromConfigFile
306+
}
307+
308+
if err := CheckOriginalProjectNameIsNormalized(
309+
projectNameBeforeNormalization, projectName); err != nil {
310+
return "", err
290311
}
291312

292313
if _, ok := details.Environment[consts.ComposeProjectName]; !ok && projectName != "" {

0 commit comments

Comments
 (0)