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