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:
Vincent Demeester 2018-01-29 13:18:43 -08:00
parent 3e344ae425
commit 570ee9cb54
No known key found for this signature in database
GPG Key ID: 083CC6FD6EB699A3
12 changed files with 1130 additions and 884 deletions

View File

@ -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

View File

@ -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 // LoadStack loads a stack from a Compose config, with a given name.
if !strings.HasPrefix(composePath, "/") { func LoadStack(name, version string, cfg *composetypes.Config) (*apiv1beta1.Stack, error) {
composePath = filepath.Join(workingDir, composeFile) res, err := yaml.Marshal(versionedConfig{
} Version: version,
Config: *cfg,
if _, err := os.Stat(composePath); os.IsNotExist(err) {
return nil, nil, errors.Errorf("no compose file found in %s", filepath.Dir(composePath))
}
binary, err := ioutil.ReadFile(composePath)
if err != nil {
return nil, nil, errors.Wrap(err, "cannot read compose file")
}
env := env(workingDir)
return load(name, binary, workingDir, env)
}
func load(name string, binary []byte, workingDir string, env map[string]string) (*apiv1beta1.Stack, *composetypes.Config, error) {
processed, err := template.Substitute(string(binary), func(key string) (string, bool) { return env[key], true })
if err != nil {
return nil, nil, errors.Wrap(err, "cannot load compose file")
}
parsed, err := loader.ParseYAML([]byte(processed))
if err != nil {
return nil, nil, errors.Wrapf(err, "cannot load compose file")
}
cfg, err := loader.Load(composetypes.ConfigDetails{
WorkingDir: workingDir,
ConfigFiles: []composetypes.ConfigFile{
{
Config: parsed,
},
},
}) })
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
} }

View File

@ -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)
}
}

View File

@ -0,0 +1,152 @@
package loader
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/stack/options"
"github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/cli/compose/schema"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/pkg/errors"
)
// LoadComposefile parse the composefile specified in the cli and returns its Config and version.
func LoadComposefile(dockerCli command.Cli, opts options.Deploy) (*composetypes.Config, string, error) {
configDetails, err := getConfigDetails(opts.Composefiles, dockerCli.In())
if err != nil {
return nil, "", err
}
dicts := getDictsFrom(configDetails.ConfigFiles)
config, err := loader.Load(configDetails)
if err != nil {
if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok {
return nil, "", errors.Errorf("Compose file contains unsupported options:\n\n%s\n",
propertyWarnings(fpe.Properties))
}
return nil, "", err
}
unsupportedProperties := loader.GetUnsupportedProperties(dicts...)
if len(unsupportedProperties) > 0 {
fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n",
strings.Join(unsupportedProperties, ", "))
}
deprecatedProperties := loader.GetDeprecatedProperties(dicts...)
if len(deprecatedProperties) > 0 {
fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n",
propertyWarnings(deprecatedProperties))
}
return config, configDetails.Version, nil
}
func getDictsFrom(configFiles []composetypes.ConfigFile) []map[string]interface{} {
dicts := []map[string]interface{}{}
for _, configFile := range configFiles {
dicts = append(dicts, configFile.Config)
}
return dicts
}
func propertyWarnings(properties map[string]string) string {
var msgs []string
for name, description := range properties {
msgs = append(msgs, fmt.Sprintf("%s: %s", name, description))
}
sort.Strings(msgs)
return strings.Join(msgs, "\n\n")
}
func getConfigDetails(composefiles []string, stdin io.Reader) (composetypes.ConfigDetails, error) {
var details composetypes.ConfigDetails
if len(composefiles) == 0 {
return details, errors.New("no composefile(s)")
}
if composefiles[0] == "-" && len(composefiles) == 1 {
workingDir, err := os.Getwd()
if err != nil {
return details, err
}
details.WorkingDir = workingDir
} else {
absPath, err := filepath.Abs(composefiles[0])
if err != nil {
return details, err
}
details.WorkingDir = filepath.Dir(absPath)
}
var err error
details.ConfigFiles, err = loadConfigFiles(composefiles, stdin)
if err != nil {
return details, err
}
// Take the first file version (2 files can't have different version)
details.Version = schema.Version(details.ConfigFiles[0].Config)
details.Environment, err = buildEnvironment(os.Environ())
return details, err
}
func buildEnvironment(env []string) (map[string]string, error) {
result := make(map[string]string, len(env))
for _, s := range env {
// if value is empty, s is like "K=", not "K".
if !strings.Contains(s, "=") {
return result, errors.Errorf("unexpected environment %q", s)
}
kv := strings.SplitN(s, "=", 2)
result[kv[0]] = kv[1]
}
return result, nil
}
func loadConfigFiles(filenames []string, stdin io.Reader) ([]composetypes.ConfigFile, error) {
var configFiles []composetypes.ConfigFile
for _, filename := range filenames {
configFile, err := loadConfigFile(filename, stdin)
if err != nil {
return configFiles, err
}
configFiles = append(configFiles, *configFile)
}
return configFiles, nil
}
func loadConfigFile(filename string, stdin io.Reader) (*composetypes.ConfigFile, error) {
var bytes []byte
var err error
if filename == "-" {
bytes, err = ioutil.ReadAll(stdin)
} else {
bytes, err = ioutil.ReadFile(filename)
}
if err != nil {
return nil, err
}
config, err := loader.ParseYAML(bytes)
if err != nil {
return nil, err
}
return &composetypes.ConfigFile{
Filename: filename,
Config: config,
}, nil
}

