Skip to content

Commit

Permalink
Update spec and implement project name
Browse files Browse the repository at this point in the history
Signed-off-by: Ulysses Souza <ulyssessouza@gmail.com>
  • Loading branch information
ulyssessouza committed Feb 23, 2022
1 parent ad79316 commit a8ca8af
Show file tree
Hide file tree
Showing 10 changed files with 100 additions and 47 deletions.
39 changes: 14 additions & 25 deletions cli/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import (
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"

"github.com/compose-spec/compose-go/consts"
"github.com/compose-spec/compose-go/dotenv"
"github.com/compose-spec/compose-go/errdefs"
"github.com/compose-spec/compose-go/loader"
Expand Down Expand Up @@ -87,11 +87,11 @@ func WithConfigFileEnv(o *ProjectOptions) error {
if len(o.ConfigPaths) > 0 {
return nil
}
sep := o.Environment[ComposePathSeparator]
sep := o.Environment[consts.ComposePathSeparator]
if sep == "" {
sep = string(os.PathListSeparator)
}
f, ok := o.Environment[ComposeFilePath]
f, ok := o.Environment[consts.ComposeFilePath]
if ok {
paths, err := absolutePaths(strings.Split(f, sep))
o.ConfigPaths = paths
Expand Down Expand Up @@ -276,12 +276,6 @@ var DefaultFileNames = []string{"compose.yaml", "compose.yml", "docker-compose.y
// DefaultOverrideFileNames defines the Compose override file names for auto-discovery (in order of preference)
var DefaultOverrideFileNames = []string{"compose.override.yml", "compose.override.yaml", "docker-compose.override.yml", "docker-compose.override.yaml"}

const (
ComposeProjectName = "COMPOSE_PROJECT_NAME"
ComposePathSeparator = "COMPOSE_PATH_SEPARATOR"
ComposeFilePath = "COMPOSE_FILE"
)

func (o ProjectOptions) GetWorkingDir() (string, error) {
if o.WorkingDir != "" {
return o.WorkingDir, nil
Expand Down Expand Up @@ -338,17 +332,7 @@ func ProjectFromOptions(options *ProjectOptions) (*types.Project, error) {
return nil, err
}

var nameLoadOpt = func(opts *loader.Options) {
if options.Name != "" {
opts.Name = options.Name
} else if nameFromEnv, ok := options.Environment[ComposeProjectName]; ok && nameFromEnv != "" {
opts.Name = nameFromEnv
} else {
opts.Name = filepath.Base(absWorkingDir)
}
opts.Name = normalizeName(opts.Name)
}
options.loadOptions = append(options.loadOptions, nameLoadOpt)
options.loadOptions = append(options.loadOptions, withNamePrecedenceLoad(absWorkingDir, options))

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

func normalizeName(s string) string {
r := regexp.MustCompile("[a-z0-9_-]")
s = strings.ToLower(s)
s = strings.Join(r.FindAllString(s, -1), "")
return strings.TrimLeft(s, "_-")
func withNamePrecedenceLoad(absWorkingDir string, options *ProjectOptions) func(*loader.Options) {
return func(opts *loader.Options) {
if options.Name != "" {
opts.SetProjectName(options.Name, true)
} else if nameFromEnv, ok := options.Environment[consts.ComposeProjectName]; ok && nameFromEnv != "" {
opts.SetProjectName(nameFromEnv, true)
} else {
opts.SetProjectName(filepath.Base(absWorkingDir), false)
}
}
}

// getConfigPathsFromOptions retrieves the config files for project based on project options
Expand Down
7 changes: 4 additions & 3 deletions cli/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"path/filepath"
"testing"

"github.com/compose-spec/compose-go/consts"
"gotest.tools/v3/assert"
)

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

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

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

opts, err = NewProjectOptions([]string{"testdata/simple/compose.yaml"}, WithEnv([]string{
fmt.Sprintf("%s=%s", ComposeProjectName, "_my_project"),
fmt.Sprintf("%s=%s", consts.ComposeProjectName, "_my_project"),
}))
assert.NilError(t, err)
p, err = ProjectFromOptions(opts)
Expand Down
7 changes: 7 additions & 0 deletions consts/consts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package consts

const (
ComposeProjectName = "COMPOSE_PROJECT_NAME"
ComposePathSeparator = "COMPOSE_PATH_SEPARATOR"
ComposeFilePath = "COMPOSE_FILE"
)
1 change: 1 addition & 0 deletions loader/full-example.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
name: Full_Example_project_name
services:
foo:

Expand Down
16 changes: 9 additions & 7 deletions loader/full-struct_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (

func fullExampleConfig(workingDir, homeDir string) *types.Config {
return &types.Config{
Name: "full_example_project_name",
Services: services(workingDir, homeDir),
Networks: networks(),
Volumes: volumes(),
Expand Down Expand Up @@ -214,7 +215,7 @@ func services(workingDir, homeDir string) []types.ServiceConfig {
},
Pid: "host",
Ports: []types.ServicePortConfig{
//"3000",
// "3000",
{
Mode: "ingress",
Target: 3000,
Expand Down Expand Up @@ -245,14 +246,14 @@ func services(workingDir, homeDir string) []types.ServiceConfig {
Target: 3005,
Protocol: "tcp",
},
//"8000:8000",
// "8000:8000",
{
Mode: "ingress",
Target: 8000,
Published: 8000,
Protocol: "tcp",
},
//"9090-9091:8080-8081",
// "9090-9091:8080-8081",
{
Mode: "ingress",
Target: 8080,
Expand All @@ -265,22 +266,22 @@ func services(workingDir, homeDir string) []types.ServiceConfig {
Published: 9091,
Protocol: "tcp",
},
//"49100:22",
// "49100:22",
{
Mode: "ingress",
Target: 22,
Published: 49100,
Protocol: "tcp",
},
//"127.0.0.1:8001:8001",
// "127.0.0.1:8001:8001",
{
Mode: "ingress",
HostIP: "127.0.0.1",
Target: 8001,
Published: 8001,
Protocol: "tcp",
},
//"127.0.0.1:5000-5010:5000-5010",
// "127.0.0.1:5000-5010:5000-5010",
{
Mode: "ingress",
HostIP: "127.0.0.1",
Expand Down Expand Up @@ -560,7 +561,8 @@ func secrets(workingDir string) map[string]types.SecretConfig {
}

func fullExampleYAML(workingDir, homeDir string) string {
return fmt.Sprintf(`services:
return fmt.Sprintf(`name: full_example_project_name
services:
foo:
build:
context: ./dir
Expand Down
44 changes: 40 additions & 4 deletions loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ import (
"path"
"path/filepath"
"reflect"
"regexp"
"sort"
"strings"
"time"

"github.com/compose-spec/compose-go/consts"
"github.com/compose-spec/compose-go/dotenv"
interp "github.com/compose-spec/compose-go/interpolation"
"github.com/compose-spec/compose-go/schema"
Expand Down Expand Up @@ -58,8 +60,19 @@ type Options struct {
Interpolate *interp.Options
// Discard 'env_file' entries after resolving to 'environment' section
discardEnvFiles bool
// Set project name
Name string
// Set project projectName
projectName string
// Indicates when the projectName was imperatively set or guessed from path
projectNameImperativelySet bool
}

func (o *Options) SetProjectName(name string, imperativelySet bool) {
o.projectName = normalizeProjectName(name)
o.projectNameImperativelySet = imperativelySet
}

func (o Options) GetProjectName() (string, bool) {
return o.projectName, o.projectNameImperativelySet
}

// serviceRef identifies a reference to a service. It's used to detect cyclic
Expand Down Expand Up @@ -192,8 +205,17 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
s.EnvFile = newEnvFiles
}

projectName, projectNameImperativelySet := opts.GetProjectName()
model.Name = normalizeProjectName(model.Name)
if !projectNameImperativelySet && model.Name != "" {
projectName = model.Name
}

if projectName != "" {
configDetails.Environment[consts.ComposeProjectName] = projectName
}
project := &types.Project{
Name: opts.Name,
Name: projectName,
WorkingDir: configDetails.WorkingDir,
Services: model.Services,
Networks: model.Networks,
Expand Down Expand Up @@ -221,6 +243,13 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
return project, nil
}

func normalizeProjectName(s string) string {
r := regexp.MustCompile("[a-z0-9_-]")
s = strings.ToLower(s)
s = strings.Join(r.FindAllString(s, -1), "")
return strings.TrimLeft(s, "_-")
}

func parseConfig(b []byte, opts *Options) (map[string]interface{}, error) {
yml, err := ParseYAML(b)
if err != nil {
Expand Down Expand Up @@ -254,7 +283,14 @@ func loadSections(filename string, config map[string]interface{}, configDetails
cfg := types.Config{
Filename: filename,
}

name := ""
if n, ok := config["name"]; ok {
name, ok = n.(string)
if !ok {
return nil, errors.New("project name must be a string")
}
}
cfg.Name = name
cfg.Services, err = LoadServices(filename, getSection(config, "services"), configDetails.WorkingDir, configDetails.LookupEnv, opts)
if err != nil {
return nil, err
Expand Down
5 changes: 3 additions & 2 deletions loader/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -913,20 +913,21 @@ func uint32Ptr(value uint32) *uint32 {
}

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

homeDir, err := os.UserHomeDir()
assert.NilError(t, err)
env := map[string]string{"HOME": homeDir, "QUX": "qux_from_environment"}
config, err := loadYAMLWithEnv(string(bytes), env)
config, err := loadYAMLWithEnv(string(b), env)
assert.NilError(t, err)

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

expectedConfig := fullExampleConfig(workingDir, homeDir)

assert.Check(t, is.DeepEqual(expectedConfig.Name, config.Name))
assert.Check(t, is.DeepEqual(expectedConfig.Services, config.Services))
assert.Check(t, is.DeepEqual(expectedConfig.Networks, config.Networks))
assert.Check(t, is.DeepEqual(expectedConfig.Volumes, config.Volumes))
Expand Down
12 changes: 10 additions & 2 deletions loader/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ func merge(configs []*types.Config) (*types.Config, error) {
base := configs[0]
for _, override := range configs[1:] {
var err error
base.Name = mergeNames(base.Name, override.Name)
base.Services, err = mergeServices(base.Services, override.Services)
if err != nil {
return base, errors.Wrapf(err, "cannot merge services from %s", override.Filename)
Expand Down Expand Up @@ -81,6 +82,13 @@ func merge(configs []*types.Config) (*types.Config, error) {
return base, nil
}

func mergeNames(base, override string) string {
if override != "" {
return override
}
return base
}

func mergeServices(base, override []types.ServiceConfig) ([]types.ServiceConfig, error) {
baseServices := mapByName(base)
overrideServices := mapByName(override)
Expand Down Expand Up @@ -291,15 +299,15 @@ func mergeLoggingConfig(dst, src reflect.Value) error {
return nil
}

//nolint: unparam
// nolint: unparam
func mergeUlimitsConfig(dst, src reflect.Value) error {
if src.Interface() != reflect.Zero(reflect.TypeOf(src.Interface())).Interface() {
dst.Elem().Set(src.Elem())
}
return nil
}

//nolint: unparam
// nolint: unparam
func mergeServiceNetworkConfig(dst, src reflect.Value) error {
if src.Interface() != reflect.Zero(reflect.TypeOf(src.Interface())).Interface() {
dst.Elem().FieldByName("Aliases").Set(src.Elem().FieldByName("Aliases"))
Expand Down
15 changes: 11 additions & 4 deletions schema/compose-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
"properties": {
"version": {
"type": "string",
"description": "Version of the Compose specification used. Tools not implementing required version MUST reject the configuration file."
"description": "declared for backward compatibility, ignored."
},

"name": {
"type": "string",
"description": "define the Compose project name, until user defines one explicitly."
},

"services": {
Expand Down Expand Up @@ -318,7 +323,7 @@
"mode": {"type": "string"},
"host_ip": {"type": "string"},
"target": {"type": "integer"},
"published": {"type": "integer"},
"published": {"type": ["string", "integer"]},
"protocol": {"type": "string"}
},
"additionalProperties": false,
Expand Down Expand Up @@ -410,7 +415,8 @@
"type": "object",
"properties": {
"propagation": {"type": "string"},
"create_host_path": {"type": "boolean"}
"create_host_path": {"type": "boolean"},
"selinux": {"type": "string", "enum": ["z", "Z"]}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
Expand Down Expand Up @@ -519,7 +525,8 @@
"type": "object",
"properties": {
"cpus": {"type": ["number", "string"]},
"memory": {"type": "string"}
"memory": {"type": "string"},
"pids": {"type": "integer"}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
Expand Down
1 change: 1 addition & 0 deletions types/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type ConfigFile struct {
// Config is a full compose file configuration and model
type Config struct {
Filename string `yaml:"-" json:"-"`
Name string `yaml:",omitempty" json:"name,omitempty"`
Services Services `json:"services"`
Networks Networks `yaml:",omitempty" json:"networks,omitempty"`
Volumes Volumes `yaml:",omitempty" json:"volumes,omitempty"`
Expand Down

0 comments on commit a8ca8af

Please sign in to comment.