Skip to content

Commit 8c0547c

Browse files
authored
Merge pull request #231 from compose-spec/add-project-name
2 parents c8389aa + 07ff73d commit 8c0547c

File tree

9 files changed

+105
-43
lines changed

9 files changed

+105
-43
lines changed

cli/options.go

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ import (
2121
"io/ioutil"
2222
"os"
2323
"path/filepath"
24-
"regexp"
2524
"strings"
2625

26+
"github.com/compose-spec/compose-go/consts"
2727
"github.com/compose-spec/compose-go/dotenv"
2828
"github.com/compose-spec/compose-go/errdefs"
2929
"github.com/compose-spec/compose-go/loader"
@@ -87,11 +87,11 @@ func WithConfigFileEnv(o *ProjectOptions) error {
8787
if len(o.ConfigPaths) > 0 {
8888
return nil
8989
}
90-
sep := o.Environment[ComposePathSeparator]
90+
sep := o.Environment[consts.ComposePathSeparator]
9191
if sep == "" {
9292
sep = string(os.PathListSeparator)
9393
}
94-
f, ok := o.Environment[ComposeFilePath]
94+
f, ok := o.Environment[consts.ComposeFilePath]
9595
if ok {
9696
paths, err := absolutePaths(strings.Split(f, sep))
9797
o.ConfigPaths = paths
@@ -276,12 +276,6 @@ var DefaultFileNames = []string{"compose.yaml", "compose.yml", "docker-compose.y
276276
// DefaultOverrideFileNames defines the Compose override file names for auto-discovery (in order of preference)
277277
var DefaultOverrideFileNames = []string{"compose.override.yml", "compose.override.yaml", "docker-compose.override.yml", "docker-compose.override.yaml"}
278278

279-
const (
280-
ComposeProjectName = "COMPOSE_PROJECT_NAME"
281-
ComposePathSeparator = "COMPOSE_PATH_SEPARATOR"
282-
ComposeFilePath = "COMPOSE_FILE"
283-
)
284-
285279
func (o ProjectOptions) GetWorkingDir() (string, error) {
286280
if o.WorkingDir != "" {
287281
return o.WorkingDir, nil
@@ -338,17 +332,7 @@ func ProjectFromOptions(options *ProjectOptions) (*types.Project, error) {
338332
return nil, err
339333
}
340334

341-
var nameLoadOpt = func(opts *loader.Options) {
342-
if options.Name != "" {
343-
opts.Name = options.Name
344-
} else if nameFromEnv, ok := options.Environment[ComposeProjectName]; ok && nameFromEnv != "" {
345-
opts.Name = nameFromEnv
346-
} else {
347-
opts.Name = filepath.Base(absWorkingDir)
348-
}
349-
opts.Name = normalizeName(opts.Name)
350-
}
351-
options.loadOptions = append(options.loadOptions, nameLoadOpt)
335+
options.loadOptions = append(options.loadOptions, withNamePrecedenceLoad(absWorkingDir, options))
352336

353337
project, err := loader.Load(types.ConfigDetails{
354338
ConfigFiles: configs,
@@ -363,11 +347,16 @@ func ProjectFromOptions(options *ProjectOptions) (*types.Project, error) {
363347
return project, nil
364348
}
365349

366-
func normalizeName(s string) string {
367-
r := regexp.MustCompile("[a-z0-9_-]")
368-
s = strings.ToLower(s)
369-
s = strings.Join(r.FindAllString(s, -1), "")
370-
return strings.TrimLeft(s, "_-")
350+
func withNamePrecedenceLoad(absWorkingDir string, options *ProjectOptions) func(*loader.Options) {
351+
return func(opts *loader.Options) {
352+
if options.Name != "" {
353+
opts.SetProjectName(options.Name, true)
354+
} else if nameFromEnv, ok := options.Environment[consts.ComposeProjectName]; ok && nameFromEnv != "" {
355+
opts.SetProjectName(nameFromEnv, true)
356+
} else {
357+
opts.SetProjectName(filepath.Base(absWorkingDir), false)
358+
}
359+
}
371360
}
372361

373362
// getConfigPathsFromOptions retrieves the config files for project based on project options

cli/options_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"path/filepath"
2323
"testing"
2424

25+
"github.com/compose-spec/compose-go/consts"
2526
"gotest.tools/v3/assert"
2627
)
2728

@@ -42,7 +43,7 @@ func TestProjectName(t *testing.T) {
4243
assert.Equal(t, p.Name, "42my_project_num")
4344

4445
opts, err = NewProjectOptions([]string{"testdata/simple/compose.yaml"}, WithEnv([]string{
45-
fmt.Sprintf("%s=%s", ComposeProjectName, "42my_project_env"),
46+
fmt.Sprintf("%s=%s", consts.ComposeProjectName, "42my_project_env"),
4647
}))
4748
assert.NilError(t, err)
4849
p, err = ProjectFromOptions(opts)
@@ -58,7 +59,7 @@ func TestProjectName(t *testing.T) {
5859
assert.Equal(t, p.Name, "my_project")
5960

6061
opts, err = NewProjectOptions([]string{"testdata/simple/compose.yaml"}, WithEnv([]string{
61-
fmt.Sprintf("%s=%s", ComposeProjectName, "-my_project"),
62+
fmt.Sprintf("%s=%s", consts.ComposeProjectName, "-my_project"),
6263
}))
6364
assert.NilError(t, err)
6465
p, err = ProjectFromOptions(opts)
@@ -74,7 +75,7 @@ func TestProjectName(t *testing.T) {
7475
assert.Equal(t, p.Name, "my_project")
7576

7677
opts, err = NewProjectOptions([]string{"testdata/simple/compose.yaml"}, WithEnv([]string{
77-
fmt.Sprintf("%s=%s", ComposeProjectName, "_my_project"),
78+
fmt.Sprintf("%s=%s", consts.ComposeProjectName, "_my_project"),
7879
}))
7980
assert.NilError(t, err)
8081
p, err = ProjectFromOptions(opts)

consts/consts.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
Copyright 2020 The Compose Specification Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package consts
18+
19+
const (
20+
ComposeProjectName = "COMPOSE_PROJECT_NAME"
21+
ComposePathSeparator = "COMPOSE_PATH_SEPARATOR"
22+
ComposeFilePath = "COMPOSE_FILE"
23+
)

loader/full-example.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
name: Full_Example_project_name
12
services:
23
foo:
34

loader/full-struct_test.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727

2828
func fullExampleConfig(workingDir, homeDir string) *types.Config {
2929
return &types.Config{
30+
Name: "full_example_project_name",
3031
Services: services(workingDir, homeDir),
3132
Networks: networks(),
3233
Volumes: volumes(),
@@ -214,7 +215,7 @@ func services(workingDir, homeDir string) []types.ServiceConfig {
214215
},
215216
Pid: "host",
216217
Ports: []types.ServicePortConfig{
217-
//"3000",
218+
// "3000",
218219
{
219220
Mode: "ingress",
220221
Target: 3000,
@@ -245,14 +246,14 @@ func services(workingDir, homeDir string) []types.ServiceConfig {
245246
Target: 3005,
246247
Protocol: "tcp",
247248
},
248-
//"8000:8000",
249+
// "8000:8000",
249250
{
250251
Mode: "ingress",
251252
Target: 8000,
252253
Published: "8000",
253254
Protocol: "tcp",
254255
},
255-
//"9090-9091:8080-8081",
256+
// "9090-9091:8080-8081",
256257
{
257258
Mode: "ingress",
258259
Target: 8080,
@@ -265,22 +266,22 @@ func services(workingDir, homeDir string) []types.ServiceConfig {
265266
Published: "9091",
266267
Protocol: "tcp",
267268
},
268-
//"49100:22",
269+
// "49100:22",
269270
{
270271
Mode: "ingress",
271272
Target: 22,
272273
Published: "49100",
273274
Protocol: "tcp",
274275
},
275-
//"127.0.0.1:8001:8001",
276+
// "127.0.0.1:8001:8001",
276277
{
277278
Mode: "ingress",
278279
HostIP: "127.0.0.1",
279280
Target: 8001,
280281
Published: "8001",
281282
Protocol: "tcp",
282283
},
283-
//"127.0.0.1:5000-5010:5000-5010",
284+
// "127.0.0.1:5000-5010:5000-5010",
284285
{
285286
Mode: "ingress",
286287
HostIP: "127.0.0.1",
@@ -560,7 +561,8 @@ func secrets(workingDir string) map[string]types.SecretConfig {
560561
}
561562

562563
func fullExampleYAML(workingDir, homeDir string) string {
563-
return fmt.Sprintf(`services:
564+
return fmt.Sprintf(`name: full_example_project_name
565+
services:
564566
foo:
565567
build:
566568
context: ./dir

loader/loader.go

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ import (
2323
"path"
2424
"path/filepath"
2525
"reflect"
26+
"regexp"
2627
"sort"
2728
"strconv"
2829
"strings"
2930
"time"
3031

32+
"github.com/compose-spec/compose-go/consts"
3133
"github.com/compose-spec/compose-go/dotenv"
3234
interp "github.com/compose-spec/compose-go/interpolation"
3335
"github.com/compose-spec/compose-go/schema"
@@ -59,8 +61,19 @@ type Options struct {
5961
Interpolate *interp.Options
6062
// Discard 'env_file' entries after resolving to 'environment' section
6163
discardEnvFiles bool
62-
// Set project name
63-
Name string
64+
// Set project projectName
65+
projectName string
66+
// Indicates when the projectName was imperatively set or guessed from path
67+
projectNameImperativelySet bool
68+
}
69+
70+
func (o *Options) SetProjectName(name string, imperativelySet bool) {
71+
o.projectName = normalizeProjectName(name)
72+
o.projectNameImperativelySet = imperativelySet
73+
}
74+
75+
func (o Options) GetProjectName() (string, bool) {
76+
return o.projectName, o.projectNameImperativelySet
6477
}
6578

6679
// serviceRef identifies a reference to a service. It's used to detect cyclic
@@ -193,8 +206,17 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
193206
s.EnvFile = newEnvFiles
194207
}
195208

209+
projectName, projectNameImperativelySet := opts.GetProjectName()
210+
model.Name = normalizeProjectName(model.Name)
211+
if !projectNameImperativelySet && model.Name != "" {
212+
projectName = model.Name
213+
}
214+
215+
if projectName != "" {
216+
configDetails.Environment[consts.ComposeProjectName] = projectName
217+
}
196218
project := &types.Project{
197-
Name: opts.Name,
219+
Name: projectName,
198220
WorkingDir: configDetails.WorkingDir,
199221
Services: model.Services,
200222
Networks: model.Networks,
@@ -222,6 +244,13 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
222244
return project, nil
223245
}
224246

247+
func normalizeProjectName(s string) string {
248+
r := regexp.MustCompile("[a-z0-9_-]")
249+
s = strings.ToLower(s)
250+
s = strings.Join(r.FindAllString(s, -1), "")
251+
return strings.TrimLeft(s, "_-")
252+
}
253+
225254
func parseConfig(b []byte, opts *Options) (map[string]interface{}, error) {
226255
yml, err := ParseYAML(b)
227256
if err != nil {
@@ -255,7 +284,14 @@ func loadSections(filename string, config map[string]interface{}, configDetails
255284
cfg := types.Config{
256285
Filename: filename,
257286
}
258-
287+
name := ""
288+
if n, ok := config["name"]; ok {
289+
name, ok = n.(string)
290+
if !ok {
291+
return nil, errors.New("project name must be a string")
292+
}
293+
}
294+
cfg.Name = name
259295
cfg.Services, err = LoadServices(filename, getSection(config, "services"), configDetails.WorkingDir, configDetails.LookupEnv, opts)
260296
if err != nil {
261297
return nil, err

loader/loader_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -912,20 +912,21 @@ func uint32Ptr(value uint32) *uint32 {
912912
}
913913

914914
func TestFullExample(t *testing.T) {
915-
bytes, err := ioutil.ReadFile("full-example.yml")
915+
b, err := ioutil.ReadFile("full-example.yml")
916916
assert.NilError(t, err)
917917

918918
homeDir, err := os.UserHomeDir()
919919
assert.NilError(t, err)
920920
env := map[string]string{"HOME": homeDir, "QUX": "qux_from_environment"}
921-
config, err := loadYAMLWithEnv(string(bytes), env)
921+
config, err := loadYAMLWithEnv(string(b), env)
922922
assert.NilError(t, err)
923923

924924
workingDir, err := os.Getwd()
925925
assert.NilError(t, err)
926926

927927
expectedConfig := fullExampleConfig(workingDir, homeDir)
928928

929+
assert.Check(t, is.DeepEqual(expectedConfig.Name, config.Name))
929930
assert.Check(t, is.DeepEqual(expectedConfig.Services, config.Services))
930931
assert.Check(t, is.DeepEqual(expectedConfig.Networks, config.Networks))
931932
assert.Check(t, is.DeepEqual(expectedConfig.Volumes, config.Volumes))

loader/merge.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ func merge(configs []*types.Config) (*types.Config, error) {
5353
base := configs[0]
5454
for _, override := range configs[1:] {
5555
var err error
56+
base.Name = mergeNames(base.Name, override.Name)
5657
base.Services, err = mergeServices(base.Services, override.Services)
5758
if err != nil {
5859
return base, errors.Wrapf(err, "cannot merge services from %s", override.Filename)
@@ -81,6 +82,13 @@ func merge(configs []*types.Config) (*types.Config, error) {
8182
return base, nil
8283
}
8384

85+
func mergeNames(base, override string) string {
86+
if override != "" {
87+
return override
88+
}
89+
return base
90+
}
91+
8492
func mergeServices(base, override []types.ServiceConfig) ([]types.ServiceConfig, error) {
8593
baseServices := mapByName(base)
8694
overrideServices := mapByName(override)
@@ -291,15 +299,15 @@ func mergeLoggingConfig(dst, src reflect.Value) error {
291299
return nil
292300
}
293301

294-
//nolint: unparam
302+
// nolint: unparam
295303
func mergeUlimitsConfig(dst, src reflect.Value) error {
296304
if src.Interface() != reflect.Zero(reflect.TypeOf(src.Interface())).Interface() {
297305
dst.Elem().Set(src.Elem())
298306
}
299307
return nil
300308
}
301309

302-
//nolint: unparam
310+
// nolint: unparam
303311
func mergeServiceNetworkConfig(dst, src reflect.Value) error {
304312
if src.Interface() != reflect.Zero(reflect.TypeOf(src.Interface())).Interface() {
305313
dst.Elem().FieldByName("Aliases").Set(src.Elem().FieldByName("Aliases"))

types/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ type ConfigFile struct {
4949
// Config is a full compose file configuration and model
5050
type Config struct {
5151
Filename string `yaml:"-" json:"-"`
52+
Name string `yaml:",omitempty" json:"name,omitempty"`
5253
Services Services `json:"services"`
5354
Networks Networks `yaml:",omitempty" json:"networks,omitempty"`
5455
Volumes Volumes `yaml:",omitempty" json:"volumes,omitempty"`

0 commit comments

Comments
 (0)