mirror of
https://github.com/cuigh/swirl
synced 2025-06-26 18:16:50 +00:00
parent
7415db10cc
commit
9f028a3ce4
@ -4,6 +4,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
composetypes "github.com/cuigh/swirl/biz/docker/compose/types"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
networktypes "github.com/docker/docker/api/types/network"
|
networktypes "github.com/docker/docker/api/types/network"
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
@ -48,12 +49,12 @@ func AddStackLabel(namespace Namespace, labels map[string]string) map[string]str
|
|||||||
return labels
|
return labels
|
||||||
}
|
}
|
||||||
|
|
||||||
type networkMap map[string]NetworkConfig
|
type networkMap map[string]composetypes.NetworkConfig
|
||||||
|
|
||||||
// Networks from the compose-file type to the engine API type
|
// 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) {
|
func Networks(namespace Namespace, networks networkMap, servicesNetworks map[string]struct{}) (map[string]types.NetworkCreate, []string) {
|
||||||
if networks == nil {
|
if networks == nil {
|
||||||
networks = make(map[string]NetworkConfig)
|
networks = make(map[string]composetypes.NetworkConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
externalNetworks := []string{}
|
externalNetworks := []string{}
|
||||||
@ -61,7 +62,7 @@ func Networks(namespace Namespace, networks networkMap, servicesNetworks map[str
|
|||||||
for internalName := range servicesNetworks {
|
for internalName := range servicesNetworks {
|
||||||
network := networks[internalName]
|
network := networks[internalName]
|
||||||
if network.External.External {
|
if network.External.External {
|
||||||
externalNetworks = append(externalNetworks, network.External.Name)
|
externalNetworks = append(externalNetworks, network.Name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,38 +87,54 @@ func Networks(namespace Namespace, networks networkMap, servicesNetworks map[str
|
|||||||
}
|
}
|
||||||
createOpts.IPAM.Config = append(createOpts.IPAM.Config, config)
|
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
|
return result, externalNetworks
|
||||||
}
|
}
|
||||||
|
|
||||||
// Secrets converts secrets from the Compose type to the engine API type
|
// 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{}
|
result := []swarm.SecretSpec{}
|
||||||
for name, secret := range secrets {
|
for name, secret := range secrets {
|
||||||
if secret.External.External {
|
if secret.External.External {
|
||||||
continue
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
spec := swarm.SecretSpec{Annotations: obj.Annotations, Data: obj.Data}
|
||||||
result = append(result, swarm.SecretSpec{
|
if secret.Driver != "" {
|
||||||
Annotations: swarm.Annotations{
|
spec.Driver = &swarm.Driver{
|
||||||
Name: namespace.Scope(name),
|
Name: secret.Driver,
|
||||||
Labels: AddStackLabel(namespace, secret.Labels),
|
Options: secret.DriverOpts,
|
||||||
},
|
}
|
||||||
Data: data,
|
}
|
||||||
})
|
if secret.TemplateDriver != "" {
|
||||||
|
spec.Templating = &swarm.Driver{
|
||||||
|
Name: secret.TemplateDriver,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, spec)
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configs converts config objects from the Compose type to the engine API type
|
// 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{}
|
result := []swarm.ConfigSpec{}
|
||||||
for name, config := range configs {
|
for name, config := range configs {
|
||||||
if config.External.External {
|
if config.External.External {
|
||||||
@ -139,3 +156,45 @@ func Configs(namespace Namespace, configs map[string]ConfigObjConfig) ([]swarm.C
|
|||||||
}
|
}
|
||||||
return result, nil
|
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
|
||||||
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
package compose
|
package compose
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"context"
|
composetypes "github.com/cuigh/swirl/biz/docker/compose/types"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
@ -27,7 +27,7 @@ const (
|
|||||||
// Services from compose-file types to engine API types
|
// Services from compose-file types to engine API types
|
||||||
func Services(
|
func Services(
|
||||||
namespace Namespace,
|
namespace Namespace,
|
||||||
config *Config,
|
config *composetypes.Config,
|
||||||
client *client.Client,
|
client *client.Client,
|
||||||
) (map[string]swarm.ServiceSpec, error) {
|
) (map[string]swarm.ServiceSpec, error) {
|
||||||
result := make(map[string]swarm.ServiceSpec)
|
result := make(map[string]swarm.ServiceSpec)
|
||||||
@ -41,7 +41,7 @@ func Services(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "service %s", service.Name)
|
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 {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "service %s", service.Name)
|
return nil, errors.Wrapf(err, "service %s", service.Name)
|
||||||
}
|
}
|
||||||
@ -60,9 +60,9 @@ func Services(
|
|||||||
func Service(
|
func Service(
|
||||||
apiVersion string,
|
apiVersion string,
|
||||||
namespace Namespace,
|
namespace Namespace,
|
||||||
service ServiceConfig,
|
service composetypes.ServiceConfig,
|
||||||
networkConfigs map[string]NetworkConfig,
|
networkConfigs map[string]composetypes.NetworkConfig,
|
||||||
volumes map[string]VolumeConfig,
|
volumes map[string]composetypes.VolumeConfig,
|
||||||
secrets []*swarm.SecretReference,
|
secrets []*swarm.SecretReference,
|
||||||
configs []*swarm.ConfigReference,
|
configs []*swarm.ConfigReference,
|
||||||
) (swarm.ServiceSpec, error) {
|
) (swarm.ServiceSpec, error) {
|
||||||
@ -110,7 +110,9 @@ func Service(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var privileges swarm.Privileges
|
var privileges swarm.Privileges
|
||||||
privileges.CredentialSpec, err = convertCredentialSpec(namespace, service.CredentialSpec, configs)
|
privileges.CredentialSpec, err = convertCredentialSpec(
|
||||||
|
namespace, service.CredentialSpec, configs,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return swarm.ServiceSpec{}, err
|
return swarm.ServiceSpec{}, err
|
||||||
}
|
}
|
||||||
@ -134,7 +136,7 @@ func Service(
|
|||||||
Command: service.Entrypoint,
|
Command: service.Entrypoint,
|
||||||
Args: service.Command,
|
Args: service.Command,
|
||||||
Hostname: service.Hostname,
|
Hostname: service.Hostname,
|
||||||
Hosts: sortStrings(convertExtraHosts(service.ExtraHosts)),
|
Hosts: convertExtraHosts(service.ExtraHosts),
|
||||||
DNSConfig: dnsConfig,
|
DNSConfig: dnsConfig,
|
||||||
Healthcheck: healthcheck,
|
Healthcheck: healthcheck,
|
||||||
Env: sortStrings(convertEnvironment(service.Environment)),
|
Env: sortStrings(convertEnvironment(service.Environment)),
|
||||||
@ -142,7 +144,7 @@ func Service(
|
|||||||
Dir: service.WorkingDir,
|
Dir: service.WorkingDir,
|
||||||
User: service.User,
|
User: service.User,
|
||||||
Mounts: mounts,
|
Mounts: mounts,
|
||||||
StopGracePeriod: service.StopGracePeriod,
|
StopGracePeriod: composetypes.ConvertDurationPtr(service.StopGracePeriod),
|
||||||
StopSignal: service.StopSignal,
|
StopSignal: service.StopSignal,
|
||||||
TTY: service.Tty,
|
TTY: service.Tty,
|
||||||
OpenStdin: service.StdinOpen,
|
OpenStdin: service.StdinOpen,
|
||||||
@ -150,6 +152,9 @@ func Service(
|
|||||||
Configs: configs,
|
Configs: configs,
|
||||||
ReadOnly: service.ReadOnly,
|
ReadOnly: service.ReadOnly,
|
||||||
Privileges: &privileges,
|
Privileges: &privileges,
|
||||||
|
Isolation: container.Isolation(service.Isolation),
|
||||||
|
Init: service.Init,
|
||||||
|
Sysctls: service.Sysctls,
|
||||||
},
|
},
|
||||||
LogDriver: logDriver,
|
LogDriver: logDriver,
|
||||||
Resources: resources,
|
Resources: resources,
|
||||||
@ -157,11 +162,13 @@ func Service(
|
|||||||
Placement: &swarm.Placement{
|
Placement: &swarm.Placement{
|
||||||
Constraints: service.Deploy.Placement.Constraints,
|
Constraints: service.Deploy.Placement.Constraints,
|
||||||
Preferences: getPlacementPreference(service.Deploy.Placement.Preferences),
|
Preferences: getPlacementPreference(service.Deploy.Placement.Preferences),
|
||||||
|
MaxReplicas: service.Deploy.Placement.MaxReplicas,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
EndpointSpec: endpoint,
|
EndpointSpec: endpoint,
|
||||||
Mode: mode,
|
Mode: mode,
|
||||||
UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig),
|
UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig),
|
||||||
|
RollbackConfig: convertUpdateConfig(service.Deploy.RollbackConfig),
|
||||||
}
|
}
|
||||||
|
|
||||||
// add an image label to serviceSpec
|
// add an image label to serviceSpec
|
||||||
@ -182,7 +189,7 @@ func Service(
|
|||||||
return serviceSpec, nil
|
return serviceSpec, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPlacementPreference(preferences []PlacementPreferences) []swarm.PlacementPreference {
|
func getPlacementPreference(preferences []composetypes.PlacementPreferences) []swarm.PlacementPreference {
|
||||||
result := []swarm.PlacementPreference{}
|
result := []swarm.PlacementPreference{}
|
||||||
for _, preference := range preferences {
|
for _, preference := range preferences {
|
||||||
spreadDescriptor := preference.Spread
|
spreadDescriptor := preference.Spread
|
||||||
@ -200,20 +207,14 @@ func sortStrings(strs []string) []string {
|
|||||||
return strs
|
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(
|
func convertServiceNetworks(
|
||||||
networks map[string]*ServiceNetworkConfig,
|
networks map[string]*composetypes.ServiceNetworkConfig,
|
||||||
networkConfigs networkMap,
|
networkConfigs networkMap,
|
||||||
namespace Namespace,
|
namespace Namespace,
|
||||||
name string,
|
name string,
|
||||||
) ([]swarm.NetworkAttachmentConfig, error) {
|
) ([]swarm.NetworkAttachmentConfig, error) {
|
||||||
if len(networks) == 0 {
|
if len(networks) == 0 {
|
||||||
networks = map[string]*ServiceNetworkConfig{
|
networks = map[string]*composetypes.ServiceNetworkConfig{
|
||||||
defaultNetwork: {},
|
defaultNetwork: {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -229,8 +230,8 @@ func convertServiceNetworks(
|
|||||||
aliases = network.Aliases
|
aliases = network.Aliases
|
||||||
}
|
}
|
||||||
target := namespace.Scope(networkName)
|
target := namespace.Scope(networkName)
|
||||||
if networkConfig.External.External {
|
if networkConfig.Name != "" {
|
||||||
target = networkConfig.External.Name
|
target = networkConfig.Name
|
||||||
}
|
}
|
||||||
netAttachConfig := swarm.NetworkAttachmentConfig{
|
netAttachConfig := swarm.NetworkAttachmentConfig{
|
||||||
Target: target,
|
Target: target,
|
||||||
@ -244,7 +245,9 @@ func convertServiceNetworks(
|
|||||||
nets = append(nets, netAttachConfig)
|
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
|
return nets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,47 +255,28 @@ func convertServiceNetworks(
|
|||||||
func convertServiceSecrets(
|
func convertServiceSecrets(
|
||||||
client *client.Client,
|
client *client.Client,
|
||||||
namespace Namespace,
|
namespace Namespace,
|
||||||
secrets []ServiceSecretConfig,
|
secrets []composetypes.ServiceSecretConfig,
|
||||||
secretSpecs map[string]SecretConfig,
|
secretSpecs map[string]composetypes.SecretConfig,
|
||||||
) ([]*swarm.SecretReference, error) {
|
) ([]*swarm.SecretReference, error) {
|
||||||
refs := []*swarm.SecretReference{}
|
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 {
|
if !exists {
|
||||||
return nil, errors.Errorf("undefined secret %q", secret.Source)
|
return composetypes.FileObjectConfig{}, errors.Errorf("undefined secret %q", key)
|
||||||
}
|
}
|
||||||
|
return composetypes.FileObjectConfig(secretSpec), nil
|
||||||
source := namespace.Scope(secret.Source)
|
}
|
||||||
if secretSpec.External.External {
|
for _, secret := range secrets {
|
||||||
source = secretSpec.External.Name
|
obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(secret), lookup)
|
||||||
}
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
uid := secret.UID
|
|
||||||
gid := secret.GID
|
|
||||||
if uid == "" {
|
|
||||||
uid = "0"
|
|
||||||
}
|
|
||||||
if gid == "" {
|
|
||||||
gid = "0"
|
|
||||||
}
|
|
||||||
mode := secret.Mode
|
|
||||||
if mode == nil {
|
|
||||||
mode = uint32Ptr(0444)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
file := swarm.SecretReferenceFileTarget(obj.File)
|
||||||
refs = append(refs, &swarm.SecretReference{
|
refs = append(refs, &swarm.SecretReference{
|
||||||
File: &swarm.SecretReferenceFileTarget{
|
File: &file,
|
||||||
Name: target,
|
SecretName: obj.Name,
|
||||||
UID: uid,
|
|
||||||
GID: gid,
|
|
||||||
Mode: os.FileMode(*mode),
|
|
||||||
},
|
|
||||||
SecretName: source,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,50 +293,63 @@ func convertServiceSecrets(
|
|||||||
func convertServiceConfigObjs(
|
func convertServiceConfigObjs(
|
||||||
client *client.Client,
|
client *client.Client,
|
||||||
namespace Namespace,
|
namespace Namespace,
|
||||||
configs []ServiceConfigObjConfig,
|
service composetypes.ServiceConfig,
|
||||||
configSpecs map[string]ConfigObjConfig,
|
configSpecs map[string]composetypes.ConfigObjConfig,
|
||||||
) ([]*swarm.ConfigReference, error) {
|
) ([]*swarm.ConfigReference, error) {
|
||||||
refs := []*swarm.ConfigReference{}
|
refs := []*swarm.ConfigReference{}
|
||||||
for _, config := range configs {
|
|
||||||
target := config.Target
|
|
||||||
if target == "" {
|
|
||||||
target = config.Source
|
|
||||||
}
|
|
||||||
|
|
||||||
configSpec, exists := configSpecs[config.Source]
|
lookup := func(key string) (composetypes.FileObjectConfig, error) {
|
||||||
|
configSpec, exists := configSpecs[key]
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, errors.Errorf("undefined config %q", config.Source)
|
return composetypes.FileObjectConfig{}, errors.Errorf("undefined config %q", key)
|
||||||
}
|
}
|
||||||
|
return composetypes.FileObjectConfig(configSpec), nil
|
||||||
source := namespace.Scope(config.Source)
|
}
|
||||||
if configSpec.External.External {
|
for _, config := range service.Configs {
|
||||||
source = configSpec.External.Name
|
obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(config), lookup)
|
||||||
}
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
uid := config.UID
|
|
||||||
gid := config.GID
|
|
||||||
if uid == "" {
|
|
||||||
uid = "0"
|
|
||||||
}
|
|
||||||
if gid == "" {
|
|
||||||
gid = "0"
|
|
||||||
}
|
|
||||||
mode := config.Mode
|
|
||||||
if mode == nil {
|
|
||||||
mode = uint32Ptr(0444)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
file := swarm.ConfigReferenceFileTarget(obj.File)
|
||||||
refs = append(refs, &swarm.ConfigReference{
|
refs = append(refs, &swarm.ConfigReference{
|
||||||
File: &swarm.ConfigReferenceFileTarget{
|
File: &file,
|
||||||
Name: target,
|
ConfigName: obj.Name,
|
||||||
UID: uid,
|
|
||||||
GID: gid,
|
|
||||||
Mode: os.FileMode(*mode),
|
|
||||||
},
|
|
||||||
ConfigName: source,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
confs, err := ParseConfigs(client, refs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -362,24 +359,86 @@ func convertServiceConfigObjs(
|
|||||||
return confs, err
|
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 obj.Name != "" {
|
||||||
|
source = obj.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
target := config.Target
|
||||||
|
if target == "" {
|
||||||
|
target = config.Source
|
||||||
|
}
|
||||||
|
|
||||||
|
uid := config.UID
|
||||||
|
gid := config.GID
|
||||||
|
if uid == "" {
|
||||||
|
uid = "0"
|
||||||
|
}
|
||||||
|
if gid == "" {
|
||||||
|
gid = "0"
|
||||||
|
}
|
||||||
|
mode := config.Mode
|
||||||
|
if mode == nil {
|
||||||
|
mode = uint32Ptr(0444)
|
||||||
|
}
|
||||||
|
|
||||||
|
return swarmReferenceObject{
|
||||||
|
File: swarmReferenceTarget{
|
||||||
|
Name: target,
|
||||||
|
UID: uid,
|
||||||
|
GID: gid,
|
||||||
|
Mode: os.FileMode(*mode),
|
||||||
|
},
|
||||||
|
Name: source,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func uint32Ptr(value uint32) *uint32 {
|
func uint32Ptr(value uint32) *uint32 {
|
||||||
return &value
|
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{}
|
hosts := []string{}
|
||||||
for host, ip := range extraHosts {
|
for _, hostIP := range extraHosts {
|
||||||
hosts = append(hosts, fmt.Sprintf("%s %s", ip, host))
|
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
|
return hosts
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertHealthcheck(healthcheck *HealthCheckConfig) (*container.HealthConfig, error) {
|
func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container.HealthConfig, error) {
|
||||||
if healthcheck == nil {
|
if healthcheck == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
timeout, interval, startPeriod time.Duration
|
timeout, interval, startPeriod composetypes.Duration
|
||||||
retries int
|
retries int
|
||||||
)
|
)
|
||||||
if healthcheck.Disable {
|
if healthcheck.Disable {
|
||||||
@ -405,14 +464,14 @@ func convertHealthcheck(healthcheck *HealthCheckConfig) (*container.HealthConfig
|
|||||||
}
|
}
|
||||||
return &container.HealthConfig{
|
return &container.HealthConfig{
|
||||||
Test: healthcheck.Test,
|
Test: healthcheck.Test,
|
||||||
Timeout: timeout,
|
Timeout: time.Duration(timeout),
|
||||||
Interval: interval,
|
Interval: time.Duration(interval),
|
||||||
Retries: retries,
|
Retries: retries,
|
||||||
StartPeriod: startPeriod,
|
StartPeriod: time.Duration(startPeriod),
|
||||||
}, nil
|
}, 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
|
// TODO: log if restart is being ignored
|
||||||
if source == nil {
|
if source == nil {
|
||||||
policy, err := ParseRestartPolicy(restart)
|
policy, err := ParseRestartPolicy(restart)
|
||||||
@ -438,13 +497,13 @@ func convertRestartPolicy(restart string, source *RestartPolicy) (*swarm.Restart
|
|||||||
}
|
}
|
||||||
return &swarm.RestartPolicy{
|
return &swarm.RestartPolicy{
|
||||||
Condition: swarm.RestartPolicyCondition(source.Condition),
|
Condition: swarm.RestartPolicyCondition(source.Condition),
|
||||||
Delay: source.Delay,
|
Delay: composetypes.ConvertDurationPtr(source.Delay),
|
||||||
MaxAttempts: source.MaxAttempts,
|
MaxAttempts: source.MaxAttempts,
|
||||||
Window: source.Window,
|
Window: composetypes.ConvertDurationPtr(source.Window),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertUpdateConfig(source *UpdateConfig) *swarm.UpdateConfig {
|
func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig {
|
||||||
if source == nil {
|
if source == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -454,15 +513,15 @@ func convertUpdateConfig(source *UpdateConfig) *swarm.UpdateConfig {
|
|||||||
}
|
}
|
||||||
return &swarm.UpdateConfig{
|
return &swarm.UpdateConfig{
|
||||||
Parallelism: parallel,
|
Parallelism: parallel,
|
||||||
Delay: source.Delay,
|
Delay: time.Duration(source.Delay),
|
||||||
FailureAction: source.FailureAction,
|
FailureAction: source.FailureAction,
|
||||||
Monitor: source.Monitor,
|
Monitor: time.Duration(source.Monitor),
|
||||||
MaxFailureRatio: source.MaxFailureRatio,
|
MaxFailureRatio: source.MaxFailureRatio,
|
||||||
Order: source.Order,
|
Order: source.Order,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertResources(source Resources) (*swarm.ResourceRequirements, error) {
|
func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) {
|
||||||
resources := &swarm.ResourceRequirements{}
|
resources := &swarm.ResourceRequirements{}
|
||||||
var err error
|
var err error
|
||||||
if source.Limits != nil {
|
if source.Limits != nil {
|
||||||
@ -486,21 +545,31 @@ func convertResources(source Resources) (*swarm.ResourceRequirements, error) {
|
|||||||
return nil, err
|
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{
|
resources.Reservations = &swarm.Resources{
|
||||||
NanoCPUs: cpus,
|
NanoCPUs: cpus,
|
||||||
MemoryBytes: int64(source.Reservations.MemoryBytes),
|
MemoryBytes: int64(source.Reservations.MemoryBytes),
|
||||||
|
GenericResources: generic,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return resources, nil
|
return resources, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type byPublishedPort []swarm.PortConfig
|
func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortConfig) (*swarm.EndpointSpec, error) {
|
||||||
|
|
||||||
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) {
|
|
||||||
portConfigs := []swarm.PortConfig{}
|
portConfigs := []swarm.PortConfig{}
|
||||||
for _, port := range source {
|
for _, port := range source {
|
||||||
portConfig := swarm.PortConfig{
|
portConfig := swarm.PortConfig{
|
||||||
@ -512,7 +581,10 @@ func convertEndpointSpec(endpointMode string, source []ServicePortConfig) (*swar
|
|||||||
portConfigs = append(portConfigs, portConfig)
|
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{
|
return &swarm.EndpointSpec{
|
||||||
Mode: swarm.ResolutionMode(strings.ToLower(endpointMode)),
|
Mode: swarm.ResolutionMode(strings.ToLower(endpointMode)),
|
||||||
Ports: portConfigs,
|
Ports: portConfigs,
|
||||||
@ -561,7 +633,7 @@ func convertDNSConfig(DNS []string, DNSSearch []string) (*swarm.DNSConfig, error
|
|||||||
return nil, nil
|
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
|
var o []string
|
||||||
|
|
||||||
// Config was added in API v1.40
|
// Config was added in API v1.40
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
package compose
|
package compose
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
composetypes "github.com/cuigh/swirl/biz/docker/compose/types"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/mount"
|
"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
|
// 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
|
var mounts []mount.Mount
|
||||||
|
|
||||||
for _, volumeConfig := range serviceVolumes {
|
for _, volumeConfig := range serviceVolumes {
|
||||||
@ -23,43 +22,37 @@ func Volumes(serviceVolumes []ServiceVolumeConfig, stackVolumes volumes, namespa
|
|||||||
return mounts, nil
|
return mounts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertVolumeToMount(
|
func createMountFromVolume(volume composetypes.ServiceVolumeConfig) mount.Mount {
|
||||||
volume ServiceVolumeConfig,
|
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,
|
stackVolumes volumes,
|
||||||
namespace Namespace,
|
namespace Namespace,
|
||||||
) (mount.Mount, error) {
|
) (mount.Mount, error) {
|
||||||
result := mount.Mount{
|
result := createMountFromVolume(volume)
|
||||||
Type: mount.Type(volume.Type),
|
|
||||||
Source: volume.Source,
|
|
||||||
Target: volume.Target,
|
|
||||||
ReadOnly: volume.ReadOnly,
|
|
||||||
Consistency: mount.Consistency(volume.Consistency),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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
|
// Anonymous volumes
|
||||||
if volume.Source == "" {
|
if volume.Source == "" {
|
||||||
return result, nil
|
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]
|
stackVolume, exists := stackVolumes[volume.Source]
|
||||||
if !exists {
|
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)
|
result.Source = namespace.Scope(volume.Source)
|
||||||
@ -69,16 +62,15 @@ func convertVolumeToMount(
|
|||||||
result.VolumeOptions.NoCopy = volume.Volume.NoCopy
|
result.VolumeOptions.NoCopy = volume.Volume.NoCopy
|
||||||
}
|
}
|
||||||
|
|
||||||
// External named volumes
|
|
||||||
if stackVolume.External.External {
|
|
||||||
result.Source = stackVolume.External.Name
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if stackVolume.Name != "" {
|
if stackVolume.Name != "" {
|
||||||
result.Source = stackVolume.Name
|
result.Source = stackVolume.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// External named volumes
|
||||||
|
if stackVolume.External.External {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
result.VolumeOptions.Labels = AddStackLabel(namespace, stackVolume.Labels)
|
result.VolumeOptions.Labels = AddStackLabel(namespace, stackVolume.Labels)
|
||||||
if stackVolume.Driver != "" || stackVolume.DriverOpts != nil {
|
if stackVolume.Driver != "" || stackVolume.DriverOpts != nil {
|
||||||
result.VolumeOptions.DriverConfig = &mount.Driver{
|
result.VolumeOptions.DriverConfig = &mount.Driver{
|
||||||
@ -87,6 +79,84 @@ func convertVolumeToMount(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Named volumes
|
|
||||||
return result, nil
|
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")
|
||||||
|
}
|
||||||
|
69
biz/docker/compose/interpolate.go
Normal file
69
biz/docker/compose/interpolate.go
Normal 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)
|
||||||
|
}
|
@ -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
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
163
biz/docker/compose/interpolation/interpolation.go
Normal file
163
biz/docker/compose/interpolation/interpolation.go
Normal 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
|
||||||
|
}
|
@ -1,22 +1,38 @@
|
|||||||
package compose
|
package compose
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/cuigh/auxo/log"
|
"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"
|
"github.com/docker/go-connections/nat"
|
||||||
units "github.com/docker/go-units"
|
"github.com/docker/go-units"
|
||||||
shellwords "github.com/mattn/go-shellwords"
|
"github.com/mattn/go-shellwords"
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/yaml.v2"
|
"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
|
// ParseYAML reads the bytes from a file, parses the bytes into a mapping
|
||||||
// structure, and returns it.
|
// structure, and returns it.
|
||||||
func ParseYAML(source []byte) (map[string]interface{}, error) {
|
func ParseYAML(source []byte) (map[string]interface{}, error) {
|
||||||
@ -36,90 +52,152 @@ func ParseYAML(source []byte) (map[string]interface{}, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load reads a ConfigDetails and returns a fully loaded configuration
|
// 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 {
|
if len(configDetails.ConfigFiles) < 1 {
|
||||||
return nil, errors.New("No files specified")
|
return nil, errors.Errorf("No files specified")
|
||||||
}
|
|
||||||
if len(configDetails.ConfigFiles) > 1 {
|
|
||||||
return nil, errors.New("Multiple files are not yet supported")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
configDict := getConfigDict(configDetails)
|
opts := &Options{
|
||||||
|
Interpolate: &interp.Options{
|
||||||
|
Substitute: template.Substitute,
|
||||||
|
LookupValue: configDetails.LookupEnv,
|
||||||
|
TypeCastMapping: interpolateTypeCastMapping,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
if services, ok := configDict["services"]; ok {
|
for _, op := range options {
|
||||||
if servicesDict, ok := services.(map[string]interface{}); ok {
|
op(opts)
|
||||||
forbidden := getProperties(servicesDict, ForbiddenProperties)
|
}
|
||||||
|
|
||||||
if len(forbidden) > 0 {
|
configs := []*composetypes.Config{}
|
||||||
return nil, &ForbiddenPropertiesError{Properties: forbidden}
|
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 !opts.SkipValidation {
|
||||||
//if err := schema.Validate(configDict, schema.Version(configDict)); err != nil {
|
// if err := schema.Validate(configDict, configDetails.Version); err != nil {
|
||||||
// return nil, err
|
// return nil, err
|
||||||
//}
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
cfg := Config{}
|
cfg, err := loadSections(configDict, configDetails)
|
||||||
|
|
||||||
config, err := interpolateConfig(configDict, configDetails.LookupEnv)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Services, err = LoadServices(config["services"], configDetails.WorkingDir, configDetails.LookupEnv)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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]
|
|
||||||
if !ok {
|
|
||||||
config[key] = make(map[string]interface{})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
config[key], err = Interpolate(section.(map[string]interface{}), key, lookupEnv)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
cfg.Filename = file.Filename
|
||||||
|
|
||||||
|
configs = append(configs, cfg)
|
||||||
}
|
}
|
||||||
return config, nil
|
|
||||||
|
return merge(configs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateForbidden(configDict map[string]interface{}) error {
|
||||||
|
servicesDict, ok := configDict["services"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
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
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
// GetUnsupportedProperties returns the list of any unsupported properties that are
|
||||||
// used in the Compose files.
|
// used in the Compose files.
|
||||||
func GetUnsupportedProperties(configDetails ConfigDetails) []string {
|
func GetUnsupportedProperties(configDicts ...map[string]interface{}) []string {
|
||||||
unsupported := map[string]bool{}
|
unsupported := map[string]bool{}
|
||||||
|
|
||||||
for _, service := range getServices(getConfigDict(configDetails)) {
|
for _, configDict := range configDicts {
|
||||||
serviceDict := service.(map[string]interface{})
|
for _, service := range getServices(configDict) {
|
||||||
for _, property := range UnsupportedProperties {
|
serviceDict := service.(map[string]interface{})
|
||||||
if _, isSet := serviceDict[property]; isSet {
|
for _, property := range composetypes.UnsupportedProperties {
|
||||||
unsupported[property] = true
|
if _, isSet := serviceDict[property]; isSet {
|
||||||
|
unsupported[property] = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -138,8 +216,17 @@ func sortedKeys(set map[string]bool) []string {
|
|||||||
|
|
||||||
// GetDeprecatedProperties returns the list of any deprecated properties that
|
// GetDeprecatedProperties returns the list of any deprecated properties that
|
||||||
// are used in the compose files.
|
// are used in the compose files.
|
||||||
func GetDeprecatedProperties(configDetails ConfigDetails) map[string]string {
|
func GetDeprecatedProperties(configDicts ...map[string]interface{}) map[string]string {
|
||||||
return getProperties(getServices(getConfigDict(configDetails)), DeprecatedProperties)
|
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 {
|
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"
|
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{} {
|
func getServices(configDict map[string]interface{}) map[string]interface{} {
|
||||||
if services, ok := configDict["services"]; ok {
|
if services, ok := configDict["services"]; ok {
|
||||||
if servicesDict, ok := services.(map[string]interface{}); 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{}{}
|
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{}
|
data := mapstructure.Metadata{}
|
||||||
config := &mapstructure.DecoderConfig{
|
config := &mapstructure.DecoderConfig{
|
||||||
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
||||||
createTransformHook(),
|
createTransformHook(additionalTransformers...),
|
||||||
mapstructure.StringToTimeDurationHookFunc()),
|
mapstructure.StringToTimeDurationHookFunc()),
|
||||||
Result: target,
|
Result: target,
|
||||||
Metadata: &data,
|
Metadata: &data,
|
||||||
@ -199,25 +283,38 @@ func transform(source map[string]interface{}, target interface{}) error {
|
|||||||
return decoder.Decode(source)
|
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){
|
transforms := map[reflect.Type]func(interface{}) (interface{}, error){
|
||||||
reflect.TypeOf(External{}): transformExternal,
|
reflect.TypeOf(composetypes.External{}): transformExternal,
|
||||||
reflect.TypeOf(HealthCheckTest{}): transformHealthCheckTest,
|
reflect.TypeOf(composetypes.HealthCheckTest{}): transformHealthCheckTest,
|
||||||
reflect.TypeOf(ShellCommand{}): transformShellCommand,
|
reflect.TypeOf(composetypes.ShellCommand{}): transformShellCommand,
|
||||||
reflect.TypeOf(StringList{}): transformStringList,
|
reflect.TypeOf(composetypes.StringList{}): transformStringList,
|
||||||
reflect.TypeOf(map[string]string{}): transformMapStringString,
|
reflect.TypeOf(map[string]string{}): transformMapStringString,
|
||||||
reflect.TypeOf(UlimitsConfig{}): transformUlimits,
|
reflect.TypeOf(composetypes.UlimitsConfig{}): transformUlimits,
|
||||||
reflect.TypeOf(UnitBytes(0)): transformSize,
|
reflect.TypeOf(composetypes.UnitBytes(0)): transformSize,
|
||||||
reflect.TypeOf([]ServicePortConfig{}): transformServicePort,
|
reflect.TypeOf([]composetypes.ServicePortConfig{}): transformServicePort,
|
||||||
reflect.TypeOf(ServiceSecretConfig{}): transformStringSourceMap,
|
reflect.TypeOf(composetypes.ServiceSecretConfig{}): transformStringSourceMap,
|
||||||
reflect.TypeOf(ServiceConfigObjConfig{}): transformStringSourceMap,
|
reflect.TypeOf(composetypes.ServiceConfigObjConfig{}): transformStringSourceMap,
|
||||||
reflect.TypeOf(StringOrNumberList{}): transformStringOrNumberList,
|
reflect.TypeOf(composetypes.StringOrNumberList{}): transformStringOrNumberList,
|
||||||
reflect.TypeOf(map[string]*ServiceNetworkConfig{}): transformServiceNetworkMap,
|
reflect.TypeOf(map[string]*composetypes.ServiceNetworkConfig{}): transformServiceNetworkMap,
|
||||||
reflect.TypeOf(MappingWithEquals{}): transformMappingOrListFunc("=", true),
|
reflect.TypeOf(composetypes.Mapping{}): transformMappingOrListFunc("=", false),
|
||||||
reflect.TypeOf(Labels{}): transformMappingOrListFunc("=", false),
|
reflect.TypeOf(composetypes.MappingWithEquals{}): transformMappingOrListFunc("=", true),
|
||||||
reflect.TypeOf(MappingWithColon{}): transformMappingOrListFunc(":", false),
|
reflect.TypeOf(composetypes.Labels{}): transformMappingOrListFunc("=", false),
|
||||||
reflect.TypeOf(ServiceVolumeConfig{}): transformServiceVolumeConfig,
|
reflect.TypeOf(composetypes.MappingWithColon{}): transformMappingOrListFunc(":", false),
|
||||||
reflect.TypeOf(BuildConfig{}): transformBuildConfig,
|
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) {
|
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
|
// LoadServices produces a ServiceConfig map from a compose file Dict
|
||||||
// the servicesDict is not validated if directly used. Use Load() to enable validation
|
// 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) {
|
func LoadServices(servicesDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) ([]composetypes.ServiceConfig, error) {
|
||||||
var services []ServiceConfig
|
var services []composetypes.ServiceConfig
|
||||||
|
|
||||||
for name, serviceDef := range servicesDict {
|
for name, serviceDef := range servicesDict {
|
||||||
serviceConfig, err := LoadService(name, serviceDef.(map[string]interface{}), workingDir, lookupEnv)
|
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
|
// LoadService produces a single ServiceConfig from a compose file Dict
|
||||||
// the serviceDict is not validated if directly used. Use Load() to enable validation
|
// 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) {
|
func LoadService(name string, serviceDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) (*composetypes.ServiceConfig, error) {
|
||||||
serviceConfig := &ServiceConfig{}
|
serviceConfig := &composetypes.ServiceConfig{}
|
||||||
if err := transform(serviceDict, serviceConfig); err != nil {
|
if err := Transform(serviceDict, serviceConfig); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
serviceConfig.Name = name
|
serviceConfig.Name = name
|
||||||
@ -306,11 +403,36 @@ func LoadService(name string, serviceDict map[string]interface{}, workingDir str
|
|||||||
return nil, err
|
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
|
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 {
|
for k, v := range vars {
|
||||||
interpolatedV, ok := lookupEnv(k)
|
interpolatedV, ok := lookupEnv(k)
|
||||||
if (v == nil || *v == "") && ok {
|
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)
|
environment := make(map[string]*string)
|
||||||
|
|
||||||
if len(serviceConfig.EnvFile) > 0 {
|
if len(serviceConfig.EnvFile) > 0 {
|
||||||
@ -345,12 +467,16 @@ func resolveEnvironment(serviceConfig *ServiceConfig, workingDir string, lookupE
|
|||||||
return nil
|
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 {
|
for i, volume := range volumes {
|
||||||
if volume.Type != "bind" {
|
if volume.Type != "bind" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if volume.Source == "" {
|
||||||
|
return errors.New(`invalid mount config for type "bind": field Source must not be empty`)
|
||||||
|
}
|
||||||
|
|
||||||
filePath := expandUser(volume.Source, lookupEnv)
|
filePath := expandUser(volume.Source, lookupEnv)
|
||||||
// Check for a Unix absolute path first, to handle a Windows client
|
// Check for a Unix absolute path first, to handle a Windows client
|
||||||
// with a Unix daemon. This handles a Windows client connecting to a
|
// 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
|
volume.Source = filePath
|
||||||
volumes[i] = volume
|
volumes[i] = volume
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// TODO: make this more robust
|
// TODO: make this more robust
|
||||||
func expandUser(path string, lookupEnv Mapping) string {
|
func expandUser(path string, lookupEnv template.Mapping) string {
|
||||||
if strings.HasPrefix(path, "~") {
|
if strings.HasPrefix(path, "~") {
|
||||||
home, ok := lookupEnv("HOME")
|
home, ok := lookupEnv("HOME")
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -381,9 +509,9 @@ func expandUser(path string, lookupEnv Mapping) string {
|
|||||||
func transformUlimits(data interface{}) (interface{}, error) {
|
func transformUlimits(data interface{}) (interface{}, error) {
|
||||||
switch value := data.(type) {
|
switch value := data.(type) {
|
||||||
case int:
|
case int:
|
||||||
return UlimitsConfig{Single: value}, nil
|
return composetypes.UlimitsConfig{Single: value}, nil
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
ulimit := UlimitsConfig{}
|
ulimit := composetypes.UlimitsConfig{}
|
||||||
ulimit.Soft = value["soft"].(int)
|
ulimit.Soft = value["soft"].(int)
|
||||||
ulimit.Hard = value["hard"].(int)
|
ulimit.Hard = value["hard"].(int)
|
||||||
return ulimit, nil
|
return ulimit, nil
|
||||||
@ -394,17 +522,31 @@ func transformUlimits(data interface{}) (interface{}, error) {
|
|||||||
|
|
||||||
// LoadNetworks produces a NetworkConfig map from a compose file Dict
|
// LoadNetworks produces a NetworkConfig map from a compose file Dict
|
||||||
// the source Dict is not validated if directly used. Use Load() to enable validation
|
// the source Dict is not validated if directly used. Use Load() to enable validation
|
||||||
func LoadNetworks(source map[string]interface{}) (map[string]NetworkConfig, error) {
|
func LoadNetworks(source map[string]interface{}, version string) (map[string]composetypes.NetworkConfig, error) {
|
||||||
networks := make(map[string]NetworkConfig)
|
networks := make(map[string]composetypes.NetworkConfig)
|
||||||
err := transform(source, &networks)
|
err := Transform(source, &networks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return networks, err
|
return networks, err
|
||||||
}
|
}
|
||||||
for name, network := range networks {
|
for name, network := range networks {
|
||||||
if network.External.External && network.External.Name == "" {
|
if !network.External.External {
|
||||||
network.External.Name = name
|
continue
|
||||||
networks[name] = network
|
|
||||||
}
|
}
|
||||||
|
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
|
return networks, nil
|
||||||
}
|
}
|
||||||
@ -417,76 +559,110 @@ func externalVolumeError(volume, key string) error {
|
|||||||
|
|
||||||
// LoadVolumes produces a VolumeConfig map from a compose file Dict
|
// LoadVolumes produces a VolumeConfig map from a compose file Dict
|
||||||
// the source Dict is not validated if directly used. Use Load() to enable validation
|
// the source Dict is not validated if directly used. Use Load() to enable validation
|
||||||
func LoadVolumes(source map[string]interface{}) (map[string]VolumeConfig, error) {
|
func LoadVolumes(source map[string]interface{}, version string) (map[string]composetypes.VolumeConfig, error) {
|
||||||
volumes := make(map[string]VolumeConfig)
|
volumes := make(map[string]composetypes.VolumeConfig)
|
||||||
err := transform(source, &volumes)
|
if err := Transform(source, &volumes); err != nil {
|
||||||
if err != nil {
|
|
||||||
return volumes, err
|
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)
|
|
||||||
|
|
||||||
if volume.Name != "" {
|
for name, volume := range volumes {
|
||||||
return nil, fmt.Errorf("volume %s: volume.external.name and volume.name conflict; only use volume.name", name)
|
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, 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
|
return volumes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadSecrets produces a SecretConfig map from a compose file Dict
|
// LoadSecrets produces a SecretConfig map from a compose file Dict
|
||||||
// the source Dict is not validated if directly used. Use Load() to enable validation
|
// 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) {
|
func LoadSecrets(source map[string]interface{}, details composetypes.ConfigDetails) (map[string]composetypes.SecretConfig, error) {
|
||||||
secrets := make(map[string]SecretConfig)
|
secrets := make(map[string]composetypes.SecretConfig)
|
||||||
if err := transform(source, &secrets); err != nil {
|
if err := Transform(source, &secrets); err != nil {
|
||||||
return secrets, err
|
return secrets, err
|
||||||
}
|
}
|
||||||
for name, secret := range secrets {
|
for name, secret := range secrets {
|
||||||
if secret.External.External && secret.External.Name == "" {
|
obj, err := loadFileObjectConfig(name, "secret", composetypes.FileObjectConfig(secret), details)
|
||||||
secret.External.Name = name
|
if err != nil {
|
||||||
secrets[name] = secret
|
return nil, err
|
||||||
}
|
|
||||||
if secret.File != "" {
|
|
||||||
secret.File = absPath(workingDir, secret.File)
|
|
||||||
}
|
}
|
||||||
|
secretConfig := composetypes.SecretConfig(obj)
|
||||||
|
secretConfig.Extras = loadExtras(name, source)
|
||||||
|
secrets[name] = secretConfig
|
||||||
}
|
}
|
||||||
return secrets, nil
|
return secrets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfigObjs produces a ConfigObjConfig map from a compose file Dict
|
// LoadConfigObjs produces a ConfigObjConfig map from a compose file Dict
|
||||||
// the source Dict is not validated if directly used. Use Load() to enable validation
|
// 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) {
|
func LoadConfigObjs(source map[string]interface{}, details composetypes.ConfigDetails) (map[string]composetypes.ConfigObjConfig, error) {
|
||||||
configs := make(map[string]ConfigObjConfig)
|
configs := make(map[string]composetypes.ConfigObjConfig)
|
||||||
if err := transform(source, &configs); err != nil {
|
if err := Transform(source, &configs); err != nil {
|
||||||
return configs, err
|
return configs, err
|
||||||
}
|
}
|
||||||
for name, config := range configs {
|
for name, config := range configs {
|
||||||
if config.External.External && config.External.Name == "" {
|
obj, err := loadFileObjectConfig(name, "config", composetypes.FileObjectConfig(config), details)
|
||||||
config.External.Name = name
|
if err != nil {
|
||||||
configs[name] = config
|
return nil, err
|
||||||
}
|
|
||||||
if config.File != "" {
|
|
||||||
config.File = absPath(workingDir, config.File)
|
|
||||||
}
|
}
|
||||||
|
configConfig := composetypes.ConfigObjConfig(obj)
|
||||||
|
configConfig.Extras = loadExtras(name, source)
|
||||||
|
configs[name] = configConfig
|
||||||
}
|
}
|
||||||
return configs, nil
|
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 {
|
func absPath(workingDir string, filePath string) string {
|
||||||
if filepath.IsAbs(filePath) {
|
if filepath.IsAbs(filePath) {
|
||||||
return 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))
|
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) {
|
func transformShellCommand(value interface{}) (interface{}, error) {
|
||||||
if str, ok := value.(string); ok {
|
if str, ok := value.(string); ok {
|
||||||
return shellwords.Parse(str)
|
return shellwords.Parse(str)
|
||||||
@ -670,6 +862,19 @@ func transformSize(value interface{}) (interface{}, error) {
|
|||||||
panic(fmt.Errorf("invalid type for size %T", value))
|
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) {
|
func toServicePortConfigs(value string) ([]interface{}, error) {
|
||||||
var portConfigs []interface{}
|
var portConfigs []interface{}
|
||||||
|
|
||||||
@ -691,7 +896,7 @@ func toServicePortConfigs(value string) ([]interface{}, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, p := range portConfig {
|
for _, p := range portConfig {
|
||||||
portConfigs = append(portConfigs, ServicePortConfig{
|
portConfigs = append(portConfigs, composetypes.ServicePortConfig{
|
||||||
Protocol: string(p.Protocol),
|
Protocol: string(p.Protocol),
|
||||||
Target: p.TargetPort,
|
Target: p.TargetPort,
|
||||||
Published: p.PublishedPort,
|
Published: p.PublishedPort,
|
||||||
@ -721,3 +926,15 @@ func toString(value interface{}, allowNil bool) interface{} {
|
|||||||
return ""
|
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
233
biz/docker/compose/merge.go
Normal 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
|
||||||
|
}
|
@ -5,9 +5,11 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"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)
|
//absPath, err := filepath.Abs(composefile)
|
||||||
//if err != nil {
|
//if err != nil {
|
||||||
// return details, err
|
// return details, err
|
||||||
@ -24,8 +26,8 @@ func Parse(name, content string) (*Config, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
details := ConfigDetails{
|
details := composetypes.ConfigDetails{
|
||||||
ConfigFiles: []ConfigFile{*configFile},
|
ConfigFiles: []composetypes.ConfigFile{*configFile},
|
||||||
Environment: env,
|
Environment: env,
|
||||||
}
|
}
|
||||||
cfg, err := Load(details)
|
cfg, err := Load(details)
|
||||||
@ -47,12 +49,12 @@ func propertyWarnings(properties map[string]string) string {
|
|||||||
return strings.Join(msgs, "\n\n")
|
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))
|
config, err := ParseYAML([]byte(content))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &ConfigFile{
|
return &composetypes.ConfigFile{
|
||||||
Filename: name,
|
Filename: name,
|
||||||
Config: config,
|
Config: config,
|
||||||
}, nil
|
}, nil
|
||||||
@ -71,7 +73,7 @@ func buildEnvironment(env []string) (map[string]string, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetServicesDeclaredNetworks(serviceConfigs []ServiceConfig) map[string]struct{} {
|
func GetServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
|
||||||
serviceNetworks := map[string]struct{}{}
|
serviceNetworks := map[string]struct{}{}
|
||||||
for _, serviceConfig := range serviceConfigs {
|
for _, serviceConfig := range serviceConfigs {
|
||||||
if len(serviceConfig.Networks) == 0 {
|
if len(serviceConfig.Networks) == 0 {
|
||||||
|
161
biz/docker/compose/schema/schema.go
Normal file
161
biz/docker/compose/schema/schema.go
Normal 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(), "."))
|
||||||
|
//}
|
@ -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, ""
|
|
||||||
}
|
|
245
biz/docker/compose/template/template.go
Normal file
245
biz/docker/compose/template/template.go
Normal 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, ""
|
||||||
|
}
|
@ -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
|
|
524
biz/docker/compose/types/types.go
Normal file
524
biz/docker/compose/types/types.go
Normal 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
|
@ -5,6 +5,7 @@ import (
|
|||||||
"unicode"
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/cuigh/swirl/biz/docker/compose/types"
|
||||||
"github.com/docker/docker/api/types/mount"
|
"github.com/docker/docker/api/types/mount"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
@ -12,8 +13,8 @@ import (
|
|||||||
const endOfSpec = rune(0)
|
const endOfSpec = rune(0)
|
||||||
|
|
||||||
// ParseVolume parses a volume spec without any knowledge of the target platform
|
// ParseVolume parses a volume spec without any knowledge of the target platform
|
||||||
func ParseVolume(spec string) (ServiceVolumeConfig, error) {
|
func ParseVolume(spec string) (types.ServiceVolumeConfig, error) {
|
||||||
volume := ServiceVolumeConfig{}
|
volume := types.ServiceVolumeConfig{}
|
||||||
|
|
||||||
switch len(spec) {
|
switch len(spec) {
|
||||||
case 0:
|
case 0:
|
||||||
@ -48,7 +49,7 @@ func isWindowsDrive(buffer []rune, char rune) bool {
|
|||||||
return char == ':' && len(buffer) == 1 && unicode.IsLetter(buffer[0])
|
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)
|
strBuffer := string(buffer)
|
||||||
switch {
|
switch {
|
||||||
case len(buffer) == 0:
|
case len(buffer) == 0:
|
||||||
@ -73,10 +74,10 @@ func populateFieldFromBuffer(char rune, buffer []rune, volume *ServiceVolumeConf
|
|||||||
case "rw":
|
case "rw":
|
||||||
volume.ReadOnly = false
|
volume.ReadOnly = false
|
||||||
case "nocopy":
|
case "nocopy":
|
||||||
volume.Volume = &ServiceVolumeVolume{NoCopy: true}
|
volume.Volume = &types.ServiceVolumeVolume{NoCopy: true}
|
||||||
default:
|
default:
|
||||||
if isBindOption(option) {
|
if isBindOption(option) {
|
||||||
volume.Bind = &ServiceVolumeBind{Propagation: option}
|
volume.Bind = &types.ServiceVolumeBind{Propagation: option}
|
||||||
}
|
}
|
||||||
// ignore unknown options
|
// ignore unknown options
|
||||||
}
|
}
|
||||||
@ -93,7 +94,7 @@ func isBindOption(option string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func populateType(volume *ServiceVolumeConfig) {
|
func populateType(volume *types.ServiceVolumeConfig) {
|
||||||
switch {
|
switch {
|
||||||
// Anonymous volume
|
// Anonymous volume
|
||||||
case volume.Source == "":
|
case volume.Source == "":
|
||||||
@ -111,6 +112,11 @@ func isFilePath(source string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// windows named pipes
|
||||||
|
if strings.HasPrefix(source, `\\`) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
first, nextIndex := utf8.DecodeRuneInString(source)
|
first, nextIndex := utf8.DecodeRuneInString(source)
|
||||||
return isWindowsDrive([]rune{first}, rune(source[nextIndex]))
|
return isWindowsDrive([]rune{first}, rune(source[nextIndex]))
|
||||||
}
|
}
|
||||||
|
1
go.mod
1
go.mod
@ -19,6 +19,7 @@ require (
|
|||||||
github.com/gobwas/ws v1.0.0
|
github.com/gobwas/ws v1.0.0
|
||||||
github.com/google/go-cmp v0.2.0 // indirect
|
github.com/google/go-cmp v0.2.0 // indirect
|
||||||
github.com/gorilla/mux v1.7.1 // 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/kr/pretty v0.1.0 // indirect
|
||||||
github.com/mattn/go-shellwords v1.0.3
|
github.com/mattn/go-shellwords v1.0.3
|
||||||
github.com/mitchellh/mapstructure v1.0.0
|
github.com/mitchellh/mapstructure v1.0.0
|
||||||
|
2
go.sum
2
go.sum
@ -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/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 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
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 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
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=
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
|
Loading…
Reference in New Issue
Block a user