mirror of https://github.com/docker/cli.git
Share the compose loading code between swarm and k8s stack deploy
To ensure we are loading the composefile the same wether we are pointing to swarm or kubernetes, we need to share the loading code between both. Signed-off-by: Vincent Demeester <vincent@sbr.pm>
This commit is contained in:
parent
3e344ae425
commit
570ee9cb54
|
@ -5,8 +5,9 @@ import (
|
|||
"io/ioutil"
|
||||
"path"
|
||||
|
||||
"github.com/docker/cli/cli/command/stack/loader"
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
composeTypes "github.com/docker/cli/cli/compose/types"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/pkg/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
|
@ -19,6 +20,17 @@ func RunDeploy(dockerCli *KubeCli, opts options.Deploy) error {
|
|||
if len(opts.Composefiles) == 0 {
|
||||
return errors.Errorf("Please specify only one compose file (with --compose-file).")
|
||||
}
|
||||
|
||||
// Parse the compose file
|
||||
cfg, version, err := loader.LoadComposefile(dockerCli, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stack, err := LoadStack(opts.Namespace, version, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Initialize clients
|
||||
stacks, err := dockerCli.stacks()
|
||||
if err != nil {
|
||||
|
@ -36,12 +48,6 @@ func RunDeploy(dockerCli *KubeCli, opts options.Deploy) error {
|
|||
Pods: pods,
|
||||
}
|
||||
|
||||
// Parse the compose file
|
||||
stack, cfg, err := LoadStack(opts.Namespace, opts.Composefiles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// FIXME(vdemeester) handle warnings server-side
|
||||
if err = IsColliding(services, stack, cfg); err != nil {
|
||||
return err
|
||||
|
@ -82,7 +88,7 @@ func RunDeploy(dockerCli *KubeCli, opts options.Deploy) error {
|
|||
}
|
||||
|
||||
// createFileBasedConfigMaps creates a Kubernetes ConfigMap for each Compose global file-based config.
|
||||
func createFileBasedConfigMaps(stackName string, globalConfigs map[string]composeTypes.ConfigObjConfig, configMaps corev1.ConfigMapInterface) error {
|
||||
func createFileBasedConfigMaps(stackName string, globalConfigs map[string]composetypes.ConfigObjConfig, configMaps corev1.ConfigMapInterface) error {
|
||||
for name, config := range globalConfigs {
|
||||
if config.File == "" {
|
||||
continue
|
||||
|
@ -102,7 +108,7 @@ func createFileBasedConfigMaps(stackName string, globalConfigs map[string]compos
|
|||
return nil
|
||||
}
|
||||
|
||||
func serviceNames(cfg *composeTypes.Config) []string {
|
||||
func serviceNames(cfg *composetypes.Config) []string {
|
||||
names := []string{}
|
||||
|
||||
for _, service := range cfg.Services {
|
||||
|
@ -113,7 +119,7 @@ func serviceNames(cfg *composeTypes.Config) []string {
|
|||
}
|
||||
|
||||
// createFileBasedSecrets creates a Kubernetes Secret for each Compose global file-based secret.
|
||||
func createFileBasedSecrets(stackName string, globalSecrets map[string]composeTypes.SecretConfig, secrets corev1.SecretInterface) error {
|
||||
func createFileBasedSecrets(stackName string, globalSecrets map[string]composetypes.SecretConfig, secrets corev1.SecretInterface) error {
|
||||
for name, secret := range globalSecrets {
|
||||
if secret.File == "" {
|
||||
continue
|
||||
|
|
|
@ -1,170 +1,32 @@
|
|||
package kubernetes
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
"github.com/docker/cli/cli/compose/template"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
apiv1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1"
|
||||
"github.com/pkg/errors"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// LoadStack loads a stack from a Compose file, with a given name.
|
||||
// FIXME(vdemeester) remove this and use cli/compose/loader for both swarm and kubernetes
|
||||
func LoadStack(name string, composeFiles []string) (*apiv1beta1.Stack, *composetypes.Config, error) {
|
||||
if len(composeFiles) != 1 {
|
||||
return nil, nil, errors.New("compose-file must be set (and only one)")
|
||||
}
|
||||
composeFile := composeFiles[0]
|
||||
|
||||
workingDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
type versionedConfig struct {
|
||||
composetypes.Config
|
||||
Version string
|
||||
}
|
||||
|
||||
composePath := composeFile
|
||||
if !strings.HasPrefix(composePath, "/") {
|
||||
composePath = filepath.Join(workingDir, composeFile)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(composePath); os.IsNotExist(err) {
|
||||
return nil, nil, errors.Errorf("no compose file found in %s", filepath.Dir(composePath))
|
||||
}
|
||||
|
||||
binary, err := ioutil.ReadFile(composePath)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "cannot read compose file")
|
||||
}
|
||||
|
||||
env := env(workingDir)
|
||||
return load(name, binary, workingDir, env)
|
||||
}
|
||||
|
||||
func load(name string, binary []byte, workingDir string, env map[string]string) (*apiv1beta1.Stack, *composetypes.Config, error) {
|
||||
processed, err := template.Substitute(string(binary), func(key string) (string, bool) { return env[key], true })
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "cannot load compose file")
|
||||
}
|
||||
|
||||
parsed, err := loader.ParseYAML([]byte(processed))
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "cannot load compose file")
|
||||
}
|
||||
|
||||
cfg, err := loader.Load(composetypes.ConfigDetails{
|
||||
WorkingDir: workingDir,
|
||||
ConfigFiles: []composetypes.ConfigFile{
|
||||
{
|
||||
Config: parsed,
|
||||
},
|
||||
},
|
||||
// LoadStack loads a stack from a Compose config, with a given name.
|
||||
func LoadStack(name, version string, cfg *composetypes.Config) (*apiv1beta1.Stack, error) {
|
||||
res, err := yaml.Marshal(versionedConfig{
|
||||
Version: version,
|
||||
Config: *cfg,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "cannot load compose file")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := processEnvFiles(processed, parsed, cfg)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "cannot load compose file")
|
||||
}
|
||||
|
||||
return &apiv1beta1.Stack{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Spec: apiv1beta1.StackSpec{
|
||||
ComposeFile: result,
|
||||
ComposeFile: string(res),
|
||||
},
|
||||
}, cfg, nil
|
||||
}
|
||||
|
||||
type iMap = map[string]interface{}
|
||||
|
||||
func processEnvFiles(input string, parsed map[string]interface{}, config *composetypes.Config) (string, error) {
|
||||
changed := false
|
||||
|
||||
for _, svc := range config.Services {
|
||||
if len(svc.EnvFile) == 0 {
|
||||
continue
|
||||
}
|
||||
// Load() processed the env_file for us, we just need to inject back into
|
||||
// the intermediate representation
|
||||
env := iMap{}
|
||||
for k, v := range svc.Environment {
|
||||
env[k] = v
|
||||
}
|
||||
parsed["services"].(iMap)[svc.Name].(iMap)["environment"] = env
|
||||
delete(parsed["services"].(iMap)[svc.Name].(iMap), "env_file")
|
||||
changed = true
|
||||
}
|
||||
if !changed {
|
||||
return input, nil
|
||||
}
|
||||
res, err := yaml.Marshal(parsed)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(res), nil
|
||||
}
|
||||
|
||||
func env(workingDir string) map[string]string {
|
||||
// Apply .env file first
|
||||
config := readEnvFile(filepath.Join(workingDir, ".env"))
|
||||
|
||||
// Apply env variables
|
||||
for k, v := range envToMap(os.Environ()) {
|
||||
config[k] = v
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func readEnvFile(path string) map[string]string {
|
||||
config := map[string]string{}
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return config // Ignore
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(strings.TrimSpace(line), "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
key := parts[0]
|
||||
value := parts[1]
|
||||
|
||||
config[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func envToMap(env []string) map[string]string {
|
||||
config := map[string]string{}
|
||||
|
||||
for _, value := range env {
|
||||
parts := strings.SplitN(value, "=", 2)
|
||||
|
||||
key := parts[0]
|
||||
value := parts[1]
|
||||
|
||||
config[key] = value
|
||||
}
|
||||
|
||||
return config
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
package kubernetes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPlaceholders(t *testing.T) {
|
||||
env := map[string]string{
|
||||
"TAG": "_latest_",
|
||||
"K1": "V1",
|
||||
"K2": "V2",
|
||||
}
|
||||
|
||||
prefix := "version: '3'\nvolumes:\n data:\n external:\n name: "
|
||||
var tests = []struct {
|
||||
input string
|
||||
expectedOutput string
|
||||
}{
|
||||
{prefix + "BEFORE${TAG}AFTER", prefix + "BEFORE_latest_AFTER"},
|
||||
{prefix + "BEFORE${K1}${K2}AFTER", prefix + "BEFOREV1V2AFTER"},
|
||||
{prefix + "BEFORE$TAG AFTER", prefix + "BEFORE_latest_ AFTER"},
|
||||
{prefix + "BEFORE$$TAG AFTER", prefix + "BEFORE$TAG AFTER"},
|
||||
{prefix + "BEFORE $UNKNOWN AFTER", prefix + "BEFORE AFTER"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
output, _, err := load("stack", []byte(test.input), ".", env)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expectedOutput, output.Spec.ComposeFile)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
package loader
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
"github.com/docker/cli/cli/compose/schema"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// LoadComposefile parse the composefile specified in the cli and returns its Config and version.
|
||||
func LoadComposefile(dockerCli command.Cli, opts options.Deploy) (*composetypes.Config, string, error) {
|
||||
configDetails, err := getConfigDetails(opts.Composefiles, dockerCli.In())
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
dicts := getDictsFrom(configDetails.ConfigFiles)
|
||||
config, err := loader.Load(configDetails)
|
||||
if err != nil {
|
||||
if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok {
|
||||
return nil, "", errors.Errorf("Compose file contains unsupported options:\n\n%s\n",
|
||||
propertyWarnings(fpe.Properties))
|
||||
}
|
||||
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
unsupportedProperties := loader.GetUnsupportedProperties(dicts...)
|
||||
if len(unsupportedProperties) > 0 {
|
||||
fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n",
|
||||
strings.Join(unsupportedProperties, ", "))
|
||||
}
|
||||
|
||||
deprecatedProperties := loader.GetDeprecatedProperties(dicts...)
|
||||
if len(deprecatedProperties) > 0 {
|
||||
fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n",
|
||||
propertyWarnings(deprecatedProperties))
|
||||
}
|
||||
return config, configDetails.Version, nil
|
||||
}
|
||||
|
||||
func getDictsFrom(configFiles []composetypes.ConfigFile) []map[string]interface{} {
|
||||
dicts := []map[string]interface{}{}
|
||||
|
||||
for _, configFile := range configFiles {
|
||||
dicts = append(dicts, configFile.Config)
|
||||
}
|
||||
|
||||
return dicts
|
||||
}
|
||||
|
||||
func propertyWarnings(properties map[string]string) string {
|
||||
var msgs []string
|
||||
for name, description := range properties {
|
||||
msgs = append(msgs, fmt.Sprintf("%s: %s", name, description))
|
||||
}
|
||||
sort.Strings(msgs)
|
||||
return strings.Join(msgs, "\n\n")
|
||||
}
|
||||
|
||||
func getConfigDetails(composefiles []string, stdin io.Reader) (composetypes.ConfigDetails, error) {
|
||||
var details composetypes.ConfigDetails
|
||||
|
||||
if len(composefiles) == 0 {
|
||||
return details, errors.New("no composefile(s)")
|
||||
}
|
||||
|
||||
if composefiles[0] == "-" && len(composefiles) == 1 {
|
||||
workingDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return details, err
|
||||
}
|
||||
details.WorkingDir = workingDir
|
||||
} else {
|
||||
absPath, err := filepath.Abs(composefiles[0])
|
||||
if err != nil {
|
||||
return details, err
|
||||
}
|
||||
details.WorkingDir = filepath.Dir(absPath)
|
||||
}
|
||||
|
||||
var err error
|
||||
details.ConfigFiles, err = loadConfigFiles(composefiles, stdin)
|
||||
if err != nil {
|
||||
return details, err
|
||||
}
|
||||
// Take the first file version (2 files can't have different version)
|
||||
details.Version = schema.Version(details.ConfigFiles[0].Config)
|
||||
details.Environment, err = buildEnvironment(os.Environ())
|
||||
return details, err
|
||||
}
|
||||
|
||||
func buildEnvironment(env []string) (map[string]string, error) {
|
||||
result := make(map[string]string, len(env))
|
||||
for _, s := range env {
|
||||
// if value is empty, s is like "K=", not "K".
|
||||
if !strings.Contains(s, "=") {
|
||||
return result, errors.Errorf("unexpected environment %q", s)
|
||||
}
|
||||
kv := strings.SplitN(s, "=", 2)
|
||||
result[kv[0]] = kv[1]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func loadConfigFiles(filenames []string, stdin io.Reader) ([]composetypes.ConfigFile, error) {
|
||||
var configFiles []composetypes.ConfigFile
|
||||
|
||||
for _, filename := range filenames {
|
||||
configFile, err := loadConfigFile(filename, stdin)
|
||||
if err != nil {
|
||||
return configFiles, err
|
||||
}
|
||||
configFiles = append(configFiles, *configFile)
|
||||
}
|
||||
|
||||
return configFiles, nil
|
||||
}
|
||||
|
||||
func loadConfigFile(filename string, stdin io.Reader) (*composetypes.ConfigFile, error) {
|
||||
var bytes []byte
|
||||
var err error
|
||||
|
||||
if filename == "-" {
|
||||
bytes, err = ioutil.ReadAll(stdin)
|
||||
} else {
|
||||
bytes, err = ioutil.ReadFile(filename)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := loader.ParseYAML(bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &composetypes.ConfigFile{
|
||||
Filename: filename,
|
||||
Config: config,
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package loader
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gotestyourself/gotestyourself/fs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetConfigDetails(t *testing.T) {
|
||||
content := `
|
||||
version: "3.0"
|
||||
services:
|
||||
foo:
|
||||
image: alpine:3.5
|
||||
`
|
||||
file := fs.NewFile(t, "test-get-config-details", fs.WithContent(content))
|
||||
defer file.Remove()
|
||||
|
||||
details, err := getConfigDetails([]string{file.Path()}, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, filepath.Dir(file.Path()), details.WorkingDir)
|
||||
require.Len(t, details.ConfigFiles, 1)
|
||||
assert.Equal(t, "3.0", details.ConfigFiles[0].Config["version"])
|
||||
assert.Len(t, details.Environment, len(os.Environ()))
|
||||
}
|
||||
|
||||
func TestGetConfigDetailsStdin(t *testing.T) {
|
||||
content := `
|
||||
version: "3.0"
|
||||
services:
|
||||
foo:
|
||||
image: alpine:3.5
|
||||
`
|
||||
details, err := getConfigDetails([]string{"-"}, strings.NewReader(content))
|
||||
require.NoError(t, err)
|
||||
cwd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, cwd, details.WorkingDir)
|
||||
require.Len(t, details.ConfigFiles, 1)
|
||||
assert.Equal(t, "3.0", details.ConfigFiles[0].Config["version"])
|
||||
assert.Len(t, details.Environment, len(os.Environ()))
|
||||
}
|
|
@ -2,17 +2,11 @@ package swarm
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/stack/loader"
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
"github.com/docker/cli/cli/compose/convert"
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
|
@ -24,34 +18,11 @@ import (
|
|||
)
|
||||
|
||||
func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Deploy) error {
|
||||
configDetails, err := getConfigDetails(opts.Composefiles, dockerCli.In())
|
||||
config, _, err := loader.LoadComposefile(dockerCli, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config, err := loader.Load(configDetails)
|
||||
if err != nil {
|
||||
if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok {
|
||||
return errors.Errorf("Compose file contains unsupported options:\n\n%s\n",
|
||||
propertyWarnings(fpe.Properties))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
dicts := getDictsFrom(configDetails.ConfigFiles)
|
||||
unsupportedProperties := loader.GetUnsupportedProperties(dicts...)
|
||||
if len(unsupportedProperties) > 0 {
|
||||
fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n",
|
||||
strings.Join(unsupportedProperties, ", "))
|
||||
}
|
||||
|
||||
deprecatedProperties := loader.GetDeprecatedProperties(dicts...)
|
||||
if len(deprecatedProperties) > 0 {
|
||||
fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n",
|
||||
propertyWarnings(deprecatedProperties))
|
||||
}
|
||||
|
||||
if err := checkDaemonIsSwarmManager(ctx, dockerCli); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -98,16 +69,6 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Depl
|
|||
return deployServices(ctx, dockerCli, services, namespace, opts.SendRegistryAuth, opts.ResolveImage)
|
||||
}
|
||||
|
||||
func getDictsFrom(configFiles []composetypes.ConfigFile) []map[string]interface{} {
|
||||
dicts := []map[string]interface{}{}
|
||||
|
||||
for _, configFile := range configFiles {
|
||||
dicts = append(dicts, configFile.Config)
|
||||
}
|
||||
|
||||
return dicts
|
||||
}
|
||||
|
||||
func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
|
||||
serviceNetworks := map[string]struct{}{}
|
||||
for _, serviceConfig := range serviceConfigs {
|
||||
|
@ -122,96 +83,6 @@ func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) ma
|
|||
return serviceNetworks
|
||||
}
|
||||
|
||||
func propertyWarnings(properties map[string]string) string {
|
||||
var msgs []string
|
||||
for name, description := range properties {
|
||||
msgs = append(msgs, fmt.Sprintf("%s: %s", name, description))
|
||||
}
|
||||
sort.Strings(msgs)
|
||||
return strings.Join(msgs, "\n\n")
|
||||
}
|
||||
|
||||
func getConfigDetails(composefiles []string, stdin io.Reader) (composetypes.ConfigDetails, error) {
|
||||
var details composetypes.ConfigDetails
|
||||
|
||||
if len(composefiles) == 0 {
|
||||
return details, errors.New("no composefile(s)")
|
||||
}
|
||||
|
||||
if composefiles[0] == "-" && len(composefiles) == 1 {
|
||||
workingDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return details, err
|
||||
}
|
||||
details.WorkingDir = workingDir
|
||||
} else {
|
||||
absPath, err := filepath.Abs(composefiles[0])
|
||||
if err != nil {
|
||||
return details, err
|
||||
}
|
||||
details.WorkingDir = filepath.Dir(absPath)
|
||||
}
|
||||
|
||||
var err error
|
||||
details.ConfigFiles, err = loadConfigFiles(composefiles, stdin)
|
||||
if err != nil {
|
||||
return details, err
|
||||
}
|
||||
details.Environment, err = buildEnvironment(os.Environ())
|
||||
return details, err
|
||||
}
|
||||
|
||||
func buildEnvironment(env []string) (map[string]string, error) {
|
||||
result := make(map[string]string, len(env))
|
||||
for _, s := range env {
|
||||
// if value is empty, s is like "K=", not "K".
|
||||
if !strings.Contains(s, "=") {
|
||||
return result, errors.Errorf("unexpected environment %q", s)
|
||||
}
|
||||
kv := strings.SplitN(s, "=", 2)
|
||||
result[kv[0]] = kv[1]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func loadConfigFiles(filenames []string, stdin io.Reader) ([]composetypes.ConfigFile, error) {
|
||||
var configFiles []composetypes.ConfigFile
|
||||
|
||||
for _, filename := range filenames {
|
||||
configFile, err := loadConfigFile(filename, stdin)
|
||||
if err != nil {
|
||||
return configFiles, err
|
||||
}
|
||||
configFiles = append(configFiles, *configFile)
|
||||
}
|
||||
|
||||
return configFiles, nil
|
||||
}
|
||||
|
||||
func loadConfigFile(filename string, stdin io.Reader) (*composetypes.ConfigFile, error) {
|
||||
var bytes []byte
|
||||
var err error
|
||||
|
||||
if filename == "-" {
|
||||
bytes, err = ioutil.ReadAll(stdin)
|
||||
} else {
|
||||
bytes, err = ioutil.ReadFile(filename)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := loader.ParseYAML(bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &composetypes.ConfigFile{
|
||||
Filename: filename,
|
||||
Config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validateExternalNetworks(
|
||||
ctx context.Context,
|
||||
client dockerclient.NetworkAPIClient,
|
||||
|
|
|
@ -1,56 +1,16 @@
|
|||
package swarm
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test/network"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/gotestyourself/gotestyourself/fs"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestGetConfigDetails(t *testing.T) {
|
||||
content := `
|
||||
version: "3.0"
|
||||
services:
|
||||
foo:
|
||||
image: alpine:3.5
|
||||
`
|
||||
file := fs.NewFile(t, "test-get-config-details", fs.WithContent(content))
|
||||
defer file.Remove()
|
||||
|
||||
details, err := getConfigDetails([]string{file.Path()}, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, filepath.Dir(file.Path()), details.WorkingDir)
|
||||
require.Len(t, details.ConfigFiles, 1)
|
||||
assert.Equal(t, "3.0", details.ConfigFiles[0].Config["version"])
|
||||
assert.Len(t, details.Environment, len(os.Environ()))
|
||||
}
|
||||
|
||||
func TestGetConfigDetailsStdin(t *testing.T) {
|
||||
content := `
|
||||
version: "3.0"
|
||||
services:
|
||||
foo:
|
||||
image: alpine:3.5
|
||||
`
|
||||
details, err := getConfigDetails([]string{"-"}, strings.NewReader(content))
|
||||
require.NoError(t, err)
|
||||
cwd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, cwd, details.WorkingDir)
|
||||
require.Len(t, details.ConfigFiles, 1)
|
||||
assert.Equal(t, "3.0", details.ConfigFiles[0].Config["version"])
|
||||
assert.Len(t, details.Environment, len(os.Environ()))
|
||||
}
|
||||
|
||||
type notFound struct {
|
||||
error
|
||||
}
|
||||
|
|
|
@ -193,7 +193,7 @@ services:
|
|||
|
||||
ports:
|
||||
- 3000
|
||||
- "3000-3005"
|
||||
- "3001-3005"
|
||||
- "8000:8000"
|
||||
- "9090-9091:8080-8081"
|
||||
- "49100:22"
|
||||
|
|
|
@ -0,0 +1,390 @@
|
|||
package loader
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/compose/types"
|
||||
)
|
||||
|
||||
func fullExampleConfig(workingDir, homeDir string) *types.Config {
|
||||
return &types.Config{
|
||||
Services: services(workingDir, homeDir),
|
||||
Networks: networks(),
|
||||
Volumes: volumes(),
|
||||
}
|
||||
}
|
||||
|
||||
func services(workingDir, homeDir string) []types.ServiceConfig {
|
||||
return []types.ServiceConfig{
|
||||
{
|
||||
Name: "foo",
|
||||
|
||||
Build: types.BuildConfig{
|
||||
Context: "./dir",
|
||||
Dockerfile: "Dockerfile",
|
||||
Args: map[string]*string{"foo": strPtr("bar")},
|
||||
Target: "foo",
|
||||
Network: "foo",
|
||||
CacheFrom: []string{"foo", "bar"},
|
||||
Labels: map[string]string{"FOO": "BAR"},
|
||||
},
|
||||
CapAdd: []string{"ALL"},
|
||||
CapDrop: []string{"NET_ADMIN", "SYS_ADMIN"},
|
||||
CgroupParent: "m-executor-abcd",
|
||||
Command: []string{"bundle", "exec", "thin", "-p", "3000"},
|
||||
ContainerName: "my-web-container",
|
||||
DependsOn: []string{"db", "redis"},
|
||||
Deploy: types.DeployConfig{
|
||||
Mode: "replicated",
|
||||
Replicas: uint64Ptr(6),
|
||||
Labels: map[string]string{"FOO": "BAR"},
|
||||
UpdateConfig: &types.UpdateConfig{
|
||||
Parallelism: uint64Ptr(3),
|
||||
Delay: time.Duration(10 * time.Second),
|
||||
FailureAction: "continue",
|
||||
Monitor: time.Duration(60 * time.Second),
|
||||
MaxFailureRatio: 0.3,
|
||||
Order: "start-first",
|
||||
},
|
||||
Resources: types.Resources{
|
||||
Limits: &types.Resource{
|
||||
NanoCPUs: "0.001",
|
||||
MemoryBytes: 50 * 1024 * 1024,
|
||||
},
|
||||
Reservations: &types.Resource{
|
||||
NanoCPUs: "0.0001",
|
||||
MemoryBytes: 20 * 1024 * 1024,
|
||||
GenericResources: []types.GenericResource{
|
||||
{
|
||||
DiscreteResourceSpec: &types.DiscreteGenericResource{
|
||||
Kind: "gpu",
|
||||
Value: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
DiscreteResourceSpec: &types.DiscreteGenericResource{
|
||||
Kind: "ssd",
|
||||
Value: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
RestartPolicy: &types.RestartPolicy{
|
||||
Condition: "on-failure",
|
||||
Delay: durationPtr(5 * time.Second),
|
||||
MaxAttempts: uint64Ptr(3),
|
||||
Window: durationPtr(2 * time.Minute),
|
||||
},
|
||||
Placement: types.Placement{
|
||||
Constraints: []string{"node=foo"},
|
||||
Preferences: []types.PlacementPreferences{
|
||||
{
|
||||
Spread: "node.labels.az",
|
||||
},
|
||||
},
|
||||
},
|
||||
EndpointMode: "dnsrr",
|
||||
},
|
||||
Devices: []string{"/dev/ttyUSB0:/dev/ttyUSB0"},
|
||||
DNS: []string{"8.8.8.8", "9.9.9.9"},
|
||||
DNSSearch: []string{"dc1.example.com", "dc2.example.com"},
|
||||
DomainName: "foo.com",
|
||||
Entrypoint: []string{"/code/entrypoint.sh", "-p", "3000"},
|
||||
Environment: map[string]*string{
|
||||
"FOO": strPtr("foo_from_env_file"),
|
||||
"BAR": strPtr("bar_from_env_file_2"),
|
||||
"BAZ": strPtr("baz_from_service_def"),
|
||||
"QUX": strPtr("qux_from_environment"),
|
||||
},
|
||||
EnvFile: []string{
|
||||
"./example1.env",
|
||||
"./example2.env",
|
||||
},
|
||||
Expose: []string{"3000", "8000"},
|
||||
ExternalLinks: []string{
|
||||
"redis_1",
|
||||
"project_db_1:mysql",
|
||||
"project_db_1:postgresql",
|
||||
},
|
||||
ExtraHosts: []string{
|
||||
"somehost:162.242.195.82",
|
||||
"otherhost:50.31.209.229",
|
||||
},
|
||||
HealthCheck: &types.HealthCheckConfig{
|
||||
Test: types.HealthCheckTest([]string{"CMD-SHELL", "echo \"hello world\""}),
|
||||
Interval: durationPtr(10 * time.Second),
|
||||
Timeout: durationPtr(1 * time.Second),
|
||||
Retries: uint64Ptr(5),
|
||||
StartPeriod: durationPtr(15 * time.Second),
|
||||
},
|
||||
Hostname: "foo",
|
||||
Image: "redis",
|
||||
Ipc: "host",
|
||||
Labels: map[string]string{
|
||||
"com.example.description": "Accounting webapp",
|
||||
"com.example.number": "42",
|
||||
"com.example.empty-label": "",
|
||||
},
|
||||
Links: []string{
|
||||
"db",
|
||||
"db:database",
|
||||
"redis",
|
||||
},
|
||||
Logging: &types.LoggingConfig{
|
||||
Driver: "syslog",
|
||||
Options: map[string]string{
|
||||
"syslog-address": "tcp://192.168.0.42:123",
|
||||
},
|
||||
},
|
||||
MacAddress: "02:42:ac:11:65:43",
|
||||
NetworkMode: "container:0cfeab0f748b9a743dc3da582046357c6ef497631c1a016d28d2bf9b4f899f7b",
|
||||
Networks: map[string]*types.ServiceNetworkConfig{
|
||||
"some-network": {
|
||||
Aliases: []string{"alias1", "alias3"},
|
||||
Ipv4Address: "",
|
||||
Ipv6Address: "",
|
||||
},
|
||||
"other-network": {
|
||||
Ipv4Address: "172.16.238.10",
|
||||
Ipv6Address: "2001:3984:3989::10",
|
||||
},
|
||||
"other-other-network": nil,
|
||||
},
|
||||
Pid: "host",
|
||||
Ports: []types.ServicePortConfig{
|
||||
//"3000",
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 3000,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 3001,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 3002,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 3003,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 3004,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 3005,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
//"8000:8000",
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 8000,
|
||||
Published: 8000,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
//"9090-9091:8080-8081",
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 8080,
|
||||
Published: 9090,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 8081,
|
||||
Published: 9091,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
//"49100:22",
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 22,
|
||||
Published: 49100,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
//"127.0.0.1:8001:8001",
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 8001,
|
||||
Published: 8001,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
//"127.0.0.1:5000-5010:5000-5010",
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5000,
|
||||
Published: 5000,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5001,
|
||||
Published: 5001,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5002,
|
||||
Published: 5002,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5003,
|
||||
Published: 5003,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5004,
|
||||
Published: 5004,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5005,
|
||||
Published: 5005,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5006,
|
||||
Published: 5006,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5007,
|
||||
Published: 5007,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5008,
|
||||
Published: 5008,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5009,
|
||||
Published: 5009,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5010,
|
||||
Published: 5010,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
},
|
||||
Privileged: true,
|
||||
ReadOnly: true,
|
||||
Restart: "always",
|
||||
SecurityOpt: []string{
|
||||
"label=level:s0:c100,c200",
|
||||
"label=type:svirt_apache_t",
|
||||
},
|
||||
StdinOpen: true,
|
||||
StopSignal: "SIGUSR1",
|
||||
StopGracePeriod: durationPtr(time.Duration(20 * time.Second)),
|
||||
Tmpfs: []string{"/run", "/tmp"},
|
||||
Tty: true,
|
||||
Ulimits: map[string]*types.UlimitsConfig{
|
||||
"nproc": {
|
||||
Single: 65535,
|
||||
},
|
||||
"nofile": {
|
||||
Soft: 20000,
|
||||
Hard: 40000,
|
||||
},
|
||||
},
|
||||
User: "someone",
|
||||
Volumes: []types.ServiceVolumeConfig{
|
||||
{Target: "/var/lib/mysql", Type: "volume"},
|
||||
{Source: "/opt/data", Target: "/var/lib/mysql", Type: "bind"},
|
||||
{Source: workingDir, Target: "/code", Type: "bind"},
|
||||
{Source: workingDir + "/static", Target: "/var/www/html", Type: "bind"},
|
||||
{Source: homeDir + "/configs", Target: "/etc/configs/", Type: "bind", ReadOnly: true},
|
||||
{Source: "datavolume", Target: "/var/lib/mysql", Type: "volume"},
|
||||
{Source: workingDir + "/opt", Target: "/opt", Consistency: "cached", Type: "bind"},
|
||||
{Target: "/opt", Type: "tmpfs", Tmpfs: &types.ServiceVolumeTmpfs{
|
||||
Size: int64(10000),
|
||||
}},
|
||||
},
|
||||
WorkingDir: "/code",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func networks() map[string]types.NetworkConfig {
|
||||
return map[string]types.NetworkConfig{
|
||||
"some-network": {},
|
||||
|
||||
"other-network": {
|
||||
Driver: "overlay",
|
||||
DriverOpts: map[string]string{
|
||||
"foo": "bar",
|
||||
"baz": "1",
|
||||
},
|
||||
Ipam: types.IPAMConfig{
|
||||
Driver: "overlay",
|
||||
Config: []*types.IPAMPool{
|
||||
{Subnet: "172.16.238.0/24"},
|
||||
{Subnet: "2001:3984:3989::/64"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"external-network": {
|
||||
Name: "external-network",
|
||||
External: types.External{External: true},
|
||||
},
|
||||
|
||||
"other-external-network": {
|
||||
Name: "my-cool-network",
|
||||
External: types.External{External: true},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func volumes() map[string]types.VolumeConfig {
|
||||
return map[string]types.VolumeConfig{
|
||||
"some-volume": {},
|
||||
"other-volume": {
|
||||
Driver: "flocker",
|
||||
DriverOpts: map[string]string{
|
||||
"foo": "bar",
|
||||
"baz": "1",
|
||||
},
|
||||
},
|
||||
"another-volume": {
|
||||
Name: "user_specified_name",
|
||||
Driver: "vsphere",
|
||||
DriverOpts: map[string]string{
|
||||
"foo": "bar",
|
||||
"baz": "1",
|
||||
},
|
||||
},
|
||||
"external-volume": {
|
||||
Name: "external-volume",
|
||||
External: types.External{External: true},
|
||||
},
|
||||
"other-external-volume": {
|
||||
Name: "my-cool-volume",
|
||||
External: types.External{External: true},
|
||||
},
|
||||
"external-volume3": {
|
||||
Name: "this-is-volume3",
|
||||
External: types.External{External: true},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -842,386 +842,11 @@ func TestFullExample(t *testing.T) {
|
|||
workingDir, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
|
||||
stopGracePeriod := time.Duration(20 * time.Second)
|
||||
expectedConfig := fullExampleConfig(workingDir, homeDir)
|
||||
|
||||
expectedServiceConfig := types.ServiceConfig{
|
||||
Name: "foo",
|
||||
|
||||
Build: types.BuildConfig{
|
||||
Context: "./dir",
|
||||
Dockerfile: "Dockerfile",
|
||||
Args: map[string]*string{"foo": strPtr("bar")},
|
||||
Target: "foo",
|
||||
Network: "foo",
|
||||
CacheFrom: []string{"foo", "bar"},
|
||||
Labels: map[string]string{"FOO": "BAR"},
|
||||
},
|
||||
CapAdd: []string{"ALL"},
|
||||
CapDrop: []string{"NET_ADMIN", "SYS_ADMIN"},
|
||||
CgroupParent: "m-executor-abcd",
|
||||
Command: []string{"bundle", "exec", "thin", "-p", "3000"},
|
||||
ContainerName: "my-web-container",
|
||||
DependsOn: []string{"db", "redis"},
|
||||
Deploy: types.DeployConfig{
|
||||
Mode: "replicated",
|
||||
Replicas: uint64Ptr(6),
|
||||
Labels: map[string]string{"FOO": "BAR"},
|
||||
UpdateConfig: &types.UpdateConfig{
|
||||
Parallelism: uint64Ptr(3),
|
||||
Delay: time.Duration(10 * time.Second),
|
||||
FailureAction: "continue",
|
||||
Monitor: time.Duration(60 * time.Second),
|
||||
MaxFailureRatio: 0.3,
|
||||
Order: "start-first",
|
||||
},
|
||||
Resources: types.Resources{
|
||||
Limits: &types.Resource{
|
||||
NanoCPUs: "0.001",
|
||||
MemoryBytes: 50 * 1024 * 1024,
|
||||
},
|
||||
Reservations: &types.Resource{
|
||||
NanoCPUs: "0.0001",
|
||||
MemoryBytes: 20 * 1024 * 1024,
|
||||
GenericResources: []types.GenericResource{
|
||||
{
|
||||
DiscreteResourceSpec: &types.DiscreteGenericResource{
|
||||
Kind: "gpu",
|
||||
Value: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
DiscreteResourceSpec: &types.DiscreteGenericResource{
|
||||
Kind: "ssd",
|
||||
Value: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
RestartPolicy: &types.RestartPolicy{
|
||||
Condition: "on-failure",
|
||||
Delay: durationPtr(5 * time.Second),
|
||||
MaxAttempts: uint64Ptr(3),
|
||||
Window: durationPtr(2 * time.Minute),
|
||||
},
|
||||
Placement: types.Placement{
|
||||
Constraints: []string{"node=foo"},
|
||||
Preferences: []types.PlacementPreferences{
|
||||
{
|
||||
Spread: "node.labels.az",
|
||||
},
|
||||
},
|
||||
},
|
||||
EndpointMode: "dnsrr",
|
||||
},
|
||||
Devices: []string{"/dev/ttyUSB0:/dev/ttyUSB0"},
|
||||
DNS: []string{"8.8.8.8", "9.9.9.9"},
|
||||
DNSSearch: []string{"dc1.example.com", "dc2.example.com"},
|
||||
DomainName: "foo.com",
|
||||
Entrypoint: []string{"/code/entrypoint.sh", "-p", "3000"},
|
||||
Environment: map[string]*string{
|
||||
"FOO": strPtr("foo_from_env_file"),
|
||||
"BAR": strPtr("bar_from_env_file_2"),
|
||||
"BAZ": strPtr("baz_from_service_def"),
|
||||
"QUX": strPtr("qux_from_environment"),
|
||||
},
|
||||
EnvFile: []string{
|
||||
"./example1.env",
|
||||
"./example2.env",
|
||||
},
|
||||
Expose: []string{"3000", "8000"},
|
||||
ExternalLinks: []string{
|
||||
"redis_1",
|
||||
"project_db_1:mysql",
|
||||
"project_db_1:postgresql",
|
||||
},
|
||||
ExtraHosts: []string{
|
||||
"somehost:162.242.195.82",
|
||||
"otherhost:50.31.209.229",
|
||||
},
|
||||
HealthCheck: &types.HealthCheckConfig{
|
||||
Test: types.HealthCheckTest([]string{"CMD-SHELL", "echo \"hello world\""}),
|
||||
Interval: durationPtr(10 * time.Second),
|
||||
Timeout: durationPtr(1 * time.Second),
|
||||
Retries: uint64Ptr(5),
|
||||
StartPeriod: durationPtr(15 * time.Second),
|
||||
},
|
||||
Hostname: "foo",
|
||||
Image: "redis",
|
||||
Ipc: "host",
|
||||
Labels: map[string]string{
|
||||
"com.example.description": "Accounting webapp",
|
||||
"com.example.number": "42",
|
||||
"com.example.empty-label": "",
|
||||
},
|
||||
Links: []string{
|
||||
"db",
|
||||
"db:database",
|
||||
"redis",
|
||||
},
|
||||
Logging: &types.LoggingConfig{
|
||||
Driver: "syslog",
|
||||
Options: map[string]string{
|
||||
"syslog-address": "tcp://192.168.0.42:123",
|
||||
},
|
||||
},
|
||||
MacAddress: "02:42:ac:11:65:43",
|
||||
NetworkMode: "container:0cfeab0f748b9a743dc3da582046357c6ef497631c1a016d28d2bf9b4f899f7b",
|
||||
Networks: map[string]*types.ServiceNetworkConfig{
|
||||
"some-network": {
|
||||
Aliases: []string{"alias1", "alias3"},
|
||||
Ipv4Address: "",
|
||||
Ipv6Address: "",
|
||||
},
|
||||
"other-network": {
|
||||
Ipv4Address: "172.16.238.10",
|
||||
Ipv6Address: "2001:3984:3989::10",
|
||||
},
|
||||
"other-other-network": nil,
|
||||
},
|
||||
Pid: "host",
|
||||
Ports: []types.ServicePortConfig{
|
||||
//"3000",
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 3000,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
//"3000-3005",
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 3000,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 3001,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 3002,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 3003,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 3004,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 3005,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
//"8000:8000",
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 8000,
|
||||
Published: 8000,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
//"9090-9091:8080-8081",
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 8080,
|
||||
Published: 9090,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 8081,
|
||||
Published: 9091,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
//"49100:22",
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 22,
|
||||
Published: 49100,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
//"127.0.0.1:8001:8001",
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 8001,
|
||||
Published: 8001,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
//"127.0.0.1:5000-5010:5000-5010",
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5000,
|
||||
Published: 5000,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5001,
|
||||
Published: 5001,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5002,
|
||||
Published: 5002,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5003,
|
||||
Published: 5003,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5004,
|
||||
Published: 5004,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5005,
|
||||
Published: 5005,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5006,
|
||||
Published: 5006,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5007,
|
||||
Published: 5007,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5008,
|
||||
Published: 5008,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5009,
|
||||
Published: 5009,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
{
|
||||
Mode: "ingress",
|
||||
Target: 5010,
|
||||
Published: 5010,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
},
|
||||
Privileged: true,
|
||||
ReadOnly: true,
|
||||
Restart: "always",
|
||||
SecurityOpt: []string{
|
||||
"label=level:s0:c100,c200",
|
||||
"label=type:svirt_apache_t",
|
||||
},
|
||||
StdinOpen: true,
|
||||
StopSignal: "SIGUSR1",
|
||||
StopGracePeriod: &stopGracePeriod,
|
||||
Tmpfs: []string{"/run", "/tmp"},
|
||||
Tty: true,
|
||||
Ulimits: map[string]*types.UlimitsConfig{
|
||||
"nproc": {
|
||||
Single: 65535,
|
||||
},
|
||||
"nofile": {
|
||||
Soft: 20000,
|
||||
Hard: 40000,
|
||||
},
|
||||
},
|
||||
User: "someone",
|
||||
Volumes: []types.ServiceVolumeConfig{
|
||||
{Target: "/var/lib/mysql", Type: "volume"},
|
||||
{Source: "/opt/data", Target: "/var/lib/mysql", Type: "bind"},
|
||||
{Source: workingDir, Target: "/code", Type: "bind"},
|
||||
{Source: workingDir + "/static", Target: "/var/www/html", Type: "bind"},
|
||||
{Source: homeDir + "/configs", Target: "/etc/configs/", Type: "bind", ReadOnly: true},
|
||||
{Source: "datavolume", Target: "/var/lib/mysql", Type: "volume"},
|
||||
{Source: workingDir + "/opt", Target: "/opt", Consistency: "cached", Type: "bind"},
|
||||
{Target: "/opt", Type: "tmpfs", Tmpfs: &types.ServiceVolumeTmpfs{
|
||||
Size: int64(10000),
|
||||
}},
|
||||
},
|
||||
WorkingDir: "/code",
|
||||
}
|
||||
|
||||
assert.Equal(t, []types.ServiceConfig{expectedServiceConfig}, config.Services)
|
||||
|
||||
expectedNetworkConfig := map[string]types.NetworkConfig{
|
||||
"some-network": {},
|
||||
|
||||
"other-network": {
|
||||
Driver: "overlay",
|
||||
DriverOpts: map[string]string{
|
||||
"foo": "bar",
|
||||
"baz": "1",
|
||||
},
|
||||
Ipam: types.IPAMConfig{
|
||||
Driver: "overlay",
|
||||
Config: []*types.IPAMPool{
|
||||
{Subnet: "172.16.238.0/24"},
|
||||
{Subnet: "2001:3984:3989::/64"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"external-network": {
|
||||
Name: "external-network",
|
||||
External: types.External{External: true},
|
||||
},
|
||||
|
||||
"other-external-network": {
|
||||
Name: "my-cool-network",
|
||||
External: types.External{External: true},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedNetworkConfig, config.Networks)
|
||||
|
||||
expectedVolumeConfig := map[string]types.VolumeConfig{
|
||||
"some-volume": {},
|
||||
"other-volume": {
|
||||
Driver: "flocker",
|
||||
DriverOpts: map[string]string{
|
||||
"foo": "bar",
|
||||
"baz": "1",
|
||||
},
|
||||
},
|
||||
"another-volume": {
|
||||
Name: "user_specified_name",
|
||||
Driver: "vsphere",
|
||||
DriverOpts: map[string]string{
|
||||
"foo": "bar",
|
||||
"baz": "1",
|
||||
},
|
||||
},
|
||||
"external-volume": {
|
||||
Name: "external-volume",
|
||||
External: types.External{External: true},
|
||||
},
|
||||
"other-external-volume": {
|
||||
Name: "my-cool-volume",
|
||||
External: types.External{External: true},
|
||||
},
|
||||
"external-volume3": {
|
||||
Name: "this-is-volume3",
|
||||
External: types.External{External: true},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedVolumeConfig, config.Volumes)
|
||||
assert.Equal(t, expectedConfig.Services, config.Services)
|
||||
assert.Equal(t, expectedConfig.Networks, config.Networks)
|
||||
assert.Equal(t, expectedConfig.Volumes, config.Volumes)
|
||||
}
|
||||
|
||||
func TestLoadTmpfsVolume(t *testing.T) {
|
||||
|
|
|
@ -0,0 +1,328 @@
|
|||
package loader
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func TestMarshallConfig(t *testing.T) {
|
||||
cfg := fullExampleConfig("/foo", "/bar")
|
||||
expected := `configs: {}
|
||||
networks:
|
||||
external-network:
|
||||
name: external-network
|
||||
external: true
|
||||
other-external-network:
|
||||
name: my-cool-network
|
||||
external: true
|
||||
other-network:
|
||||
driver: overlay
|
||||
driver_opts:
|
||||
baz: "1"
|
||||
foo: bar
|
||||
ipam:
|
||||
driver: overlay
|
||||
config:
|
||||
- subnet: 172.16.238.0/24
|
||||
- subnet: 2001:3984:3989::/64
|
||||
some-network: {}
|
||||
secrets: {}
|
||||
services:
|
||||
foo:
|
||||
build:
|
||||
context: ./dir
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
foo: bar
|
||||
labels:
|
||||
FOO: BAR
|
||||
cache_from:
|
||||
- foo
|
||||
- bar
|
||||
network: foo
|
||||
target: foo
|
||||
cap_add:
|
||||
- ALL
|
||||
cap_drop:
|
||||
- NET_ADMIN
|
||||
- SYS_ADMIN
|
||||
cgroup_parent: m-executor-abcd
|
||||
command:
|
||||
- bundle
|
||||
- exec
|
||||
- thin
|
||||
- -p
|
||||
- "3000"
|
||||
container_name: my-web-container
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 6
|
||||
labels:
|
||||
FOO: BAR
|
||||
update_config:
|
||||
parallelism: 3
|
||||
delay: 10s
|
||||
failure_action: continue
|
||||
monitor: 1m0s
|
||||
max_failure_ratio: 0.3
|
||||
order: start-first
|
||||
resources:
|
||||
limits:
|
||||
cpus: "0.001"
|
||||
memory: "52428800"
|
||||
reservations:
|
||||
cpus: "0.0001"
|
||||
memory: "20971520"
|
||||
generic_resources:
|
||||
- discrete_resource_spec:
|
||||
kind: gpu
|
||||
value: 2
|
||||
- discrete_resource_spec:
|
||||
kind: ssd
|
||||
value: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
max_attempts: 3
|
||||
window: 2m0s
|
||||
placement:
|
||||
constraints:
|
||||
- node=foo
|
||||
preferences:
|
||||
- spread: node.labels.az
|
||||
endpoint_mode: dnsrr
|
||||
devices:
|
||||
- /dev/ttyUSB0:/dev/ttyUSB0
|
||||
dns:
|
||||
- 8.8.8.8
|
||||
- 9.9.9.9
|
||||
dns_search:
|
||||
- dc1.example.com
|
||||
- dc2.example.com
|
||||
domainname: foo.com
|
||||
entrypoint:
|
||||
- /code/entrypoint.sh
|
||||
- -p
|
||||
- "3000"
|
||||
environment:
|
||||
BAR: bar_from_env_file_2
|
||||
BAZ: baz_from_service_def
|
||||
FOO: foo_from_env_file
|
||||
QUX: qux_from_environment
|
||||
env_file:
|
||||
- ./example1.env
|
||||
- ./example2.env
|
||||
expose:
|
||||
- "3000"
|
||||
- "8000"
|
||||
external_links:
|
||||
- redis_1
|
||||
- project_db_1:mysql
|
||||
- project_db_1:postgresql
|
||||
extra_hosts:
|
||||
- somehost:162.242.195.82
|
||||
- otherhost:50.31.209.229
|
||||
hostname: foo
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- echo "hello world"
|
||||
timeout: 1s
|
||||
interval: 10s
|
||||
retries: 5
|
||||
start_period: 15s
|
||||
image: redis
|
||||
ipc: host
|
||||
labels:
|
||||
com.example.description: Accounting webapp
|
||||
com.example.empty-label: ""
|
||||
com.example.number: "42"
|
||||
links:
|
||||
- db
|
||||
- db:database
|
||||
- redis
|
||||
logging:
|
||||
driver: syslog
|
||||
options:
|
||||
syslog-address: tcp://192.168.0.42:123
|
||||
mac_address: 02:42:ac:11:65:43
|
||||
network_mode: container:0cfeab0f748b9a743dc3da582046357c6ef497631c1a016d28d2bf9b4f899f7b
|
||||
networks:
|
||||
other-network:
|
||||
ipv4_address: 172.16.238.10
|
||||
ipv6_address: 2001:3984:3989::10
|
||||
other-other-network: null
|
||||
some-network:
|
||||
aliases:
|
||||
- alias1
|
||||
- alias3
|
||||
pid: host
|
||||
ports:
|
||||
- mode: ingress
|
||||
target: 3000
|
||||
protocol: tcp
|
||||
- mode: ingress
|
||||
target: 3001
|
||||
protocol: tcp
|
||||
- mode: ingress
|
||||
target: 3002
|
||||
protocol: tcp
|
||||
- mode: ingress
|
||||
target: 3003
|
||||
protocol: tcp
|
||||
- mode: ingress
|
||||
target: 3004
|
||||
protocol: tcp
|
||||
- mode: ingress
|
||||
target: 3005
|
||||
protocol: tcp
|
||||
- mode: ingress
|
||||
target: 8000
|
||||
published: 8000
|
||||
protocol: tcp
|
||||
- mode: ingress
|
||||
target: 8080
|
||||
published: 9090
|
||||
protocol: tcp
|
||||
- mode: ingress
|
||||
target: 8081
|
||||
published: 9091
|
||||
protocol: tcp
|
||||
- mode: ingress
|
||||
target: 22
|
||||
published: 49100
|
||||
protocol: tcp
|
||||
- mode: ingress
|
||||
target: 8001
|
||||
published: 8001
|
||||
protocol: tcp
|
||||
- mode: ingress
|
||||
target: 5000
|
||||
published: 5000
|
||||
protocol: tcp
|
||||
- mode: ingress
|
||||
target: 5001
|
||||
published: 5001
|
||||
protocol: tcp
|
||||
- mode: ingress
|
||||
target: 5002
|
||||
published: 5002
|
||||
protocol: tcp
|
||||
- mode: ingress
|
||||
target: 5003
|
||||
published: 5003
|
||||
protocol: tcp
|
||||
- mode: ingress
|
||||
target: 5004
|
||||
published: 5004
|
||||
protocol: tcp
|
||||
- mode: ingress
|
||||
target: 5005
|
||||
published: 5005
|
||||
protocol: tcp
|
||||
- mode: ingress
|
||||
target: 5006
|
||||
published: 5006
|
||||
protocol: tcp
|
||||
- mode: ingress
|
||||
target: 5007
|
||||
published: 5007
|
||||
protocol: tcp
|
||||
- mode: ingress
|
||||
target: 5008
|
||||
published: 5008
|
||||
protocol: tcp
|
||||
- mode: ingress
|
||||
target: 5009
|
||||
published: 5009
|
||||
protocol: tcp
|
||||
- mode: ingress
|
||||
target: 5010
|
||||
published: 5010
|
||||
protocol: tcp
|
||||
privileged: true
|
||||
read_only: true
|
||||
restart: always
|
||||
security_opt:
|
||||
- label=level:s0:c100,c200
|
||||
- label=type:svirt_apache_t
|
||||
stdin_open: true
|
||||
stop_grace_period: 20s
|
||||
stop_signal: SIGUSR1
|
||||
tmpfs:
|
||||
- /run
|
||||
- /tmp
|
||||
tty: true
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 20000
|
||||
hard: 40000
|
||||
nproc: 65535
|
||||
user: someone
|
||||
volumes:
|
||||
- type: volume
|
||||
target: /var/lib/mysql
|
||||
- type: bind
|
||||
source: /opt/data
|
||||
target: /var/lib/mysql
|
||||
- type: bind
|
||||
source: /foo
|
||||
target: /code
|
||||
- type: bind
|
||||
source: /foo/static
|
||||
target: /var/www/html
|
||||
- type: bind
|
||||
source: /bar/configs
|
||||
target: /etc/configs/
|
||||
read_only: true
|
||||
- type: volume
|
||||
source: datavolume
|
||||
target: /var/lib/mysql
|
||||
- type: bind
|
||||
source: /foo/opt
|
||||
target: /opt
|
||||
consistency: cached
|
||||
- type: tmpfs
|
||||
target: /opt
|
||||
tmpfs:
|
||||
size: 10000
|
||||
working_dir: /code
|
||||
volumes:
|
||||
another-volume:
|
||||
name: user_specified_name
|
||||
driver: vsphere
|
||||
driver_opts:
|
||||
baz: "1"
|
||||
foo: bar
|
||||
external-volume:
|
||||
name: external-volume
|
||||
external: true
|
||||
external-volume3:
|
||||
name: this-is-volume3
|
||||
external: true
|
||||
other-external-volume:
|
||||
name: my-cool-volume
|
||||
external: true
|
||||
other-volume:
|
||||
driver: flocker
|
||||
driver_opts:
|
||||
baz: "1"
|
||||
foo: bar
|
||||
some-volume: {}
|
||||
`
|
||||
|
||||
actual, err := yaml.Marshal(cfg)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, string(actual))
|
||||
|
||||
// Make sure the expected still
|
||||
dict, err := ParseYAML([]byte("version: '3.6'\n" + expected))
|
||||
assert.NoError(t, err)
|
||||
_, err = Load(buildConfigDetails(dict, map[string]string{}))
|
||||
assert.NoError(t, err)
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -77,69 +78,86 @@ type Config struct {
|
|||
Configs map[string]ConfigObjConfig
|
||||
}
|
||||
|
||||
// MarshalYAML makes Config implement yaml.Marshaller
|
||||
func (c *Config) MarshalYAML() (interface{}, error) {
|
||||
m := map[string]interface{}{}
|
||||
services := map[string]ServiceConfig{}
|
||||
for _, service := range c.Services {
|
||||
s := service
|
||||
s.Name = ""
|
||||
services[service.Name] = s
|
||||
}
|
||||
m["services"] = services
|
||||
m["networks"] = c.Networks
|
||||
m["volumes"] = c.Volumes
|
||||
m["secrets"] = c.Secrets
|
||||
m["configs"] = c.Configs
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// ServiceConfig is the configuration of one service
|
||||
type ServiceConfig struct {
|
||||
Name string
|
||||
Name string `yaml:",omitempty"`
|
||||
|
||||
Build BuildConfig
|
||||
CapAdd []string `mapstructure:"cap_add"`
|
||||
CapDrop []string `mapstructure:"cap_drop"`
|
||||
CgroupParent string `mapstructure:"cgroup_parent"`
|
||||
Command ShellCommand
|
||||
Configs []ServiceConfigObjConfig
|
||||
ContainerName string `mapstructure:"container_name"`
|
||||
CredentialSpec CredentialSpecConfig `mapstructure:"credential_spec"`
|
||||
DependsOn []string `mapstructure:"depends_on"`
|
||||
Deploy DeployConfig
|
||||
Devices []string
|
||||
DNS StringList
|
||||
DNSSearch StringList `mapstructure:"dns_search"`
|
||||
DomainName string `mapstructure:"domainname"`
|
||||
Entrypoint ShellCommand
|
||||
Environment MappingWithEquals
|
||||
EnvFile StringList `mapstructure:"env_file"`
|
||||
Expose StringOrNumberList
|
||||
ExternalLinks []string `mapstructure:"external_links"`
|
||||
ExtraHosts HostsList `mapstructure:"extra_hosts"`
|
||||
Hostname string
|
||||
HealthCheck *HealthCheckConfig
|
||||
Image string
|
||||
Ipc string
|
||||
Labels Labels
|
||||
Links []string
|
||||
Logging *LoggingConfig
|
||||
MacAddress string `mapstructure:"mac_address"`
|
||||
NetworkMode string `mapstructure:"network_mode"`
|
||||
Networks map[string]*ServiceNetworkConfig
|
||||
Pid string
|
||||
Ports []ServicePortConfig
|
||||
Privileged bool
|
||||
ReadOnly bool `mapstructure:"read_only"`
|
||||
Restart string
|
||||
Secrets []ServiceSecretConfig
|
||||
SecurityOpt []string `mapstructure:"security_opt"`
|
||||
StdinOpen bool `mapstructure:"stdin_open"`
|
||||
StopGracePeriod *time.Duration `mapstructure:"stop_grace_period"`
|
||||
StopSignal string `mapstructure:"stop_signal"`
|
||||
Tmpfs StringList
|
||||
Tty bool `mapstructure:"tty"`
|
||||
Ulimits map[string]*UlimitsConfig
|
||||
User string
|
||||
Volumes []ServiceVolumeConfig
|
||||
WorkingDir string `mapstructure:"working_dir"`
|
||||
Isolation string `mapstructure:"isolation"`
|
||||
Build BuildConfig `yaml:",omitempty"`
|
||||
CapAdd []string `mapstructure:"cap_add" yaml:"cap_add,omitempty"`
|
||||
CapDrop []string `mapstructure:"cap_drop" yaml:"cap_drop,omitempty"`
|
||||
CgroupParent string `mapstructure:"cgroup_parent" yaml:"cgroup_parent,omitempty"`
|
||||
Command ShellCommand `yaml:",omitempty"`
|
||||
Configs []ServiceConfigObjConfig `yaml:",omitempty"`
|
||||
ContainerName string `mapstructure:"container_name" yaml:"container_name,omitempty"`
|
||||
CredentialSpec CredentialSpecConfig `mapstructure:"credential_spec" yaml:"credential_spec,omitempty"`
|
||||
DependsOn []string `mapstructure:"depends_on" yaml:"depends_on,omitempty"`
|
||||
Deploy DeployConfig `yaml:",omitempty"`
|
||||
Devices []string `yaml:",omitempty"`
|
||||
DNS StringList `yaml:",omitempty"`
|
||||
DNSSearch StringList `mapstructure:"dns_search" yaml:"dns_search,omitempty"`
|
||||
DomainName string `mapstructure:"domainname" yaml:"domainname,omitempty"`
|
||||
Entrypoint ShellCommand `yaml:",omitempty"`
|
||||
Environment MappingWithEquals `yaml:",omitempty"`
|
||||
EnvFile StringList `mapstructure:"env_file" yaml:"env_file,omitempty"`
|
||||
Expose StringOrNumberList `yaml:",omitempty"`
|
||||
ExternalLinks []string `mapstructure:"external_links" yaml:"external_links,omitempty"`
|
||||
ExtraHosts HostsList `mapstructure:"extra_hosts" yaml:"extra_hosts,omitempty"`
|
||||
Hostname string `yaml:",omitempty"`
|
||||
HealthCheck *HealthCheckConfig `yaml:",omitempty"`
|
||||
Image string `yaml:",omitempty"`
|
||||
Ipc string `yaml:",omitempty"`
|
||||
Labels Labels `yaml:",omitempty"`
|
||||
Links []string `yaml:",omitempty"`
|
||||
Logging *LoggingConfig `yaml:",omitempty"`
|
||||
MacAddress string `mapstructure:"mac_address" yaml:"mac_address,omitempty"`
|
||||
NetworkMode string `mapstructure:"network_mode" yaml:"network_mode,omitempty"`
|
||||
Networks map[string]*ServiceNetworkConfig `yaml:",omitempty"`
|
||||
Pid string `yaml:",omitempty"`
|
||||
Ports []ServicePortConfig `yaml:",omitempty"`
|
||||
Privileged bool `yaml:",omitempty"`
|
||||
ReadOnly bool `mapstructure:"read_only" yaml:"read_only,omitempty"`
|
||||
Restart string `yaml:",omitempty"`
|
||||
Secrets []ServiceSecretConfig `yaml:",omitempty"`
|
||||
SecurityOpt []string `mapstructure:"security_opt" yaml:"security_opt,omitempty"`
|
||||
StdinOpen bool `mapstructure:"stdin_open" yaml:"stdin_open,omitempty"`
|
||||
StopGracePeriod *time.Duration `mapstructure:"stop_grace_period" yaml:"stop_grace_period,omitempty"`
|
||||
StopSignal string `mapstructure:"stop_signal" yaml:"stop_signal,omitempty"`
|
||||
Tmpfs StringList `yaml:",omitempty"`
|
||||
Tty bool `mapstructure:"tty" yaml:"tty,omitempty"`
|
||||
Ulimits map[string]*UlimitsConfig `yaml:",omitempty"`
|
||||
User string `yaml:",omitempty"`
|
||||
Volumes []ServiceVolumeConfig `yaml:",omitempty"`
|
||||
WorkingDir string `mapstructure:"working_dir" yaml:"working_dir,omitempty"`
|
||||
Isolation string `mapstructure:"isolation" yaml:"isolation,omitempty"`
|
||||
}
|
||||
|
||||
// BuildConfig is a type for build
|
||||
// using the same format at libcompose: https://github.com/docker/libcompose/blob/master/yaml/build.go#L12
|
||||
type BuildConfig struct {
|
||||
Context string
|
||||
Dockerfile string
|
||||
Args MappingWithEquals
|
||||
Labels Labels
|
||||
CacheFrom StringList `mapstructure:"cache_from"`
|
||||
Network string
|
||||
Target string
|
||||
Context string `yaml:",omitempty"`
|
||||
Dockerfile string `yaml:",omitempty"`
|
||||
Args MappingWithEquals `yaml:",omitempty"`
|
||||
Labels Labels `yaml:",omitempty"`
|
||||
CacheFrom StringList `mapstructure:"cache_from" yaml:"cache_from,omitempty"`
|
||||
Network string `yaml:",omitempty"`
|
||||
Target string `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
// ShellCommand is a string or list of string args
|
||||
|
@ -170,30 +188,30 @@ type HostsList []string
|
|||
|
||||
// LoggingConfig the logging configuration for a service
|
||||
type LoggingConfig struct {
|
||||
Driver string
|
||||
Options map[string]string
|
||||
Driver string `yaml:",omitempty"`
|
||||
Options map[string]string `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
// DeployConfig the deployment configuration for a service
|
||||
type DeployConfig struct {
|
||||
Mode string
|
||||
Replicas *uint64
|
||||
Labels Labels
|
||||
UpdateConfig *UpdateConfig `mapstructure:"update_config"`
|
||||
Resources Resources
|
||||
RestartPolicy *RestartPolicy `mapstructure:"restart_policy"`
|
||||
Placement Placement
|
||||
EndpointMode string `mapstructure:"endpoint_mode"`
|
||||
Mode string `yaml:",omitempty"`
|
||||
Replicas *uint64 `yaml:",omitempty"`
|
||||
Labels Labels `yaml:",omitempty"`
|
||||
UpdateConfig *UpdateConfig `mapstructure:"update_config" yaml:"update_config,omitempty"`
|
||||
Resources Resources `yaml:",omitempty"`
|
||||
RestartPolicy *RestartPolicy `mapstructure:"restart_policy" yaml:"restart_policy,omitempty"`
|
||||
Placement Placement `yaml:",omitempty"`
|
||||
EndpointMode string `mapstructure:"endpoint_mode" yaml:"endpoint_mode,omitempty"`
|
||||
}
|
||||
|
||||
// HealthCheckConfig the healthcheck configuration for a service
|
||||
type HealthCheckConfig struct {
|
||||
Test HealthCheckTest
|
||||
Timeout *time.Duration
|
||||
Interval *time.Duration
|
||||
Retries *uint64
|
||||
StartPeriod *time.Duration `mapstructure:"start_period"`
|
||||
Disable bool
|
||||
Test HealthCheckTest `yaml:",omitempty"`
|
||||
Timeout *time.Duration `yaml:",omitempty"`
|
||||
Interval *time.Duration `yaml:",omitempty"`
|
||||
Retries *uint64 `yaml:",omitempty"`
|
||||
StartPeriod *time.Duration `mapstructure:"start_period" yaml:"start_period,omitempty"`
|
||||
Disable bool `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
// HealthCheckTest is the command run to test the health of a service
|
||||
|
@ -201,32 +219,32 @@ type HealthCheckTest []string
|
|||
|
||||
// UpdateConfig the service update configuration
|
||||
type UpdateConfig struct {
|
||||
Parallelism *uint64
|
||||
Delay time.Duration
|
||||
FailureAction string `mapstructure:"failure_action"`
|
||||
Monitor time.Duration
|
||||
MaxFailureRatio float32 `mapstructure:"max_failure_ratio"`
|
||||
Order string
|
||||
Parallelism *uint64 `yaml:",omitempty"`
|
||||
Delay time.Duration `yaml:",omitempty"`
|
||||
FailureAction string `mapstructure:"failure_action" yaml:"failure_action,omitempty"`
|
||||
Monitor time.Duration `yaml:",omitempty"`
|
||||
MaxFailureRatio float32 `mapstructure:"max_failure_ratio" yaml:"max_failure_ratio,omitempty"`
|
||||
Order string `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
// Resources the resource limits and reservations
|
||||
type Resources struct {
|
||||
Limits *Resource
|
||||
Reservations *Resource
|
||||
Limits *Resource `yaml:",omitempty"`
|
||||
Reservations *Resource `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
// Resource is a resource to be limited or reserved
|
||||
type Resource struct {
|
||||
// TODO: types to convert from units and ratios
|
||||
NanoCPUs string `mapstructure:"cpus"`
|
||||
MemoryBytes UnitBytes `mapstructure:"memory"`
|
||||
GenericResources []GenericResource `mapstructure:"generic_resources"`
|
||||
NanoCPUs string `mapstructure:"cpus" yaml:"cpus,omitempty"`
|
||||
MemoryBytes UnitBytes `mapstructure:"memory" yaml:"memory,omitempty"`
|
||||
GenericResources []GenericResource `mapstructure:"generic_resources" yaml:"generic_resources,omitempty"`
|
||||
}
|
||||
|
||||
// GenericResource represents a "user defined" resource which can
|
||||
// only be an integer (e.g: SSD=3) for a service
|
||||
type GenericResource struct {
|
||||
DiscreteResourceSpec *DiscreteGenericResource `mapstructure:"discrete_resource_spec"`
|
||||
DiscreteResourceSpec *DiscreteGenericResource `mapstructure:"discrete_resource_spec" yaml:"discrete_resource_spec,omitempty"`
|
||||
}
|
||||
|
||||
// DiscreteGenericResource represents a "user defined" resource which is defined
|
||||
|
@ -241,74 +259,79 @@ type DiscreteGenericResource struct {
|
|||
// UnitBytes is the bytes type
|
||||
type UnitBytes int64
|
||||
|
||||
// MarshalYAML makes UnitBytes implement yaml.Marshaller
|
||||
func (u UnitBytes) MarshalYAML() (interface{}, error) {
|
||||
return fmt.Sprintf("%d", u), nil
|
||||
}
|
||||
|
||||
// RestartPolicy the service restart policy
|
||||
type RestartPolicy struct {
|
||||
Condition string
|
||||
Delay *time.Duration
|
||||
MaxAttempts *uint64 `mapstructure:"max_attempts"`
|
||||
Window *time.Duration
|
||||
Condition string `yaml:",omitempty"`
|
||||
Delay *time.Duration `yaml:",omitempty"`
|
||||
MaxAttempts *uint64 `mapstructure:"max_attempts" yaml:"max_attempts,omitempty"`
|
||||
Window *time.Duration `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
// Placement constraints for the service
|
||||
type Placement struct {
|
||||
Constraints []string
|
||||
Preferences []PlacementPreferences
|
||||
Constraints []string `yaml:",omitempty"`
|
||||
Preferences []PlacementPreferences `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
// PlacementPreferences is the preferences for a service placement
|
||||
type PlacementPreferences struct {
|
||||
Spread string
|
||||
Spread string `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
// ServiceNetworkConfig is the network configuration for a service
|
||||
type ServiceNetworkConfig struct {
|
||||
Aliases []string
|
||||
Ipv4Address string `mapstructure:"ipv4_address"`
|
||||
Ipv6Address string `mapstructure:"ipv6_address"`
|
||||
Aliases []string `yaml:",omitempty"`
|
||||
Ipv4Address string `mapstructure:"ipv4_address" yaml:"ipv4_address,omitempty"`
|
||||
Ipv6Address string `mapstructure:"ipv6_address" yaml:"ipv6_address,omitempty"`
|
||||
}
|
||||
|
||||
// ServicePortConfig is the port configuration for a service
|
||||
type ServicePortConfig struct {
|
||||
Mode string
|
||||
Target uint32
|
||||
Published uint32
|
||||
Protocol string
|
||||
Mode string `yaml:",omitempty"`
|
||||
Target uint32 `yaml:",omitempty"`
|
||||
Published uint32 `yaml:",omitempty"`
|
||||
Protocol string `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
// ServiceVolumeConfig are references to a volume used by a service
|
||||
type ServiceVolumeConfig struct {
|
||||
Type string
|
||||
Source string
|
||||
Target string
|
||||
ReadOnly bool `mapstructure:"read_only"`
|
||||
Consistency string
|
||||
Bind *ServiceVolumeBind
|
||||
Volume *ServiceVolumeVolume
|
||||
Tmpfs *ServiceVolumeTmpfs
|
||||
Type string `yaml:",omitempty"`
|
||||
Source string `yaml:",omitempty"`
|
||||
Target string `yaml:",omitempty"`
|
||||
ReadOnly bool `mapstructure:"read_only" yaml:"read_only,omitempty"`
|
||||
Consistency string `yaml:",omitempty"`
|
||||
Bind *ServiceVolumeBind `yaml:",omitempty"`
|
||||
Volume *ServiceVolumeVolume `yaml:",omitempty"`
|
||||
Tmpfs *ServiceVolumeTmpfs `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
// ServiceVolumeBind are options for a service volume of type bind
|
||||
type ServiceVolumeBind struct {
|
||||
Propagation string
|
||||
Propagation string `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
// ServiceVolumeVolume are options for a service volume of type volume
|
||||
type ServiceVolumeVolume struct {
|
||||
NoCopy bool `mapstructure:"nocopy"`
|
||||
NoCopy bool `mapstructure:"nocopy" yaml:"nocopy,omitempty"`
|
||||
}
|
||||
|
||||
// ServiceVolumeTmpfs are options for a service volume of type tmpfs
|
||||
type ServiceVolumeTmpfs struct {
|
||||
Size int64
|
||||
Size int64 `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
// FileReferenceConfig for a reference to a swarm file object
|
||||
type FileReferenceConfig struct {
|
||||
Source string
|
||||
Target string
|
||||
UID string
|
||||
GID string
|
||||
Mode *uint32
|
||||
Source string `yaml:",omitempty"`
|
||||
Target string `yaml:",omitempty"`
|
||||
UID string `yaml:",omitempty"`
|
||||
GID string `yaml:",omitempty"`
|
||||
Mode *uint32 `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
// ServiceConfigObjConfig is the config obj configuration for a service
|
||||
|
@ -319,63 +342,79 @@ type ServiceSecretConfig FileReferenceConfig
|
|||
|
||||
// UlimitsConfig the ulimit configuration
|
||||
type UlimitsConfig struct {
|
||||
Single int
|
||||
Soft int
|
||||
Hard int
|
||||
Single int `yaml:",omitempty"`
|
||||
Soft int `yaml:",omitempty"`
|
||||
Hard int `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
// MarshalYAML makes UlimitsConfig implement yaml.Marshaller
|
||||
func (u *UlimitsConfig) MarshalYAML() (interface{}, error) {
|
||||
if u.Single != 0 {
|
||||
return u.Single, nil
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// NetworkConfig for a network
|
||||
type NetworkConfig struct {
|
||||
Name string
|
||||
Driver string
|
||||
DriverOpts map[string]string `mapstructure:"driver_opts"`
|
||||
Ipam IPAMConfig
|
||||
External External
|
||||
Internal bool
|
||||
Attachable bool
|
||||
Labels Labels
|
||||
Name string `yaml:",omitempty"`
|
||||
Driver string `yaml:",omitempty"`
|
||||
DriverOpts map[string]string `mapstructure:"driver_opts" yaml:"driver_opts,omitempty"`
|
||||
Ipam IPAMConfig `yaml:",omitempty"`
|
||||
External External `yaml:",omitempty"`
|
||||
Internal bool `yaml:",omitempty"`
|
||||
Attachable bool `yaml:",omitempty"`
|
||||
Labels Labels `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
// IPAMConfig for a network
|
||||
type IPAMConfig struct {
|
||||
Driver string
|
||||
Config []*IPAMPool
|
||||
Driver string `yaml:",omitempty"`
|
||||
Config []*IPAMPool `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
// IPAMPool for a network
|
||||
type IPAMPool struct {
|
||||
Subnet string
|
||||
Subnet string `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
// VolumeConfig for a volume
|
||||
type VolumeConfig struct {
|
||||
Name string
|
||||
Driver string
|
||||
DriverOpts map[string]string `mapstructure:"driver_opts"`
|
||||
External External
|
||||
Labels Labels
|
||||
Name string `yaml:",omitempty"`
|
||||
Driver string `yaml:",omitempty"`
|
||||
DriverOpts map[string]string `mapstructure:"driver_opts" yaml:"driver_opts,omitempty"`
|
||||
External External `yaml:",omitempty"`
|
||||
Labels Labels `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
// External identifies a Volume or Network as a reference to a resource that is
|
||||
// not managed, and should already exist.
|
||||
// External.name is deprecated and replaced by Volume.name
|
||||
type External struct {
|
||||
Name string
|
||||
External bool
|
||||
Name string `yaml:",omitempty"`
|
||||
External bool `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
// MarshalYAML makes External implement yaml.Marshaller
|
||||
func (e External) MarshalYAML() (interface{}, error) {
|
||||
if e.Name == "" {
|
||||
return e.External, nil
|
||||
}
|
||||
return External{Name: e.Name}, nil
|
||||
}
|
||||
|
||||
// CredentialSpecConfig for credential spec on Windows
|
||||
type CredentialSpecConfig struct {
|
||||
File string
|
||||
Registry string
|
||||
File string `yaml:",omitempty"`
|
||||
Registry string `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
// FileObjectConfig is a config type for a file used by a service
|
||||
type FileObjectConfig struct {
|
||||
Name string
|
||||
File string
|
||||
External External
|
||||
Labels Labels
|
||||
Name string `yaml:",omitempty"`
|
||||
File string `yaml:",omitempty"`
|
||||
External External `yaml:",omitempty"`
|
||||
Labels Labels `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
// SecretConfig for a secret
|
||||
|
|
Loading…
Reference in New Issue