View File

@ -0,0 +1,47 @@
package loader
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/gotestyourself/gotestyourself/fs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetConfigDetails(t *testing.T) {
content := `
version: "3.0"
services:
foo:
image: alpine:3.5
`
file := fs.NewFile(t, "test-get-config-details", fs.WithContent(content))
defer file.Remove()
details, err := getConfigDetails([]string{file.Path()}, nil)
require.NoError(t, err)
assert.Equal(t, filepath.Dir(file.Path()), details.WorkingDir)
require.Len(t, details.ConfigFiles, 1)
assert.Equal(t, "3.0", details.ConfigFiles[0].Config["version"])
assert.Len(t, details.Environment, len(os.Environ()))
}
func TestGetConfigDetailsStdin(t *testing.T) {
content := `
version: "3.0"
services:
foo:
image: alpine:3.5
`
details, err := getConfigDetails([]string{"-"}, strings.NewReader(content))
require.NoError(t, err)
cwd, err := os.Getwd()
require.NoError(t, err)
assert.Equal(t, cwd, details.WorkingDir)
require.Len(t, details.ConfigFiles, 1)
assert.Equal(t, "3.0", details.ConfigFiles[0].Config["version"])
assert.Len(t, details.Environment, len(os.Environ()))
}

View File

@ -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,

View File

@ -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
} }

View File

@ -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"

View File

