Merge pull request #845 from vdemeester/stack-load-the-same
[compose] Share the compose loading code between swarm and k8s stack deploy
This commit is contained in:
commit
f56265ae3e
@ -5,8 +5,9 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/command/stack/loader"
|
||||||
"github.com/docker/cli/cli/command/stack/options"
|
"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"
|
"github.com/pkg/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
corev1 "k8s.io/client-go/kubernetes/typed/core/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 {
|
if len(opts.Composefiles) == 0 {
|
||||||
return errors.Errorf("Please specify only one compose file (with --compose-file).")
|
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
|
// Initialize clients
|
||||||
stacks, err := dockerCli.stacks()
|
stacks, err := dockerCli.stacks()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -36,12 +48,6 @@ func RunDeploy(dockerCli *KubeCli, opts options.Deploy) error {
|
|||||||
Pods: pods,
|
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
|
// FIXME(vdemeester) handle warnings server-side
|
||||||
if err = IsColliding(services, stack, cfg); err != nil {
|
if err = IsColliding(services, stack, cfg); err != nil {
|
||||||
return err
|
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.
|
// 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 {
|
for name, config := range globalConfigs {
|
||||||
if config.File == "" {
|
if config.File == "" {
|
||||||
continue
|
continue
|
||||||
@ -102,7 +108,7 @@ func createFileBasedConfigMaps(stackName string, globalConfigs map[string]compos
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func serviceNames(cfg *composeTypes.Config) []string {
|
func serviceNames(cfg *composetypes.Config) []string {
|
||||||
names := []string{}
|
names := []string{}
|
||||||
|
|
||||||
for _, service := range cfg.Services {
|
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.
|
// 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 {
|
for name, secret := range globalSecrets {
|
||||||
if secret.File == "" {
|
if secret.File == "" {
|
||||||
continue
|
continue
|
||||||
|
@ -1,170 +1,32 @@
|
|||||||
package kubernetes
|
package kubernetes
|
||||||
|
|
||||||
import (
|
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"
|
composetypes "github.com/docker/cli/cli/compose/types"
|
||||||
apiv1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1"
|
apiv1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1"
|
||||||
"github.com/pkg/errors"
|
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LoadStack loads a stack from a Compose file, with a given name.
|
type versionedConfig struct {
|
||||||
// FIXME(vdemeester) remove this and use cli/compose/loader for both swarm and kubernetes
|
composetypes.Config
|
||||||
func LoadStack(name string, composeFiles []string) (*apiv1beta1.Stack, *composetypes.Config, error) {
|
Version string
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
// LoadStack loads a stack from a Compose config, with a given name.
|
||||||
processed, err := template.Substitute(string(binary), func(key string) (string, bool) { return env[key], true })
|
func LoadStack(name, version string, cfg *composetypes.Config) (*apiv1beta1.Stack, error) {
|
||||||
if err != nil {
|
res, err := yaml.Marshal(versionedConfig{
|
||||||
return nil, nil, errors.Wrap(err, "cannot load compose file")
|
Version: version,
|
||||||
}
|
Config: *cfg,
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
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{
|
return &apiv1beta1.Stack{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: name,
|
Name: name,
|
||||||
},
|
},
|
||||||
Spec: apiv1beta1.StackSpec{
|
Spec: apiv1beta1.StackSpec{
|
||||||
ComposeFile: result,
|
ComposeFile: string(res),
|
||||||
},
|
},
|
||||||
}, cfg, nil
|
}, 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
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
152
cli/command/stack/loader/loader.go
Normal file
152
cli/command/stack/loader/loader.go
Normal file
@ -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
|
||||||
|
}
|
47
cli/command/stack/loader/loader_test.go
Normal file
47
cli/command/stack/loader/loader_test.go
Normal file
@ -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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command"
|
"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/command/stack/options"
|
||||||
"github.com/docker/cli/cli/compose/convert"
|
"github.com/docker/cli/cli/compose/convert"
|
||||||
"github.com/docker/cli/cli/compose/loader"
|
|
||||||
composetypes "github.com/docker/cli/cli/compose/types"
|
composetypes "github.com/docker/cli/cli/compose/types"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
@ -24,34 +18,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Deploy) error {
|
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 {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err := checkDaemonIsSwarmManager(ctx, dockerCli); err != nil {
|
||||||
return err
|
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)
|
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{} {
|
func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
|
||||||
serviceNetworks := map[string]struct{}{}
|
serviceNetworks := map[string]struct{}{}
|
||||||
for _, serviceConfig := range serviceConfigs {
|
for _, serviceConfig := range serviceConfigs {
|
||||||
@ -122,96 +83,6 @@ func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) ma
|
|||||||
return serviceNetworks
|
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(
|
func validateExternalNetworks(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
client dockerclient.NetworkAPIClient,
|
client dockerclient.NetworkAPIClient,
|
||||||
|
@ -1,56 +1,16 @@
|
|||||||
package swarm
|
package swarm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/internal/test/network"
|
"github.com/docker/cli/internal/test/network"
|
||||||
"github.com/docker/cli/internal/test/testutil"
|
"github.com/docker/cli/internal/test/testutil"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/gotestyourself/gotestyourself/fs"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"golang.org/x/net/context"
|
"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 {
|
type notFound struct {
|
||||||
error
|
error
|
||||||
}
|
}
|
||||||
|
@ -193,7 +193,7 @@ services:
|
|||||||
|
|
||||||
ports:
|
ports:
|
||||||
- 3000
|
- 3000
|
||||||
- "3000-3005"
|
- "3001-3005"
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
- "9090-9091:8080-8081"
|
- "9090-9091:8080-8081"
|
||||||
- "49100:22"
|
- "49100:22"
|
||||||
|
390
cli/compose/loader/full-struct_test.go
Normal file
390
cli/compose/loader/full-struct_test.go
Normal file
@ -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()
|
workingDir, err := os.Getwd()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
stopGracePeriod := time.Duration(20 * time.Second)
|
expectedConfig := fullExampleConfig(workingDir, homeDir)
|
||||||
|
|
||||||
expectedServiceConfig := types.ServiceConfig{
|
assert.Equal(t, expectedConfig.Services, config.Services)
|
||||||
Name: "foo",
|
assert.Equal(t, expectedConfig.Networks, config.Networks)
|
||||||
|
assert.Equal(t, expectedConfig.Volumes, config.Volumes)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadTmpfsVolume(t *testing.T) {
|
func TestLoadTmpfsVolume(t *testing.T) {
|
||||||
|
328
cli/compose/loader/types_test.go
Normal file
328
cli/compose/loader/types_test.go
Normal file
@ -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
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -77,69 +78,86 @@ type Config struct {
|
|||||||
Configs map[string]ConfigObjConfig
|
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
|
// ServiceConfig is the configuration of one service
|
||||||
type ServiceConfig struct {
|
type ServiceConfig struct {
|
||||||
Name string
|
Name string `yaml:",omitempty"`
|
||||||
|
|
||||||
Build BuildConfig
|
Build BuildConfig `yaml:",omitempty"`
|
||||||
CapAdd []string `mapstructure:"cap_add"`
|
CapAdd []string `mapstructure:"cap_add" yaml:"cap_add,omitempty"`
|
||||||
CapDrop []string `mapstructure:"cap_drop"`
|
CapDrop []string `mapstructure:"cap_drop" yaml:"cap_drop,omitempty"`
|
||||||
CgroupParent string `mapstructure:"cgroup_parent"`
|
CgroupParent string `mapstructure:"cgroup_parent" yaml:"cgroup_parent,omitempty"`
|
||||||
Command ShellCommand
|
Command ShellCommand `yaml:",omitempty"`
|
||||||
Configs []ServiceConfigObjConfig
|
Configs []ServiceConfigObjConfig `yaml:",omitempty"`
|
||||||
ContainerName string `mapstructure:"container_name"`
|
ContainerName string `mapstructure:"container_name" yaml:"container_name,omitempty"`
|
||||||
CredentialSpec CredentialSpecConfig `mapstructure:"credential_spec"`
|
CredentialSpec CredentialSpecConfig `mapstructure:"credential_spec" yaml:"credential_spec,omitempty"`
|
||||||
DependsOn []string `mapstructure:"depends_on"`
|
DependsOn []string `mapstructure:"depends_on" yaml:"depends_on,omitempty"`
|
||||||
Deploy DeployConfig
|
Deploy DeployConfig `yaml:",omitempty"`
|
||||||
Devices []string
|
Devices []string `yaml:",omitempty"`
|
||||||
DNS StringList
|
DNS StringList `yaml:",omitempty"`
|
||||||
DNSSearch StringList `mapstructure:"dns_search"`
|
DNSSearch StringList `mapstructure:"dns_search" yaml:"dns_search,omitempty"`
|
||||||
DomainName string `mapstructure:"domainname"`
|
DomainName string `mapstructure:"domainname" yaml:"domainname,omitempty"`
|
||||||
Entrypoint ShellCommand
|
Entrypoint ShellCommand `yaml:",omitempty"`
|
||||||
Environment MappingWithEquals
|
Environment MappingWithEquals `yaml:",omitempty"`
|
||||||
EnvFile StringList `mapstructure:"env_file"`
|
EnvFile StringList `mapstructure:"env_file" yaml:"env_file,omitempty"`
|
||||||
Expose StringOrNumberList
|
Expose StringOrNumberList `yaml:",omitempty"`
|
||||||
ExternalLinks []string `mapstructure:"external_links"`
|
ExternalLinks []string `mapstructure:"external_links" yaml:"external_links,omitempty"`
|
||||||
ExtraHosts HostsList `mapstructure:"extra_hosts"`
|
ExtraHosts HostsList `mapstructure:"extra_hosts" yaml:"extra_hosts,omitempty"`
|
||||||
Hostname string
|
Hostname string `yaml:",omitempty"`
|
||||||
HealthCheck *HealthCheckConfig
|
HealthCheck *HealthCheckConfig `yaml:",omitempty"`
|
||||||
Image string
|
Image string `yaml:",omitempty"`
|
||||||
Ipc string
|
Ipc string `yaml:",omitempty"`
|
||||||
Labels Labels
|
Labels Labels `yaml:",omitempty"`
|
||||||
Links []string
|
Links []string `yaml:",omitempty"`
|
||||||
Logging *LoggingConfig
|
Logging *LoggingConfig `yaml:",omitempty"`
|
||||||
MacAddress string `mapstructure:"mac_address"`
|
MacAddress string `mapstructure:"mac_address" yaml:"mac_address,omitempty"`
|
||||||
NetworkMode string `mapstructure:"network_mode"`
|
NetworkMode string `mapstructure:"network_mode" yaml:"network_mode,omitempty"`
|
||||||
Networks map[string]*ServiceNetworkConfig
|
Networks map[string]*ServiceNetworkConfig `yaml:",omitempty"`
|
||||||
Pid string
|
Pid string `yaml:",omitempty"`
|
||||||
Ports []ServicePortConfig
|
Ports []ServicePortConfig `yaml:",omitempty"`
|
||||||
Privileged bool
|
Privileged bool `yaml:",omitempty"`
|
||||||
ReadOnly bool `mapstructure:"read_only"`
|
ReadOnly bool `mapstructure:"read_only" yaml:"read_only,omitempty"`
|
||||||
Restart string
|
Restart string `yaml:",omitempty"`
|
||||||
Secrets []ServiceSecretConfig
|
Secrets []ServiceSecretConfig `yaml:",omitempty"`
|
||||||
SecurityOpt []string `mapstructure:"security_opt"`
|
SecurityOpt []string `mapstructure:"security_opt" yaml:"security_opt,omitempty"`
|
||||||
StdinOpen bool `mapstructure:"stdin_open"`
|
StdinOpen bool `mapstructure:"stdin_open" yaml:"stdin_open,omitempty"`
|
||||||
StopGracePeriod *time.Duration `mapstructure:"stop_grace_period"`
|
StopGracePeriod *time.Duration `mapstructure:"stop_grace_period" yaml:"stop_grace_period,omitempty"`
|
||||||
StopSignal string `mapstructure:"stop_signal"`
|
StopSignal string `mapstructure:"stop_signal" yaml:"stop_signal,omitempty"`
|
||||||
Tmpfs StringList
|
Tmpfs StringList `yaml:",omitempty"`
|
||||||
Tty bool `mapstructure:"tty"`
|
Tty bool `mapstructure:"tty" yaml:"tty,omitempty"`
|
||||||
Ulimits map[string]*UlimitsConfig
|
Ulimits map[string]*UlimitsConfig `yaml:",omitempty"`
|
||||||
User string
|
User string `yaml:",omitempty"`
|
||||||
Volumes []ServiceVolumeConfig
|
Volumes []ServiceVolumeConfig `yaml:",omitempty"`
|
||||||
WorkingDir string `mapstructure:"working_dir"`
|
WorkingDir string `mapstructure:"working_dir" yaml:"working_dir,omitempty"`
|
||||||
Isolation string `mapstructure:"isolation"`
|
Isolation string `mapstructure:"isolation" yaml:"isolation,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildConfig is a type for build
|
// BuildConfig is a type for build
|
||||||
// using the same format at libcompose: https://github.com/docker/libcompose/blob/master/yaml/build.go#L12
|
// using the same format at libcompose: https://github.com/docker/libcompose/blob/master/yaml/build.go#L12
|
||||||
type BuildConfig struct {
|
type BuildConfig struct {
|
||||||
Context string
|
Context string `yaml:",omitempty"`
|
||||||
Dockerfile string
|
Dockerfile string `yaml:",omitempty"`
|
||||||
Args MappingWithEquals
|
Args MappingWithEquals `yaml:",omitempty"`
|
||||||
Labels Labels
|
Labels Labels `yaml:",omitempty"`
|
||||||
CacheFrom StringList `mapstructure:"cache_from"`
|
CacheFrom StringList `mapstructure:"cache_from" yaml:"cache_from,omitempty"`
|
||||||
Network string
|
Network string `yaml:",omitempty"`
|
||||||
Target string
|
Target string `yaml:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShellCommand is a string or list of string args
|
// ShellCommand is a string or list of string args
|
||||||
@ -170,30 +188,30 @@ type HostsList []string
|
|||||||
|
|
||||||
// LoggingConfig the logging configuration for a service
|
// LoggingConfig the logging configuration for a service
|
||||||
type LoggingConfig struct {
|
type LoggingConfig struct {
|
||||||
Driver string
|
Driver string `yaml:",omitempty"`
|
||||||
Options map[string]string
|
Options map[string]string `yaml:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeployConfig the deployment configuration for a service
|
// DeployConfig the deployment configuration for a service
|
||||||
type DeployConfig struct {
|
type DeployConfig struct {
|
||||||
Mode string
|
Mode string `yaml:",omitempty"`
|
||||||
Replicas *uint64
|
Replicas *uint64 `yaml:",omitempty"`
|
||||||
Labels Labels
|
Labels Labels `yaml:",omitempty"`
|
||||||
UpdateConfig *UpdateConfig `mapstructure:"update_config"`
|
UpdateConfig *UpdateConfig `mapstructure:"update_config" yaml:"update_config,omitempty"`
|
||||||
Resources Resources
|
Resources Resources `yaml:",omitempty"`
|
||||||
RestartPolicy *RestartPolicy `mapstructure:"restart_policy"`
|
RestartPolicy *RestartPolicy `mapstructure:"restart_policy" yaml:"restart_policy,omitempty"`
|
||||||
Placement Placement
|
Placement Placement `yaml:",omitempty"`
|
||||||
EndpointMode string `mapstructure:"endpoint_mode"`
|
EndpointMode string `mapstructure:"endpoint_mode" yaml:"endpoint_mode,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HealthCheckConfig the healthcheck configuration for a service
|
// HealthCheckConfig the healthcheck configuration for a service
|
||||||
type HealthCheckConfig struct {
|
type HealthCheckConfig struct {
|
||||||
Test HealthCheckTest
|
Test HealthCheckTest `yaml:",omitempty"`
|
||||||
Timeout *time.Duration
|
Timeout *time.Duration `yaml:",omitempty"`
|
||||||
Interval *time.Duration
|
Interval *time.Duration `yaml:",omitempty"`
|
||||||
Retries *uint64
|
Retries *uint64 `yaml:",omitempty"`
|
||||||
StartPeriod *time.Duration `mapstructure:"start_period"`
|
StartPeriod *time.Duration `mapstructure:"start_period" yaml:"start_period,omitempty"`
|
||||||
Disable bool
|
Disable bool `yaml:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HealthCheckTest is the command run to test the health of a service
|
// HealthCheckTest is the command run to test the health of a service
|
||||||
@ -201,32 +219,32 @@ type HealthCheckTest []string
|
|||||||
|
|
||||||
// UpdateConfig the service update configuration
|
// UpdateConfig the service update configuration
|
||||||
type UpdateConfig struct {
|
type UpdateConfig struct {
|
||||||
Parallelism *uint64
|
Parallelism *uint64 `yaml:",omitempty"`
|
||||||
Delay time.Duration
|
Delay time.Duration `yaml:",omitempty"`
|
||||||
FailureAction string `mapstructure:"failure_action"`
|
FailureAction string `mapstructure:"failure_action" yaml:"failure_action,omitempty"`
|
||||||
Monitor time.Duration
|
Monitor time.Duration `yaml:",omitempty"`
|
||||||
MaxFailureRatio float32 `mapstructure:"max_failure_ratio"`
|
MaxFailureRatio float32 `mapstructure:"max_failure_ratio" yaml:"max_failure_ratio,omitempty"`
|
||||||
Order string
|
Order string `yaml:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resources the resource limits and reservations
|
// Resources the resource limits and reservations
|
||||||
type Resources struct {
|
type Resources struct {
|
||||||
Limits *Resource
|
Limits *Resource `yaml:",omitempty"`
|
||||||
Reservations *Resource
|
Reservations *Resource `yaml:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resource is a resource to be limited or reserved
|
// Resource is a resource to be limited or reserved
|
||||||
type Resource struct {
|
type Resource struct {
|
||||||
// TODO: types to convert from units and ratios
|
// TODO: types to convert from units and ratios
|
||||||
NanoCPUs string `mapstructure:"cpus"`
|
NanoCPUs string `mapstructure:"cpus" yaml:"cpus,omitempty"`
|
||||||
MemoryBytes UnitBytes `mapstructure:"memory"`
|
MemoryBytes UnitBytes `mapstructure:"memory" yaml:"memory,omitempty"`
|
||||||
GenericResources []GenericResource `mapstructure:"generic_resources"`
|
GenericResources []GenericResource `mapstructure:"generic_resources" yaml:"generic_resources,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenericResource represents a "user defined" resource which can
|
// GenericResource represents a "user defined" resource which can
|
||||||
// only be an integer (e.g: SSD=3) for a service
|
// only be an integer (e.g: SSD=3) for a service
|
||||||
type GenericResource struct {
|
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
|
// DiscreteGenericResource represents a "user defined" resource which is defined
|
||||||
@ -241,74 +259,79 @@ type DiscreteGenericResource struct {
|
|||||||
// UnitBytes is the bytes type
|
// UnitBytes is the bytes type
|
||||||
type UnitBytes int64
|
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
|
// RestartPolicy the service restart policy
|
||||||
type RestartPolicy struct {
|
type RestartPolicy struct {
|
||||||
Condition string
|
Condition string `yaml:",omitempty"`
|
||||||
Delay *time.Duration
|
Delay *time.Duration `yaml:",omitempty"`
|
||||||
MaxAttempts *uint64 `mapstructure:"max_attempts"`
|
MaxAttempts *uint64 `mapstructure:"max_attempts" yaml:"max_attempts,omitempty"`
|
||||||
Window *time.Duration
|
Window *time.Duration `yaml:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Placement constraints for the service
|
// Placement constraints for the service
|
||||||
type Placement struct {
|
type Placement struct {
|
||||||
Constraints []string
|
Constraints []string `yaml:",omitempty"`
|
||||||
Preferences []PlacementPreferences
|
Preferences []PlacementPreferences `yaml:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlacementPreferences is the preferences for a service placement
|
// PlacementPreferences is the preferences for a service placement
|
||||||
type PlacementPreferences struct {
|
type PlacementPreferences struct {
|
||||||
Spread string
|
Spread string `yaml:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceNetworkConfig is the network configuration for a service
|
// ServiceNetworkConfig is the network configuration for a service
|
||||||
type ServiceNetworkConfig struct {
|
type ServiceNetworkConfig struct {
|
||||||
Aliases []string
|
Aliases []string `yaml:",omitempty"`
|
||||||
Ipv4Address string `mapstructure:"ipv4_address"`
|
Ipv4Address string `mapstructure:"ipv4_address" yaml:"ipv4_address,omitempty"`
|
||||||
Ipv6Address string `mapstructure:"ipv6_address"`
|
Ipv6Address string `mapstructure:"ipv6_address" yaml:"ipv6_address,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServicePortConfig is the port configuration for a service
|
// ServicePortConfig is the port configuration for a service
|
||||||
type ServicePortConfig struct {
|
type ServicePortConfig struct {
|
||||||
Mode string
|
Mode string `yaml:",omitempty"`
|
||||||
Target uint32
|
Target uint32 `yaml:",omitempty"`
|
||||||
Published uint32
|
Published uint32 `yaml:",omitempty"`
|
||||||
Protocol string
|
Protocol string `yaml:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceVolumeConfig are references to a volume used by a service
|
// ServiceVolumeConfig are references to a volume used by a service
|
||||||
type ServiceVolumeConfig struct {
|
type ServiceVolumeConfig struct {
|
||||||
Type string
|
Type string `yaml:",omitempty"`
|
||||||
Source string
|
Source string `yaml:",omitempty"`
|
||||||
Target string
|
Target string `yaml:",omitempty"`
|
||||||
ReadOnly bool `mapstructure:"read_only"`
|
ReadOnly bool `mapstructure:"read_only" yaml:"read_only,omitempty"`
|
||||||
Consistency string
|
Consistency string `yaml:",omitempty"`
|
||||||
Bind *ServiceVolumeBind
|
Bind *ServiceVolumeBind `yaml:",omitempty"`
|
||||||
Volume *ServiceVolumeVolume
|
Volume *ServiceVolumeVolume `yaml:",omitempty"`
|
||||||
Tmpfs *ServiceVolumeTmpfs
|
Tmpfs *ServiceVolumeTmpfs `yaml:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceVolumeBind are options for a service volume of type bind
|
// ServiceVolumeBind are options for a service volume of type bind
|
||||||
type ServiceVolumeBind struct {
|
type ServiceVolumeBind struct {
|
||||||
Propagation string
|
Propagation string `yaml:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceVolumeVolume are options for a service volume of type volume
|
// ServiceVolumeVolume are options for a service volume of type volume
|
||||||
type ServiceVolumeVolume struct {
|
type ServiceVolumeVolume struct {
|
||||||
NoCopy bool `mapstructure:"nocopy"`
|
NoCopy bool `mapstructure:"nocopy" yaml:"nocopy,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceVolumeTmpfs are options for a service volume of type tmpfs
|
// ServiceVolumeTmpfs are options for a service volume of type tmpfs
|
||||||
type ServiceVolumeTmpfs struct {
|
type ServiceVolumeTmpfs struct {
|
||||||
Size int64
|
Size int64 `yaml:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileReferenceConfig for a reference to a swarm file object
|
// FileReferenceConfig for a reference to a swarm file object
|
||||||
type FileReferenceConfig struct {
|
type FileReferenceConfig struct {
|
||||||
Source string
|
Source string `yaml:",omitempty"`
|
||||||
Target string
|
Target string `yaml:",omitempty"`
|
||||||
UID string
|
UID string `yaml:",omitempty"`
|
||||||
GID string
|
GID string `yaml:",omitempty"`
|
||||||
Mode *uint32
|
Mode *uint32 `yaml:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceConfigObjConfig is the config obj configuration for a service
|
// ServiceConfigObjConfig is the config obj configuration for a service
|
||||||
@ -319,63 +342,79 @@ type ServiceSecretConfig FileReferenceConfig
|
|||||||
|
|
||||||
// UlimitsConfig the ulimit configuration
|
// UlimitsConfig the ulimit configuration
|
||||||
type UlimitsConfig struct {
|
type UlimitsConfig struct {
|
||||||
Single int
|
Single int `yaml:",omitempty"`
|
||||||
Soft int
|
Soft int `yaml:",omitempty"`
|
||||||
Hard int
|
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
|
// NetworkConfig for a network
|
||||||
type NetworkConfig struct {
|
type NetworkConfig struct {
|
||||||
Name string
|
Name string `yaml:",omitempty"`
|
||||||
Driver string
|
Driver string `yaml:",omitempty"`
|
||||||
DriverOpts map[string]string `mapstructure:"driver_opts"`
|
DriverOpts map[string]string `mapstructure:"driver_opts" yaml:"driver_opts,omitempty"`
|
||||||
Ipam IPAMConfig
|
Ipam IPAMConfig `yaml:",omitempty"`
|
||||||
External External
|
External External `yaml:",omitempty"`
|
||||||
Internal bool
|
Internal bool `yaml:",omitempty"`
|
||||||
Attachable bool
|
Attachable bool `yaml:",omitempty"`
|
||||||
Labels Labels
|
Labels Labels `yaml:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IPAMConfig for a network
|
// IPAMConfig for a network
|
||||||
type IPAMConfig struct {
|
type IPAMConfig struct {
|
||||||
Driver string
|
Driver string `yaml:",omitempty"`
|
||||||
Config []*IPAMPool
|
Config []*IPAMPool `yaml:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IPAMPool for a network
|
// IPAMPool for a network
|
||||||
type IPAMPool struct {
|
type IPAMPool struct {
|
||||||
Subnet string
|
Subnet string `yaml:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// VolumeConfig for a volume
|
// VolumeConfig for a volume
|
||||||
type VolumeConfig struct {
|
type VolumeConfig struct {
|
||||||
Name string
|
Name string `yaml:",omitempty"`
|
||||||
Driver string
|
Driver string `yaml:",omitempty"`
|
||||||
DriverOpts map[string]string `mapstructure:"driver_opts"`
|
DriverOpts map[string]string `mapstructure:"driver_opts" yaml:"driver_opts,omitempty"`
|
||||||
External External
|
External External `yaml:",omitempty"`
|
||||||
Labels Labels
|
Labels Labels `yaml:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// External identifies a Volume or Network as a reference to a resource that is
|
// External identifies a Volume or Network as a reference to a resource that is
|
||||||
// not managed, and should already exist.
|
// not managed, and should already exist.
|
||||||
// External.name is deprecated and replaced by Volume.name
|
// External.name is deprecated and replaced by Volume.name
|
||||||
type External struct {
|
type External struct {
|
||||||
Name string
|
Name string `yaml:",omitempty"`
|
||||||
External bool
|
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
|
// CredentialSpecConfig for credential spec on Windows
|
||||||
type CredentialSpecConfig struct {
|
type CredentialSpecConfig struct {
|
||||||
File string
|
File string `yaml:",omitempty"`
|
||||||
Registry string
|
Registry string `yaml:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileObjectConfig is a config type for a file used by a service
|
// FileObjectConfig is a config type for a file used by a service
|
||||||
type FileObjectConfig struct {
|
type FileObjectConfig struct {
|
||||||
Name string
|
Name string `yaml:",omitempty"`
|
||||||
File string
|
File string `yaml:",omitempty"`
|
||||||
External External
|
External External `yaml:",omitempty"`
|
||||||
Labels Labels
|
Labels Labels `yaml:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SecretConfig for a secret
|
// SecretConfig for a secret
|
||||||
|
Loading…
x
Reference in New Issue
Block a user