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"
|
||||
"strings"
|
||||
|
||||
composetypes "github.com/cuigh/swirl/biz/docker/compose/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
networktypes "github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
@ -48,12 +49,12 @@ func AddStackLabel(namespace Namespace, labels map[string]string) map[string]str
|
||||
return labels
|
||||
}
|
||||
|
||||
type networkMap map[string]NetworkConfig
|
||||
type networkMap map[string]composetypes.NetworkConfig
|
||||
|
||||
// Networks from the compose-file type to the engine API type
|
||||
func Networks(namespace Namespace, networks networkMap, servicesNetworks map[string]struct{}) (map[string]types.NetworkCreate, []string) {
|
||||
if networks == nil {
|
||||
networks = make(map[string]NetworkConfig)
|
||||
networks = make(map[string]composetypes.NetworkConfig)
|
||||
}
|
||||
|
||||
externalNetworks := []string{}
|
||||
@ -61,7 +62,7 @@ func Networks(namespace Namespace, networks networkMap, servicesNetworks map[str
|
||||
for internalName := range servicesNetworks {
|
||||
network := networks[internalName]
|
||||
if network.External.External {
|
||||
externalNetworks = append(externalNetworks, network.External.Name)
|
||||
externalNetworks = append(externalNetworks, network.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
@ -86,38 +87,54 @@ func Networks(namespace Namespace, networks networkMap, servicesNetworks map[str
|
||||
}
|
||||
createOpts.IPAM.Config = append(createOpts.IPAM.Config, config)
|
||||
}
|
||||
result[internalName] = createOpts
|
||||
|
||||
networkName := namespace.Scope(internalName)
|
||||
if network.Name != "" {
|
||||
networkName = network.Name
|
||||
}
|
||||
result[networkName] = createOpts
|
||||
}
|
||||
|
||||
return result, externalNetworks
|
||||
}
|
||||
|
||||
// Secrets converts secrets from the Compose type to the engine API type
|
||||
func Secrets(namespace Namespace, secrets map[string]SecretConfig) ([]swarm.SecretSpec, error) {
|
||||
func Secrets(namespace Namespace, secrets map[string]composetypes.SecretConfig) ([]swarm.SecretSpec, error) {
|
||||
result := []swarm.SecretSpec{}
|
||||
for name, secret := range secrets {
|
||||
if secret.External.External {
|
||||
continue
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(secret.File)
|
||||
var obj swarmFileObject
|
||||
var err error
|
||||
if secret.Driver != "" {
|
||||
obj, err = driverObjectConfig(namespace, name, composetypes.FileObjectConfig(secret))
|
||||
} else {
|
||||
obj, err = fileObjectConfig(namespace, name, composetypes.FileObjectConfig(secret))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result = append(result, swarm.SecretSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: namespace.Scope(name),
|
||||
Labels: AddStackLabel(namespace, secret.Labels),
|
||||
},
|
||||
Data: data,
|
||||
})
|
||||
spec := swarm.SecretSpec{Annotations: obj.Annotations, Data: obj.Data}
|
||||
if secret.Driver != "" {
|
||||
spec.Driver = &swarm.Driver{
|
||||
Name: secret.Driver,
|
||||
Options: secret.DriverOpts,
|
||||
}
|
||||
}
|
||||
if secret.TemplateDriver != "" {
|
||||
spec.Templating = &swarm.Driver{
|
||||
Name: secret.TemplateDriver,
|
||||
}
|
||||
}
|
||||
result = append(result, spec)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Configs converts config objects from the Compose type to the engine API type
|
||||
func Configs(namespace Namespace, configs map[string]ConfigObjConfig) ([]swarm.ConfigSpec, error) {
|
||||
func Configs(namespace Namespace, configs map[string]composetypes.ConfigObjConfig) ([]swarm.ConfigSpec, error) {
|
||||
result := []swarm.ConfigSpec{}
|
||||
for name, config := range configs {
|
||||
if config.External.External {
|
||||
@ -139,3 +156,45 @@ func Configs(namespace Namespace, configs map[string]ConfigObjConfig) ([]swarm.C
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type swarmFileObject struct {
|
||||
Annotations swarm.Annotations
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func driverObjectConfig(namespace Namespace, name string, obj composetypes.FileObjectConfig) (swarmFileObject, error) {
|
||||
if obj.Name != "" {
|
||||
name = obj.Name
|
||||
} else {
|
||||
name = namespace.Scope(name)
|
||||
}
|
||||
|
||||
return swarmFileObject{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: name,
|
||||
Labels: AddStackLabel(namespace, obj.Labels),
|
||||
},
|
||||
Data: []byte{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func fileObjectConfig(namespace Namespace, name string, obj composetypes.FileObjectConfig) (swarmFileObject, error) {
|
||||
data, err := ioutil.ReadFile(obj.File)
|
||||
if err != nil {
|
||||
return swarmFileObject{}, err
|
||||
}
|
||||
|
||||
if obj.Name != "" {
|
||||
name = obj.Name
|
||||
} else {
|
||||
name = namespace.Scope(name)
|
||||
}
|
||||
|
||||
return swarmFileObject{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: name,
|
||||
Labels: AddStackLabel(namespace, obj.Labels),
|
||||
},
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"context"
|
||||
|
||||
composetypes "github.com/cuigh/swirl/biz/docker/compose/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
@ -27,7 +27,7 @@ const (
|
||||
// Services from compose-file types to engine API types
|
||||
func Services(
|
||||
namespace Namespace,
|
||||
config *Config,
|
||||
config *composetypes.Config,
|
||||
client *client.Client,
|
||||
) (map[string]swarm.ServiceSpec, error) {
|
||||
result := make(map[string]swarm.ServiceSpec)
|
||||
@ -41,7 +41,7 @@ func Services(
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "service %s", service.Name)
|
||||
}
|
||||
configs, err := convertServiceConfigObjs(client, namespace, service.Configs, config.Configs)
|
||||
configs, err := convertServiceConfigObjs(client, namespace, service, config.Configs)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "service %s", service.Name)
|
||||
}
|
||||
@ -60,9 +60,9 @@ func Services(
|
||||
func Service(
|
||||
apiVersion string,
|
||||
namespace Namespace,
|
||||
service ServiceConfig,
|
||||
networkConfigs map[string]NetworkConfig,
|
||||
volumes map[string]VolumeConfig,
|
||||
service composetypes.ServiceConfig,
|
||||
networkConfigs map[string]composetypes.NetworkConfig,
|
||||
volumes map[string]composetypes.VolumeConfig,
|
||||
secrets []*swarm.SecretReference,
|
||||
configs []*swarm.ConfigReference,
|
||||
) (swarm.ServiceSpec, error) {
|
||||
@ -110,7 +110,9 @@ func Service(
|
||||
}
|
||||
|
||||
var privileges swarm.Privileges
|
||||
privileges.CredentialSpec, err = convertCredentialSpec(namespace, service.CredentialSpec, configs)
|
||||
privileges.CredentialSpec, err = convertCredentialSpec(
|
||||
namespace, service.CredentialSpec, configs,
|
||||
)
|
||||
if err != nil {
|
||||
return swarm.ServiceSpec{}, err
|
||||
}
|
||||
@ -134,7 +136,7 @@ func Service(
|
||||
Command: service.Entrypoint,
|
||||
Args: service.Command,
|
||||
Hostname: service.Hostname,
|
||||
Hosts: sortStrings(convertExtraHosts(service.ExtraHosts)),
|
||||
Hosts: convertExtraHosts(service.ExtraHosts),
|
||||
DNSConfig: dnsConfig,
|
||||
Healthcheck: healthcheck,
|
||||
Env: sortStrings(convertEnvironment(service.Environment)),
|
||||
@ -142,7 +144,7 @@ func Service(
|
||||
Dir: service.WorkingDir,
|
||||
User: service.User,
|
||||
Mounts: mounts,
|
||||
StopGracePeriod: service.StopGracePeriod,
|
||||
StopGracePeriod: composetypes.ConvertDurationPtr(service.StopGracePeriod),
|
||||
StopSignal: service.StopSignal,
|
||||
TTY: service.Tty,
|
||||
OpenStdin: service.StdinOpen,
|
||||
@ -150,6 +152,9 @@ func Service(
|
||||
Configs: configs,
|
||||
ReadOnly: service.ReadOnly,
|
||||
Privileges: &privileges,
|
||||
Isolation: container.Isolation(service.Isolation),
|
||||
Init: service.Init,
|
||||
Sysctls: service.Sysctls,
|
||||
},
|
||||
LogDriver: logDriver,
|
||||
Resources: resources,
|
||||
@ -157,11 +162,13 @@ func Service(
|
||||
Placement: &swarm.Placement{
|
||||
Constraints: service.Deploy.Placement.Constraints,
|
||||
Preferences: getPlacementPreference(service.Deploy.Placement.Preferences),
|
||||
MaxReplicas: service.Deploy.Placement.MaxReplicas,
|
||||
},
|
||||
},
|
||||
EndpointSpec: endpoint,
|
||||
Mode: mode,
|
||||
UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig),
|
||||
RollbackConfig: convertUpdateConfig(service.Deploy.RollbackConfig),
|
||||
}
|
||||
|
||||
// add an image label to serviceSpec
|
||||
@ -182,7 +189,7 @@ func Service(
|
||||
return serviceSpec, nil
|
||||
}
|
||||
|
||||
func getPlacementPreference(preferences []PlacementPreferences) []swarm.PlacementPreference {
|
||||
func getPlacementPreference(preferences []composetypes.PlacementPreferences) []swarm.PlacementPreference {
|
||||
result := []swarm.PlacementPreference{}
|
||||
for _, preference := range preferences {
|
||||
spreadDescriptor := preference.Spread
|
||||
@ -200,20 +207,14 @@ func sortStrings(strs []string) []string {
|
||||
return strs
|
||||
}
|
||||
|
||||
type byNetworkTarget []swarm.NetworkAttachmentConfig
|
||||
|
||||
func (a byNetworkTarget) Len() int { return len(a) }
|
||||
func (a byNetworkTarget) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a byNetworkTarget) Less(i, j int) bool { return a[i].Target < a[j].Target }
|
||||
|
||||
func convertServiceNetworks(
|
||||
networks map[string]*ServiceNetworkConfig,
|
||||
networks map[string]*composetypes.ServiceNetworkConfig,
|
||||
networkConfigs networkMap,
|
||||
namespace Namespace,
|
||||
name string,
|
||||
) ([]swarm.NetworkAttachmentConfig, error) {
|
||||
if len(networks) == 0 {
|
||||
networks = map[string]*ServiceNetworkConfig{
|
||||
networks = map[string]*composetypes.ServiceNetworkConfig{
|
||||
defaultNetwork: {},
|
||||
}
|
||||
}
|
||||
@ -229,8 +230,8 @@ func convertServiceNetworks(
|
||||
aliases = network.Aliases
|
||||
}
|
||||
target := namespace.Scope(networkName)
|
||||
if networkConfig.External.External {
|
||||
target = networkConfig.External.Name
|
||||
if networkConfig.Name != "" {
|
||||
target = networkConfig.Name
|
||||
}
|
||||
netAttachConfig := swarm.NetworkAttachmentConfig{
|
||||
Target: target,
|
||||
@ -244,7 +245,9 @@ func convertServiceNetworks(
|
||||
nets = append(nets, netAttachConfig)
|
||||
}
|
||||
|
||||
sort.Sort(byNetworkTarget(nets))
|
||||
sort.Slice(nets, func(i, j int) bool {
|
||||
return nets[i].Target < nets[j].Target
|
||||
})
|
||||
return nets, nil
|
||||
}
|
||||
|
||||
@ -252,47 +255,28 @@ func convertServiceNetworks(
|
||||
func convertServiceSecrets(
|
||||
client *client.Client,
|
||||
namespace Namespace,
|
||||
secrets []ServiceSecretConfig,
|
||||
secretSpecs map[string]SecretConfig,
|
||||
secrets []composetypes.ServiceSecretConfig,
|
||||
secretSpecs map[string]composetypes.SecretConfig,
|
||||
) ([]*swarm.SecretReference, error) {
|
||||
refs := []*swarm.SecretReference{}
|
||||
for _, secret := range secrets {
|
||||
target := secret.Target
|
||||
if target == "" {
|
||||
target = secret.Source
|
||||
}
|
||||
|
||||
secretSpec, exists := secretSpecs[secret.Source]
|
||||
lookup := func(key string) (composetypes.FileObjectConfig, error) {
|
||||
secretSpec, exists := secretSpecs[key]
|
||||
if !exists {
|
||||
return nil, errors.Errorf("undefined secret %q", secret.Source)
|
||||
}
|
||||
|
||||
source := namespace.Scope(secret.Source)
|
||||
if secretSpec.External.External {
|
||||
source = secretSpec.External.Name
|
||||
}
|
||||
|
||||
uid := secret.UID
|
||||
gid := secret.GID
|
||||
if uid == "" {
|
||||
uid = "0"
|
||||
}
|
||||
if gid == "" {
|
||||
gid = "0"
|
||||
}
|
||||
mode := secret.Mode
|
||||
if mode == nil {
|
||||
mode = uint32Ptr(0444)
|
||||
return composetypes.FileObjectConfig{}, errors.Errorf("undefined secret %q", key)
|
||||
}
|
||||
return composetypes.FileObjectConfig(secretSpec), nil
|
||||
}
|
||||
for _, secret := range secrets {
|
||||
obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(secret), lookup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file := swarm.SecretReferenceFileTarget(obj.File)
|
||||
refs = append(refs, &swarm.SecretReference{
|
||||
File: &swarm.SecretReferenceFileTarget{
|
||||
Name: target,
|
||||
UID: uid,
|
||||
GID: gid,
|
||||
Mode: os.FileMode(*mode),
|
||||
},
|
||||
SecretName: source,
|
||||
File: &file,
|
||||
SecretName: obj.Name,
|
||||
})
|
||||
}
|
||||
|
||||
@ -309,50 +293,63 @@ func convertServiceSecrets(
|
||||
func convertServiceConfigObjs(
|
||||
client *client.Client,
|
||||
namespace Namespace,
|
||||
configs []ServiceConfigObjConfig,
|
||||
configSpecs map[string]ConfigObjConfig,
|
||||
service composetypes.ServiceConfig,
|
||||
configSpecs map[string]composetypes.ConfigObjConfig,
|
||||
) ([]*swarm.ConfigReference, error) {
|
||||
refs := []*swarm.ConfigReference{}
|
||||
for _, config := range configs {
|
||||
target := config.Target
|
||||
if target == "" {
|
||||
target = config.Source
|
||||
}
|
||||
|
||||
configSpec, exists := configSpecs[config.Source]
|
||||
lookup := func(key string) (composetypes.FileObjectConfig, error) {
|
||||
configSpec, exists := configSpecs[key]
|
||||
if !exists {
|
||||
return nil, errors.Errorf("undefined config %q", config.Source)
|
||||
}
|
||||
|
||||
source := namespace.Scope(config.Source)
|
||||
if configSpec.External.External {
|
||||
source = configSpec.External.Name
|
||||
}
|
||||
|
||||
uid := config.UID
|
||||
gid := config.GID
|
||||
if uid == "" {
|
||||
uid = "0"
|
||||
}
|
||||
if gid == "" {
|
||||
gid = "0"
|
||||
}
|
||||
mode := config.Mode
|
||||
if mode == nil {
|
||||
mode = uint32Ptr(0444)
|
||||
return composetypes.FileObjectConfig{}, errors.Errorf("undefined config %q", key)
|
||||
}
|
||||
return composetypes.FileObjectConfig(configSpec), nil
|
||||
}
|
||||
for _, config := range service.Configs {
|
||||
obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(config), lookup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file := swarm.ConfigReferenceFileTarget(obj.File)
|
||||
refs = append(refs, &swarm.ConfigReference{
|
||||
File: &swarm.ConfigReferenceFileTarget{
|
||||
Name: target,
|
||||
UID: uid,
|
||||
GID: gid,
|
||||
Mode: os.FileMode(*mode),
|
||||
},
|
||||
ConfigName: source,
|
||||
File: &file,
|
||||
ConfigName: obj.Name,
|
||||
})
|
||||
}
|
||||
|
||||
// finally, after converting all of the file objects, create any
|
||||
// Runtime-type configs that are needed. these are configs that are not
|
||||
// mounted into the container, but are used in some other way by the
|
||||
// container runtime. Currently, this only means CredentialSpecs, but in
|
||||
// the future it may be used for other fields
|
||||
|
||||
// grab the CredentialSpec out of the Service
|
||||
credSpec := service.CredentialSpec
|
||||
// if the credSpec uses a config, then we should grab the config name, and
|
||||
// create a config reference for it. A File or Registry-type CredentialSpec
|
||||
// does not need this operation.
|
||||
if credSpec.Config != "" {
|
||||
// look up the config in the configSpecs.
|
||||
obj, err := lookup(credSpec.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get the actual correct name.
|
||||
name := namespace.Scope(credSpec.Config)
|
||||
if obj.Name != "" {
|
||||
name = obj.Name
|
||||
}
|
||||
|
||||
// now append a Runtime-type config.
|
||||
refs = append(refs, &swarm.ConfigReference{
|
||||
ConfigName: name,
|
||||
Runtime: &swarm.ConfigReferenceRuntimeTarget{},
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
confs, err := ParseConfigs(client, refs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -362,24 +359,86 @@ func convertServiceConfigObjs(
|
||||
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 {
|
||||
return &value
|
||||
}
|
||||
|
||||
func convertExtraHosts(extraHosts map[string]string) []string {
|
||||
// convertExtraHosts converts <host>:<ip> mappings to SwarmKit notation:
|
||||
// "IP-address hostname(s)". The original order of mappings is preserved.
|
||||
func convertExtraHosts(extraHosts composetypes.HostsList) []string {
|
||||
hosts := []string{}
|
||||
for host, ip := range extraHosts {
|
||||
hosts = append(hosts, fmt.Sprintf("%s %s", ip, host))
|
||||
for _, hostIP := range extraHosts {
|
||||
if v := strings.SplitN(hostIP, ":", 2); len(v) == 2 {
|
||||
// Convert to SwarmKit notation: IP-address hostname(s)
|
||||
hosts = append(hosts, fmt.Sprintf("%s %s", v[1], v[0]))
|
||||
}
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
|
||||
func convertHealthcheck(healthcheck *HealthCheckConfig) (*container.HealthConfig, error) {
|
||||
func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container.HealthConfig, error) {
|
||||
if healthcheck == nil {
|
||||
return nil, nil
|
||||
}
|
||||
var (
|
||||
timeout, interval, startPeriod time.Duration
|
||||
timeout, interval, startPeriod composetypes.Duration
|
||||
retries int
|
||||
)
|
||||
if healthcheck.Disable {
|
||||
@ -405,14 +464,14 @@ func convertHealthcheck(healthcheck *HealthCheckConfig) (*container.HealthConfig
|
||||
}
|
||||
return &container.HealthConfig{
|
||||
Test: healthcheck.Test,
|
||||
Timeout: timeout,
|
||||
Interval: interval,
|
||||
Timeout: time.Duration(timeout),
|
||||
Interval: time.Duration(interval),
|
||||
Retries: retries,
|
||||
StartPeriod: startPeriod,
|
||||
StartPeriod: time.Duration(startPeriod),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func convertRestartPolicy(restart string, source *RestartPolicy) (*swarm.RestartPolicy, error) {
|
||||
func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) {
|
||||
// TODO: log if restart is being ignored
|
||||
if source == nil {
|
||||
policy, err := ParseRestartPolicy(restart)
|
||||
@ -438,13 +497,13 @@ func convertRestartPolicy(restart string, source *RestartPolicy) (*swarm.Restart
|
||||
}
|
||||
return &swarm.RestartPolicy{
|
||||
Condition: swarm.RestartPolicyCondition(source.Condition),
|
||||
Delay: source.Delay,
|
||||
Delay: composetypes.ConvertDurationPtr(source.Delay),
|
||||
MaxAttempts: source.MaxAttempts,
|
||||
Window: source.Window,
|
||||
Window: composetypes.ConvertDurationPtr(source.Window),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func convertUpdateConfig(source *UpdateConfig) *swarm.UpdateConfig {
|
||||
func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig {
|
||||
if source == nil {
|
||||
return nil
|
||||
}
|
||||
@ -454,15 +513,15 @@ func convertUpdateConfig(source *UpdateConfig) *swarm.UpdateConfig {
|
||||
}
|
||||
return &swarm.UpdateConfig{
|
||||
Parallelism: parallel,
|
||||
Delay: source.Delay,
|
||||
Delay: time.Duration(source.Delay),
|
||||
FailureAction: source.FailureAction,
|
||||
Monitor: source.Monitor,
|
||||
Monitor: time.Duration(source.Monitor),
|
||||
MaxFailureRatio: source.MaxFailureRatio,
|
||||
Order: source.Order,
|
||||
}
|
||||
}
|
||||
|
||||
func convertResources(source Resources) (*swarm.ResourceRequirements, error) {
|
||||
func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) {
|
||||
resources := &swarm.ResourceRequirements{}
|
||||
var err error
|
||||
if source.Limits != nil {
|
||||
@ -486,21 +545,31 @@ func convertResources(source Resources) (*swarm.ResourceRequirements, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var generic []swarm.GenericResource
|
||||
for _, res := range source.Reservations.GenericResources {
|
||||
var r swarm.GenericResource
|
||||
|
||||
if res.DiscreteResourceSpec != nil {
|
||||
r.DiscreteResourceSpec = &swarm.DiscreteGenericResource{
|
||||
Kind: res.DiscreteResourceSpec.Kind,
|
||||
Value: res.DiscreteResourceSpec.Value,
|
||||
}
|
||||
}
|
||||
|
||||
generic = append(generic, r)
|
||||
}
|
||||
|
||||
resources.Reservations = &swarm.Resources{
|
||||
NanoCPUs: cpus,
|
||||
MemoryBytes: int64(source.Reservations.MemoryBytes),
|
||||
NanoCPUs: cpus,
|
||||
MemoryBytes: int64(source.Reservations.MemoryBytes),
|
||||
GenericResources: generic,
|
||||
}
|
||||
}
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
type byPublishedPort []swarm.PortConfig
|
||||
|
||||
func (a byPublishedPort) Len() int { return len(a) }
|
||||
func (a byPublishedPort) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a byPublishedPort) Less(i, j int) bool { return a[i].PublishedPort < a[j].PublishedPort }
|
||||
|
||||
func convertEndpointSpec(endpointMode string, source []ServicePortConfig) (*swarm.EndpointSpec, error) {
|
||||
func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortConfig) (*swarm.EndpointSpec, error) {
|
||||
portConfigs := []swarm.PortConfig{}
|
||||
for _, port := range source {
|
||||
portConfig := swarm.PortConfig{
|
||||
@ -512,7 +581,10 @@ func convertEndpointSpec(endpointMode string, source []ServicePortConfig) (*swar
|
||||
portConfigs = append(portConfigs, portConfig)
|
||||
}
|
||||
|
||||
sort.Sort(byPublishedPort(portConfigs))
|
||||
sort.Slice(portConfigs, func(i, j int) bool {
|
||||
return portConfigs[i].PublishedPort < portConfigs[j].PublishedPort
|
||||
})
|
||||
|
||||
return &swarm.EndpointSpec{
|
||||
Mode: swarm.ResolutionMode(strings.ToLower(endpointMode)),
|
||||
Ports: portConfigs,
|
||||
@ -561,7 +633,7 @@ func convertDNSConfig(DNS []string, DNSSearch []string) (*swarm.DNSConfig, error
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func convertCredentialSpec(namespace Namespace, spec CredentialSpecConfig, refs []*swarm.ConfigReference) (*swarm.CredentialSpec, error) {
|
||||
func convertCredentialSpec(namespace Namespace, spec composetypes.CredentialSpecConfig, refs []*swarm.ConfigReference) (*swarm.CredentialSpec, error) {
|
||||
var o []string
|
||||
|
||||
// Config was added in API v1.40
|
||||
|
@ -1,16 +1,15 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
composetypes "github.com/cuigh/swirl/biz/docker/compose/types"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type volumes map[string]VolumeConfig
|
||||
type volumes map[string]composetypes.VolumeConfig
|
||||
|
||||
// Volumes from compose-file types to engine api types
|
||||
func Volumes(serviceVolumes []ServiceVolumeConfig, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) {
|
||||
func Volumes(serviceVolumes []composetypes.ServiceVolumeConfig, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) {
|
||||
var mounts []mount.Mount
|
||||
|
||||
for _, volumeConfig := range serviceVolumes {
|
||||
@ -23,43 +22,37 @@ func Volumes(serviceVolumes []ServiceVolumeConfig, stackVolumes volumes, namespa
|
||||
return mounts, nil
|
||||
}
|
||||
|
||||
func convertVolumeToMount(
|
||||
volume ServiceVolumeConfig,
|
||||
func createMountFromVolume(volume composetypes.ServiceVolumeConfig) mount.Mount {
|
||||
return mount.Mount{
|
||||
Type: mount.Type(volume.Type),
|
||||
Target: volume.Target,
|
||||
ReadOnly: volume.ReadOnly,
|
||||
Source: volume.Source,
|
||||
Consistency: mount.Consistency(volume.Consistency),
|
||||
}
|
||||
}
|
||||
|
||||
func handleVolumeToMount(
|
||||
volume composetypes.ServiceVolumeConfig,
|
||||
stackVolumes volumes,
|
||||
namespace Namespace,
|
||||
) (mount.Mount, error) {
|
||||
result := mount.Mount{
|
||||
Type: mount.Type(volume.Type),
|
||||
Source: volume.Source,
|
||||
Target: volume.Target,
|
||||
ReadOnly: volume.ReadOnly,
|
||||
Consistency: mount.Consistency(volume.Consistency),
|
||||
}
|
||||
result := createMountFromVolume(volume)
|
||||
|
||||
if volume.Tmpfs != nil {
|
||||
return mount.Mount{}, errors.New("tmpfs options are incompatible with type volume")
|
||||
}
|
||||
if volume.Bind != nil {
|
||||
return mount.Mount{}, errors.New("bind options are incompatible with type volume")
|
||||
}
|
||||
// Anonymous volumes
|
||||
if volume.Source == "" {
|
||||
return result, nil
|
||||
}
|
||||
if volume.Type == "volume" && volume.Bind != nil {
|
||||
return result, errors.New("bind options are incompatible with type volume")
|
||||
}
|
||||
if volume.Type == "bind" && volume.Volume != nil {
|
||||
return result, errors.New("volume options are incompatible with type bind")
|
||||
}
|
||||
|
||||
if volume.Bind != nil {
|
||||
result.BindOptions = &mount.BindOptions{
|
||||
Propagation: mount.Propagation(volume.Bind.Propagation),
|
||||
}
|
||||
}
|
||||
// Binds volumes
|
||||
if volume.Type == "bind" {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
stackVolume, exists := stackVolumes[volume.Source]
|
||||
if !exists {
|
||||
return result, fmt.Errorf("undefined volume %q", volume.Source)
|
||||
return mount.Mount{}, errors.Errorf("undefined volume %q", volume.Source)
|
||||
}
|
||||
|
||||
result.Source = namespace.Scope(volume.Source)
|
||||
@ -69,16 +62,15 @@ func convertVolumeToMount(
|
||||
result.VolumeOptions.NoCopy = volume.Volume.NoCopy
|
||||
}
|
||||
|
||||
// External named volumes
|
||||
if stackVolume.External.External {
|
||||
result.Source = stackVolume.External.Name
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if stackVolume.Name != "" {
|
||||
result.Source = stackVolume.Name
|
||||
}
|
||||
|
||||
// External named volumes
|
||||
if stackVolume.External.External {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
result.VolumeOptions.Labels = AddStackLabel(namespace, stackVolume.Labels)
|
||||
if stackVolume.Driver != "" || stackVolume.DriverOpts != nil {
|
||||
result.VolumeOptions.DriverConfig = &mount.Driver{
|
||||
@ -87,6 +79,84 @@ func convertVolumeToMount(
|
||||
}
|
||||
}
|
||||
|
||||
// Named volumes
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func handleBindToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
|
||||
result := createMountFromVolume(volume)
|
||||
|
||||
if volume.Source == "" {
|
||||
return mount.Mount{}, errors.New("invalid bind source, source cannot be empty")
|
||||
}
|
||||
if volume.Volume != nil {
|
||||
return mount.Mount{}, errors.New("volume options are incompatible with type bind")
|
||||
}
|
||||
if volume.Tmpfs != nil {
|
||||
return mount.Mount{}, errors.New("tmpfs options are incompatible with type bind")
|
||||
}
|
||||
if volume.Bind != nil {
|
||||
result.BindOptions = &mount.BindOptions{
|
||||
Propagation: mount.Propagation(volume.Bind.Propagation),
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func handleTmpfsToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
|
||||
result := createMountFromVolume(volume)
|
||||
|
||||
if volume.Source != "" {
|
||||
return mount.Mount{}, errors.New("invalid tmpfs source, source must be empty")
|
||||
}
|
||||
if volume.Bind != nil {
|
||||
return mount.Mount{}, errors.New("bind options are incompatible with type tmpfs")
|
||||
}
|
||||
if volume.Volume != nil {
|
||||
return mount.Mount{}, errors.New("volume options are incompatible with type tmpfs")
|
||||
}
|
||||
if volume.Tmpfs != nil {
|
||||
result.TmpfsOptions = &mount.TmpfsOptions{
|
||||
SizeBytes: volume.Tmpfs.Size,
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func handleNpipeToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
|
||||
result := createMountFromVolume(volume)
|
||||
|
||||
if volume.Source == "" {
|
||||
return mount.Mount{}, errors.New("invalid npipe source, source cannot be empty")
|
||||
}
|
||||
if volume.Volume != nil {
|
||||
return mount.Mount{}, errors.New("volume options are incompatible with type npipe")
|
||||
}
|
||||
if volume.Tmpfs != nil {
|
||||
return mount.Mount{}, errors.New("tmpfs options are incompatible with type npipe")
|
||||
}
|
||||
if volume.Bind != nil {
|
||||
result.BindOptions = &mount.BindOptions{
|
||||
Propagation: mount.Propagation(volume.Bind.Propagation),
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func convertVolumeToMount(
|
||||
volume composetypes.ServiceVolumeConfig,
|
||||
stackVolumes volumes,
|
||||
namespace Namespace,
|
||||
) (mount.Mount, error) {
|
||||
|
||||
switch volume.Type {
|
||||
case "volume", "":
|
||||
return handleVolumeToMount(volume, stackVolumes, namespace)
|
||||
case "bind":
|
||||
return handleBindToMount(volume)
|
||||
case "tmpfs":
|
||||
return handleTmpfsToMount(volume)
|
||||
case "npipe":
|
||||
return handleNpipeToMount(volume)
|
||||
}
|
||||
return mount.Mount{}, errors.New("volume type must be volume, bind, tmpfs or npipe")
|
||||
}
|
||||
|
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
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cuigh/auxo/log"
|
||||
interp "github.com/cuigh/swirl/biz/docker/compose/interpolation"
|
||||
"github.com/cuigh/swirl/biz/docker/compose/schema"
|
||||
"github.com/cuigh/swirl/biz/docker/compose/template"
|
||||
composetypes "github.com/cuigh/swirl/biz/docker/compose/types"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/go-connections/nat"
|
||||
units "github.com/docker/go-units"
|
||||
shellwords "github.com/mattn/go-shellwords"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/mattn/go-shellwords"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Options supported by Load
|
||||
type Options struct {
|
||||
// Skip schema validation
|
||||
SkipValidation bool
|
||||
// Skip interpolation
|
||||
SkipInterpolation bool
|
||||
// Interpolation options
|
||||
Interpolate *interp.Options
|
||||
}
|
||||
|
||||
// ParseYAML reads the bytes from a file, parses the bytes into a mapping
|
||||
// structure, and returns it.
|
||||
func ParseYAML(source []byte) (map[string]interface{}, error) {
|
||||
@ -36,90 +52,152 @@ func ParseYAML(source []byte) (map[string]interface{}, error) {
|
||||
}
|
||||
|
||||
// Load reads a ConfigDetails and returns a fully loaded configuration
|
||||
func Load(configDetails ConfigDetails) (*Config, error) {
|
||||
func Load(configDetails composetypes.ConfigDetails, options ...func(*Options)) (*composetypes.Config, error) {
|
||||
if len(configDetails.ConfigFiles) < 1 {
|
||||
return nil, errors.New("No files specified")
|
||||
}
|
||||
if len(configDetails.ConfigFiles) > 1 {
|
||||
return nil, errors.New("Multiple files are not yet supported")
|
||||
return nil, errors.Errorf("No files specified")
|
||||
}
|
||||
|
||||
configDict := getConfigDict(configDetails)
|
||||
opts := &Options{
|
||||
Interpolate: &interp.Options{
|
||||
Substitute: template.Substitute,
|
||||
LookupValue: configDetails.LookupEnv,
|
||||
TypeCastMapping: interpolateTypeCastMapping,
|
||||
},
|
||||
}
|
||||
|
||||
if services, ok := configDict["services"]; ok {
|
||||
if servicesDict, ok := services.(map[string]interface{}); ok {
|
||||
forbidden := getProperties(servicesDict, ForbiddenProperties)
|
||||
for _, op := range options {
|
||||
op(opts)
|
||||
}
|
||||
|
||||
if len(forbidden) > 0 {
|
||||
return nil, &ForbiddenPropertiesError{Properties: forbidden}
|
||||
configs := []*composetypes.Config{}
|
||||
var err error
|
||||
|
||||
for _, file := range configDetails.ConfigFiles {
|
||||
configDict := file.Config
|
||||
version := schema.Version(configDict)
|
||||
if configDetails.Version == "" {
|
||||
configDetails.Version = version
|
||||
}
|
||||
if configDetails.Version != version {
|
||||
return nil, errors.Errorf("version mismatched between two composefiles : %v and %v", configDetails.Version, version)
|
||||
}
|
||||
|
||||
if err := validateForbidden(configDict); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !opts.SkipInterpolation {
|
||||
configDict, err = interpolateConfig(configDict, *opts.Interpolate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// todo: Add validation
|
||||
//if err := schema.Validate(configDict, schema.Version(configDict)); err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
//if !opts.SkipValidation {
|
||||
// if err := schema.Validate(configDict, configDetails.Version); err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
//}
|
||||
|
||||
cfg := Config{}
|
||||
|
||||
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)
|
||||
cfg, err := loadSections(configDict, configDetails)
|
||||
if err != nil {
|
||||
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
|
||||
// used in the Compose files.
|
||||
func GetUnsupportedProperties(configDetails ConfigDetails) []string {
|
||||
func GetUnsupportedProperties(configDicts ...map[string]interface{}) []string {
|
||||
unsupported := map[string]bool{}
|
||||
|
||||
for _, service := range getServices(getConfigDict(configDetails)) {
|
||||
serviceDict := service.(map[string]interface{})
|
||||
for _, property := range UnsupportedProperties {
|
||||
if _, isSet := serviceDict[property]; isSet {
|
||||
unsupported[property] = true
|
||||
for _, configDict := range configDicts {
|
||||
for _, service := range getServices(configDict) {
|
||||
serviceDict := service.(map[string]interface{})
|
||||
for _, property := range composetypes.UnsupportedProperties {
|
||||
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
|
||||
// are used in the compose files.
|
||||
func GetDeprecatedProperties(configDetails ConfigDetails) map[string]string {
|
||||
return getProperties(getServices(getConfigDict(configDetails)), DeprecatedProperties)
|
||||
func GetDeprecatedProperties(configDicts ...map[string]interface{}) map[string]string {
|
||||
deprecated := map[string]string{}
|
||||
|
||||
for _, configDict := range configDicts {
|
||||
deprecatedProperties := getProperties(getServices(configDict), composetypes.DeprecatedProperties)
|
||||
for key, value := range deprecatedProperties {
|
||||
deprecated[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return deprecated
|
||||
}
|
||||
|
||||
func getProperties(services map[string]interface{}, propertyMap map[string]string) map[string]string {
|
||||
@ -168,11 +255,6 @@ func (e *ForbiddenPropertiesError) Error() string {
|
||||
return "Configuration contains forbidden properties"
|
||||
}
|
||||
|
||||
// TODO: resolve multiple files into a single config
|
||||
func getConfigDict(configDetails ConfigDetails) map[string]interface{} {
|
||||
return configDetails.ConfigFiles[0].Config
|
||||
}
|
||||
|
||||
func getServices(configDict map[string]interface{}) map[string]interface{} {
|
||||
if services, ok := configDict["services"]; ok {
|
||||
if servicesDict, ok := services.(map[string]interface{}); ok {
|
||||
@ -183,11 +265,13 @@ func getServices(configDict map[string]interface{}) map[string]interface{} {
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
|
||||
func transform(source map[string]interface{}, target interface{}) error {
|
||||
// Transform converts the source into the target struct with compose types transformer
|
||||
// and the specified transformers if any.
|
||||
func Transform(source interface{}, target interface{}, additionalTransformers ...Transformer) error {
|
||||
data := mapstructure.Metadata{}
|
||||
config := &mapstructure.DecoderConfig{
|
||||
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
||||
createTransformHook(),
|
||||
createTransformHook(additionalTransformers...),
|
||||
mapstructure.StringToTimeDurationHookFunc()),
|
||||
Result: target,
|
||||
Metadata: &data,
|
||||
@ -199,25 +283,38 @@ func transform(source map[string]interface{}, target interface{}) error {
|
||||
return decoder.Decode(source)
|
||||
}
|
||||
|
||||
func createTransformHook() mapstructure.DecodeHookFuncType {
|
||||
// Transformer defines a map to type transformer
|
||||
type Transformer struct {
|
||||
TypeOf reflect.Type
|
||||
Func func(interface{}) (interface{}, error)
|
||||
}
|
||||
|
||||
func createTransformHook(additionalTransformers ...Transformer) mapstructure.DecodeHookFuncType {
|
||||
transforms := map[reflect.Type]func(interface{}) (interface{}, error){
|
||||
reflect.TypeOf(External{}): transformExternal,
|
||||
reflect.TypeOf(HealthCheckTest{}): transformHealthCheckTest,
|
||||
reflect.TypeOf(ShellCommand{}): transformShellCommand,
|
||||
reflect.TypeOf(StringList{}): transformStringList,
|
||||
reflect.TypeOf(map[string]string{}): transformMapStringString,
|
||||
reflect.TypeOf(UlimitsConfig{}): transformUlimits,
|
||||
reflect.TypeOf(UnitBytes(0)): transformSize,
|
||||
reflect.TypeOf([]ServicePortConfig{}): transformServicePort,
|
||||
reflect.TypeOf(ServiceSecretConfig{}): transformStringSourceMap,
|
||||
reflect.TypeOf(ServiceConfigObjConfig{}): transformStringSourceMap,
|
||||
reflect.TypeOf(StringOrNumberList{}): transformStringOrNumberList,
|
||||
reflect.TypeOf(map[string]*ServiceNetworkConfig{}): transformServiceNetworkMap,
|
||||
reflect.TypeOf(MappingWithEquals{}): transformMappingOrListFunc("=", true),
|
||||
reflect.TypeOf(Labels{}): transformMappingOrListFunc("=", false),
|
||||
reflect.TypeOf(MappingWithColon{}): transformMappingOrListFunc(":", false),
|
||||
reflect.TypeOf(ServiceVolumeConfig{}): transformServiceVolumeConfig,
|
||||
reflect.TypeOf(BuildConfig{}): transformBuildConfig,
|
||||
reflect.TypeOf(composetypes.External{}): transformExternal,
|
||||
reflect.TypeOf(composetypes.HealthCheckTest{}): transformHealthCheckTest,
|
||||
reflect.TypeOf(composetypes.ShellCommand{}): transformShellCommand,
|
||||
reflect.TypeOf(composetypes.StringList{}): transformStringList,
|
||||
reflect.TypeOf(map[string]string{}): transformMapStringString,
|
||||
reflect.TypeOf(composetypes.UlimitsConfig{}): transformUlimits,
|
||||
reflect.TypeOf(composetypes.UnitBytes(0)): transformSize,
|
||||
reflect.TypeOf([]composetypes.ServicePortConfig{}): transformServicePort,
|
||||
reflect.TypeOf(composetypes.ServiceSecretConfig{}): transformStringSourceMap,
|
||||
reflect.TypeOf(composetypes.ServiceConfigObjConfig{}): transformStringSourceMap,
|
||||
reflect.TypeOf(composetypes.StringOrNumberList{}): transformStringOrNumberList,
|
||||
reflect.TypeOf(map[string]*composetypes.ServiceNetworkConfig{}): transformServiceNetworkMap,
|
||||
reflect.TypeOf(composetypes.Mapping{}): transformMappingOrListFunc("=", false),
|
||||
reflect.TypeOf(composetypes.MappingWithEquals{}): transformMappingOrListFunc("=", true),
|
||||
reflect.TypeOf(composetypes.Labels{}): transformMappingOrListFunc("=", false),
|
||||
reflect.TypeOf(composetypes.MappingWithColon{}): transformMappingOrListFunc(":", false),
|
||||
reflect.TypeOf(composetypes.HostsList{}): transformListOrMappingFunc(":", false),
|
||||
reflect.TypeOf(composetypes.ServiceVolumeConfig{}): transformServiceVolumeConfig,
|
||||
reflect.TypeOf(composetypes.BuildConfig{}): transformBuildConfig,
|
||||
reflect.TypeOf(composetypes.Duration(0)): transformStringToDuration,
|
||||
}
|
||||
|
||||
for _, transformer := range additionalTransformers {
|
||||
transforms[transformer.TypeOf] = transformer.Func
|
||||
}
|
||||
|
||||
return func(_ reflect.Type, target reflect.Type, data interface{}) (interface{}, error) {
|
||||
@ -279,8 +376,8 @@ func formatInvalidKeyError(keyPrefix string, key interface{}) error {
|
||||
|
||||
// LoadServices produces a ServiceConfig map from a compose file Dict
|
||||
// the servicesDict is not validated if directly used. Use Load() to enable validation
|
||||
func LoadServices(servicesDict map[string]interface{}, workingDir string, lookupEnv Mapping) ([]ServiceConfig, error) {
|
||||
var services []ServiceConfig
|
||||
func LoadServices(servicesDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) ([]composetypes.ServiceConfig, error) {
|
||||
var services []composetypes.ServiceConfig
|
||||
|
||||
for name, serviceDef := range servicesDict {
|
||||
serviceConfig, err := LoadService(name, serviceDef.(map[string]interface{}), workingDir, lookupEnv)
|
||||
@ -295,9 +392,9 @@ func LoadServices(servicesDict map[string]interface{}, workingDir string, lookup
|
||||
|
||||
// LoadService produces a single ServiceConfig from a compose file Dict
|
||||
// the serviceDict is not validated if directly used. Use Load() to enable validation
|
||||
func LoadService(name string, serviceDict map[string]interface{}, workingDir string, lookupEnv Mapping) (*ServiceConfig, error) {
|
||||
serviceConfig := &ServiceConfig{}
|
||||
if err := transform(serviceDict, serviceConfig); err != nil {
|
||||
func LoadService(name string, serviceDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) (*composetypes.ServiceConfig, error) {
|
||||
serviceConfig := &composetypes.ServiceConfig{}
|
||||
if err := Transform(serviceDict, serviceConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serviceConfig.Name = name
|
||||
@ -306,11 +403,36 @@ func LoadService(name string, serviceDict map[string]interface{}, workingDir str
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resolveVolumePaths(serviceConfig.Volumes, workingDir, lookupEnv)
|
||||
if err := resolveVolumePaths(serviceConfig.Volumes, workingDir, lookupEnv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serviceConfig.Extras = getExtras(serviceDict)
|
||||
|
||||
return serviceConfig, nil
|
||||
}
|
||||
|
||||
func updateEnvironment(environment map[string]*string, vars map[string]*string, lookupEnv Mapping) {
|
||||
func loadExtras(name string, source map[string]interface{}) map[string]interface{} {
|
||||
if dict, ok := source[name].(map[string]interface{}); ok {
|
||||
return getExtras(dict)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getExtras(dict map[string]interface{}) map[string]interface{} {
|
||||
extras := map[string]interface{}{}
|
||||
for key, value := range dict {
|
||||
if strings.HasPrefix(key, "x-") {
|
||||
extras[key] = value
|
||||
}
|
||||
}
|
||||
if len(extras) == 0 {
|
||||
return nil
|
||||
}
|
||||
return extras
|
||||
}
|
||||
|
||||
func updateEnvironment(environment map[string]*string, vars map[string]*string, lookupEnv template.Mapping) {
|
||||
for k, v := range vars {
|
||||
interpolatedV, ok := lookupEnv(k)
|
||||
if (v == nil || *v == "") && ok {
|
||||
@ -322,7 +444,7 @@ func updateEnvironment(environment map[string]*string, vars map[string]*string,
|
||||
}
|
||||
}
|
||||
|
||||
func resolveEnvironment(serviceConfig *ServiceConfig, workingDir string, lookupEnv Mapping) error {
|
||||
func resolveEnvironment(serviceConfig *composetypes.ServiceConfig, workingDir string, lookupEnv template.Mapping) error {
|
||||
environment := make(map[string]*string)
|
||||
|
||||
if len(serviceConfig.EnvFile) > 0 {
|
||||
@ -345,12 +467,16 @@ func resolveEnvironment(serviceConfig *ServiceConfig, workingDir string, lookupE
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveVolumePaths(volumes []ServiceVolumeConfig, workingDir string, lookupEnv Mapping) {
|
||||
func resolveVolumePaths(volumes []composetypes.ServiceVolumeConfig, workingDir string, lookupEnv template.Mapping) error {
|
||||
for i, volume := range volumes {
|
||||
if volume.Type != "bind" {
|
||||
continue
|
||||
}
|
||||
|
||||
if volume.Source == "" {
|
||||
return errors.New(`invalid mount config for type "bind": field Source must not be empty`)
|
||||
}
|
||||
|
||||
filePath := expandUser(volume.Source, lookupEnv)
|
||||
// Check for a Unix absolute path first, to handle a Windows client
|
||||
// with a Unix daemon. This handles a Windows client connecting to a
|
||||
@ -363,10 +489,12 @@ func resolveVolumePaths(volumes []ServiceVolumeConfig, workingDir string, lookup
|
||||
volume.Source = filePath
|
||||
volumes[i] = volume
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// TODO: make this more robust
|
||||
func expandUser(path string, lookupEnv Mapping) string {
|
||||
func expandUser(path string, lookupEnv template.Mapping) string {
|
||||
if strings.HasPrefix(path, "~") {
|
||||
home, ok := lookupEnv("HOME")
|
||||
if !ok {
|
||||
@ -381,9 +509,9 @@ func expandUser(path string, lookupEnv Mapping) string {
|
||||
func transformUlimits(data interface{}) (interface{}, error) {
|
||||
switch value := data.(type) {
|
||||
case int:
|
||||
return UlimitsConfig{Single: value}, nil
|
||||
return composetypes.UlimitsConfig{Single: value}, nil
|
||||
case map[string]interface{}:
|
||||
ulimit := UlimitsConfig{}
|
||||
ulimit := composetypes.UlimitsConfig{}
|
||||
ulimit.Soft = value["soft"].(int)
|
||||
ulimit.Hard = value["hard"].(int)
|
||||
return ulimit, nil
|
||||
@ -394,17 +522,31 @@ func transformUlimits(data interface{}) (interface{}, error) {
|
||||
|
||||
// LoadNetworks produces a NetworkConfig map from a compose file Dict
|
||||
// the source Dict is not validated if directly used. Use Load() to enable validation
|
||||
func LoadNetworks(source map[string]interface{}) (map[string]NetworkConfig, error) {
|
||||
networks := make(map[string]NetworkConfig)
|
||||
err := transform(source, &networks)
|
||||
func LoadNetworks(source map[string]interface{}, version string) (map[string]composetypes.NetworkConfig, error) {
|
||||
networks := make(map[string]composetypes.NetworkConfig)
|
||||
err := Transform(source, &networks)
|
||||
if err != nil {
|
||||
return networks, err
|
||||
}
|
||||
for name, network := range networks {
|
||||
if network.External.External && network.External.Name == "" {
|
||||
network.External.Name = name
|
||||
networks[name] = network
|
||||
if !network.External.External {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case network.External.Name != "":
|
||||
if network.Name != "" {
|
||||
return nil, errors.Errorf("network %s: network.external.name and network.name conflict; only use network.name", name)
|
||||
}
|
||||
if versions.GreaterThanOrEqualTo(version, "3.5") {
|
||||
log.Get("compose").Warnf("network %s: network.external.name is deprecated in favor of network.name", name)
|
||||
}
|
||||
network.Name = network.External.Name
|
||||
network.External.Name = ""
|
||||
case network.Name == "":
|
||||
network.Name = name
|
||||
}
|
||||
network.Extras = loadExtras(name, source)
|
||||
networks[name] = network
|
||||
}
|
||||
return networks, nil
|
||||
}
|
||||
@ -417,76 +559,110 @@ func externalVolumeError(volume, key string) error {
|
||||
|
||||
// LoadVolumes produces a VolumeConfig map from a compose file Dict
|
||||
// the source Dict is not validated if directly used. Use Load() to enable validation
|
||||
func LoadVolumes(source map[string]interface{}) (map[string]VolumeConfig, error) {
|
||||
volumes := make(map[string]VolumeConfig)
|
||||
err := transform(source, &volumes)
|
||||
if err != nil {
|
||||
func LoadVolumes(source map[string]interface{}, version string) (map[string]composetypes.VolumeConfig, error) {
|
||||
volumes := make(map[string]composetypes.VolumeConfig)
|
||||
if err := Transform(source, &volumes); err != nil {
|
||||
return volumes, err
|
||||
}
|
||||
for name, volume := range volumes {
|
||||
if volume.External.External {
|
||||
if volume.Driver != "" {
|
||||
return nil, externalVolumeError(name, "driver")
|
||||
}
|
||||
if len(volume.DriverOpts) > 0 {
|
||||
return nil, externalVolumeError(name, "driver_opts")
|
||||
}
|
||||
if len(volume.Labels) > 0 {
|
||||
return nil, externalVolumeError(name, "labels")
|
||||
}
|
||||
if volume.External.Name == "" {
|
||||
volume.External.Name = name
|
||||
volumes[name] = volume
|
||||
} else {
|
||||
log.Get("compose").Warnf("volume %s: volume.external.name is deprecated in favor of volume.name", name)
|
||||
|
||||
if volume.Name != "" {
|
||||
return nil, fmt.Errorf("volume %s: volume.external.name and volume.name conflict; only use volume.name", name)
|
||||
}
|
||||
}
|
||||
for name, volume := range volumes {
|
||||
if !volume.External.External {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case volume.Driver != "":
|
||||
return nil, externalVolumeError(name, "driver")
|
||||
case len(volume.DriverOpts) > 0:
|
||||
return nil, externalVolumeError(name, "driver_opts")
|
||||
case len(volume.Labels) > 0:
|
||||
return nil, externalVolumeError(name, "labels")
|
||||
case volume.External.Name != "":
|
||||
if volume.Name != "" {
|
||||
return nil, errors.Errorf("volume %s: volume.external.name and volume.name conflict; only use volume.name", name)
|
||||
}
|
||||
if versions.GreaterThanOrEqualTo(version, "3.4") {
|
||||
log.Get("compose").Warnf("volume %s: volume.external.name is deprecated in favor of volume.name", name)
|
||||
}
|
||||
volume.Name = volume.External.Name
|
||||
volume.External.Name = ""
|
||||
case volume.Name == "":
|
||||
volume.Name = name
|
||||
}
|
||||
volume.Extras = loadExtras(name, source)
|
||||
volumes[name] = volume
|
||||
}
|
||||
return volumes, nil
|
||||
}
|
||||
|
||||
// LoadSecrets produces a SecretConfig map from a compose file Dict
|
||||
// the source Dict is not validated if directly used. Use Load() to enable validation
|
||||
func LoadSecrets(source map[string]interface{}, workingDir string) (map[string]SecretConfig, error) {
|
||||
secrets := make(map[string]SecretConfig)
|
||||
if err := transform(source, &secrets); err != nil {
|
||||
func LoadSecrets(source map[string]interface{}, details composetypes.ConfigDetails) (map[string]composetypes.SecretConfig, error) {
|
||||
secrets := make(map[string]composetypes.SecretConfig)
|
||||
if err := Transform(source, &secrets); err != nil {
|
||||
return secrets, err
|
||||
}
|
||||
for name, secret := range secrets {
|
||||
if secret.External.External && secret.External.Name == "" {
|
||||
secret.External.Name = name
|
||||
secrets[name] = secret
|
||||
}
|
||||
if secret.File != "" {
|
||||
secret.File = absPath(workingDir, secret.File)
|
||||
obj, err := loadFileObjectConfig(name, "secret", composetypes.FileObjectConfig(secret), details)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
secretConfig := composetypes.SecretConfig(obj)
|
||||
secretConfig.Extras = loadExtras(name, source)
|
||||
secrets[name] = secretConfig
|
||||
}
|
||||
return secrets, nil
|
||||
}
|
||||
|
||||
// LoadConfigObjs produces a ConfigObjConfig map from a compose file Dict
|
||||
// the source Dict is not validated if directly used. Use Load() to enable validation
|
||||
func LoadConfigObjs(source map[string]interface{}, workingDir string) (map[string]ConfigObjConfig, error) {
|
||||
configs := make(map[string]ConfigObjConfig)
|
||||
if err := transform(source, &configs); err != nil {
|
||||
func LoadConfigObjs(source map[string]interface{}, details composetypes.ConfigDetails) (map[string]composetypes.ConfigObjConfig, error) {
|
||||
configs := make(map[string]composetypes.ConfigObjConfig)
|
||||
if err := Transform(source, &configs); err != nil {
|
||||
return configs, err
|
||||
}
|
||||
for name, config := range configs {
|
||||
if config.External.External && config.External.Name == "" {
|
||||
config.External.Name = name
|
||||
configs[name] = config
|
||||
}
|
||||
if config.File != "" {
|
||||
config.File = absPath(workingDir, config.File)
|
||||
obj, err := loadFileObjectConfig(name, "config", composetypes.FileObjectConfig(config), details)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configConfig := composetypes.ConfigObjConfig(obj)
|
||||
configConfig.Extras = loadExtras(name, source)
|
||||
configs[name] = configConfig
|
||||
}
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
func loadFileObjectConfig(name string, objType string, obj composetypes.FileObjectConfig, details composetypes.ConfigDetails) (composetypes.FileObjectConfig, error) {
|
||||
// if "external: true"
|
||||
switch {
|
||||
case obj.External.External:
|
||||
// handle deprecated external.name
|
||||
if obj.External.Name != "" {
|
||||
if obj.Name != "" {
|
||||
return obj, errors.Errorf("%[1]s %[2]s: %[1]s.external.name and %[1]s.name conflict; only use %[1]s.name", objType, name)
|
||||
}
|
||||
if versions.GreaterThanOrEqualTo(details.Version, "3.5") {
|
||||
log.Get("compose").Warnf("%[1]s %[2]s: %[1]s.external.name is deprecated in favor of %[1]s.name", objType, name)
|
||||
}
|
||||
obj.Name = obj.External.Name
|
||||
obj.External.Name = ""
|
||||
} else {
|
||||
if obj.Name == "" {
|
||||
obj.Name = name
|
||||
}
|
||||
}
|
||||
// if not "external: true"
|
||||
case obj.Driver != "":
|
||||
if obj.File != "" {
|
||||
return obj, errors.Errorf("%[1]s %[2]s: %[1]s.driver and %[1]s.file conflict; only use %[1]s.driver", objType, name)
|
||||
}
|
||||
default:
|
||||
obj.File = absPath(details.WorkingDir, obj.File)
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func absPath(workingDir string, filePath string) string {
|
||||
if filepath.IsAbs(filePath) {
|
||||
return filePath
|
||||
@ -642,6 +818,22 @@ func transformMappingOrList(mappingOrList interface{}, sep string, allowNil bool
|
||||
panic(fmt.Errorf("expected a map or a list, got %T: %#v", mappingOrList, mappingOrList))
|
||||
}
|
||||
|
||||
func transformListOrMappingFunc(sep string, allowNil bool) func(interface{}) (interface{}, error) {
|
||||
return func(data interface{}) (interface{}, error) {
|
||||
return transformListOrMapping(data, sep, allowNil), nil
|
||||
}
|
||||
}
|
||||
|
||||
func transformListOrMapping(listOrMapping interface{}, sep string, allowNil bool) interface{} {
|
||||
switch value := listOrMapping.(type) {
|
||||
case map[string]interface{}:
|
||||
return toStringList(value, sep, allowNil)
|
||||
case []interface{}:
|
||||
return listOrMapping
|
||||
}
|
||||
panic(errors.Errorf("expected a map or a list, got %T: %#v", listOrMapping, listOrMapping))
|
||||
}
|
||||
|
||||
func transformShellCommand(value interface{}) (interface{}, error) {
|
||||
if str, ok := value.(string); ok {
|
||||
return shellwords.Parse(str)
|
||||
@ -670,6 +862,19 @@ func transformSize(value interface{}) (interface{}, error) {
|
||||
panic(fmt.Errorf("invalid type for size %T", value))
|
||||
}
|
||||
|
||||
func transformStringToDuration(value interface{}) (interface{}, error) {
|
||||
switch value := value.(type) {
|
||||
case string:
|
||||
d, err := time.ParseDuration(value)
|
||||
if err != nil {
|
||||
return value, err
|
||||
}
|
||||
return composetypes.Duration(d), nil
|
||||
default:
|
||||
return value, errors.Errorf("invalid type %T for duration", value)
|
||||
}
|
||||
}
|
||||
|
||||
func toServicePortConfigs(value string) ([]interface{}, error) {
|
||||
var portConfigs []interface{}
|
||||
|
||||
@ -691,7 +896,7 @@ func toServicePortConfigs(value string) ([]interface{}, error) {
|
||||
return nil, err
|
||||
}
|
||||
for _, p := range portConfig {
|
||||
portConfigs = append(portConfigs, ServicePortConfig{
|
||||
portConfigs = append(portConfigs, composetypes.ServicePortConfig{
|
||||
Protocol: string(p.Protocol),
|
||||
Target: p.TargetPort,
|
||||
Published: p.PublishedPort,
|
||||
@ -721,3 +926,15 @@ func toString(value interface{}, allowNil bool) interface{} {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func toStringList(value map[string]interface{}, separator string, allowNil bool) []string {
|
||||
output := []string{}
|
||||
for key, value := range value {
|
||||
if value == nil && !allowNil {
|
||||
continue
|
||||
}
|
||||
output = append(output, fmt.Sprintf("%s%s%s", key, separator, value))
|
||||
}
|
||||
sort.Strings(output)
|
||||
return output
|
||||
}
|
||||
|
233
biz/docker/compose/merge.go
Normal file
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"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
composetypes "github.com/cuigh/swirl/biz/docker/compose/types"
|
||||
)
|
||||
|
||||
func Parse(name, content string) (*Config, error) {
|
||||
func Parse(name, content string) (*composetypes.Config, error) {
|
||||
//absPath, err := filepath.Abs(composefile)
|
||||
//if err != nil {
|
||||
// return details, err
|
||||
@ -24,8 +26,8 @@ func Parse(name, content string) (*Config, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
details := ConfigDetails{
|
||||
ConfigFiles: []ConfigFile{*configFile},
|
||||
details := composetypes.ConfigDetails{
|
||||
ConfigFiles: []composetypes.ConfigFile{*configFile},
|
||||
Environment: env,
|
||||
}
|
||||
cfg, err := Load(details)
|
||||
@ -47,12 +49,12 @@ func propertyWarnings(properties map[string]string) string {
|
||||
return strings.Join(msgs, "\n\n")
|
||||
}
|
||||
|
||||
func getConfigFile(name, content string) (*ConfigFile, error) {
|
||||
func getConfigFile(name, content string) (*composetypes.ConfigFile, error) {
|
||||
config, err := ParseYAML([]byte(content))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ConfigFile{
|
||||
return &composetypes.ConfigFile{
|
||||
Filename: name,
|
||||
Config: config,
|
||||
}, nil
|
||||
@ -71,7 +73,7 @@ func buildEnvironment(env []string) (map[string]string, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func GetServicesDeclaredNetworks(serviceConfigs []ServiceConfig) map[string]struct{} {
|
||||
func GetServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
|
||||
serviceNetworks := map[string]struct{}{}
|
||||
for _, serviceConfig := range serviceConfigs {
|
||||
if len(serviceConfig.Networks) == 0 {
|
||||
|
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/utf8"
|
||||
|
||||
"github.com/cuigh/swirl/biz/docker/compose/types"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@ -12,8 +13,8 @@ import (
|
||||
const endOfSpec = rune(0)
|
||||
|
||||
// ParseVolume parses a volume spec without any knowledge of the target platform
|
||||
func ParseVolume(spec string) (ServiceVolumeConfig, error) {
|
||||
volume := ServiceVolumeConfig{}
|
||||
func ParseVolume(spec string) (types.ServiceVolumeConfig, error) {
|
||||
volume := types.ServiceVolumeConfig{}
|
||||
|
||||
switch len(spec) {
|
||||
case 0:
|
||||
@ -48,7 +49,7 @@ func isWindowsDrive(buffer []rune, char rune) bool {
|
||||
return char == ':' && len(buffer) == 1 && unicode.IsLetter(buffer[0])
|
||||
}
|
||||
|
||||
func populateFieldFromBuffer(char rune, buffer []rune, volume *ServiceVolumeConfig) error {
|
||||
func populateFieldFromBuffer(char rune, buffer []rune, volume *types.ServiceVolumeConfig) error {
|
||||
strBuffer := string(buffer)
|
||||
switch {
|
||||
case len(buffer) == 0:
|
||||
@ -73,10 +74,10 @@ func populateFieldFromBuffer(char rune, buffer []rune, volume *ServiceVolumeConf
|
||||
case "rw":
|
||||
volume.ReadOnly = false
|
||||
case "nocopy":
|
||||
volume.Volume = &ServiceVolumeVolume{NoCopy: true}
|
||||
volume.Volume = &types.ServiceVolumeVolume{NoCopy: true}
|
||||
default:
|
||||
if isBindOption(option) {
|
||||
volume.Bind = &ServiceVolumeBind{Propagation: option}
|
||||
volume.Bind = &types.ServiceVolumeBind{Propagation: option}
|
||||
}
|
||||
// ignore unknown options
|
||||
}
|
||||
@ -93,7 +94,7 @@ func isBindOption(option string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func populateType(volume *ServiceVolumeConfig) {
|
||||
func populateType(volume *types.ServiceVolumeConfig) {
|
||||
switch {
|
||||
// Anonymous volume
|
||||
case volume.Source == "":
|
||||
@ -111,6 +112,11 @@ func isFilePath(source string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// windows named pipes
|
||||
if strings.HasPrefix(source, `\\`) {
|
||||
return true
|
||||
}
|
||||
|
||||
first, nextIndex := utf8.DecodeRuneInString(source)
|
||||
return isWindowsDrive([]rune{first}, rune(source[nextIndex]))
|
||||
}
|
||||
|
1
go.mod
1
go.mod
@ -19,6 +19,7 @@ require (
|
||||
github.com/gobwas/ws v1.0.0
|
||||
github.com/google/go-cmp v0.2.0 // indirect
|
||||
github.com/gorilla/mux v1.7.1 // indirect
|
||||
github.com/imdario/mergo v0.3.7
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/mattn/go-shellwords v1.0.3
|
||||
github.com/mitchellh/mapstructure v1.0.0
|
||||
|
2
go.sum
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/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
|
||||
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
|
Loading…
Reference in New Issue
Block a user