compose/compose/project.go

173 lines
4.0 KiB
Go

/*
Copyright 2020 Docker, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/compose-spec/compose-go/loader"
"github.com/compose-spec/compose-go/types"
"github.com/sirupsen/logrus"
)
var supportedFilenames = []string{
"compose.yml",
"compose.yaml",
"docker-compose.yml",
"docker-compose.yaml",
}
// ProjectOptions configures a compose project
type ProjectOptions struct {
Name string
WorkDir string
ConfigPaths []string
Environment []string
}
// Project represents a compose project with a name
type Project struct {
types.Config
projectDir string
Name string `yaml:"-" json:"-"`
}
// ProjectFromOptions load a compose project based on given options
func ProjectFromOptions(options *ProjectOptions) (*Project, error) {
configPath, err := getConfigPathFromOptions(options)
if err != nil {
return nil, err
}
configs, err := parseConfigs(configPath)
if err != nil {
return nil, err
}
name := options.Name
if name == "" {
r := regexp.MustCompile(`[^a-z0-9\\-_]+`)
absPath, err := filepath.Abs(options.WorkDir)
if err != nil {
return nil, err
}
name = r.ReplaceAllString(strings.ToLower(filepath.Base(absPath)), "")
}
return newProject(types.ConfigDetails{
WorkingDir: options.WorkDir,
ConfigFiles: configs,
Environment: getAsEqualsMap(options.Environment),
}, name)
}
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
}
func getConfigPathFromOptions(options *ProjectOptions) ([]string, error) {
var paths []string
pwd := options.WorkDir
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
}
for {
var 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. Is %q the right directory?", pwd)
}
pwd = parent
}
}
func parseConfigs(configPaths []string) ([]types.ConfigFile, error) {
var files []types.ConfigFile
for _, f := range configPaths {
var b []byte
var err error
if f == "-" {
b, err = ioutil.ReadAll(os.Stdin)
} else {
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
}
// 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
}