@ -0,0 +1,390 @@
package loader
import (
"time"
"github.com/docker/cli/cli/compose/types"
)
func fullExampleConfig(workingDir, homeDir string) *types.Config {
return &types.Config{
Services: services(workingDir, homeDir),
Networks: networks(),
Volumes: volumes(),
}
}
func services(workingDir, homeDir string) []types.ServiceConfig {
return []types.ServiceConfig{
{
Name: "foo",
Build: types.BuildConfig{
Context: "./dir",
Dockerfile: "Dockerfile",
Args: map[string]*string{"foo": strPtr("bar")},
Target: "foo",
Network: "foo",
CacheFrom: []string{"foo", "bar"},
Labels: map[string]string{"FOO": "BAR"},
},
CapAdd: []string{"ALL"},
CapDrop: []string{"NET_ADMIN", "SYS_ADMIN"},
CgroupParent: "m-executor-abcd",
Command: []string{"bundle", "exec", "thin", "-p", "3000"},
ContainerName: "my-web-container",
DependsOn: []string{"db", "redis"},
Deploy: types.DeployConfig{
Mode: "replicated",
Replicas: uint64Ptr(6),
Labels: map[string]string{"FOO": "BAR"},
UpdateConfig: &types.UpdateConfig{
Parallelism: uint64Ptr(3),
Delay: time.Duration(10 * time.Second),
FailureAction: "continue",
Monitor: time.Duration(60 * time.Second),
MaxFailureRatio: 0.3,
Order: "start-first",
},
Resources: types.Resources{
Limits: &types.Resource{
NanoCPUs: "0.001",
MemoryBytes: 50 * 1024 * 1024,
},
Reservations: &types.Resource{
NanoCPUs: "0.0001",
MemoryBytes: 20 * 1024 * 1024,
GenericResources: []types.GenericResource{
{
DiscreteResourceSpec: &types.DiscreteGenericResource{
Kind: "gpu",
Value: 2,
},
},
{
DiscreteResourceSpec: &types.DiscreteGenericResource{
Kind: "ssd",
Value: 1,
},
},
},
},
},
RestartPolicy: &types.RestartPolicy{
Condition: "on-failure",
Delay: durationPtr(5 * time.Second),
MaxAttempts: uint64Ptr(3),
Window: durationPtr(2 * time.Minute),
},
Placement: types.Placement{
Constraints: []string{"node=foo"},
Preferences: []types.PlacementPreferences{
{
Spread: "node.labels.az",
},
},
},
EndpointMode: "dnsrr",
},
Devices: []string{"/dev/ttyUSB0:/dev/ttyUSB0"},
DNS: []string{"8.8.8.8", "9.9.9.9"},
DNSSearch: []string{"dc1.example.com", "dc2.example.com"},
DomainName: "foo.com",
Entrypoint: []string{"/code/entrypoint.sh", "-p", "3000"},
Environment: map[string]*string{
"FOO": strPtr("foo_from_env_file"),
"BAR": strPtr("bar_from_env_file_2"),
"BAZ": strPtr("baz_from_service_def"),
"QUX": strPtr("qux_from_environment"),
},
EnvFile: []string{
"./example1.env",
"./example2.env",
},
Expose: []string{"3000", "8000"},
ExternalLinks: []string{
"redis_1",
"project_db_1:mysql",
"project_db_1:postgresql",
},
ExtraHosts: []string{
"somehost:162.242.195.82",
"otherhost:50.31.209.229",
},
HealthCheck: &types.HealthCheckConfig{
Test: types.HealthCheckTest([]string{"CMD-SHELL", "echo \"hello world\""}),
Interval: durationPtr(10 * time.Second),
Timeout: durationPtr(1 * time.Second),
Retries: uint64Ptr(5),
StartPeriod: durationPtr(15 * time.Second),
},
Hostname: "foo",
Image: "redis",
Ipc: "host",
Labels: map[string]string{
"com.example.description": "Accounting webapp",
"com.example.number": "42",
"com.example.empty-label": "",
},
Links: []string{
"db",
"db:database",
"redis",
},
Logging: &types.LoggingConfig{
Driver: "syslog",
Options: map[string]string{
"syslog-address": "tcp://192.168.0.42:123",
},
},
MacAddress: "02:42:ac:11:65:43",
NetworkMode: "container:0cfeab0f748b9a743dc3da582046357c6ef497631c1a016d28d2bf9b4f899f7b",
Networks: map[string]*types.ServiceNetworkConfig{
"some-network": {
Aliases: []string{"alias1", "alias3"},
Ipv4Address: "",
Ipv6Address: "",
},
"other-network": {
Ipv4Address: "172.16.238.10",
Ipv6Address: "2001:3984:3989::10",
},
"other-other-network": nil,
},
Pid: "host",
Ports: []types.ServicePortConfig{
//"3000",
{
Mode: "ingress",
Target: 3000,
Protocol: "tcp",
},
{
Mode: "ingress",
Target: 3001,
Protocol: "tcp",
},
{
Mode: "ingress",
Target: 3002,
Protocol: "tcp",
},
{
Mode: "ingress",
Target: 3003,
Protocol: "tcp",
},
{
Mode: "ingress",
Target: 3004,
Protocol: "tcp",
},
{
Mode: "ingress",
Target: 3005,
Protocol: "tcp",
},
//"8000:8000",
{
Mode: "ingress",
Target: 8000,
Published: 8000,
Protocol: "tcp",
},
//"9090-9091:8080-8081",
{
Mode: "ingress",
Target: 8080,
Published: 9090,
Protocol: "tcp",
},
{
Mode: "ingress",
Target: 8081,
Published: 9091,
Protocol: "tcp",
},
//"49100:22",
{
Mode: "ingress",
Target: 22,
Published: 49100,
Protocol: "tcp",
},
//"127.0.0.1:8001:8001",
{
Mode: "ingress",
Target: 8001,
Published: 8001,
Protocol: "tcp",
},
//"127.0.0.1:5000-5010:5000-5010",
{
Mode: "ingress",
Target: 5000,
Published: 5000,
Protocol: "tcp",
},
{
Mode: "ingress",
Target: 5001,
Published: 5001,
Protocol: "tcp",
},
{
Mode: "ingress",
Target: 5002,
Published: 5002,
Protocol: "tcp",
},
{
Mode: "ingress",
Target: 5003,
Published: 5003,
Protocol: "tcp",
},
{
Mode: "ingress",
Target: 5004,
Published: 5004,
Protocol: "tcp",
},
{
Mode: "ingress",
Target: 5005,
Published: 5005,
Protocol: "tcp",
},
{
Mode: "ingress",
Target: 5006,
Published: 5006,
Protocol: "tcp",
},
{
Mode: "ingress",
Target: 5007,
Published: 5007,
Protocol: "tcp",
},
{
Mode: "ingress",
Target: 5008,
Published: 5008,
Protocol: "tcp",
},
{
Mode: "ingress",
Target: 5009,
Published: 5009,
Protocol: "tcp",
},
{
Mode: "ingress",
Target: 5010,
Published: 5010,
Protocol: "tcp",
},
},
Privileged: true,
ReadOnly: true,
Restart: "always",
SecurityOpt: []string{
"label=level:s0:c100,c200",
"label=type:svirt_apache_t",
},
StdinOpen: true,
StopSignal: "SIGUSR1",
StopGracePeriod: durationPtr(time.Duration(20 * time.Second)),
Tmpfs: []string{"/run", "/tmp"},
Tty: true,
Ulimits: map[string]*types.UlimitsConfig{
"nproc": {
Single: 65535,
},
"nofile": {
Soft: 20000,
Hard: 40000,
},
},
User: "someone",
Volumes: []types.ServiceVolumeConfig{
{Target: "/var/lib/mysql", Type: "volume"},
{Source: "/opt/data", Target: "/var/lib/mysql", Type: "bind"},
{Source: workingDir, Target: "/code", Type: "bind"},
{Source: workingDir + "/static", Target: "/var/www/html", Type: "bind"},
{Source: homeDir + "/configs", Target: "/etc/configs/", Type: "bind", ReadOnly: true},
{Source: "datavolume", Target: "/var/lib/mysql", Type: "volume"},
{Source: workingDir + "/opt", Target: "/opt", Consistency: "cached", Type: "bind"},
{Target: "/opt", Type: "tmpfs", Tmpfs: &types.ServiceVolumeTmpfs{
Size: int64(10000),
}},
},
WorkingDir: "/code",
},
}
}
func networks() map[string]types.NetworkConfig {
return map[string]types.NetworkConfig{
"some-network": {},
"other-network": {
Driver: "overlay",
DriverOpts: map[string]string{
"foo": "bar",
"baz": "1",
},
Ipam: types.IPAMConfig{
Driver: "overlay",
Config: []*types.IPAMPool{
{Subnet: "172.16.238.0/24"},
{Subnet: "2001:3984:3989::/64"},
},
},
},
"external-network": {
Name: "external-network",
External: types.External{External: true},
},
"other-external-network": {
Name: "my-cool-network",
External: types.External{External: true},
},
}
}
func volumes() map[string]types.VolumeConfig {
return map[string]types.VolumeConfig{
"some-volume": {},
"other-volume": {
Driver: "flocker",
DriverOpts: map[string]string{
"foo": "bar",
"baz": "1",
},
},
"another-volume": {
Name: "user_specified_name",
Driver: "vsphere",
DriverOpts: map[string]string{
"foo": "bar",
"baz": "1",
},
},
"external-volume": {
Name: "external-volume",
External: types.External{External: true},
},
"other-external-volume": {
Name: "my-cool-volume",
External: types.External{External: true},
},
"external-volume3": {
Name: "this-is-volume3",
External: types.External{External: true},
},
}
}

