Support more stack features

Closes #27
This commit is contained in:
cuigh 2019-04-24 17:10:18 +08:00
parent 7415db10cc
commit 9f028a3ce4
17 changed files with 2161 additions and 885 deletions

View File

@ -4,6 +4,7 @@ import (
"io/ioutil"
"strings"
composetypes "github.com/cuigh/swirl/biz/docker/compose/types"
"github.com/docker/docker/api/types"
networktypes "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/swarm"
@ -48,12 +49,12 @@ func AddStackLabel(namespace Namespace, labels map[string]string) map[string]str
return labels
}
type networkMap map[string]NetworkConfig
type networkMap map[string]composetypes.NetworkConfig
// Networks from the compose-file type to the engine API type
func Networks(namespace Namespace, networks networkMap, servicesNetworks map[string]struct{}) (map[string]types.NetworkCreate, []string) {
if networks == nil {
networks = make(map[string]NetworkConfig)
networks = make(map[string]composetypes.NetworkConfig)
}
externalNetworks := []string{}
@ -61,7 +62,7 @@ func Networks(namespace Namespace, networks networkMap, servicesNetworks map[str
for internalName := range servicesNetworks {
network := networks[internalName]
if network.External.External {
externalNetworks = append(externalNetworks, network.External.Name)
externalNetworks = append(externalNetworks, network.Name)
continue
}
@ -86,38 +87,54 @@ func Networks(namespace Namespace, networks networkMap, servicesNetworks map[str
}
createOpts.IPAM.Config = append(createOpts.IPAM.Config, config)
}
result[internalName] = createOpts
networkName := namespace.Scope(internalName)
if network.Name != "" {
networkName = network.Name
}
result[networkName] = createOpts
}
return result, externalNetworks
}
// Secrets converts secrets from the Compose type to the engine API type
func Secrets(namespace Namespace, secrets map[string]SecretConfig) ([]swarm.SecretSpec, error) {
func Secrets(namespace Namespace, secrets map[string]composetypes.SecretConfig) ([]swarm.SecretSpec, error) {
result := []swarm.SecretSpec{}
for name, secret := range secrets {
if secret.External.External {
continue
}
data, err := ioutil.ReadFile(secret.File)
var obj swarmFileObject
var err error
if secret.Driver != "" {
obj, err = driverObjectConfig(namespace, name, composetypes.FileObjectConfig(secret))
} else {
obj, err = fileObjectConfig(namespace, name, composetypes.FileObjectConfig(secret))
}
if err != nil {
return nil, err
}
result = append(result, swarm.SecretSpec{
Annotations: swarm.Annotations{
Name: namespace.Scope(name),
Labels: AddStackLabel(namespace, secret.Labels),
},
Data: data,
})
spec := swarm.SecretSpec{Annotations: obj.Annotations, Data: obj.Data}
if secret.Driver != "" {
spec.Driver = &swarm.Driver{
Name: secret.Driver,
Options: secret.DriverOpts,
}
}
if secret.TemplateDriver != "" {
spec.Templating = &swarm.Driver{
Name: secret.TemplateDriver,
}
}
result = append(result, spec)
}
return result, nil
}
// Configs converts config objects from the Compose type to the engine API type
func Configs(namespace Namespace, configs map[string]ConfigObjConfig) ([]swarm.ConfigSpec, error) {
func Configs(namespace Namespace, configs map[string]composetypes.ConfigObjConfig) ([]swarm.ConfigSpec, error) {
result := []swarm.ConfigSpec{}
for name, config := range configs {
if config.External.External {
@ -139,3 +156,45 @@ func Configs(namespace Namespace, configs map[string]ConfigObjConfig) ([]swarm.C
}
return result, nil
}
type swarmFileObject struct {
Annotations swarm.Annotations
Data []byte
}
func driverObjectConfig(namespace Namespace, name string, obj composetypes.FileObjectConfig) (swarmFileObject, error) {
if obj.Name != "" {
name = obj.Name
} else {
name = namespace.Scope(name)
}
return swarmFileObject{
Annotations: swarm.Annotations{
Name: name,
Labels: AddStackLabel(namespace, obj.Labels),
},
Data: []byte{},
}, nil
}
func fileObjectConfig(namespace Namespace, name string, obj composetypes.FileObjectConfig) (swarmFileObject, error) {
data, err := ioutil.ReadFile(obj.File)
if err != nil {
return swarmFileObject{}, err
}
if obj.Name != "" {
name = obj.Name
} else {
name = namespace.Scope(name)
}
return swarmFileObject{
Annotations: swarm.Annotations{
Name: name,
Labels: AddStackLabel(namespace, obj.Labels),
},
Data: data,
}, nil
}

View File

@ -1,14 +1,14 @@
package compose
import (
"context"
"fmt"
"os"
"sort"
"strings"
"time"
"context"
composetypes "github.com/cuigh/swirl/biz/docker/compose/types"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
@ -27,7 +27,7 @@ const (
// Services from compose-file types to engine API types
func Services(
namespace Namespace,
config *Config,
config *composetypes.Config,
client *client.Client,
) (map[string]swarm.ServiceSpec, error) {
result := make(map[string]swarm.ServiceSpec)
@ -41,7 +41,7 @@ func Services(
if err != nil {
return nil, errors.Wrapf(err, "service %s", service.Name)
}
configs, err := convertServiceConfigObjs(client, namespace, service.Configs, config.Configs)
configs, err := convertServiceConfigObjs(client, namespace, service, config.Configs)
if err != nil {
return nil, errors.Wrapf(err, "service %s", service.Name)
}
@ -60,9 +60,9 @@ func Services(
func Service(
apiVersion string,
namespace Namespace,
service ServiceConfig,
networkConfigs map[string]NetworkConfig,
volumes map[string]VolumeConfig,
service composetypes.ServiceConfig,
networkConfigs map[string]composetypes.NetworkConfig,
volumes map[string]composetypes.VolumeConfig,
secrets []*swarm.SecretReference,
configs []*swarm.ConfigReference,
) (swarm.ServiceSpec, error) {
@ -110,7 +110,9 @@ func Service(
}
var privileges swarm.Privileges
privileges.CredentialSpec, err = convertCredentialSpec(namespace, service.CredentialSpec, configs)
privileges.CredentialSpec, err = convertCredentialSpec(
namespace, service.CredentialSpec, configs,
)
if err != nil {
return swarm.ServiceSpec{}, err
}
@ -134,7 +136,7 @@ func Service(
Command: service.Entrypoint,
Args: service.Command,
Hostname: service.Hostname,
Hosts: sortStrings(convertExtraHosts(service.ExtraHosts)),
Hosts: convertExtraHosts(service.ExtraHosts),
DNSConfig: dnsConfig,
Healthcheck: healthcheck,
Env: sortStrings(convertEnvironment(service.Environment)),
@ -142,7 +144,7 @@ func Service(
Dir: service.WorkingDir,
User: service.User,
Mounts: mounts,
StopGracePeriod: service.StopGracePeriod,
StopGracePeriod: composetypes.ConvertDurationPtr(service.StopGracePeriod),
StopSignal: service.StopSignal,
TTY: service.Tty,
OpenStdin: service.StdinOpen,
@ -150,6 +152,9 @@ func Service(
Configs: configs,
ReadOnly: service.ReadOnly,
Privileges: &privileges,
Isolation: container.Isolation(service.Isolation),
Init: service.Init,
Sysctls: service.Sysctls,
},
LogDriver: logDriver,
Resources: resources,
@ -157,11 +162,13 @@ func Service(
Placement: &swarm.Placement{
Constraints: service.Deploy.Placement.Constraints,
Preferences: getPlacementPreference(service.Deploy.Placement.Preferences),
MaxReplicas: service.Deploy.Placement.MaxReplicas,
},
},
EndpointSpec: endpoint,
Mode: mode,
UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig),
RollbackConfig: convertUpdateConfig(service.Deploy.RollbackConfig),
}
// add an image label to serviceSpec
@ -182,7 +189,7 @@ func Service(
return serviceSpec, nil
}
func getPlacementPreference(preferences []PlacementPreferences) []swarm.PlacementPreference {
func getPlacementPreference(preferences []composetypes.PlacementPreferences) []swarm.PlacementPreference {
result := []swarm.PlacementPreference{}
for _, preference := range preferences {
spreadDescriptor := preference.Spread
@ -200,20 +207,14 @@ func sortStrings(strs []string) []string {
return strs
}
type byNetworkTarget []swarm.NetworkAttachmentConfig
func (a byNetworkTarget) Len() int { return len(a) }
func (a byNetworkTarget) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byNetworkTarget) Less(i, j int) bool { return a[i].Target < a[j].Target }
func convertServiceNetworks(
networks map[string]*ServiceNetworkConfig,
networks map[string]*composetypes.ServiceNetworkConfig,
networkConfigs networkMap,
namespace Namespace,
name string,
) ([]swarm.NetworkAttachmentConfig, error) {
if len(networks) == 0 {
networks = map[string]*ServiceNetworkConfig{
networks = map[string]*composetypes.ServiceNetworkConfig{
defaultNetwork: {},
}
}
@ -229,8 +230,8 @@ func convertServiceNetworks(
aliases = network.Aliases
}
target := namespace.Scope(networkName)
if networkConfig.External.External {
target = networkConfig.External.Name
if networkConfig.Name != "" {
target = networkConfig.Name
}
netAttachConfig := swarm.NetworkAttachmentConfig{
Target: target,
@ -244,7 +245,9 @@ func convertServiceNetworks(
nets = append(nets, netAttachConfig)
}
sort.Sort(byNetworkTarget(nets))
sort.Slice(nets, func(i, j int) bool {
return nets[i].Target < nets[j].Target
})
return nets, nil
}
@ -252,47 +255,28 @@ func convertServiceNetworks(
func convertServiceSecrets(
client *client.Client,
namespace Namespace,
secrets []ServiceSecretConfig,
secretSpecs map[string]SecretConfig,
secrets []composetypes.ServiceSecretConfig,
secretSpecs map[string]composetypes.SecretConfig,
) ([]*swarm.SecretReference, error) {
refs := []*swarm.SecretReference{}
for _, secret := range secrets {
target := secret.Target
if target == "" {
target = secret.Source
}
secretSpec, exists := secretSpecs[secret.Source]
lookup := func(key string) (composetypes.FileObjectConfig, error) {
secretSpec, exists := secretSpecs[key]
if !exists {
return nil, errors.Errorf("undefined secret %q", secret.Source)
}
source := namespace.Scope(secret.Source)
if secretSpec.External.External {
source = secretSpec.External.Name
}
uid := secret.UID
gid := secret.GID
if uid == "" {
uid = "0"
}
if gid == "" {
gid = "0"
}
mode := secret.Mode
if mode == nil {
mode = uint32Ptr(0444)
return composetypes.FileObjectConfig{}, errors.Errorf("undefined secret %q", key)
}
return composetypes.FileObjectConfig(secretSpec), nil
}
for _, secret := range secrets {
obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(secret), lookup)
if err != nil {
return nil, err
}
file := swarm.SecretReferenceFileTarget(obj.File)
refs = append(refs, &swarm.SecretReference{
File: &swarm.SecretReferenceFileTarget{
Name: target,
UID: uid,
GID: gid,
Mode: os.FileMode(*mode),
},
SecretName: source,
File: &file,
SecretName: obj.Name,
})
}
@ -309,24 +293,103 @@ func convertServiceSecrets(
func convertServiceConfigObjs(
client *client.Client,
namespace Namespace,
configs []ServiceConfigObjConfig,
configSpecs map[string]ConfigObjConfig,
service composetypes.ServiceConfig,
configSpecs map[string]composetypes.ConfigObjConfig,
) ([]*swarm.ConfigReference, error) {
refs := []*swarm.ConfigReference{}
for _, config := range configs {
target := config.Target
if target == "" {
target = config.Source
lookup := func(key string) (composetypes.FileObjectConfig, error) {
configSpec, exists := configSpecs[key]
if !exists {
return composetypes.FileObjectConfig{}, errors.Errorf("undefined config %q", key)
}
return composetypes.FileObjectConfig(configSpec), nil
}
for _, config := range service.Configs {
obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(config), lookup)
if err != nil {
return nil, err
}
configSpec, exists := configSpecs[config.Source]
if !exists {
return nil, errors.Errorf("undefined config %q", config.Source)
file := swarm.ConfigReferenceFileTarget(obj.File)
refs = append(refs, &swarm.ConfigReference{
File: &file,
ConfigName: obj.Name,
})
}
// finally, after converting all of the file objects, create any
// Runtime-type configs that are needed. these are configs that are not
// mounted into the container, but are used in some other way by the
// container runtime. Currently, this only means CredentialSpecs, but in
// the future it may be used for other fields
// grab the CredentialSpec out of the Service
credSpec := service.CredentialSpec
// if the credSpec uses a config, then we should grab the config name, and
// create a config reference for it. A File or Registry-type CredentialSpec
// does not need this operation.
if credSpec.Config != "" {
// look up the config in the configSpecs.
obj, err := lookup(credSpec.Config)
if err != nil {
return nil, err
}
// get the actual correct name.
name := namespace.Scope(credSpec.Config)
if obj.Name != "" {
name = obj.Name
}
// now append a Runtime-type config.
refs = append(refs, &swarm.ConfigReference{
ConfigName: name,
Runtime: &swarm.ConfigReferenceRuntimeTarget{},
})
}
confs, err := ParseConfigs(client, refs)
if err != nil {
return nil, err
}
// sort to ensure idempotence (don't restart services just because the entries are in different order)
sort.SliceStable(confs, func(i, j int) bool { return confs[i].ConfigName < confs[j].ConfigName })
return confs, err
}
type swarmReferenceTarget struct {
Name string
UID string
GID string
Mode os.FileMode
}
type swarmReferenceObject struct {
File swarmReferenceTarget
ID string
Name string
}
func convertFileObject(
namespace Namespace,
config composetypes.FileReferenceConfig,
lookup func(key string) (composetypes.FileObjectConfig, error),
) (swarmReferenceObject, error) {
obj, err := lookup(config.Source)
if err != nil {
return swarmReferenceObject{}, err
}
source := namespace.Scope(config.Source)
if configSpec.External.External {
source = configSpec.External.Name
if obj.Name != "" {
source = obj.Name
}
target := config.Target
if target == "" {
target = config.Source
}
uid := config.UID
@ -342,44 +405,40 @@ func convertServiceConfigObjs(
mode = uint32Ptr(0444)
}
refs = append(refs, &swarm.ConfigReference{
File: &swarm.ConfigReferenceFileTarget{
return swarmReferenceObject{
File: swarmReferenceTarget{
Name: target,
UID: uid,
GID: gid,
Mode: os.FileMode(*mode),
},
ConfigName: source,
})
}
confs, err := ParseConfigs(client, refs)
if err != nil {
return nil, err
}
// sort to ensure idempotence (don't restart services just because the entries are in different order)
sort.SliceStable(confs, func(i, j int) bool { return confs[i].ConfigName < confs[j].ConfigName })
return confs, err
Name: source,
}, nil
}
func uint32Ptr(value uint32) *uint32 {
return &value
}
func convertExtraHosts(extraHosts map[string]string) []string {
// convertExtraHosts converts <host>:<ip> mappings to SwarmKit notation:
// "IP-address hostname(s)". The original order of mappings is preserved.
func convertExtraHosts(extraHosts composetypes.HostsList) []string {
hosts := []string{}
for host, ip := range extraHosts {
hosts = append(hosts, fmt.Sprintf("%s %s", ip, host))
for _, hostIP := range extraHosts {
if v := strings.SplitN(hostIP, ":", 2); len(v) == 2 {
// Convert to SwarmKit notation: IP-address hostname(s)
hosts = append(hosts, fmt.Sprintf("%s %s", v[1], v[0]))
}
}
return hosts
}
func convertHealthcheck(healthcheck *HealthCheckConfig) (*container.HealthConfig, error) {
func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container.HealthConfig, error) {
if healthcheck == nil {
return nil, nil
}
var (
timeout, interval, startPeriod time.Duration
timeout, interval, startPeriod composetypes.Duration
retries int
)
if healthcheck.Disable {
@ -405,14 +464,14 @@ func convertHealthcheck(healthcheck *HealthCheckConfig) (*container.HealthConfig
}
return &container.HealthConfig{
Test: healthcheck.Test,
Timeout: timeout,
Interval: interval,
Timeout: time.Duration(timeout),
Interval: time.Duration(interval),
Retries: retries,
StartPeriod: startPeriod,
StartPeriod: time.Duration(startPeriod),
}, nil
}
func convertRestartPolicy(restart string, source *RestartPolicy) (*swarm.RestartPolicy, error) {
func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) {
// TODO: log if restart is being ignored
if source == nil {
policy, err := ParseRestartPolicy(restart)
@ -438,13 +497,13 @@ func convertRestartPolicy(restart string, source *RestartPolicy) (*swarm.Restart
}
return &swarm.RestartPolicy{
Condition: swarm.RestartPolicyCondition(source.Condition),
Delay: source.Delay,
Delay: composetypes.ConvertDurationPtr(source.Delay),
MaxAttempts: source.MaxAttempts,
Window: source.Window,
Window: composetypes.ConvertDurationPtr(source.Window),
}, nil
}
func convertUpdateConfig(source *UpdateConfig) *swarm.UpdateConfig {
func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig {
if source == nil {
return nil
}
@ -454,15 +513,15 @@ func convertUpdateConfig(source *UpdateConfig) *swarm.UpdateConfig {
}
return &swarm.UpdateConfig{
Parallelism: parallel,
Delay: source.Delay,
Delay: time.Duration(source.Delay),
FailureAction: source.FailureAction,
Monitor: source.Monitor,
Monitor: time.Duration(source.Monitor),
MaxFailureRatio: source.MaxFailureRatio,
Order: source.Order,
}
}
func convertResources(source Resources) (*swarm.ResourceRequirements, error) {
func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) {
resources := &swarm.ResourceRequirements{}
var err error
if source.Limits != nil {
@ -486,21 +545,31 @@ func convertResources(source Resources) (*swarm.ResourceRequirements, error) {
return nil, err
}
}
var generic []swarm.GenericResource
for _, res := range source.Reservations.GenericResources {
var r swarm.GenericResource
if res.DiscreteResourceSpec != nil {
r.DiscreteResourceSpec = &swarm.DiscreteGenericResource{
Kind: res.DiscreteResourceSpec.Kind,
Value: res.DiscreteResourceSpec.Value,
}
}
generic = append(generic, r)
}
resources.Reservations = &swarm.Resources{
NanoCPUs: cpus,
MemoryBytes: int64(source.Reservations.MemoryBytes),
GenericResources: generic,
}
}
return resources, nil
}
type byPublishedPort []swarm.PortConfig
func (a byPublishedPort) Len() int { return len(a) }
func (a byPublishedPort) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byPublishedPort) Less(i, j int) bool { return a[i].PublishedPort < a[j].PublishedPort }
func convertEndpointSpec(endpointMode string, source []ServicePortConfig) (*swarm.EndpointSpec, error) {
func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortConfig) (*swarm.EndpointSpec, error) {
portConfigs := []swarm.PortConfig{}
for _, port := range source {
portConfig := swarm.PortConfig{
@ -512,7 +581,10 @@ func convertEndpointSpec(endpointMode string, source []ServicePortConfig) (*swar
portConfigs = append(portConfigs, portConfig)
}
sort.Sort(byPublishedPort(portConfigs))
sort.Slice(portConfigs, func(i, j int) bool {
return portConfigs[i].PublishedPort < portConfigs[j].PublishedPort
})
return &swarm.EndpointSpec{
Mode: swarm.ResolutionMode(strings.ToLower(endpointMode)),
Ports: portConfigs,
@ -561,7 +633,7 @@ func convertDNSConfig(DNS []string, DNSSearch []string) (*swarm.DNSConfig, error
return nil, nil
}
func convertCredentialSpec(namespace Namespace, spec CredentialSpecConfig, refs []*swarm.ConfigReference) (*swarm.CredentialSpec, error) {
func convertCredentialSpec(namespace Namespace, spec composetypes.CredentialSpecConfig, refs []*swarm.ConfigReference) (*swarm.CredentialSpec, error) {
var o []string
// Config was added in API v1.40

View File

@ -1,16 +1,15 @@
package compose
import (
"errors"
"fmt"
composetypes "github.com/cuigh/swirl/biz/docker/compose/types"
"github.com/docker/docker/api/types/mount"
"github.com/pkg/errors"
)
type volumes map[string]VolumeConfig
type volumes map[string]composetypes.VolumeConfig
// Volumes from compose-file types to engine api types
func Volumes(serviceVolumes []ServiceVolumeConfig, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) {
func Volumes(serviceVolumes []composetypes.ServiceVolumeConfig, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) {
var mounts []mount.Mount
for _, volumeConfig := range serviceVolumes {
@ -23,43 +22,37 @@ func Volumes(serviceVolumes []ServiceVolumeConfig, stackVolumes volumes, namespa
return mounts, nil
}
func convertVolumeToMount(
volume ServiceVolumeConfig,
func createMountFromVolume(volume composetypes.ServiceVolumeConfig) mount.Mount {
return mount.Mount{
Type: mount.Type(volume.Type),
Target: volume.Target,
ReadOnly: volume.ReadOnly,
Source: volume.Source,
Consistency: mount.Consistency(volume.Consistency),
}
}
func handleVolumeToMount(
volume composetypes.ServiceVolumeConfig,
stackVolumes volumes,
namespace Namespace,
) (mount.Mount, error) {
result := mount.Mount{
Type: mount.Type(volume.Type),
Source: volume.Source,
Target: volume.Target,
ReadOnly: volume.ReadOnly,
Consistency: mount.Consistency(volume.Consistency),
}
result := createMountFromVolume(volume)
if volume.Tmpfs != nil {
return mount.Mount{}, errors.New("tmpfs options are incompatible with type volume")
}
if volume.Bind != nil {
return mount.Mount{}, errors.New("bind options are incompatible with type volume")
}
// Anonymous volumes
if volume.Source == "" {
return result, nil
}
if volume.Type == "volume" && volume.Bind != nil {
return result, errors.New("bind options are incompatible with type volume")
}
if volume.Type == "bind" && volume.Volume != nil {
return result, errors.New("volume options are incompatible with type bind")
}
if volume.Bind != nil {
result.BindOptions = &mount.BindOptions{
Propagation: mount.Propagation(volume.Bind.Propagation),
}
}
// Binds volumes
if volume.Type == "bind" {
return result, nil
}
stackVolume, exists := stackVolumes[volume.Source]
if !exists {
return result, fmt.Errorf("undefined volume %q", volume.Source)
return mount.Mount{}, errors.Errorf("undefined volume %q", volume.Source)
}
result.Source = namespace.Scope(volume.Source)
@ -69,16 +62,15 @@ func convertVolumeToMount(
result.VolumeOptions.NoCopy = volume.Volume.NoCopy
}
// External named volumes
if stackVolume.External.External {
result.Source = stackVolume.External.Name
return result, nil
}
if stackVolume.Name != "" {
result.Source = stackVolume.Name
}
// External named volumes
if stackVolume.External.External {
return result, nil
}
result.VolumeOptions.Labels = AddStackLabel(namespace, stackVolume.Labels)
if stackVolume.Driver != "" || stackVolume.DriverOpts != nil {
result.VolumeOptions.DriverConfig = &mount.Driver{
@ -87,6 +79,84 @@ func convertVolumeToMount(
}
}
// Named volumes
return result, nil
}
func handleBindToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
result := createMountFromVolume(volume)
if volume.Source == "" {
return mount.Mount{}, errors.New("invalid bind source, source cannot be empty")
}
if volume.Volume != nil {
return mount.Mount{}, errors.New("volume options are incompatible with type bind")
}
if volume.Tmpfs != nil {
return mount.Mount{}, errors.New("tmpfs options are incompatible with type bind")
}
if volume.Bind != nil {
result.BindOptions = &mount.BindOptions{
Propagation: mount.Propagation(volume.Bind.Propagation),
}
}
return result, nil
}
func handleTmpfsToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
result := createMountFromVolume(volume)
if volume.Source != "" {
return mount.Mount{}, errors.New("invalid tmpfs source, source must be empty")
}
if volume.Bind != nil {
return mount.Mount{}, errors.New("bind options are incompatible with type tmpfs")
}
if volume.Volume != nil {
return mount.Mount{}, errors.New("volume options are incompatible with type tmpfs")
}
if volume.Tmpfs != nil {
result.TmpfsOptions = &mount.TmpfsOptions{
SizeBytes: volume.Tmpfs.Size,
}
}
return result, nil
}
func handleNpipeToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
result := createMountFromVolume(volume)
if volume.Source == "" {
return mount.Mount{}, errors.New("invalid npipe source, source cannot be empty")
}
if volume.Volume != nil {
return mount.Mount{}, errors.New("volume options are incompatible with type npipe")
}
if volume.Tmpfs != nil {
return mount.Mount{}, errors.New("tmpfs options are incompatible with type npipe")
}
if volume.Bind != nil {
result.BindOptions = &mount.BindOptions{
Propagation: mount.Propagation(volume.Bind.Propagation),
}
}
return result, nil
}
func convertVolumeToMount(
volume composetypes.ServiceVolumeConfig,
stackVolumes volumes,
namespace Namespace,
) (mount.Mount, error) {
switch volume.Type {
case "volume", "":
return handleVolumeToMount(volume, stackVolumes, namespace)
case "bind":
return handleBindToMount(volume)
case "tmpfs":
return handleTmpfsToMount(volume)
case "npipe":
return handleNpipeToMount(volume)
}
return mount.Mount{}, errors.New("volume type must be volume, bind, tmpfs or npipe")
}

View File

@ -0,0 +1,69 @@
package compose
import (
"strconv"
"strings"
interp "github.com/cuigh/swirl/biz/docker/compose/interpolation"
"github.com/pkg/errors"
)
var interpolateTypeCastMapping = map[interp.Path]interp.Cast{
servicePath("configs", interp.PathMatchList, "mode"): toInt,
servicePath("secrets", interp.PathMatchList, "mode"): toInt,
servicePath("healthcheck", "retries"): toInt,
servicePath("healthcheck", "disable"): toBoolean,
servicePath("deploy", "replicas"): toInt,
servicePath("deploy", "update_config", "parallelism"): toInt,
servicePath("deploy", "update_config", "max_failure_ratio"): toFloat,
servicePath("deploy", "restart_policy", "max_attempts"): toInt,
servicePath("ports", interp.PathMatchList, "target"): toInt,
servicePath("ports", interp.PathMatchList, "published"): toInt,
servicePath("ulimits", interp.PathMatchAll): toInt,
servicePath("ulimits", interp.PathMatchAll, "hard"): toInt,
servicePath("ulimits", interp.PathMatchAll, "soft"): toInt,
servicePath("privileged"): toBoolean,
servicePath("read_only"): toBoolean,
servicePath("stdin_open"): toBoolean,
servicePath("tty"): toBoolean,
servicePath("volumes", interp.PathMatchList, "read_only"): toBoolean,
servicePath("volumes", interp.PathMatchList, "volume", "nocopy"): toBoolean,
iPath("networks", interp.PathMatchAll, "external"): toBoolean,
iPath("networks", interp.PathMatchAll, "internal"): toBoolean,
iPath("networks", interp.PathMatchAll, "attachable"): toBoolean,
iPath("volumes", interp.PathMatchAll, "external"): toBoolean,
iPath("secrets", interp.PathMatchAll, "external"): toBoolean,
iPath("configs", interp.PathMatchAll, "external"): toBoolean,
}
func iPath(parts ...string) interp.Path {
return interp.NewPath(parts...)
}
func servicePath(parts ...string) interp.Path {
return iPath(append([]string{"services", interp.PathMatchAll}, parts...)...)
}
func toInt(value string) (interface{}, error) {
return strconv.Atoi(value)
}
func toFloat(value string) (interface{}, error) {
return strconv.ParseFloat(value, 64)
}
// should match http://yaml.org/type/bool.html
func toBoolean(value string) (interface{}, error) {
switch strings.ToLower(value) {
case "y", "yes", "true", "on":
return true, nil
case "n", "no", "false", "off":
return false, nil
default:
return nil, errors.Errorf("invalid boolean: %s", value)
}
}
func interpolateConfig(configDict map[string]interface{}, opts interp.Options) (map[string]interface{}, error) {
return interp.Interpolate(configDict, opts)
}

View File

@ -1,93 +0,0 @@
package compose
import "github.com/pkg/errors"
// Interpolate replaces variables in a string with the values from a mapping
func Interpolate(config map[string]interface{}, section string, mapping Mapping) (map[string]interface{}, error) {
out := map[string]interface{}{}
for name, item := range config {
if item == nil {
out[name] = nil
continue
}
mapItem, ok := item.(map[string]interface{})
if !ok {
return nil, errors.Errorf("Invalid type for %s : %T instead of %T", name, item, out)
}
interpolatedItem, err := interpolateSectionItem(name, mapItem, section, mapping)
if err != nil {
return nil, err
}
out[name] = interpolatedItem
}
return out, nil
}
func interpolateSectionItem(
name string,
item map[string]interface{},
section string,
mapping Mapping,
) (map[string]interface{}, error) {
out := map[string]interface{}{}
for key, value := range item {
interpolatedValue, err := recursiveInterpolate(value, mapping)
switch err := err.(type) {
case nil:
case *InvalidTemplateError:
return nil, errors.Errorf(
"Invalid interpolation format for %#v option in %s %#v: %#v. You may need to escape any $ with another $.",
key, section, name, err.Template,
)
default:
return nil, errors.Wrapf(err, "error while interpolating %s in %s %s", key, section, name)
}
out[key] = interpolatedValue
}
return out, nil
}
func recursiveInterpolate(
value interface{},
mapping Mapping,
) (interface{}, error) {
switch value := value.(type) {
case string:
return Substitute(value, mapping)
case map[string]interface{}:
out := map[string]interface{}{}
for key, elem := range value {
interpolatedElem, err := recursiveInterpolate(elem, mapping)
if err != nil {
return nil, err
}
out[key] = interpolatedElem
}
return out, nil
case []interface{}:
out := make([]interface{}, len(value))
for i, elem := range value {
interpolatedElem, err := recursiveInterpolate(elem, mapping)
if err != nil {
return nil, err
}
out[i] = interpolatedElem
}
return out, nil
default:
return value, nil
}
}

View File

@ -0,0 +1,163 @@
package interpolation
import (
"os"
"strings"
"github.com/cuigh/swirl/biz/docker/compose/template"
"github.com/pkg/errors"
)
// Options supported by Interpolate
type Options struct {
// LookupValue from a key
LookupValue LookupValue
// TypeCastMapping maps key paths to functions to cast to a type
TypeCastMapping map[Path]Cast
// Substitution function to use
Substitute func(string, template.Mapping) (string, error)
}
// LookupValue is a function which maps from variable names to values.
// Returns the value as a string and a bool indicating whether
// the value is present, to distinguish between an empty string
// and the absence of a value.
type LookupValue func(key string) (string, bool)
// Cast a value to a new type, or return an error if the value can't be cast
type Cast func(value string) (interface{}, error)
// Interpolate replaces variables in a string with the values from a mapping
func Interpolate(config map[string]interface{}, opts Options) (map[string]interface{}, error) {
if opts.LookupValue == nil {
opts.LookupValue = os.LookupEnv
}
if opts.TypeCastMapping == nil {
opts.TypeCastMapping = make(map[Path]Cast)
}
if opts.Substitute == nil {
opts.Substitute = template.Substitute
}
out := map[string]interface{}{}
for key, value := range config {
interpolatedValue, err := recursiveInterpolate(value, NewPath(key), opts)
if err != nil {
return out, err
}
out[key] = interpolatedValue
}
return out, nil
}
func recursiveInterpolate(value interface{}, path Path, opts Options) (interface{}, error) {
switch value := value.(type) {
case string:
newValue, err := opts.Substitute(value, template.Mapping(opts.LookupValue))
if err != nil || newValue == value {
return value, newPathError(path, err)
}
caster, ok := opts.getCasterForPath(path)
if !ok {
return newValue, nil
}
casted, err := caster(newValue)
return casted, newPathError(path, errors.Wrap(err, "failed to cast to expected type"))
case map[string]interface{}:
out := map[string]interface{}{}
for key, elem := range value {
interpolatedElem, err := recursiveInterpolate(elem, path.Next(key), opts)
if err != nil {
return nil, err
}
out[key] = interpolatedElem
}
return out, nil
case []interface{}:
out := make([]interface{}, len(value))
for i, elem := range value {
interpolatedElem, err := recursiveInterpolate(elem, path.Next(PathMatchList), opts)
if err != nil {
return nil, err
}
out[i] = interpolatedElem
}
return out, nil
default:
return value, nil
}
}
func newPathError(path Path, err error) error {
switch err := err.(type) {
case nil:
return nil
case *template.InvalidTemplateError:
return errors.Errorf(
"invalid interpolation format for %s: %#v. You may need to escape any $ with another $.",
path, err.Template)
default:
return errors.Wrapf(err, "error while interpolating %s", path)
}
}
const pathSeparator = "."
// PathMatchAll is a token used as part of a Path to match any key at that level
// in the nested structure
const PathMatchAll = "*"
// PathMatchList is a token used as part of a Path to match items in a list
const PathMatchList = "[]"
// Path is a dotted path of keys to a value in a nested mapping structure. A *
// section in a path will match any key in the mapping structure.
type Path string
// NewPath returns a new Path
func NewPath(items ...string) Path {
return Path(strings.Join(items, pathSeparator))
}
// Next returns a new path by append part to the current path
func (p Path) Next(part string) Path {
return Path(string(p) + pathSeparator + part)
}
func (p Path) parts() []string {
return strings.Split(string(p), pathSeparator)
}
func (p Path) matches(pattern Path) bool {
patternParts := pattern.parts()
parts := p.parts()
if len(patternParts) != len(parts) {
return false
}
for index, part := range parts {
switch patternParts[index] {
case PathMatchAll, part:
continue
default:
return false
}
}
return true
}
func (o Options) getCasterForPath(path Path) (Cast, bool) {
for pattern, caster := range o.TypeCastMapping {
if path.matches(pattern) {
return caster, true
}
}
return nil, false
}

View File

@ -1,22 +1,38 @@
package compose
import (
"errors"
"fmt"
"path"
"path/filepath"
"reflect"
"sort"
"strings"
"time"
"github.com/cuigh/auxo/log"
interp "github.com/cuigh/swirl/biz/docker/compose/interpolation"
"github.com/cuigh/swirl/biz/docker/compose/schema"
"github.com/cuigh/swirl/biz/docker/compose/template"
composetypes "github.com/cuigh/swirl/biz/docker/compose/types"
"github.com/docker/docker/api/types/versions"
"github.com/docker/go-connections/nat"
units "github.com/docker/go-units"
shellwords "github.com/mattn/go-shellwords"
"github.com/docker/go-units"
"github.com/mattn/go-shellwords"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
)
// Options supported by Load
type Options struct {
// Skip schema validation
SkipValidation bool
// Skip interpolation
SkipInterpolation bool
// Interpolation options
Interpolate *interp.Options
}
// ParseYAML reads the bytes from a file, parses the bytes into a mapping
// structure, and returns it.
func ParseYAML(source []byte) (map[string]interface{}, error) {
@ -36,93 +52,155 @@ func ParseYAML(source []byte) (map[string]interface{}, error) {
}
// Load reads a ConfigDetails and returns a fully loaded configuration
func Load(configDetails ConfigDetails) (*Config, error) {
func Load(configDetails composetypes.ConfigDetails, options ...func(*Options)) (*composetypes.Config, error) {
if len(configDetails.ConfigFiles) < 1 {
return nil, errors.New("No files specified")
}
if len(configDetails.ConfigFiles) > 1 {
return nil, errors.New("Multiple files are not yet supported")
return nil, errors.Errorf("No files specified")
}
configDict := getConfigDict(configDetails)
if services, ok := configDict["services"]; ok {
if servicesDict, ok := services.(map[string]interface{}); ok {
forbidden := getProperties(servicesDict, ForbiddenProperties)
if len(forbidden) > 0 {
return nil, &ForbiddenPropertiesError{Properties: forbidden}
opts := &Options{
Interpolate: &interp.Options{
Substitute: template.Substitute,
LookupValue: configDetails.LookupEnv,
TypeCastMapping: interpolateTypeCastMapping,
},
}
for _, op := range options {
op(opts)
}
configs := []*composetypes.Config{}
var err error
for _, file := range configDetails.ConfigFiles {
configDict := file.Config
version := schema.Version(configDict)
if configDetails.Version == "" {
configDetails.Version = version
}
if configDetails.Version != version {
return nil, errors.Errorf("version mismatched between two composefiles : %v and %v", configDetails.Version, version)
}
if err := validateForbidden(configDict); err != nil {
return nil, err
}
if !opts.SkipInterpolation {
configDict, err = interpolateConfig(configDict, *opts.Interpolate)
if err != nil {
return nil, err
}
}
// todo: Add validation
//if err := schema.Validate(configDict, schema.Version(configDict)); err != nil {
//if !opts.SkipValidation {
// if err := schema.Validate(configDict, configDetails.Version); err != nil {
// return nil, err
// }
//}
cfg := Config{}
config, err := interpolateConfig(configDict, configDetails.LookupEnv)
cfg, err := loadSections(configDict, configDetails)
if err != nil {
return nil, err
}
cfg.Filename = file.Filename
cfg.Services, err = LoadServices(config["services"], configDetails.WorkingDir, configDetails.LookupEnv)
if err != nil {
return nil, err
configs = append(configs, cfg)
}
cfg.Networks, err = LoadNetworks(config["networks"])
if err != nil {
return nil, err
}
cfg.Volumes, err = LoadVolumes(config["volumes"])
if err != nil {
return nil, err
}
cfg.Secrets, err = LoadSecrets(config["secrets"], configDetails.WorkingDir)
if err != nil {
return nil, err
}
cfg.Configs, err = LoadConfigObjs(config["configs"], configDetails.WorkingDir)
return &cfg, err
return merge(configs)
}
func interpolateConfig(configDict map[string]interface{}, lookupEnv Mapping) (map[string]map[string]interface{}, error) {
config := make(map[string]map[string]interface{})
for _, key := range []string{"services", "networks", "volumes", "secrets", "configs"} {
section, ok := configDict[key]
func validateForbidden(configDict map[string]interface{}) error {
servicesDict, ok := configDict["services"].(map[string]interface{})
if !ok {
config[key] = make(map[string]interface{})
continue
return nil
}
forbidden := getProperties(servicesDict, composetypes.ForbiddenProperties)
if len(forbidden) > 0 {
return &ForbiddenPropertiesError{Properties: forbidden}
}
return nil
}
func loadSections(config map[string]interface{}, configDetails composetypes.ConfigDetails) (*composetypes.Config, error) {
var err error
config[key], err = Interpolate(section.(map[string]interface{}), key, lookupEnv)
if err != nil {
cfg := composetypes.Config{
Version: schema.Version(config),
}
var loaders = []struct {
key string
fnc func(config map[string]interface{}) error
}{
{
key: "services",
fnc: func(config map[string]interface{}) error {
cfg.Services, err = LoadServices(config, configDetails.WorkingDir, configDetails.LookupEnv)
return err
},
},
{
key: "networks",
fnc: func(config map[string]interface{}) error {
cfg.Networks, err = LoadNetworks(config, configDetails.Version)
return err
},
},
{
key: "volumes",
fnc: func(config map[string]interface{}) error {
cfg.Volumes, err = LoadVolumes(config, configDetails.Version)
return err
},
},
{
key: "secrets",
fnc: func(config map[string]interface{}) error {
cfg.Secrets, err = LoadSecrets(config, configDetails)
return err
},
},
{
key: "configs",
fnc: func(config map[string]interface{}) error {
cfg.Configs, err = LoadConfigObjs(config, configDetails)
return err
},
},
}
for _, loader := range loaders {
if err := loader.fnc(getSection(config, loader.key)); err != nil {
return nil, err
}
}
return config, nil
cfg.Extras = getExtras(config)
return &cfg, nil
}
func getSection(config map[string]interface{}, key string) map[string]interface{} {
section, ok := config[key]
if !ok {
return make(map[string]interface{})
}
return section.(map[string]interface{})
}
// GetUnsupportedProperties returns the list of any unsupported properties that are
// used in the Compose files.
func GetUnsupportedProperties(configDetails ConfigDetails) []string {
func GetUnsupportedProperties(configDicts ...map[string]interface{}) []string {
unsupported := map[string]bool{}
for _, service := range getServices(getConfigDict(configDetails)) {
for _, configDict := range configDicts {
for _, service := range getServices(configDict) {
serviceDict := service.(map[string]interface{})
for _, property := range UnsupportedProperties {
for _, property := range composetypes.UnsupportedProperties {
if _, isSet := serviceDict[property]; isSet {
unsupported[property] = true
}
}
}
}
return sortedKeys(unsupported)
}
@ -138,8 +216,17 @@ func sortedKeys(set map[string]bool) []string {
// GetDeprecatedProperties returns the list of any deprecated properties that
// are used in the compose files.
func GetDeprecatedProperties(configDetails ConfigDetails) map[string]string {
return getProperties(getServices(getConfigDict(configDetails)), DeprecatedProperties)
func GetDeprecatedProperties(configDicts ...map[string]interface{}) map[string]string {
deprecated := map[string]string{}
for _, configDict := range configDicts {
deprecatedProperties := getProperties(getServices(configDict), composetypes.DeprecatedProperties)
for key, value := range deprecatedProperties {
deprecated[key] = value
}
}
return deprecated
}
func getProperties(services map[string]interface{}, propertyMap map[string]string) map[string]string {
@ -168,11 +255,6 @@ func (e *ForbiddenPropertiesError) Error() string {
return "Configuration contains forbidden properties"
}
// TODO: resolve multiple files into a single config
func getConfigDict(configDetails ConfigDetails) map[string]interface{} {
return configDetails.ConfigFiles[0].Config
}
func getServices(configDict map[string]interface{}) map[string]interface{} {
if services, ok := configDict["services"]; ok {
if servicesDict, ok := services.(map[string]interface{}); ok {
@ -183,11 +265,13 @@ func getServices(configDict map[string]interface{}) map[string]interface{} {
return map[string]interface{}{}
}
func transform(source map[string]interface{}, target interface{}) error {
// Transform converts the source into the target struct with compose types transformer
// and the specified transformers if any.
func Transform(source interface{}, target interface{}, additionalTransformers ...Transformer) error {
data := mapstructure.Metadata{}
config := &mapstructure.DecoderConfig{
DecodeHook: mapstructure.ComposeDecodeHookFunc(
createTransformHook(),
createTransformHook(additionalTransformers...),
mapstructure.StringToTimeDurationHookFunc()),
Result: target,
Metadata: &data,
@ -199,25 +283,38 @@ func transform(source map[string]interface{}, target interface{}) error {
return decoder.Decode(source)
}
func createTransformHook() mapstructure.DecodeHookFuncType {
// Transformer defines a map to type transformer
type Transformer struct {
TypeOf reflect.Type
Func func(interface{}) (interface{}, error)
}
func createTransformHook(additionalTransformers ...Transformer) mapstructure.DecodeHookFuncType {
transforms := map[reflect.Type]func(interface{}) (interface{}, error){
reflect.TypeOf(External{}): transformExternal,
reflect.TypeOf(HealthCheckTest{}): transformHealthCheckTest,
reflect.TypeOf(ShellCommand{}): transformShellCommand,
reflect.TypeOf(StringList{}): transformStringList,
reflect.TypeOf(composetypes.External{}): transformExternal,
reflect.TypeOf(composetypes.HealthCheckTest{}): transformHealthCheckTest,
reflect.TypeOf(composetypes.ShellCommand{}): transformShellCommand,
reflect.TypeOf(composetypes.StringList{}): transformStringList,
reflect.TypeOf(map[string]string{}): transformMapStringString,
reflect.TypeOf(UlimitsConfig{}): transformUlimits,
reflect.TypeOf(UnitBytes(0)): transformSize,
reflect.TypeOf([]ServicePortConfig{}): transformServicePort,
reflect.TypeOf(ServiceSecretConfig{}): transformStringSourceMap,
reflect.TypeOf(ServiceConfigObjConfig{}): transformStringSourceMap,
reflect.TypeOf(StringOrNumberList{}): transformStringOrNumberList,
reflect.TypeOf(map[string]*ServiceNetworkConfig{}): transformServiceNetworkMap,
reflect.TypeOf(MappingWithEquals{}): transformMappingOrListFunc("=", true),
reflect.TypeOf(Labels{}): transformMappingOrListFunc("=", false),
reflect.TypeOf(MappingWithColon{}): transformMappingOrListFunc(":", false),
reflect.TypeOf(ServiceVolumeConfig{}): transformServiceVolumeConfig,
reflect.TypeOf(BuildConfig{}): transformBuildConfig,
reflect.TypeOf(composetypes.UlimitsConfig{}): transformUlimits,
reflect.TypeOf(composetypes.UnitBytes(0)): transformSize,
reflect.TypeOf([]composetypes.ServicePortConfig{}): transformServicePort,
reflect.TypeOf(composetypes.ServiceSecretConfig{}): transformStringSourceMap,
reflect.TypeOf(composetypes.ServiceConfigObjConfig{}): transformStringSourceMap,
reflect.TypeOf(composetypes.StringOrNumberList{}): transformStringOrNumberList,
reflect.TypeOf(map[string]*composetypes.ServiceNetworkConfig{}): transformServiceNetworkMap,
reflect.TypeOf(composetypes.Mapping{}): transformMappingOrListFunc("=", false),
reflect.TypeOf(composetypes.MappingWithEquals{}): transformMappingOrListFunc("=", true),
reflect.TypeOf(composetypes.Labels{}): transformMappingOrListFunc("=", false),
reflect.TypeOf(composetypes.MappingWithColon{}): transformMappingOrListFunc(":", false),
reflect.TypeOf(composetypes.HostsList{}): transformListOrMappingFunc(":", false),
reflect.TypeOf(composetypes.ServiceVolumeConfig{}): transformServiceVolumeConfig,
reflect.TypeOf(composetypes.BuildConfig{}): transformBuildConfig,
reflect.TypeOf(composetypes.Duration(0)): transformStringToDuration,
}
for _, transformer := range additionalTransformers {
transforms[transformer.TypeOf] = transformer.Func
}
return func(_ reflect.Type, target reflect.Type, data interface{}) (interface{}, error) {
@ -279,8 +376,8 @@ func formatInvalidKeyError(keyPrefix string, key interface{}) error {
// LoadServices produces a ServiceConfig map from a compose file Dict
// the servicesDict is not validated if directly used. Use Load() to enable validation
func LoadServices(servicesDict map[string]interface{}, workingDir string, lookupEnv Mapping) ([]ServiceConfig, error) {
var services []ServiceConfig
func LoadServices(servicesDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) ([]composetypes.ServiceConfig, error) {
var services []composetypes.ServiceConfig
for name, serviceDef := range servicesDict {
serviceConfig, err := LoadService(name, serviceDef.(map[string]interface{}), workingDir, lookupEnv)
@ -295,9 +392,9 @@ func LoadServices(servicesDict map[string]interface{}, workingDir string, lookup
// LoadService produces a single ServiceConfig from a compose file Dict
// the serviceDict is not validated if directly used. Use Load() to enable validation
func LoadService(name string, serviceDict map[string]interface{}, workingDir string, lookupEnv Mapping) (*ServiceConfig, error) {
serviceConfig := &ServiceConfig{}
if err := transform(serviceDict, serviceConfig); err != nil {
func LoadService(name string, serviceDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) (*composetypes.ServiceConfig, error) {
serviceConfig := &composetypes.ServiceConfig{}
if err := Transform(serviceDict, serviceConfig); err != nil {
return nil, err
}
serviceConfig.Name = name
@ -306,11 +403,36 @@ func LoadService(name string, serviceDict map[string]interface{}, workingDir str
return nil, err
}
resolveVolumePaths(serviceConfig.Volumes, workingDir, lookupEnv)
if err := resolveVolumePaths(serviceConfig.Volumes, workingDir, lookupEnv); err != nil {
return nil, err
}
serviceConfig.Extras = getExtras(serviceDict)
return serviceConfig, nil
}
func updateEnvironment(environment map[string]*string, vars map[string]*string, lookupEnv Mapping) {
func loadExtras(name string, source map[string]interface{}) map[string]interface{} {
if dict, ok := source[name].(map[string]interface{}); ok {
return getExtras(dict)
}
return nil
}
func getExtras(dict map[string]interface{}) map[string]interface{} {
extras := map[string]interface{}{}
for key, value := range dict {
if strings.HasPrefix(key, "x-") {
extras[key] = value
}
}
if len(extras) == 0 {
return nil
}
return extras
}
func updateEnvironment(environment map[string]*string, vars map[string]*string, lookupEnv template.Mapping) {
for k, v := range vars {
interpolatedV, ok := lookupEnv(k)
if (v == nil || *v == "") && ok {
@ -322,7 +444,7 @@ func updateEnvironment(environment map[string]*string, vars map[string]*string,
}
}
func resolveEnvironment(serviceConfig *ServiceConfig, workingDir string, lookupEnv Mapping) error {
func resolveEnvironment(serviceConfig *composetypes.ServiceConfig, workingDir string, lookupEnv template.Mapping) error {
environment := make(map[string]*string)
if len(serviceConfig.EnvFile) > 0 {
@ -345,12 +467,16 @@ func resolveEnvironment(serviceConfig *ServiceConfig, workingDir string, lookupE
return nil
}
func resolveVolumePaths(volumes []ServiceVolumeConfig, workingDir string, lookupEnv Mapping) {
func resolveVolumePaths(volumes []composetypes.ServiceVolumeConfig, workingDir string, lookupEnv template.Mapping) error {
for i, volume := range volumes {
if volume.Type != "bind" {
continue
}
if volume.Source == "" {
return errors.New(`invalid mount config for type "bind": field Source must not be empty`)
}
filePath := expandUser(volume.Source, lookupEnv)
// Check for a Unix absolute path first, to handle a Windows client
// with a Unix daemon. This handles a Windows client connecting to a
@ -363,10 +489,12 @@ func resolveVolumePaths(volumes []ServiceVolumeConfig, workingDir string, lookup
volume.Source = filePath
volumes[i] = volume
}
return nil
}
// TODO: make this more robust
func expandUser(path string, lookupEnv Mapping) string {
func expandUser(path string, lookupEnv template.Mapping) string {
if strings.HasPrefix(path, "~") {
home, ok := lookupEnv("HOME")
if !ok {
@ -381,9 +509,9 @@ func expandUser(path string, lookupEnv Mapping) string {
func transformUlimits(data interface{}) (interface{}, error) {
switch value := data.(type) {
case int:
return UlimitsConfig{Single: value}, nil
return composetypes.UlimitsConfig{Single: value}, nil
case map[string]interface{}:
ulimit := UlimitsConfig{}
ulimit := composetypes.UlimitsConfig{}
ulimit.Soft = value["soft"].(int)
ulimit.Hard = value["hard"].(int)
return ulimit, nil
@ -394,17 +522,31 @@ func transformUlimits(data interface{}) (interface{}, error) {
// LoadNetworks produces a NetworkConfig map from a compose file Dict
// the source Dict is not validated if directly used. Use Load() to enable validation
func LoadNetworks(source map[string]interface{}) (map[string]NetworkConfig, error) {
networks := make(map[string]NetworkConfig)
err := transform(source, &networks)
func LoadNetworks(source map[string]interface{}, version string) (map[string]composetypes.NetworkConfig, error) {
networks := make(map[string]composetypes.NetworkConfig)
err := Transform(source, &networks)
if err != nil {
return networks, err
}
for name, network := range networks {
if network.External.External && network.External.Name == "" {
network.External.Name = name
networks[name] = network
if !network.External.External {
continue
}
switch {
case network.External.Name != "":
if network.Name != "" {
return nil, errors.Errorf("network %s: network.external.name and network.name conflict; only use network.name", name)
}
if versions.GreaterThanOrEqualTo(version, "3.5") {
log.Get("compose").Warnf("network %s: network.external.name is deprecated in favor of network.name", name)
}
network.Name = network.External.Name
network.External.Name = ""
case network.Name == "":
network.Name = name
}
network.Extras = loadExtras(name, source)
networks[name] = network
}
return networks, nil
}
@ -417,76 +559,110 @@ func externalVolumeError(volume, key string) error {
// LoadVolumes produces a VolumeConfig map from a compose file Dict
// the source Dict is not validated if directly used. Use Load() to enable validation
func LoadVolumes(source map[string]interface{}) (map[string]VolumeConfig, error) {
volumes := make(map[string]VolumeConfig)
err := transform(source, &volumes)
if err != nil {
func LoadVolumes(source map[string]interface{}, version string) (map[string]composetypes.VolumeConfig, error) {
volumes := make(map[string]composetypes.VolumeConfig)
if err := Transform(source, &volumes); err != nil {
return volumes, err
}
for name, volume := range volumes {
if volume.External.External {
if volume.Driver != "" {
return nil, externalVolumeError(name, "driver")
}
if len(volume.DriverOpts) > 0 {
return nil, externalVolumeError(name, "driver_opts")
}
if len(volume.Labels) > 0 {
return nil, externalVolumeError(name, "labels")
}
if volume.External.Name == "" {
volume.External.Name = name
volumes[name] = volume
} else {
log.Get("compose").Warnf("volume %s: volume.external.name is deprecated in favor of volume.name", name)
for name, volume := range volumes {
if !volume.External.External {
continue
}
switch {
case volume.Driver != "":
return nil, externalVolumeError(name, "driver")
case len(volume.DriverOpts) > 0:
return nil, externalVolumeError(name, "driver_opts")
case len(volume.Labels) > 0:
return nil, externalVolumeError(name, "labels")
case volume.External.Name != "":
if volume.Name != "" {
return nil, fmt.Errorf("volume %s: volume.external.name and volume.name conflict; only use volume.name", name)
return nil, errors.Errorf("volume %s: volume.external.name and volume.name conflict; only use volume.name", name)
}
if versions.GreaterThanOrEqualTo(version, "3.4") {
log.Get("compose").Warnf("volume %s: volume.external.name is deprecated in favor of volume.name", name)
}
volume.Name = volume.External.Name
volume.External.Name = ""
case volume.Name == "":
volume.Name = name
}
volume.Extras = loadExtras(name, source)
volumes[name] = volume
}
return volumes, nil
}
// LoadSecrets produces a SecretConfig map from a compose file Dict
// the source Dict is not validated if directly used. Use Load() to enable validation
func LoadSecrets(source map[string]interface{}, workingDir string) (map[string]SecretConfig, error) {
secrets := make(map[string]SecretConfig)
if err := transform(source, &secrets); err != nil {
func LoadSecrets(source map[string]interface{}, details composetypes.ConfigDetails) (map[string]composetypes.SecretConfig, error) {
secrets := make(map[string]composetypes.SecretConfig)
if err := Transform(source, &secrets); err != nil {
return secrets, err
}
for name, secret := range secrets {
if secret.External.External && secret.External.Name == "" {
secret.External.Name = name
secrets[name] = secret
}
if secret.File != "" {
secret.File = absPath(workingDir, secret.File)
obj, err := loadFileObjectConfig(name, "secret", composetypes.FileObjectConfig(secret), details)
if err != nil {
return nil, err
}
secretConfig := composetypes.SecretConfig(obj)
secretConfig.Extras = loadExtras(name, source)
secrets[name] = secretConfig
}
return secrets, nil
}
// LoadConfigObjs produces a ConfigObjConfig map from a compose file Dict
// the source Dict is not validated if directly used. Use Load() to enable validation
func LoadConfigObjs(source map[string]interface{}, workingDir string) (map[string]ConfigObjConfig, error) {
configs := make(map[string]ConfigObjConfig)
if err := transform(source, &configs); err != nil {
func LoadConfigObjs(source map[string]interface{}, details composetypes.ConfigDetails) (map[string]composetypes.ConfigObjConfig, error) {
configs := make(map[string]composetypes.ConfigObjConfig)
if err := Transform(source, &configs); err != nil {
return configs, err
}
for name, config := range configs {
if config.External.External && config.External.Name == "" {
config.External.Name = name
configs[name] = config
}
if config.File != "" {
config.File = absPath(workingDir, config.File)
obj, err := loadFileObjectConfig(name, "config", composetypes.FileObjectConfig(config), details)
if err != nil {
return nil, err
}
configConfig := composetypes.ConfigObjConfig(obj)
configConfig.Extras = loadExtras(name, source)
configs[name] = configConfig
}
return configs, nil
}
func loadFileObjectConfig(name string, objType string, obj composetypes.FileObjectConfig, details composetypes.ConfigDetails) (composetypes.FileObjectConfig, error) {
// if "external: true"
switch {
case obj.External.External:
// handle deprecated external.name
if obj.External.Name != "" {
if obj.Name != "" {
return obj, errors.Errorf("%[1]s %[2]s: %[1]s.external.name and %[1]s.name conflict; only use %[1]s.name", objType, name)
}
if versions.GreaterThanOrEqualTo(details.Version, "3.5") {
log.Get("compose").Warnf("%[1]s %[2]s: %[1]s.external.name is deprecated in favor of %[1]s.name", objType, name)
}
obj.Name = obj.External.Name
obj.External.Name = ""
} else {
if obj.Name == "" {
obj.Name = name
}
}
// if not "external: true"
case obj.Driver != "":
if obj.File != "" {
return obj, errors.Errorf("%[1]s %[2]s: %[1]s.driver and %[1]s.file conflict; only use %[1]s.driver", objType, name)
}
default:
obj.File = absPath(details.WorkingDir, obj.File)
}
return obj, nil
}
func absPath(workingDir string, filePath string) string {
if filepath.IsAbs(filePath) {
return filePath
@ -642,6 +818,22 @@ func transformMappingOrList(mappingOrList interface{}, sep string, allowNil bool
panic(fmt.Errorf("expected a map or a list, got %T: %#v", mappingOrList, mappingOrList))
}
func transformListOrMappingFunc(sep string, allowNil bool) func(interface{}) (interface{}, error) {
return func(data interface{}) (interface{}, error) {
return transformListOrMapping(data, sep, allowNil), nil
}
}
func transformListOrMapping(listOrMapping interface{}, sep string, allowNil bool) interface{} {
switch value := listOrMapping.(type) {
case map[string]interface{}:
return toStringList(value, sep, allowNil)
case []interface{}:
return listOrMapping
}
panic(errors.Errorf("expected a map or a list, got %T: %#v", listOrMapping, listOrMapping))
}
func transformShellCommand(value interface{}) (interface{}, error) {
if str, ok := value.(string); ok {
return shellwords.Parse(str)
@ -670,6 +862,19 @@ func transformSize(value interface{}) (interface{}, error) {
panic(fmt.Errorf("invalid type for size %T", value))
}
func transformStringToDuration(value interface{}) (interface{}, error) {
switch value := value.(type) {
case string:
d, err := time.ParseDuration(value)
if err != nil {
return value, err
}
return composetypes.Duration(d), nil
default:
return value, errors.Errorf("invalid type %T for duration", value)
}
}
func toServicePortConfigs(value string) ([]interface{}, error) {
var portConfigs []interface{}
@ -691,7 +896,7 @@ func toServicePortConfigs(value string) ([]interface{}, error) {
return nil, err
}
for _, p := range portConfig {
portConfigs = append(portConfigs, ServicePortConfig{
portConfigs = append(portConfigs, composetypes.ServicePortConfig{
Protocol: string(p.Protocol),
Target: p.TargetPort,
Published: p.PublishedPort,
@ -721,3 +926,15 @@ func toString(value interface{}, allowNil bool) interface{} {
return ""
}
}
func toStringList(value map[string]interface{}, separator string, allowNil bool) []string {
output := []string{}
for key, value := range value {
if value == nil && !allowNil {
continue
}
output = append(output, fmt.Sprintf("%s%s%s", key, separator, value))
}
sort.Strings(output)
return output
}

233
biz/docker/compose/merge.go Normal file
View File

@ -0,0 +1,233 @@
package compose
import (
"reflect"
"sort"
"github.com/cuigh/swirl/biz/docker/compose/types"
"github.com/imdario/mergo"
"github.com/pkg/errors"
)
type specials struct {
m map[reflect.Type]func(dst, src reflect.Value) error
}
func (s *specials) Transformer(t reflect.Type) func(dst, src reflect.Value) error {
if fn, ok := s.m[t]; ok {
return fn
}
return nil
}
func merge(configs []*types.Config) (*types.Config, error) {
base := configs[0]
for _, override := range configs[1:] {
var err error
base.Services, err = mergeServices(base.Services, override.Services)
if err != nil {
return base, errors.Wrapf(err, "cannot merge services from %s", override.Filename)
}
base.Volumes, err = mergeVolumes(base.Volumes, override.Volumes)
if err != nil {
return base, errors.Wrapf(err, "cannot merge volumes from %s", override.Filename)
}
base.Networks, err = mergeNetworks(base.Networks, override.Networks)
if err != nil {
return base, errors.Wrapf(err, "cannot merge networks from %s", override.Filename)
}
base.Secrets, err = mergeSecrets(base.Secrets, override.Secrets)
if err != nil {
return base, errors.Wrapf(err, "cannot merge secrets from %s", override.Filename)
}
base.Configs, err = mergeConfigs(base.Configs, override.Configs)
if err != nil {
return base, errors.Wrapf(err, "cannot merge configs from %s", override.Filename)
}
}
return base, nil
}
func mergeServices(base, override []types.ServiceConfig) ([]types.ServiceConfig, error) {
baseServices := mapByName(base)
overrideServices := mapByName(override)
specials := &specials{
m: map[reflect.Type]func(dst, src reflect.Value) error{
reflect.TypeOf(&types.LoggingConfig{}): safelyMerge(mergeLoggingConfig),
reflect.TypeOf([]types.ServicePortConfig{}): mergeSlice(toServicePortConfigsMap, toServicePortConfigsSlice),
reflect.TypeOf([]types.ServiceSecretConfig{}): mergeSlice(toServiceSecretConfigsMap, toServiceSecretConfigsSlice),
reflect.TypeOf([]types.ServiceConfigObjConfig{}): mergeSlice(toServiceConfigObjConfigsMap, toSServiceConfigObjConfigsSlice),
},
}
for name, overrideService := range overrideServices {
if baseService, ok := baseServices[name]; ok {
if err := mergo.Merge(&baseService, &overrideService, mergo.WithAppendSlice, mergo.WithOverride, mergo.WithTransformers(specials)); err != nil {
return base, errors.Wrapf(err, "cannot merge service %s", name)
}
baseServices[name] = baseService
continue
}
baseServices[name] = overrideService
}
services := []types.ServiceConfig{}
for _, baseService := range baseServices {
services = append(services, baseService)
}
sort.Slice(services, func(i, j int) bool { return services[i].Name < services[j].Name })
return services, nil
}
func toServiceSecretConfigsMap(s interface{}) (map[interface{}]interface{}, error) {
secrets, ok := s.([]types.ServiceSecretConfig)
if !ok {
return nil, errors.Errorf("not a serviceSecretConfig: %v", s)
}
m := map[interface{}]interface{}{}
for _, secret := range secrets {
m[secret.Source] = secret
}
return m, nil
}
func toServiceConfigObjConfigsMap(s interface{}) (map[interface{}]interface{}, error) {
secrets, ok := s.([]types.ServiceConfigObjConfig)
if !ok {
return nil, errors.Errorf("not a serviceSecretConfig: %v", s)
}
m := map[interface{}]interface{}{}
for _, secret := range secrets {
m[secret.Source] = secret
}
return m, nil
}
func toServicePortConfigsMap(s interface{}) (map[interface{}]interface{}, error) {
ports, ok := s.([]types.ServicePortConfig)
if !ok {
return nil, errors.Errorf("not a servicePortConfig slice: %v", s)
}
m := map[interface{}]interface{}{}
for _, p := range ports {
m[p.Published] = p
}
return m, nil
}
func toServiceSecretConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) error {
s := []types.ServiceSecretConfig{}
for _, v := range m {
s = append(s, v.(types.ServiceSecretConfig))
}
sort.Slice(s, func(i, j int) bool { return s[i].Source < s[j].Source })
dst.Set(reflect.ValueOf(s))
return nil
}
func toSServiceConfigObjConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) error {
s := []types.ServiceConfigObjConfig{}
for _, v := range m {
s = append(s, v.(types.ServiceConfigObjConfig))
}
sort.Slice(s, func(i, j int) bool { return s[i].Source < s[j].Source })
dst.Set(reflect.ValueOf(s))
return nil
}
func toServicePortConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) error {
s := []types.ServicePortConfig{}
for _, v := range m {
s = append(s, v.(types.ServicePortConfig))
}
sort.Slice(s, func(i, j int) bool { return s[i].Published < s[j].Published })
dst.Set(reflect.ValueOf(s))
return nil
}
type tomapFn func(s interface{}) (map[interface{}]interface{}, error)
type writeValueFromMapFn func(reflect.Value, map[interface{}]interface{}) error
func safelyMerge(mergeFn func(dst, src reflect.Value) error) func(dst, src reflect.Value) error {
return func(dst, src reflect.Value) error {
if src.IsNil() {
return nil
}
if dst.IsNil() {
dst.Set(src)
return nil
}
return mergeFn(dst, src)
}
}
func mergeSlice(tomap tomapFn, writeValue writeValueFromMapFn) func(dst, src reflect.Value) error {
return func(dst, src reflect.Value) error {
dstMap, err := sliceToMap(tomap, dst)
if err != nil {
return err
}
srcMap, err := sliceToMap(tomap, src)
if err != nil {
return err
}
if err := mergo.Map(&dstMap, srcMap, mergo.WithOverride); err != nil {
return err
}
return writeValue(dst, dstMap)
}
}
func sliceToMap(tomap tomapFn, v reflect.Value) (map[interface{}]interface{}, error) {
// check if valid
if !v.IsValid() {
return nil, errors.Errorf("invalid value : %+v", v)
}
return tomap(v.Interface())
}
func mergeLoggingConfig(dst, src reflect.Value) error {
// Same driver, merging options
if getLoggingDriver(dst.Elem()) == getLoggingDriver(src.Elem()) ||
getLoggingDriver(dst.Elem()) == "" || getLoggingDriver(src.Elem()) == "" {
if getLoggingDriver(dst.Elem()) == "" {
dst.Elem().FieldByName("Driver").SetString(getLoggingDriver(src.Elem()))
}
dstOptions := dst.Elem().FieldByName("Options").Interface().(map[string]string)
srcOptions := src.Elem().FieldByName("Options").Interface().(map[string]string)
return mergo.Merge(&dstOptions, srcOptions, mergo.WithOverride)
}
// Different driver, override with src
dst.Set(src)
return nil
}
func getLoggingDriver(v reflect.Value) string {
return v.FieldByName("Driver").String()
}
func mapByName(services []types.ServiceConfig) map[string]types.ServiceConfig {
m := map[string]types.ServiceConfig{}
for _, service := range services {
m[service.Name] = service
}
return m
}
func mergeVolumes(base, override map[string]types.VolumeConfig) (map[string]types.VolumeConfig, error) {
err := mergo.Map(&base, &override, mergo.WithOverride)
return base, err
}
func mergeNetworks(base, override map[string]types.NetworkConfig) (map[string]types.NetworkConfig, error) {
err := mergo.Map(&base, &override, mergo.WithOverride)
return base, err
}
func mergeSecrets(base, override map[string]types.SecretConfig) (map[string]types.SecretConfig, error) {
err := mergo.Map(&base, &override, mergo.WithOverride)
return base, err
}
func mergeConfigs(base, override map[string]types.ConfigObjConfig) (map[string]types.ConfigObjConfig, error) {
err := mergo.Map(&base, &override, mergo.WithOverride)
return base, err
}

View File

@ -5,9 +5,11 @@ import (
"os"
"sort"
"strings"
composetypes "github.com/cuigh/swirl/biz/docker/compose/types"
)
func Parse(name, content string) (*Config, error) {
func Parse(name, content string) (*composetypes.Config, error) {
//absPath, err := filepath.Abs(composefile)
//if err != nil {
// return details, err
@ -24,8 +26,8 @@ func Parse(name, content string) (*Config, error) {
return nil, err
}
details := ConfigDetails{
ConfigFiles: []ConfigFile{*configFile},
details := composetypes.ConfigDetails{
ConfigFiles: []composetypes.ConfigFile{*configFile},
Environment: env,
}
cfg, err := Load(details)
@ -47,12 +49,12 @@ func propertyWarnings(properties map[string]string) string {
return strings.Join(msgs, "\n\n")
}
func getConfigFile(name, content string) (*ConfigFile, error) {
func getConfigFile(name, content string) (*composetypes.ConfigFile, error) {
config, err := ParseYAML([]byte(content))
if err != nil {
return nil, err
}
return &ConfigFile{
return &composetypes.ConfigFile{
Filename: name,
Config: config,
}, nil
@ -71,7 +73,7 @@ func buildEnvironment(env []string) (map[string]string, error) {
return result, nil
}
func GetServicesDeclaredNetworks(serviceConfigs []ServiceConfig) map[string]struct{} {
func GetServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
serviceNetworks := map[string]struct{}{}
for _, serviceConfig := range serviceConfigs {
if len(serviceConfig.Networks) == 0 {

View File

@ -0,0 +1,161 @@
package schema
import (
"fmt"
)
const (
defaultVersion = "1.0"
versionField = "version"
)
//type portsFormatChecker struct{}
//
//func (checker portsFormatChecker) IsFormat(input interface{}) bool {
// // TODO: implement this
// return true
//}
//
//type durationFormatChecker struct{}
//
//func (checker durationFormatChecker) IsFormat(input interface{}) bool {
// _, err := time.ParseDuration(input.(string))
// return err == nil
//}
//
//func init() {
// gojsonschema.FormatCheckers.Add("expose", portsFormatChecker{})
// gojsonschema.FormatCheckers.Add("ports", portsFormatChecker{})
// gojsonschema.FormatCheckers.Add("duration", durationFormatChecker{})
//}
// Version returns the version of the config, defaulting to version 1.0
func Version(config map[string]interface{}) string {
version, ok := config[versionField]
if !ok {
return defaultVersion
}
return normalizeVersion(fmt.Sprintf("%v", version))
}
func normalizeVersion(version string) string {
switch version {
case "3":
return "3.0"
default:
return version
}
}
// Validate uses the jsonschema to validate the configuration
//func Validate(config map[string]interface{}, version string) error {
// schemaData, err := _escFSByte(false, fmt.Sprintf("/data/config_schema_v%s.json", version))
// if err != nil {
// return errors.Errorf("unsupported Compose file version: %s", version)
// }
//
// schemaLoader := gojsonschema.NewStringLoader(string(schemaData))
// dataLoader := gojsonschema.NewGoLoader(config)
//
// result, err := gojsonschema.Validate(schemaLoader, dataLoader)
// if err != nil {
// return err
// }
//
// if !result.Valid() {
// return toError(result)
// }
//
// return nil
//}
//func toError(result *gojsonschema.Result) error {
// err := getMostSpecificError(result.Errors())
// return err
//}
//const (
// jsonschemaOneOf = "number_one_of"
// jsonschemaAnyOf = "number_any_of"
//)
//func getDescription(err validationError) string {
// switch err.parent.Type() {
// case "invalid_type":
// if expectedType, ok := err.parent.Details()["expected"].(string); ok {
// return fmt.Sprintf("must be a %s", humanReadableType(expectedType))
// }
// case jsonschemaOneOf, jsonschemaAnyOf:
// if err.child == nil {
// return err.parent.Description()
// }
// return err.child.Description()
// }
// return err.parent.Description()
//}
//func humanReadableType(definition string) string {
// if definition[0:1] == "[" {
// allTypes := strings.Split(definition[1:len(definition)-1], ",")
// for i, t := range allTypes {
// allTypes[i] = humanReadableType(t)
// }
// return fmt.Sprintf(
// "%s or %s",
// strings.Join(allTypes[0:len(allTypes)-1], ", "),
// allTypes[len(allTypes)-1],
// )
// }
// if definition == "object" {
// return "mapping"
// }
// if definition == "array" {
// return "list"
// }
// return definition
//}
//type validationError struct {
// parent gojsonschema.ResultError
// child gojsonschema.ResultError
//}
//
//func (err validationError) Error() string {
// description := getDescription(err)
// return fmt.Sprintf("%s %s", err.parent.Field(), description)
//}
//func getMostSpecificError(errors []gojsonschema.ResultError) validationError {
// mostSpecificError := 0
// for i, err := range errors {
// if specificity(err) > specificity(errors[mostSpecificError]) {
// mostSpecificError = i
// continue
// }
//
// if specificity(err) == specificity(errors[mostSpecificError]) {
// // Invalid type errors win in a tie-breaker for most specific field name
// if err.Type() == "invalid_type" && errors[mostSpecificError].Type() != "invalid_type" {
// mostSpecificError = i
// }
// }
// }
//
// if mostSpecificError+1 == len(errors) {
// return validationError{parent: errors[mostSpecificError]}
// }
//
// switch errors[mostSpecificError].Type() {
// case "number_one_of", "number_any_of":
// return validationError{
// parent: errors[mostSpecificError],
// child: errors[mostSpecificError+1],
// }
// default:
// return validationError{parent: errors[mostSpecificError]}
// }
//}
//func specificity(err gojsonschema.ResultError) int {
// return len(strings.Split(err.Field(), "."))
//}

View File

@ -1,101 +0,0 @@
package compose
import (
"fmt"
"regexp"
"strings"
)
var delimiter = "\\$"
var substitution = "[_a-z][_a-z0-9]*(?::?-[^}]+)?"
var patternString = fmt.Sprintf(
"%s(?i:(?P<escaped>%s)|(?P<named>%s)|{(?P<braced>%s)}|(?P<invalid>))",
delimiter, delimiter, substitution, substitution,
)
var pattern = regexp.MustCompile(patternString)
// InvalidTemplateError is returned when a variable template is not in a valid
// format
type InvalidTemplateError struct {
Template string
}
func (e InvalidTemplateError) Error() string {
return fmt.Sprintf("Invalid template: %#v", e.Template)
}
// Mapping is a user-supplied function which maps from variable names to values.
// Returns the value as a string and a bool indicating whether
// the value is present, to distinguish between an empty string
// and the absence of a value.
type Mapping func(string) (string, bool)
// Substitute variables in the string with their values
func Substitute(template string, mapping Mapping) (string, error) {
var err error
result := pattern.ReplaceAllStringFunc(template, func(substring string) string {
matches := pattern.FindStringSubmatch(substring)
groups := make(map[string]string)
for i, name := range pattern.SubexpNames() {
if i != 0 {
groups[name] = matches[i]
}
}
substitution := groups["named"]
if substitution == "" {
substitution = groups["braced"]
}
if substitution != "" {
// Soft default (fall back if unset or empty)
if strings.Contains(substitution, ":-") {
name, defaultValue := partition(substitution, ":-")
value, ok := mapping(name)
if !ok || value == "" {
return defaultValue
}
return value
}
// Hard default (fall back if-and-only-if empty)
if strings.Contains(substitution, "-") {
name, defaultValue := partition(substitution, "-")
value, ok := mapping(name)
if !ok {
return defaultValue
}
return value
}
// No default (fall back to empty string)
value, ok := mapping(substitution)
if !ok {
return ""
}
return value
}
if escaped := groups["escaped"]; escaped != "" {
return escaped
}
err = &InvalidTemplateError{Template: template}
return ""
})
return result, err
}
// Split the string at the first occurrence of sep, and return the part before the separator,
// and the part after the separator.
//
// If the separator is not found, return the string itself, followed by an empty string.
func partition(s, sep string) (string, string) {
if strings.Contains(s, sep) {
parts := strings.SplitN(s, sep, 2)
return parts[0], parts[1]
}
return s, ""
}

View File

@ -0,0 +1,245 @@
package template
import (
"fmt"
"regexp"
"strings"
)
var delimiter = "\\$"
var substitution = "[_a-z][_a-z0-9]*(?::?[-?][^}]*)?"
var patternString = fmt.Sprintf(
"%s(?i:(?P<escaped>%s)|(?P<named>%s)|{(?P<braced>%s)}|(?P<invalid>))",
delimiter, delimiter, substitution, substitution,
)
var defaultPattern = regexp.MustCompile(patternString)
// DefaultSubstituteFuncs contains the default SubstituteFunc used by the docker cli
var DefaultSubstituteFuncs = []SubstituteFunc{
softDefault,
hardDefault,
requiredNonEmpty,
required,
}
// InvalidTemplateError is returned when a variable template is not in a valid
// format
type InvalidTemplateError struct {
Template string
}
func (e InvalidTemplateError) Error() string {
return fmt.Sprintf("Invalid template: %#v", e.Template)
}
// Mapping is a user-supplied function which maps from variable names to values.
// Returns the value as a string and a bool indicating whether
// the value is present, to distinguish between an empty string
// and the absence of a value.
type Mapping func(string) (string, bool)
// SubstituteFunc is a user-supplied function that apply substitution.
// Returns the value as a string, a bool indicating if the function could apply
// the substitution and an error.
type SubstituteFunc func(string, Mapping) (string, bool, error)
// SubstituteWith subsitute variables in the string with their values.
// It accepts additional substitute function.
func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, subsFuncs ...SubstituteFunc) (string, error) {
var err error
result := pattern.ReplaceAllStringFunc(template, func(substring string) string {
matches := pattern.FindStringSubmatch(substring)
groups := matchGroups(matches, pattern)
if escaped := groups["escaped"]; escaped != "" {
return escaped
}
substitution := groups["named"]
if substitution == "" {
substitution = groups["braced"]
}
if substitution == "" {
err = &InvalidTemplateError{Template: template}
return ""
}
for _, f := range subsFuncs {
var (
value string
applied bool
)
value, applied, err = f(substitution, mapping)
if err != nil {
return ""
}
if !applied {
continue
}
return value
}
value, _ := mapping(substitution)
return value
})
return result, err
}
// Substitute variables in the string with their values
func Substitute(template string, mapping Mapping) (string, error) {
return SubstituteWith(template, mapping, defaultPattern, DefaultSubstituteFuncs...)
}
// ExtractVariables returns a map of all the variables defined in the specified
// composefile (dict representation) and their default value if any.
func ExtractVariables(configDict map[string]interface{}, pattern *regexp.Regexp) map[string]string {
if pattern == nil {
pattern = defaultPattern
}
return recurseExtract(configDict, pattern)
}
func recurseExtract(value interface{}, pattern *regexp.Regexp) map[string]string {
m := map[string]string{}
switch value := value.(type) {
case string:
if values, is := extractVariable(value, pattern); is {
for _, v := range values {
m[v.name] = v.value
}
}
case map[string]interface{}:
for _, elem := range value {
submap := recurseExtract(elem, pattern)
for key, value := range submap {
m[key] = value
}
}
case []interface{}:
for _, elem := range value {
if values, is := extractVariable(elem, pattern); is {
for _, v := range values {
m[v.name] = v.value
}
}
}
}
return m
}
type extractedValue struct {
name string
value string
}
func extractVariable(value interface{}, pattern *regexp.Regexp) ([]extractedValue, bool) {
sValue, ok := value.(string)
if !ok {
return []extractedValue{}, false
}
matches := pattern.FindAllStringSubmatch(sValue, -1)
if len(matches) == 0 {
return []extractedValue{}, false
}
values := []extractedValue{}
for _, match := range matches {
groups := matchGroups(match, pattern)
if escaped := groups["escaped"]; escaped != "" {
continue
}
val := groups["named"]
if val == "" {
val = groups["braced"]
}
name := val
var defaultValue string
switch {
case strings.Contains(val, ":?"):
name, _ = partition(val, ":?")
case strings.Contains(val, "?"):
name, _ = partition(val, "?")
case strings.Contains(val, ":-"):
name, defaultValue = partition(val, ":-")
case strings.Contains(val, "-"):
name, defaultValue = partition(val, "-")
}
values = append(values, extractedValue{name: name, value: defaultValue})
}
return values, len(values) > 0
}
// Soft default (fall back if unset or empty)
func softDefault(substitution string, mapping Mapping) (string, bool, error) {
sep := ":-"
if !strings.Contains(substitution, sep) {
return "", false, nil
}
name, defaultValue := partition(substitution, sep)
value, ok := mapping(name)
if !ok || value == "" {
return defaultValue, true, nil
}
return value, true, nil
}
// Hard default (fall back if-and-only-if empty)
func hardDefault(substitution string, mapping Mapping) (string, bool, error) {
sep := "-"
if !strings.Contains(substitution, sep) {
return "", false, nil
}
name, defaultValue := partition(substitution, sep)
value, ok := mapping(name)
if !ok {
return defaultValue, true, nil
}
return value, true, nil
}
func requiredNonEmpty(substitution string, mapping Mapping) (string, bool, error) {
return withRequired(substitution, mapping, ":?", func(v string) bool { return v != "" })
}
func required(substitution string, mapping Mapping) (string, bool, error) {
return withRequired(substitution, mapping, "?", func(_ string) bool { return true })
}
func withRequired(substitution string, mapping Mapping, sep string, valid func(string) bool) (string, bool, error) {
if !strings.Contains(substitution, sep) {
return "", false, nil
}
name, errorMessage := partition(substitution, sep)
value, ok := mapping(name)
if !ok || !valid(value) {
return "", true, &InvalidTemplateError{
Template: fmt.Sprintf("required variable %s is missing a value: %s", name, errorMessage),
}
}
return value, true, nil
}
func matchGroups(matches []string, pattern *regexp.Regexp) map[string]string {
groups := make(map[string]string)
for i, name := range pattern.SubexpNames()[1:] {
groups[name] = matches[i+1]
}
return groups
}
// Split the string at the first occurrence of sep, and return the part before the separator,
// and the part after the separator.
//
// If the separator is not found, return the string itself, followed by an empty string.
func partition(s, sep string) (string, string) {
if strings.Contains(s, sep) {
parts := strings.SplitN(s, sep, 2)
return parts[0], parts[1]
}
return s, ""
}

View File

@ -1,354 +0,0 @@
package compose
import (
"time"
)
// UnsupportedProperties not yet supported by this implementation of the compose file
var UnsupportedProperties = []string{
"build",
"cap_add",
"cap_drop",
"cgroup_parent",
"devices",
"domainname",
"external_links",
"ipc",
"links",
"mac_address",
"network_mode",
"privileged",
"restart",
"security_opt",
"shm_size",
"sysctls",
"tmpfs",
"ulimits",
"userns_mode",
}
// DeprecatedProperties that were removed from the v3 format, but their
// use should not impact the behaviour of the application.
var DeprecatedProperties = map[string]string{
"container_name": "Setting the container name is not supported.",
"expose": "Exposing ports is unnecessary - services on the same network can access each other's containers on any port.",
}
// ForbiddenProperties that are not supported in this implementation of the
// compose file.
var ForbiddenProperties = map[string]string{
"extends": "Support for `extends` is not implemented yet.",
"volume_driver": "Instead of setting the volume driver on the service, define a volume using the top-level `volumes` option and specify the driver there.",
"volumes_from": "To share a volume between services, define it using the top-level `volumes` option and reference it from each service that shares it using the service-level `volumes` option.",
"cpu_quota": "Set resource limits using deploy.resources",
"cpu_shares": "Set resource limits using deploy.resources",
"cpuset": "Set resource limits using deploy.resources",
"mem_limit": "Set resource limits using deploy.resources",
"memswap_limit": "Set resource limits using deploy.resources",
}
// ConfigFile is a filename and the contents of the file as a Dict
type ConfigFile struct {
Filename string
Config map[string]interface{}
}
// ConfigDetails are the details about a group of ConfigFiles
type ConfigDetails struct {
WorkingDir string
ConfigFiles []ConfigFile
Environment map[string]string
}
// LookupEnv provides a lookup function for environment variables
func (cd ConfigDetails) LookupEnv(key string) (string, bool) {
v, ok := cd.Environment[key]
return v, ok
}
// Config is a full compose file configuration
type Config struct {
Services []ServiceConfig
Networks map[string]NetworkConfig
Volumes map[string]VolumeConfig
Secrets map[string]SecretConfig
Configs map[string]ConfigObjConfig
}
// ServiceConfig is the configuration of one service
type ServiceConfig struct {
Name string
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 MappingWithColon `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"`
}
// 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
}
// ShellCommand is a string or list of string args
type ShellCommand []string
// StringList is a type for fields that can be a string or list of strings
type StringList []string
// StringOrNumberList is a type for fields that can be a list of strings or
// numbers
type StringOrNumberList []string
// MappingWithEquals is a mapping type that can be converted from a list of
// key[=value] strings.
// For the key with an empty value (`key=`), the mapped value is set to a pointer to `""`.
// For the key without value (`key`), the mapped value is set to nil.
type MappingWithEquals map[string]*string
// Labels is a mapping type for labels
type Labels map[string]string
// MappingWithColon is a mapping type that can be converted from a list of
// 'key: value' strings
type MappingWithColon map[string]string
// LoggingConfig the logging configuration for a service
type LoggingConfig struct {
Driver string
Options map[string]string
}
// 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"`
}
// 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
}
// HealthCheckTest is the command run to test the health of a service
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
}
// Resources the resource limits and reservations
type Resources struct {
Limits *Resource
Reservations *Resource
}
// 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"`
}
// UnitBytes is the bytes type
type UnitBytes int64
// RestartPolicy the service restart policy
type RestartPolicy struct {
Condition string
Delay *time.Duration
MaxAttempts *uint64 `mapstructure:"max_attempts"`
Window *time.Duration
}
// Placement constraints for the service
type Placement struct {
Constraints []string
Preferences []PlacementPreferences
}
// PlacementPreferences is the preferences for a service placement
type PlacementPreferences struct {
Spread string
}
// ServiceNetworkConfig is the network configuration for a service
type ServiceNetworkConfig struct {
Aliases []string
Ipv4Address string `mapstructure:"ipv4_address"`
Ipv6Address string `mapstructure:"ipv6_address"`
}
// ServicePortConfig is the port configuration for a service
type ServicePortConfig struct {
Mode string
Target uint32
Published uint32
Protocol string
}
// 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
}
// ServiceVolumeBind are options for a service volume of type bind
type ServiceVolumeBind struct {
Propagation string
}
// ServiceVolumeVolume are options for a service volume of type volume
type ServiceVolumeVolume struct {
NoCopy bool `mapstructure:"nocopy"`
}
type fileReferenceConfig struct {
Source string
Target string
UID string
GID string
Mode *uint32
}
// ServiceConfigObjConfig is the config obj configuration for a service
type ServiceConfigObjConfig fileReferenceConfig
// ServiceSecretConfig is the secret configuration for a service
type ServiceSecretConfig fileReferenceConfig
// UlimitsConfig the ulimit configuration
type UlimitsConfig struct {
Single int
Soft int
Hard int
}
// NetworkConfig for a network
type NetworkConfig struct {
Driver string
DriverOpts map[string]string `mapstructure:"driver_opts"`
Ipam IPAMConfig
External External
Internal bool
Attachable bool
Labels Labels
}
// IPAMConfig for a network
type IPAMConfig struct {
Driver string
Config []*IPAMPool
}
// IPAMPool for a network
type IPAMPool struct {
Subnet string
}
// VolumeConfig for a volume
type VolumeConfig struct {
Name string
Driver string
DriverOpts map[string]string `mapstructure:"driver_opts"`
External External
Labels Labels
}
// 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
}
// CredentialSpecConfig for credential spec on Windows
type CredentialSpecConfig struct {
Config string `yaml:",omitempty" json:"config,omitempty"` // Config was added in API v1.40
File string `yaml:",omitempty" json:"file,omitempty"`
Registry string `yaml:",omitempty" json:"registry,omitempty"`
}
type fileObjectConfig struct {
File string
External External
Labels Labels
}
// SecretConfig for a secret
type SecretConfig fileObjectConfig
// ConfigObjConfig is the config for the swarm "Config" object
type ConfigObjConfig fileObjectConfig

View File

@ -0,0 +1,524 @@
package types
import (
"encoding/json"
"fmt"
"time"
)
// UnsupportedProperties not yet supported by this implementation of the compose file
var UnsupportedProperties = []string{
"build",
"cap_add",
"cap_drop",
"cgroup_parent",
"devices",
"domainname",
"external_links",
"ipc",
"links",
"mac_address",
"network_mode",
"pid",
"privileged",
"restart",
"security_opt",
"shm_size",
"ulimits",
"userns_mode",
}
// DeprecatedProperties that were removed from the v3 format, but their
// use should not impact the behaviour of the application.
var DeprecatedProperties = map[string]string{
"container_name": "Setting the container name is not supported.",
"expose": "Exposing ports is unnecessary - services on the same network can access each other's containers on any port.",
}
// ForbiddenProperties that are not supported in this implementation of the
// compose file.
var ForbiddenProperties = map[string]string{
"extends": "Support for `extends` is not implemented yet.",
"volume_driver": "Instead of setting the volume driver on the service, define a volume using the top-level `volumes` option and specify the driver there.",
"volumes_from": "To share a volume between services, define it using the top-level `volumes` option and reference it from each service that shares it using the service-level `volumes` option.",
"cpu_quota": "Set resource limits using deploy.resources",
"cpu_shares": "Set resource limits using deploy.resources",
"cpuset": "Set resource limits using deploy.resources",
"mem_limit": "Set resource limits using deploy.resources",
"memswap_limit": "Set resource limits using deploy.resources",
}
// ConfigFile is a filename and the contents of the file as a Dict
type ConfigFile struct {
Filename string
Config map[string]interface{}
}
// ConfigDetails are the details about a group of ConfigFiles
type ConfigDetails struct {
Version string
WorkingDir string
ConfigFiles []ConfigFile
Environment map[string]string
}
// Duration is a thin wrapper around time.Duration with improved JSON marshalling
type Duration time.Duration
func (d Duration) String() string {
return time.Duration(d).String()
}
// ConvertDurationPtr converts a typedefined Duration pointer to a time.Duration pointer with the same value.
func ConvertDurationPtr(d *Duration) *time.Duration {
if d == nil {
return nil
}
res := time.Duration(*d)
return &res
}
// MarshalJSON makes Duration implement json.Marshaler
func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(d.String())
}
// MarshalYAML makes Duration implement yaml.Marshaler
func (d Duration) MarshalYAML() (interface{}, error) {
return d.String(), nil
}
// LookupEnv provides a lookup function for environment variables
func (cd ConfigDetails) LookupEnv(key string) (string, bool) {
v, ok := cd.Environment[key]
return v, ok
}
// Config is a full compose file configuration
type Config struct {
Filename string `yaml:"-" json:"-"`
Version string `json:"version"`
Services Services `json:"services"`
Networks map[string]NetworkConfig `yaml:",omitempty" json:"networks,omitempty"`
Volumes map[string]VolumeConfig `yaml:",omitempty" json:"volumes,omitempty"`
Secrets map[string]SecretConfig `yaml:",omitempty" json:"secrets,omitempty"`
Configs map[string]ConfigObjConfig `yaml:",omitempty" json:"configs,omitempty"`
Extras map[string]interface{} `yaml:",inline", json:"-"`
}
// MarshalJSON makes Config implement json.Marshaler
func (c Config) MarshalJSON() ([]byte, error) {
m := map[string]interface{}{
"version": c.Version,
"services": c.Services,
}
if len(c.Networks) > 0 {
m["networks"] = c.Networks
}
if len(c.Volumes) > 0 {
m["volumes"] = c.Volumes
}
if len(c.Secrets) > 0 {
m["secrets"] = c.Secrets
}
if len(c.Configs) > 0 {
m["configs"] = c.Configs
}
for k, v := range c.Extras {
m[k] = v
}
return json.Marshal(m)
}
// Services is a list of ServiceConfig
type Services []ServiceConfig
// MarshalYAML makes Services implement yaml.Marshaller
func (s Services) MarshalYAML() (interface{}, error) {
services := map[string]ServiceConfig{}
for _, service := range s {
services[service.Name] = service
}
return services, nil
}
// MarshalJSON makes Services implement json.Marshaler
func (s Services) MarshalJSON() ([]byte, error) {
data, err := s.MarshalYAML()
if err != nil {
return nil, err
}
return json.MarshalIndent(data, "", " ")
}
// ServiceConfig is the configuration of one service
type ServiceConfig struct {
Name string `yaml:"-" json:"-"`
Build BuildConfig `yaml:",omitempty" json:"build,omitempty"`
CapAdd []string `mapstructure:"cap_add" yaml:"cap_add,omitempty" json:"cap_add,omitempty"`
CapDrop []string `mapstructure:"cap_drop" yaml:"cap_drop,omitempty" json:"cap_drop,omitempty"`
CgroupParent string `mapstructure:"cgroup_parent" yaml:"cgroup_parent,omitempty" json:"cgroup_parent,omitempty"`
Command ShellCommand `yaml:",omitempty" json:"command,omitempty"`
Configs []ServiceConfigObjConfig `yaml:",omitempty" json:"configs,omitempty"`
ContainerName string `mapstructure:"container_name" yaml:"container_name,omitempty" json:"container_name,omitempty"`
CredentialSpec CredentialSpecConfig `mapstructure:"credential_spec" yaml:"credential_spec,omitempty" json:"credential_spec,omitempty"`
DependsOn []string `mapstructure:"depends_on" yaml:"depends_on,omitempty" json:"depends_on,omitempty"`
Deploy DeployConfig `yaml:",omitempty" json:"deploy,omitempty"`
Devices []string `yaml:",omitempty" json:"devices,omitempty"`
DNS StringList `yaml:",omitempty" json:"dns,omitempty"`
DNSSearch StringList `mapstructure:"dns_search" yaml:"dns_search,omitempty" json:"dns_search,omitempty"`
DomainName string `mapstructure:"domainname" yaml:"domainname,omitempty" json:"domainname,omitempty"`
Entrypoint ShellCommand `yaml:",omitempty" json:"entrypoint,omitempty"`
Environment MappingWithEquals `yaml:",omitempty" json:"environment,omitempty"`
EnvFile StringList `mapstructure:"env_file" yaml:"env_file,omitempty" json:"env_file,omitempty"`
Expose StringOrNumberList `yaml:",omitempty" json:"expose,omitempty"`
ExternalLinks []string `mapstructure:"external_links" yaml:"external_links,omitempty" json:"external_links,omitempty"`
ExtraHosts HostsList `mapstructure:"extra_hosts" yaml:"extra_hosts,omitempty" json:"extra_hosts,omitempty"`
Hostname string `yaml:",omitempty" json:"hostname,omitempty"`
HealthCheck *HealthCheckConfig `yaml:",omitempty" json:"healthcheck,omitempty"`
Image string `yaml:",omitempty" json:"image,omitempty"`
Init *bool `yaml:",omitempty" json:"init,omitempty"`
Ipc string `yaml:",omitempty" json:"ipc,omitempty"`
Isolation string `mapstructure:"isolation" yaml:"isolation,omitempty" json:"isolation,omitempty"`
Labels Labels `yaml:",omitempty" json:"labels,omitempty"`
Links []string `yaml:",omitempty" json:"links,omitempty"`
Logging *LoggingConfig `yaml:",omitempty" json:"logging,omitempty"`
MacAddress string `mapstructure:"mac_address" yaml:"mac_address,omitempty" json:"mac_address,omitempty"`
NetworkMode string `mapstructure:"network_mode" yaml:"network_mode,omitempty" json:"network_mode,omitempty"`
Networks map[string]*ServiceNetworkConfig `yaml:",omitempty" json:"networks,omitempty"`
Pid string `yaml:",omitempty" json:"pid,omitempty"`
Ports []ServicePortConfig `yaml:",omitempty" json:"ports,omitempty"`
Privileged bool `yaml:",omitempty" json:"privileged,omitempty"`
ReadOnly bool `mapstructure:"read_only" yaml:"read_only,omitempty" json:"read_only,omitempty"`
Restart string `yaml:",omitempty" json:"restart,omitempty"`
Secrets []ServiceSecretConfig `yaml:",omitempty" json:"secrets,omitempty"`
SecurityOpt []string `mapstructure:"security_opt" yaml:"security_opt,omitempty" json:"security_opt,omitempty"`
ShmSize string `mapstructure:"shm_size" yaml:"shm_size,omitempty" json:"shm_size,omitempty"`
StdinOpen bool `mapstructure:"stdin_open" yaml:"stdin_open,omitempty" json:"stdin_open,omitempty"`
StopGracePeriod *Duration `mapstructure:"stop_grace_period" yaml:"stop_grace_period,omitempty" json:"stop_grace_period,omitempty"`
StopSignal string `mapstructure:"stop_signal" yaml:"stop_signal,omitempty" json:"stop_signal,omitempty"`
Sysctls Mapping `yaml:",omitempty" json:"sysctls,omitempty"`
Tmpfs StringList `yaml:",omitempty" json:"tmpfs,omitempty"`
Tty bool `mapstructure:"tty" yaml:"tty,omitempty" json:"tty,omitempty"`
Ulimits map[string]*UlimitsConfig `yaml:",omitempty" json:"ulimits,omitempty"`
User string `yaml:",omitempty" json:"user,omitempty"`
UserNSMode string `mapstructure:"userns_mode" yaml:"userns_mode,omitempty" json:"userns_mode,omitempty"`
Volumes []ServiceVolumeConfig `yaml:",omitempty" json:"volumes,omitempty"`
WorkingDir string `mapstructure:"working_dir" yaml:"working_dir,omitempty" json:"working_dir,omitempty"`
Extras map[string]interface{} `yaml:",inline" json:"-"`
}
// 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 `yaml:",omitempty" json:"context,omitempty"`
Dockerfile string `yaml:",omitempty" json:"dockerfile,omitempty"`
Args MappingWithEquals `yaml:",omitempty" json:"args,omitempty"`
Labels Labels `yaml:",omitempty" json:"labels,omitempty"`
CacheFrom StringList `mapstructure:"cache_from" yaml:"cache_from,omitempty" json:"cache_from,omitempty"`
Network string `yaml:",omitempty" json:"network,omitempty"`
Target string `yaml:",omitempty" json:"target,omitempty"`
}
// ShellCommand is a string or list of string args
type ShellCommand []string
// StringList is a type for fields that can be a string or list of strings
type StringList []string
// StringOrNumberList is a type for fields that can be a list of strings or
// numbers
type StringOrNumberList []string
// MappingWithEquals is a mapping type that can be converted from a list of
// key[=value] strings.
// For the key with an empty value (`key=`), the mapped value is set to a pointer to `""`.
// For the key without value (`key`), the mapped value is set to nil.
type MappingWithEquals map[string]*string
// Mapping is a mapping type that can be converted from a list of
// key[=value] strings.
// For the key with an empty value (`key=`), or key without value (`key`), the
// mapped value is set to an empty string `""`.
type Mapping map[string]string
// Labels is a mapping type for labels
type Labels map[string]string
// MappingWithColon is a mapping type that can be converted from a list of
// 'key: value' strings
type MappingWithColon map[string]string
// HostsList is a list of colon-separated host-ip mappings
type HostsList []string
// LoggingConfig the logging configuration for a service
type LoggingConfig struct {
Driver string `yaml:",omitempty" json:"driver,omitempty"`
Options map[string]string `yaml:",omitempty" json:"options,omitempty"`
}
// DeployConfig the deployment configuration for a service
type DeployConfig struct {
Mode string `yaml:",omitempty" json:"mode,omitempty"`
Replicas *uint64 `yaml:",omitempty" json:"replicas,omitempty"`
Labels Labels `yaml:",omitempty" json:"labels,omitempty"`
UpdateConfig *UpdateConfig `mapstructure:"update_config" yaml:"update_config,omitempty" json:"update_config,omitempty"`
RollbackConfig *UpdateConfig `mapstructure:"rollback_config" yaml:"rollback_config,omitempty" json:"rollback_config,omitempty"`
Resources Resources `yaml:",omitempty" json:"resources,omitempty"`
RestartPolicy *RestartPolicy `mapstructure:"restart_policy" yaml:"restart_policy,omitempty" json:"restart_policy,omitempty"`
Placement Placement `yaml:",omitempty" json:"placement,omitempty"`
EndpointMode string `mapstructure:"endpoint_mode" yaml:"endpoint_mode,omitempty" json:"endpoint_mode,omitempty"`
}
// HealthCheckConfig the healthcheck configuration for a service
type HealthCheckConfig struct {
Test HealthCheckTest `yaml:",omitempty" json:"test,omitempty"`
Timeout *Duration `yaml:",omitempty" json:"timeout,omitempty"`
Interval *Duration `yaml:",omitempty" json:"interval,omitempty"`
Retries *uint64 `yaml:",omitempty" json:"retries,omitempty"`
StartPeriod *Duration `mapstructure:"start_period" yaml:"start_period,omitempty" json:"start_period,omitempty"`
Disable bool `yaml:",omitempty" json:"disable,omitempty"`
}
// HealthCheckTest is the command run to test the health of a service
type HealthCheckTest []string
// UpdateConfig the service update configuration
type UpdateConfig struct {
Parallelism *uint64 `yaml:",omitempty" json:"parallelism,omitempty"`
Delay Duration `yaml:",omitempty" json:"delay,omitempty"`
FailureAction string `mapstructure:"failure_action" yaml:"failure_action,omitempty" json:"failure_action,omitempty"`
Monitor Duration `yaml:",omitempty" json:"monitor,omitempty"`
MaxFailureRatio float32 `mapstructure:"max_failure_ratio" yaml:"max_failure_ratio,omitempty" json:"max_failure_ratio,omitempty"`
Order string `yaml:",omitempty" json:"order,omitempty"`
}
// Resources the resource limits and reservations
type Resources struct {
Limits *Resource `yaml:",omitempty" json:"limits,omitempty"`
Reservations *Resource `yaml:",omitempty" json:"reservations,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" yaml:"cpus,omitempty" json:"cpus,omitempty"`
MemoryBytes UnitBytes `mapstructure:"memory" yaml:"memory,omitempty" json:"memory,omitempty"`
GenericResources []GenericResource `mapstructure:"generic_resources" yaml:"generic_resources,omitempty" json:"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" yaml:"discrete_resource_spec,omitempty" json:"discrete_resource_spec,omitempty"`
}
// DiscreteGenericResource represents a "user defined" resource which is defined
// as an integer
// "Kind" is used to describe the Kind of a resource (e.g: "GPU", "FPGA", "SSD", ...)
// Value is used to count the resource (SSD=5, HDD=3, ...)
type DiscreteGenericResource struct {
Kind string `json:"kind"`
Value int64 `json:"value"`
}
// 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
}
// MarshalJSON makes UnitBytes implement json.Marshaler
func (u UnitBytes) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"%d"`, u)), nil
}
// RestartPolicy the service restart policy
type RestartPolicy struct {
Condition string `yaml:",omitempty" json:"condition,omitempty"`
Delay *Duration `yaml:",omitempty" json:"delay,omitempty"`
MaxAttempts *uint64 `mapstructure:"max_attempts" yaml:"max_attempts,omitempty" json:"max_attempts,omitempty"`
Window *Duration `yaml:",omitempty" json:"window,omitempty"`
}
// Placement constraints for the service
type Placement struct {
Constraints []string `yaml:",omitempty" json:"constraints,omitempty"`
Preferences []PlacementPreferences `yaml:",omitempty" json:"preferences,omitempty"`
MaxReplicas uint64 `mapstructure:"max_replicas_per_node" yaml:"max_replicas_per_node,omitempty" json:"max_replicas_per_node,omitempty"`
}
// PlacementPreferences is the preferences for a service placement
type PlacementPreferences struct {
Spread string `yaml:",omitempty" json:"spread,omitempty"`
}
// ServiceNetworkConfig is the network configuration for a service
type ServiceNetworkConfig struct {
Aliases []string `yaml:",omitempty" json:"aliases,omitempty"`
Ipv4Address string `mapstructure:"ipv4_address" yaml:"ipv4_address,omitempty" json:"ipv4_address,omitempty"`
Ipv6Address string `mapstructure:"ipv6_address" yaml:"ipv6_address,omitempty" json:"ipv6_address,omitempty"`
}
// ServicePortConfig is the port configuration for a service
type ServicePortConfig struct {
Mode string `yaml:",omitempty" json:"mode,omitempty"`
Target uint32 `yaml:",omitempty" json:"target,omitempty"`
Published uint32 `yaml:",omitempty" json:"published,omitempty"`
Protocol string `yaml:",omitempty" json:"protocol,omitempty"`
}
// ServiceVolumeConfig are references to a volume used by a service
type ServiceVolumeConfig struct {
Type string `yaml:",omitempty" json:"type,omitempty"`
Source string `yaml:",omitempty" json:"source,omitempty"`
Target string `yaml:",omitempty" json:"target,omitempty"`
ReadOnly bool `mapstructure:"read_only" yaml:"read_only,omitempty" json:"read_only,omitempty"`
Consistency string `yaml:",omitempty" json:"consistency,omitempty"`
Bind *ServiceVolumeBind `yaml:",omitempty" json:"bind,omitempty"`
Volume *ServiceVolumeVolume `yaml:",omitempty" json:"volume,omitempty"`
Tmpfs *ServiceVolumeTmpfs `yaml:",omitempty" json:"tmpfs,omitempty"`
}
// ServiceVolumeBind are options for a service volume of type bind
type ServiceVolumeBind struct {
Propagation string `yaml:",omitempty" json:"propagation,omitempty"`
}
// ServiceVolumeVolume are options for a service volume of type volume
type ServiceVolumeVolume struct {
NoCopy bool `mapstructure:"nocopy" yaml:"nocopy,omitempty" json:"nocopy,omitempty"`
}
// ServiceVolumeTmpfs are options for a service volume of type tmpfs
type ServiceVolumeTmpfs struct {
Size int64 `yaml:",omitempty" json:"size,omitempty"`
}
// FileReferenceConfig for a reference to a swarm file object
type FileReferenceConfig struct {
Source string `yaml:",omitempty" json:"source,omitempty"`
Target string `yaml:",omitempty" json:"target,omitempty"`
UID string `yaml:",omitempty" json:"uid,omitempty"`
GID string `yaml:",omitempty" json:"gid,omitempty"`
Mode *uint32 `yaml:",omitempty" json:"mode,omitempty"`
}
// ServiceConfigObjConfig is the config obj configuration for a service
type ServiceConfigObjConfig FileReferenceConfig
// ServiceSecretConfig is the secret configuration for a service
type ServiceSecretConfig FileReferenceConfig
// UlimitsConfig the ulimit configuration
type UlimitsConfig struct {
Single int `yaml:",omitempty" json:"single,omitempty"`
Soft int `yaml:",omitempty" json:"soft,omitempty"`
Hard int `yaml:",omitempty" json:"hard,omitempty"`
}
// MarshalYAML makes UlimitsConfig implement yaml.Marshaller
func (u *UlimitsConfig) MarshalYAML() (interface{}, error) {
if u.Single != 0 {
return u.Single, nil
}
return u, nil
}
// MarshalJSON makes UlimitsConfig implement json.Marshaller
func (u *UlimitsConfig) MarshalJSON() ([]byte, error) {
if u.Single != 0 {
return json.Marshal(u.Single)
}
// Pass as a value to avoid re-entering this method and use the default implementation
return json.Marshal(*u)
}
// NetworkConfig for a network
type NetworkConfig struct {
Name string `yaml:",omitempty" json:"name,omitempty"`
Driver string `yaml:",omitempty" json:"driver,omitempty"`
DriverOpts map[string]string `mapstructure:"driver_opts" yaml:"driver_opts,omitempty" json:"driver_opts,omitempty"`
Ipam IPAMConfig `yaml:",omitempty" json:"ipam,omitempty"`
External External `yaml:",omitempty" json:"external,omitempty"`
Internal bool `yaml:",omitempty" json:"internal,omitempty"`
Attachable bool `yaml:",omitempty" json:"attachable,omitempty"`
Labels Labels `yaml:",omitempty" json:"labels,omitempty"`
Extras map[string]interface{} `yaml:",inline" json:"-"`
}
// IPAMConfig for a network
type IPAMConfig struct {
Driver string `yaml:",omitempty" json:"driver,omitempty"`
Config []*IPAMPool `yaml:",omitempty" json:"config,omitempty"`
}
// IPAMPool for a network
type IPAMPool struct {
Subnet string `yaml:",omitempty" json:"subnet,omitempty"`
}
// VolumeConfig for a volume
type VolumeConfig struct {
Name string `yaml:",omitempty" json:"name,omitempty"`
Driver string `yaml:",omitempty" json:"driver,omitempty"`
DriverOpts map[string]string `mapstructure:"driver_opts" yaml:"driver_opts,omitempty" json:"driver_opts,omitempty"`
External External `yaml:",omitempty" json:"external,omitempty"`
Labels Labels `yaml:",omitempty" json:"labels,omitempty"`
Extras map[string]interface{} `yaml:",inline" json:"-"`
}
// 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 `yaml:",omitempty" json:"name,omitempty"`
External bool `yaml:",omitempty" json:"external,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
}
// MarshalJSON makes External implement json.Marshaller
func (e External) MarshalJSON() ([]byte, error) {
if e.Name == "" {
return []byte(fmt.Sprintf("%v", e.External)), nil
}
return []byte(fmt.Sprintf(`{"name": %q}`, e.Name)), nil
}
// CredentialSpecConfig for credential spec on Windows
type CredentialSpecConfig struct {
Config string `yaml:",omitempty" json:"config,omitempty"` // Config was added in API v1.40
File string `yaml:",omitempty" json:"file,omitempty"`
Registry string `yaml:",omitempty" json:"registry,omitempty"`
}
// FileObjectConfig is a config type for a file used by a service
type FileObjectConfig struct {
Name string `yaml:",omitempty" json:"name,omitempty"`
File string `yaml:",omitempty" json:"file,omitempty"`
External External `yaml:",omitempty" json:"external,omitempty"`
Labels Labels `yaml:",omitempty" json:"labels,omitempty"`
Extras map[string]interface{} `yaml:",inline" json:"-"`
Driver string `yaml:",omitempty" json:"driver,omitempty"`
DriverOpts map[string]string `mapstructure:"driver_opts" yaml:"driver_opts,omitempty" json:"driver_opts,omitempty"`
TemplateDriver string `mapstructure:"template_driver" yaml:"template_driver,omitempty" json:"template_driver,omitempty"`
}
// SecretConfig for a secret
type SecretConfig FileObjectConfig
// ConfigObjConfig is the config for the swarm "Config" object
type ConfigObjConfig FileObjectConfig

View File

@ -5,6 +5,7 @@ import (
"unicode"
"unicode/utf8"
"github.com/cuigh/swirl/biz/docker/compose/types"
"github.com/docker/docker/api/types/mount"
"github.com/pkg/errors"
)
@ -12,8 +13,8 @@ import (
const endOfSpec = rune(0)
// ParseVolume parses a volume spec without any knowledge of the target platform
func ParseVolume(spec string) (ServiceVolumeConfig, error) {
volume := ServiceVolumeConfig{}
func ParseVolume(spec string) (types.ServiceVolumeConfig, error) {
volume := types.ServiceVolumeConfig{}
switch len(spec) {
case 0:
@ -48,7 +49,7 @@ func isWindowsDrive(buffer []rune, char rune) bool {
return char == ':' && len(buffer) == 1 && unicode.IsLetter(buffer[0])
}
func populateFieldFromBuffer(char rune, buffer []rune, volume *ServiceVolumeConfig) error {
func populateFieldFromBuffer(char rune, buffer []rune, volume *types.ServiceVolumeConfig) error {
strBuffer := string(buffer)
switch {
case len(buffer) == 0:
@ -73,10 +74,10 @@ func populateFieldFromBuffer(char rune, buffer []rune, volume *ServiceVolumeConf
case "rw":
volume.ReadOnly = false
case "nocopy":
volume.Volume = &ServiceVolumeVolume{NoCopy: true}
volume.Volume = &types.ServiceVolumeVolume{NoCopy: true}
default:
if isBindOption(option) {
volume.Bind = &ServiceVolumeBind{Propagation: option}
volume.Bind = &types.ServiceVolumeBind{Propagation: option}
}
// ignore unknown options
}
@ -93,7 +94,7 @@ func isBindOption(option string) bool {
return false
}
func populateType(volume *ServiceVolumeConfig) {
func populateType(volume *types.ServiceVolumeConfig) {
switch {
// Anonymous volume
case volume.Source == "":
@ -111,6 +112,11 @@ func isFilePath(source string) bool {
return true
}
// windows named pipes
if strings.HasPrefix(source, `\\`) {
return true
}
first, nextIndex := utf8.DecodeRuneInString(source)
return isWindowsDrive([]rune{first}, rune(source[nextIndex]))
}

1
go.mod
View File

@ -19,6 +19,7 @@ require (
github.com/gobwas/ws v1.0.0
github.com/google/go-cmp v0.2.0 // indirect
github.com/gorilla/mux v1.7.1 // indirect
github.com/imdario/mergo v0.3.7
github.com/kr/pretty v0.1.0 // indirect
github.com/mattn/go-shellwords v1.0.3
github.com/mitchellh/mapstructure v1.0.0

2
go.sum
View File

@ -51,6 +51,8 @@ github.com/gorilla/mux v1.7.1 h1:Dw4jY2nghMMRsh1ol8dv1axHkDwMQK2DHerMNJsIpJU=
github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=