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"
|
"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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 (
|
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"
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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…
Reference in New Issue