diff --git a/ecs/go.mod b/ecs/go.mod index 573b02b4b..1189b1d57 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -35,6 +35,7 @@ require ( github.com/miekg/pkcs11 v1.0.3 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect + github.com/sirupsen/logrus v1.5.0 github.com/spf13/cobra v0.0.5 github.com/spf13/pflag v1.0.3 github.com/theupdateframework/notary v0.6.1 // indirect @@ -44,6 +45,7 @@ require ( gopkg.in/dancannon/gorethink.v3 v3.0.5 // indirect gopkg.in/fatih/pool.v2 v2.0.0 // indirect gopkg.in/gorethink/gorethink.v3 v3.0.5 // indirect + gotest.tools/v3 v3.0.2 vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 // indirect ) diff --git a/ecs/go.sum b/ecs/go.sum index 3bc9036db..f2d87a8ca 100644 --- a/ecs/go.sum +++ b/ecs/go.sum @@ -140,6 +140,7 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= @@ -180,6 +181,7 @@ github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-shellwords v1.0.9/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw= github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= @@ -192,6 +194,7 @@ github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WT github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4= github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -280,9 +283,12 @@ github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPU github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= github.com/weppos/publicsuffix-go v0.5.0 h1:rutRtjBJViU/YjcI5d80t4JAVvDltS6bciJg2K1HrLU= github.com/weppos/publicsuffix-go v0.5.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 h1:j2hhcujLRHAg872RWAV5yaUrEjHEObwDv3aImCaNLek= github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8= @@ -392,6 +398,7 @@ gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.0 h1:d+tVGRu6X0ZBQ+kyAR8JKi6AXhTP2gmQaoIYaGFz634= gotest.tools/v3 v3.0.0/go.mod h1:TUP+/YtXl/dp++T+SZ5v2zUmLVBHmptSb/ajDLCJ+3c= +gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/ecs/pkg/compose/opts.go b/ecs/pkg/compose/opts.go index 6755abc2b..9429fb01a 100644 --- a/ecs/pkg/compose/opts.go +++ b/ecs/pkg/compose/opts.go @@ -1,7 +1,7 @@ package compose import ( - _ "github.com/compose-spec/compose-go/types" + "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -14,3 +14,17 @@ func (o *ProjectOptions) AddFlags(flags *pflag.FlagSet) { flags.StringArrayVarP(&o.ConfigPaths, "file", "f", nil, "Specify an alternate compose file") flags.StringVarP(&o.name, "project-name", "n", "", "Specify an alternate project name (default: directory name)") } + + +type ProjectFunc func(project *Project, args []string) error + +// WithProject wrap a ProjectFunc into a cobra command +func WithProject(options *ProjectOptions, f ProjectFunc) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + project, err := projectFromOptions(options) + if err != nil { + return err + } + return f(project, args) + } +} diff --git a/ecs/pkg/compose/project.go b/ecs/pkg/compose/project.go new file mode 100644 index 000000000..c2c6c007a --- /dev/null +++ b/ecs/pkg/compose/project.go @@ -0,0 +1,165 @@ +package compose + +import ( + "fmt" + "github.com/compose-spec/compose-go/loader" + "github.com/compose-spec/compose-go/types" + "github.com/sirupsen/logrus" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strings" +) + +type Project struct { + types.Config + projectDir string + Name string `yaml:"-" json:"-"` +} + +func NewProject(config types.ConfigDetails, name string) (*Project, error) { + model, err := loader.Load(config) + if err != nil { + return nil, err + } + + p := Project{ + Config: *model, + projectDir: config.WorkingDir, + Name: name, + } + return &p, nil +} + + +// projectFromOptions load a compose project based on command line options +func projectFromOptions(options *ProjectOptions) (*Project, error) { + configPath, err := getConfigPathFromOptions(options) + if err != nil { + return nil, err + } + + name := options.name + if name == "" { + name = os.Getenv("COMPOSE_PROJECT_NAME") + } + + workingDir := filepath.Dir(configPath[0]) + + if name == "" { + r := regexp.MustCompile(`[^a-z0-9\\-_]+`) + name = r.ReplaceAllString(strings.ToLower(filepath.Base(workingDir)), "") + } + + configs, err := parseConfigs(configPath) + if err != nil { + return nil, err + } + + return NewProject(types.ConfigDetails{ + WorkingDir: workingDir, + ConfigFiles: configs, + Environment: environment(), + }, name) +} + +func getConfigPathFromOptions(options *ProjectOptions) ([]string, error) { + paths := []string{} + pwd, err := os.Getwd() + if err != nil { + return nil, err + } + + if len(options.ConfigPaths) != 0 { + for _, f := range options.ConfigPaths { + if f == "-" { + paths = append(paths, f) + continue + } + if !filepath.IsAbs(f) { + f = filepath.Join(pwd, f) + } + if _, err := os.Stat(f); err != nil { + return nil, err + } + paths = append(paths, f) + } + return paths, nil + } + + sep := os.Getenv("COMPOSE_FILE_SEPARATOR") + if sep == "" { + sep = string(os.PathListSeparator) + } + f := os.Getenv("COMPOSE_FILE") + if f != "" { + return strings.Split(f, sep), nil + } + + for { + candidates := []string{} + for _, n := range SupportedFilenames { + f := filepath.Join(pwd, n) + if _, err := os.Stat(f); err == nil { + candidates = append(candidates, f) + } + } + if len(candidates) > 0 { + winner := candidates[0] + if len(candidates) > 1 { + logrus.Warnf("Found multiple config files with supported names: %s", strings.Join(candidates, ", ")) + logrus.Warnf("Using %s\n", winner) + } + return []string{winner}, nil + } + parent := filepath.Dir(pwd) + if parent == pwd { + return nil, fmt.Errorf("Can't find a suitable configuration file in this directory or any parent. Are you in the right directory?") + } + pwd = parent + } +} + +var SupportedFilenames = []string{"compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml"} + +func parseConfigs(configPaths []string) ([]types.ConfigFile, error) { + files := []types.ConfigFile{} + for _, f := range configPaths { + var ( + b []byte + err error + ) + if f == "-" { + b, err = ioutil.ReadAll(os.Stdin) + } else { + if _, err := os.Stat(f); err != nil { + return nil, err + } + b, err = ioutil.ReadFile(f) + } + if err != nil { + return nil, err + } + config, err := loader.ParseYAML(b) + if err != nil { + return nil, err + } + files = append(files, types.ConfigFile{Filename: f, Config: config}) + } + return files, nil +} + +func environment() map[string]string { + return getAsEqualsMap(os.Environ()) +} + +// getAsEqualsMap split key=value formatted strings into a key : value map +func getAsEqualsMap(em []string) map[string]string { + m := make(map[string]string) + for _, v := range em { + kv := strings.SplitN(v, "=", 2) + m[kv[0]] = kv[1] + } + return m +} diff --git a/ecs/pkg/compose/project_test.go b/ecs/pkg/compose/project_test.go new file mode 100644 index 000000000..d5f771404 --- /dev/null +++ b/ecs/pkg/compose/project_test.go @@ -0,0 +1,45 @@ +package compose + +import ( + "gotest.tools/v3/assert" + "os" + "testing" +) + +func Test_project_name(t *testing.T) { + p, err := projectFromOptions(&ProjectOptions{ + name: "my_project", + ConfigPaths: []string{"testdata/simple/compose.yaml"}, + }) + assert.NilError(t, err) + assert.Equal(t, p.Name, "my_project") + + p, err = projectFromOptions(&ProjectOptions{ + name: "", + ConfigPaths: []string{"testdata/simple/compose.yaml"}, + }) + assert.NilError(t, err) + assert.Equal(t, p.Name, "simple") + + os.Setenv("COMPOSE_PROJECT_NAME", "my_project_from_env") + p, err = projectFromOptions(&ProjectOptions{ + name: "", + ConfigPaths: []string{"testdata/simple/compose.yaml"}, + }) + assert.NilError(t, err) + assert.Equal(t, p.Name, "my_project_from_env") +} + +func Test_project_from_set_of_files(t *testing.T) { + p, err := projectFromOptions(&ProjectOptions{ + name: "my_project", + ConfigPaths: []string{ + "testdata/simple/compose.yaml", + "testdata/simple/compose-with-overrides.yaml", + }, + }) + assert.NilError(t, err) + service, err := p.GetService("simple") + assert.NilError(t, err) + assert.Equal(t, service.Image, "haproxy") +} diff --git a/ecs/pkg/compose/testdata/simple/compose-with-overrides.yaml b/ecs/pkg/compose/testdata/simple/compose-with-overrides.yaml new file mode 100644 index 000000000..3dc8a0b6f --- /dev/null +++ b/ecs/pkg/compose/testdata/simple/compose-with-overrides.yaml @@ -0,0 +1,4 @@ +version: "3" +services: + simple: + image: haproxy diff --git a/ecs/pkg/compose/testdata/simple/compose.yaml b/ecs/pkg/compose/testdata/simple/compose.yaml new file mode 100644 index 000000000..4b3f9af21 --- /dev/null +++ b/ecs/pkg/compose/testdata/simple/compose.yaml @@ -0,0 +1,4 @@ +version: "3" +services: + simple: + image: nginx \ No newline at end of file