View File

@ -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) {

View File

@ -0,0 +1,328 @@
package loader
import (
"testing"
"github.com/stretchr/testify/assert"
yaml "gopkg.in/yaml.v2"
)
func TestMarshallConfig(t *testing.T) {
cfg := fullExampleConfig("/foo", "/bar")
expected := `configs: {}
networks:
external-network:
name: external-network
external: true
other-external-network:
name: my-cool-network
external: true
other-network:
driver: overlay
driver_opts:
baz: "1"
foo: bar
ipam:
driver: overlay
config:
- subnet: 172.16.238.0/24
- subnet: 2001:3984:3989::/64
some-network: {}
secrets: {}
services:
foo:
build:
context: ./dir
dockerfile: Dockerfile
args:
foo: bar
labels:
FOO: BAR
cache_from:
- foo
- bar
network: foo
target: foo
cap_add:
- ALL
cap_drop:
- NET_ADMIN
- SYS_ADMIN
cgroup_parent: m-executor-abcd
command:
- bundle
- exec
- thin
- -p
- "3000"
container_name: my-web-container
depends_on:
- db
- redis
deploy:
mode: replicated
replicas: 6
labels:
FOO: BAR
update_config:
parallelism: 3
delay: 10s
failure_action: continue
monitor: 1m0s
max_failure_ratio: 0.3
order: start-first
resources:
limits:
cpus: "0.001"
memory: "52428800"
reservations:
cpus: "0.0001"
memory: "20971520"
generic_resources:
- discrete_resource_spec:
kind: gpu
value: 2
- discrete_resource_spec:
kind: ssd
value: 1
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 2m0s
placement:
constraints:
- node=foo
preferences:
- spread: node.labels.az
endpoint_mode: dnsrr
devices:
- /dev/ttyUSB0:/dev/ttyUSB0
dns:
- 8.8.8.8
- 9.9.9.9
dns_search:
- dc1.example.com
- dc2.example.com
domainname: foo.com
entrypoint:
- /code/entrypoint.sh
- -p
- "3000"
environment:
BAR: bar_from_env_file_2
BAZ: baz_from_service_def
FOO: foo_from_env_file
QUX: qux_from_environment
env_file:
- ./example1.env
- ./example2.env
expose:
- "3000"
- "8000"
external_links:
- redis_1
- project_db_1:mysql
- project_db_1:postgresql
extra_hosts:
- somehost:162.242.195.82
- otherhost:50.31.209.229
hostname: foo
healthcheck:
test:
- CMD-SHELL
- echo "hello world"
timeout: 1s
interval: 10s
retries: 5
start_period: 15s
image: redis
ipc: host
labels:
com.example.description: Accounting webapp
com.example.empty-label: ""
com.example.number: "42"
links:
- db
- db:database
- redis
logging:
driver: syslog
options:
syslog-address: tcp://192.168.0.42:123
mac_address: 02:42:ac:11:65:43
network_mode: container:0cfeab0f748b9a743dc3da582046357c6ef497631c1a016d28d2bf9b4f899f7b
networks:
other-network:
ipv4_address: 172.16.238.10
ipv6_address: 2001:3984:3989::10
other-other-network: null
some-network:
aliases:
- alias1
- alias3
pid: host
ports:
- mode: ingress
target: 3000
protocol: tcp
- mode: ingress
target: 3001
protocol: tcp
- mode: ingress
target: 3002
protocol: tcp
- mode: ingress
target: 3003
protocol: tcp
- mode: ingress
target: 3004
protocol: tcp
- mode: ingress
target: 3005
protocol: tcp
- mode: ingress
target: 8000
published: 8000
protocol: tcp
- mode: ingress
target: 8080
published: 9090
protocol: tcp
- mode: ingress
target: 8081
published: 9091
protocol: tcp
- mode: ingress
target: 22
published: 49100
protocol: tcp
- mode: ingress
target: 8001
published: 8001
protocol: tcp
- mode: ingress
target: 5000
published: 5000
protocol: tcp
- mode: ingress
target: 5001
published: 5001
protocol: tcp
- mode: ingress
target: 5002
published: 5002
protocol: tcp
- mode: ingress
target: 5003
published: 5003
protocol: tcp
- mode: ingress
target: 5004
published: 5004
protocol: tcp
- mode: ingress
target: 5005
published: 5005
protocol: tcp
- mode: ingress
target: 5006
published: 5006
protocol: tcp
- mode: ingress
target: 5007
published: 5007
protocol: tcp
- mode: ingress
target: 5008
published: 5008
protocol: tcp
- mode: ingress
target: 5009
published: 5009
protocol: tcp
- mode: ingress
target: 5010
published: 5010
protocol: tcp
privileged: true
read_only: true
restart: always
security_opt:
- label=level:s0:c100,c200
- label=type:svirt_apache_t
stdin_open: true
stop_grace_period: 20s
stop_signal: SIGUSR1
tmpfs:
- /run
- /tmp
tty: true
ulimits:
nofile:
soft: 20000
hard: 40000
nproc: 65535
user: someone
volumes:
- type: volume
target: /var/lib/mysql
- type: bind
source: /opt/data
target: /var/lib/mysql
- type: bind
source: /foo
target: /code
- type: bind
source: /foo/static
target: /var/www/html
- type: bind
source: /bar/configs
target: /etc/configs/
read_only: true
- type: volume
source: datavolume
target: /var/lib/mysql
- type: bind
source: /foo/opt
target: /opt
consistency: cached
- type: tmpfs
target: /opt
tmpfs:
size: 10000
working_dir: /code
volumes:
another-volume:
name: user_specified_name
driver: vsphere
driver_opts:
baz: "1"
foo: bar
external-volume:
name: external-volume
external: true
external-volume3:
name: this-is-volume3
external: true
other-external-volume:
name: my-cool-volume
external: true
other-volume:
driver: flocker
driver_opts:
baz: "1"
foo: bar
some-volume: {}
`
actual, err := yaml.Marshal(cfg)
assert.NoError(t, err)
assert.Equal(t, expected, string(actual))
// Make sure the expected still
dict, err := ParseYAML([]byte("version: '3.6'\n" + expected))
assert.NoError(t, err)
_, err = Load(buildConfigDetails(dict, map[string]string{}))
assert.NoError(t, err)
}

View File

@ -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