Add expanded mount format to stack deploy
| ... | ... |
@@ -1,21 +1,19 @@ |
| 1 | 1 |
package convert |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
- "fmt" |
|
| 5 |
- "strings" |
|
| 6 |
- |
|
| 7 | 4 |
"github.com/docker/docker/api/types/mount" |
| 8 | 5 |
composetypes "github.com/docker/docker/cli/compose/types" |
| 6 |
+ "github.com/pkg/errors" |
|
| 9 | 7 |
) |
| 10 | 8 |
|
| 11 | 9 |
type volumes map[string]composetypes.VolumeConfig |
| 12 | 10 |
|
| 13 | 11 |
// Volumes from compose-file types to engine api types |
| 14 |
-func Volumes(serviceVolumes []string, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) {
|
|
| 12 |
+func Volumes(serviceVolumes []composetypes.ServiceVolumeConfig, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) {
|
|
| 15 | 13 |
var mounts []mount.Mount |
| 16 | 14 |
|
| 17 |
- for _, volumeSpec := range serviceVolumes {
|
|
| 18 |
- mount, err := convertVolumeToMount(volumeSpec, stackVolumes, namespace) |
|
| 15 |
+ for _, volumeConfig := range serviceVolumes {
|
|
| 16 |
+ mount, err := convertVolumeToMount(volumeConfig, stackVolumes, namespace) |
|
| 19 | 17 |
if err != nil {
|
| 20 | 18 |
return nil, err |
| 21 | 19 |
} |
| ... | ... |
@@ -24,108 +22,65 @@ func Volumes(serviceVolumes []string, stackVolumes volumes, namespace Namespace) |
| 24 | 24 |
return mounts, nil |
| 25 | 25 |
} |
| 26 | 26 |
|
| 27 |
-func convertVolumeToMount(volumeSpec string, stackVolumes volumes, namespace Namespace) (mount.Mount, error) {
|
|
| 28 |
- var source, target string |
|
| 29 |
- var mode []string |
|
| 30 |
- |
|
| 31 |
- // TODO: split Windows path mappings properly |
|
| 32 |
- parts := strings.SplitN(volumeSpec, ":", 3) |
|
| 33 |
- |
|
| 34 |
- for _, part := range parts {
|
|
| 35 |
- if strings.TrimSpace(part) == "" {
|
|
| 36 |
- return mount.Mount{}, fmt.Errorf("invalid volume: %s", volumeSpec)
|
|
| 37 |
- } |
|
| 27 |
+func convertVolumeToMount( |
|
| 28 |
+ volume composetypes.ServiceVolumeConfig, |
|
| 29 |
+ stackVolumes volumes, |
|
| 30 |
+ namespace Namespace, |
|
| 31 |
+) (mount.Mount, error) {
|
|
| 32 |
+ result := mount.Mount{
|
|
| 33 |
+ Type: mount.Type(volume.Type), |
|
| 34 |
+ Source: volume.Source, |
|
| 35 |
+ Target: volume.Target, |
|
| 36 |
+ ReadOnly: volume.ReadOnly, |
|
| 38 | 37 |
} |
| 39 | 38 |
|
| 40 |
- switch len(parts) {
|
|
| 41 |
- case 3: |
|
| 42 |
- source = parts[0] |
|
| 43 |
- target = parts[1] |
|
| 44 |
- mode = strings.Split(parts[2], ",") |
|
| 45 |
- case 2: |
|
| 46 |
- source = parts[0] |
|
| 47 |
- target = parts[1] |
|
| 48 |
- case 1: |
|
| 49 |
- target = parts[0] |
|
| 39 |
+ // Anonymous volumes |
|
| 40 |
+ if volume.Source == "" {
|
|
| 41 |
+ return result, nil |
|
| 50 | 42 |
} |
| 51 |
- |
|
| 52 |
- if source == "" {
|
|
| 53 |
- // Anonymous volume |
|
| 54 |
- return mount.Mount{
|
|
| 55 |
- Type: mount.TypeVolume, |
|
| 56 |
- Target: target, |
|
| 57 |
- }, nil |
|
| 43 |
+ if volume.Type == "volume" && volume.Bind != nil {
|
|
| 44 |
+ return result, errors.New("bind options are incompatible with type volume")
|
|
| 45 |
+ } |
|
| 46 |
+ if volume.Type == "bind" && volume.Volume != nil {
|
|
| 47 |
+ return result, errors.New("volume options are incompatible with type bind")
|
|
| 58 | 48 |
} |
| 59 | 49 |
|
| 60 |
- // TODO: catch Windows paths here |
|
| 61 |
- if strings.HasPrefix(source, "/") {
|
|
| 62 |
- return mount.Mount{
|
|
| 63 |
- Type: mount.TypeBind, |
|
| 64 |
- Source: source, |
|
| 65 |
- Target: target, |
|
| 66 |
- ReadOnly: isReadOnly(mode), |
|
| 67 |
- BindOptions: getBindOptions(mode), |
|
| 68 |
- }, nil |
|
| 50 |
+ if volume.Bind != nil {
|
|
| 51 |
+ result.BindOptions = &mount.BindOptions{
|
|
| 52 |
+ Propagation: mount.Propagation(volume.Bind.Propagation), |
|
| 53 |
+ } |
|
| 54 |
+ } |
|
| 55 |
+ // Binds volumes |
|
| 56 |
+ if volume.Type == "bind" {
|
|
| 57 |
+ return result, nil |
|
| 69 | 58 |
} |
| 70 | 59 |
|
| 71 |
- stackVolume, exists := stackVolumes[source] |
|
| 60 |
+ stackVolume, exists := stackVolumes[volume.Source] |
|
| 72 | 61 |
if !exists {
|
| 73 |
- return mount.Mount{}, fmt.Errorf("undefined volume: %s", source)
|
|
| 62 |
+ return result, errors.Errorf("undefined volume: %s", volume.Source)
|
|
| 74 | 63 |
} |
| 75 | 64 |
|
| 76 |
- var volumeOptions *mount.VolumeOptions |
|
| 77 |
- if stackVolume.External.Name != "" {
|
|
| 78 |
- volumeOptions = &mount.VolumeOptions{
|
|
| 79 |
- NoCopy: isNoCopy(mode), |
|
| 80 |
- } |
|
| 81 |
- source = stackVolume.External.Name |
|
| 82 |
- } else {
|
|
| 83 |
- volumeOptions = &mount.VolumeOptions{
|
|
| 84 |
- Labels: AddStackLabel(namespace, stackVolume.Labels), |
|
| 85 |
- NoCopy: isNoCopy(mode), |
|
| 86 |
- } |
|
| 65 |
+ result.Source = namespace.Scope(volume.Source) |
|
| 66 |
+ result.VolumeOptions = &mount.VolumeOptions{}
|
|
| 87 | 67 |
|
| 88 |
- if stackVolume.Driver != "" {
|
|
| 89 |
- volumeOptions.DriverConfig = &mount.Driver{
|
|
| 90 |
- Name: stackVolume.Driver, |
|
| 91 |
- Options: stackVolume.DriverOpts, |
|
| 92 |
- } |
|
| 93 |
- } |
|
| 94 |
- source = namespace.Scope(source) |
|
| 68 |
+ if volume.Volume != nil {
|
|
| 69 |
+ result.VolumeOptions.NoCopy = volume.Volume.NoCopy |
|
| 95 | 70 |
} |
| 96 |
- return mount.Mount{
|
|
| 97 |
- Type: mount.TypeVolume, |
|
| 98 |
- Source: source, |
|
| 99 |
- Target: target, |
|
| 100 |
- ReadOnly: isReadOnly(mode), |
|
| 101 |
- VolumeOptions: volumeOptions, |
|
| 102 |
- }, nil |
|
| 103 |
-} |
|
| 104 | 71 |
|
| 105 |
-func modeHas(mode []string, field string) bool {
|
|
| 106 |
- for _, item := range mode {
|
|
| 107 |
- if item == field {
|
|
| 108 |
- return true |
|
| 109 |
- } |
|
| 72 |
+ // External named volumes |
|
| 73 |
+ if stackVolume.External.External {
|
|
| 74 |
+ result.Source = stackVolume.External.Name |
|
| 75 |
+ return result, nil |
|
| 110 | 76 |
} |
| 111 |
- return false |
|
| 112 |
-} |
|
| 113 | 77 |
|
| 114 |
-func isReadOnly(mode []string) bool {
|
|
| 115 |
- return modeHas(mode, "ro") |
|
| 116 |
-} |
|
| 117 |
- |
|
| 118 |
-func isNoCopy(mode []string) bool {
|
|
| 119 |
- return modeHas(mode, "nocopy") |
|
| 120 |
-} |
|
| 121 |
- |
|
| 122 |
-func getBindOptions(mode []string) *mount.BindOptions {
|
|
| 123 |
- for _, item := range mode {
|
|
| 124 |
- for _, propagation := range mount.Propagations {
|
|
| 125 |
- if mount.Propagation(item) == propagation {
|
|
| 126 |
- return &mount.BindOptions{Propagation: mount.Propagation(item)}
|
|
| 127 |
- } |
|
| 78 |
+ result.VolumeOptions.Labels = AddStackLabel(namespace, stackVolume.Labels) |
|
| 79 |
+ if stackVolume.Driver != "" || stackVolume.DriverOpts != nil {
|
|
| 80 |
+ result.VolumeOptions.DriverConfig = &mount.Driver{
|
|
| 81 |
+ Name: stackVolume.Driver, |
|
| 82 |
+ Options: stackVolume.DriverOpts, |
|
| 128 | 83 |
} |
| 129 | 84 |
} |
| 130 |
- return nil |
|
| 85 |
+ |
|
| 86 |
+ // Named volumes |
|
| 87 |
+ return result, nil |
|
| 131 | 88 |
} |
| ... | ... |
@@ -8,51 +8,48 @@ import ( |
| 8 | 8 |
"github.com/docker/docker/pkg/testutil/assert" |
| 9 | 9 |
) |
| 10 | 10 |
|
| 11 |
-func TestIsReadOnly(t *testing.T) {
|
|
| 12 |
- assert.Equal(t, isReadOnly([]string{"foo", "bar", "ro"}), true)
|
|
| 13 |
- assert.Equal(t, isReadOnly([]string{"ro"}), true)
|
|
| 14 |
- assert.Equal(t, isReadOnly([]string{}), false)
|
|
| 15 |
- assert.Equal(t, isReadOnly([]string{"foo", "rw"}), false)
|
|
| 16 |
- assert.Equal(t, isReadOnly([]string{"foo"}), false)
|
|
| 17 |
-} |
|
| 18 |
- |
|
| 19 |
-func TestIsNoCopy(t *testing.T) {
|
|
| 20 |
- assert.Equal(t, isNoCopy([]string{"foo", "bar", "nocopy"}), true)
|
|
| 21 |
- assert.Equal(t, isNoCopy([]string{"nocopy"}), true)
|
|
| 22 |
- assert.Equal(t, isNoCopy([]string{}), false)
|
|
| 23 |
- assert.Equal(t, isNoCopy([]string{"foo", "rw"}), false)
|
|
| 24 |
-} |
|
| 25 |
- |
|
| 26 |
-func TestGetBindOptions(t *testing.T) {
|
|
| 27 |
- opts := getBindOptions([]string{"slave"})
|
|
| 28 |
- expected := mount.BindOptions{Propagation: mount.PropagationSlave}
|
|
| 29 |
- assert.Equal(t, *opts, expected) |
|
| 30 |
-} |
|
| 31 |
- |
|
| 32 |
-func TestGetBindOptionsNone(t *testing.T) {
|
|
| 33 |
- opts := getBindOptions([]string{"ro"})
|
|
| 34 |
- assert.Equal(t, opts, (*mount.BindOptions)(nil)) |
|
| 35 |
-} |
|
| 36 |
- |
|
| 37 | 11 |
func TestConvertVolumeToMountAnonymousVolume(t *testing.T) {
|
| 38 |
- stackVolumes := volumes{}
|
|
| 39 |
- namespace := NewNamespace("foo")
|
|
| 12 |
+ config := composetypes.ServiceVolumeConfig{
|
|
| 13 |
+ Type: "volume", |
|
| 14 |
+ Target: "/foo/bar", |
|
| 15 |
+ } |
|
| 40 | 16 |
expected := mount.Mount{
|
| 41 | 17 |
Type: mount.TypeVolume, |
| 42 | 18 |
Target: "/foo/bar", |
| 43 | 19 |
} |
| 44 |
- mount, err := convertVolumeToMount("/foo/bar", stackVolumes, namespace)
|
|
| 20 |
+ mount, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
|
|
| 45 | 21 |
assert.NilError(t, err) |
| 46 | 22 |
assert.DeepEqual(t, mount, expected) |
| 47 | 23 |
} |
| 48 | 24 |
|
| 49 |
-func TestConvertVolumeToMountInvalidFormat(t *testing.T) {
|
|
| 25 |
+func TestConvertVolumeToMountConflictingOptionsBind(t *testing.T) {
|
|
| 50 | 26 |
namespace := NewNamespace("foo")
|
| 51 |
- invalids := []string{"::", "::cc", ":bb:", "aa::", "aa::cc", "aa:bb:", " : : ", " : :cc", " :bb: ", "aa: : ", "aa: :cc", "aa:bb: "}
|
|
| 52 |
- for _, vol := range invalids {
|
|
| 53 |
- _, err := convertVolumeToMount(vol, volumes{}, namespace)
|
|
| 54 |
- assert.Error(t, err, "invalid volume: "+vol) |
|
| 27 |
+ |
|
| 28 |
+ config := composetypes.ServiceVolumeConfig{
|
|
| 29 |
+ Type: "volume", |
|
| 30 |
+ Source: "foo", |
|
| 31 |
+ Target: "/target", |
|
| 32 |
+ Bind: &composetypes.ServiceVolumeBind{
|
|
| 33 |
+ Propagation: "slave", |
|
| 34 |
+ }, |
|
| 55 | 35 |
} |
| 36 |
+ _, err := convertVolumeToMount(config, volumes{}, namespace)
|
|
| 37 |
+ assert.Error(t, err, "bind options are incompatible") |
|
| 38 |
+} |
|
| 39 |
+ |
|
| 40 |
+func TestConvertVolumeToMountConflictingOptionsVolume(t *testing.T) {
|
|
| 41 |
+ namespace := NewNamespace("foo")
|
|
| 42 |
+ |
|
| 43 |
+ config := composetypes.ServiceVolumeConfig{
|
|
| 44 |
+ Type: "bind", |
|
| 45 |
+ Source: "/foo", |
|
| 46 |
+ Target: "/target", |
|
| 47 |
+ Volume: &composetypes.ServiceVolumeVolume{
|
|
| 48 |
+ NoCopy: true, |
|
| 49 |
+ }, |
|
| 50 |
+ } |
|
| 51 |
+ _, err := convertVolumeToMount(config, volumes{}, namespace)
|
|
| 52 |
+ assert.Error(t, err, "volume options are incompatible") |
|
| 56 | 53 |
} |
| 57 | 54 |
|
| 58 | 55 |
func TestConvertVolumeToMountNamedVolume(t *testing.T) {
|
| ... | ... |
@@ -84,9 +81,19 @@ func TestConvertVolumeToMountNamedVolume(t *testing.T) {
|
| 84 | 84 |
"opt": "value", |
| 85 | 85 |
}, |
| 86 | 86 |
}, |
| 87 |
+ NoCopy: true, |
|
| 88 |
+ }, |
|
| 89 |
+ } |
|
| 90 |
+ config := composetypes.ServiceVolumeConfig{
|
|
| 91 |
+ Type: "volume", |
|
| 92 |
+ Source: "normal", |
|
| 93 |
+ Target: "/foo", |
|
| 94 |
+ ReadOnly: true, |
|
| 95 |
+ Volume: &composetypes.ServiceVolumeVolume{
|
|
| 96 |
+ NoCopy: true, |
|
| 87 | 97 |
}, |
| 88 | 98 |
} |
| 89 |
- mount, err := convertVolumeToMount("normal:/foo:ro", stackVolumes, namespace)
|
|
| 99 |
+ mount, err := convertVolumeToMount(config, stackVolumes, namespace) |
|
| 90 | 100 |
assert.NilError(t, err) |
| 91 | 101 |
assert.DeepEqual(t, mount, expected) |
| 92 | 102 |
} |
| ... | ... |
@@ -109,7 +116,12 @@ func TestConvertVolumeToMountNamedVolumeExternal(t *testing.T) {
|
| 109 | 109 |
NoCopy: false, |
| 110 | 110 |
}, |
| 111 | 111 |
} |
| 112 |
- mount, err := convertVolumeToMount("outside:/foo", stackVolumes, namespace)
|
|
| 112 |
+ config := composetypes.ServiceVolumeConfig{
|
|
| 113 |
+ Type: "volume", |
|
| 114 |
+ Source: "outside", |
|
| 115 |
+ Target: "/foo", |
|
| 116 |
+ } |
|
| 117 |
+ mount, err := convertVolumeToMount(config, stackVolumes, namespace) |
|
| 113 | 118 |
assert.NilError(t, err) |
| 114 | 119 |
assert.DeepEqual(t, mount, expected) |
| 115 | 120 |
} |
| ... | ... |
@@ -132,7 +144,15 @@ func TestConvertVolumeToMountNamedVolumeExternalNoCopy(t *testing.T) {
|
| 132 | 132 |
NoCopy: true, |
| 133 | 133 |
}, |
| 134 | 134 |
} |
| 135 |
- mount, err := convertVolumeToMount("outside:/foo:nocopy", stackVolumes, namespace)
|
|
| 135 |
+ config := composetypes.ServiceVolumeConfig{
|
|
| 136 |
+ Type: "volume", |
|
| 137 |
+ Source: "outside", |
|
| 138 |
+ Target: "/foo", |
|
| 139 |
+ Volume: &composetypes.ServiceVolumeVolume{
|
|
| 140 |
+ NoCopy: true, |
|
| 141 |
+ }, |
|
| 142 |
+ } |
|
| 143 |
+ mount, err := convertVolumeToMount(config, stackVolumes, namespace) |
|
| 136 | 144 |
assert.NilError(t, err) |
| 137 | 145 |
assert.DeepEqual(t, mount, expected) |
| 138 | 146 |
} |
| ... | ... |
@@ -147,13 +167,26 @@ func TestConvertVolumeToMountBind(t *testing.T) {
|
| 147 | 147 |
ReadOnly: true, |
| 148 | 148 |
BindOptions: &mount.BindOptions{Propagation: mount.PropagationShared},
|
| 149 | 149 |
} |
| 150 |
- mount, err := convertVolumeToMount("/bar:/foo:ro,shared", stackVolumes, namespace)
|
|
| 150 |
+ config := composetypes.ServiceVolumeConfig{
|
|
| 151 |
+ Type: "bind", |
|
| 152 |
+ Source: "/bar", |
|
| 153 |
+ Target: "/foo", |
|
| 154 |
+ ReadOnly: true, |
|
| 155 |
+ Bind: &composetypes.ServiceVolumeBind{Propagation: "shared"},
|
|
| 156 |
+ } |
|
| 157 |
+ mount, err := convertVolumeToMount(config, stackVolumes, namespace) |
|
| 151 | 158 |
assert.NilError(t, err) |
| 152 | 159 |
assert.DeepEqual(t, mount, expected) |
| 153 | 160 |
} |
| 154 | 161 |
|
| 155 | 162 |
func TestConvertVolumeToMountVolumeDoesNotExist(t *testing.T) {
|
| 156 | 163 |
namespace := NewNamespace("foo")
|
| 157 |
- _, err := convertVolumeToMount("unknown:/foo:ro", volumes{}, namespace)
|
|
| 164 |
+ config := composetypes.ServiceVolumeConfig{
|
|
| 165 |
+ Type: "volume", |
|
| 166 |
+ Source: "unknown", |
|
| 167 |
+ Target: "/foo", |
|
| 168 |
+ ReadOnly: true, |
|
| 169 |
+ } |
|
| 170 |
+ _, err := convertVolumeToMount(config, volumes{}, namespace)
|
|
| 158 | 171 |
assert.Error(t, err, "undefined volume: unknown") |
| 159 | 172 |
} |
| ... | ... |
@@ -251,6 +251,8 @@ func transformHook( |
| 251 | 251 |
return transformMappingOrList(data, "="), nil |
| 252 | 252 |
case reflect.TypeOf(types.MappingWithColon{}):
|
| 253 | 253 |
return transformMappingOrList(data, ":"), nil |
| 254 |
+ case reflect.TypeOf(types.ServiceVolumeConfig{}):
|
|
| 255 |
+ return transformServiceVolumeConfig(data) |
|
| 254 | 256 |
} |
| 255 | 257 |
return data, nil |
| 256 | 258 |
} |
| ... | ... |
@@ -333,10 +335,7 @@ func LoadService(name string, serviceDict types.Dict, workingDir string) (*types |
| 333 | 333 |
return nil, err |
| 334 | 334 |
} |
| 335 | 335 |
|
| 336 |
- if err := resolveVolumePaths(serviceConfig.Volumes, workingDir); err != nil {
|
|
| 337 |
- return nil, err |
|
| 338 |
- } |
|
| 339 |
- |
|
| 336 |
+ resolveVolumePaths(serviceConfig.Volumes, workingDir) |
|
| 340 | 337 |
return serviceConfig, nil |
| 341 | 338 |
} |
| 342 | 339 |
|
| ... | ... |
@@ -369,22 +368,15 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string) e |
| 369 | 369 |
return nil |
| 370 | 370 |
} |
| 371 | 371 |
|
| 372 |
-func resolveVolumePaths(volumes []string, workingDir string) error {
|
|
| 373 |
- for i, mapping := range volumes {
|
|
| 374 |
- parts := strings.SplitN(mapping, ":", 2) |
|
| 375 |
- if len(parts) == 1 {
|
|
| 372 |
+func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string) {
|
|
| 373 |
+ for i, volume := range volumes {
|
|
| 374 |
+ if volume.Type != "bind" {
|
|
| 376 | 375 |
continue |
| 377 | 376 |
} |
| 378 | 377 |
|
| 379 |
- if strings.HasPrefix(parts[0], ".") {
|
|
| 380 |
- parts[0] = absPath(workingDir, parts[0]) |
|
| 381 |
- } |
|
| 382 |
- parts[0] = expandUser(parts[0]) |
|
| 383 |
- |
|
| 384 |
- volumes[i] = strings.Join(parts, ":") |
|
| 378 |
+ volume.Source = absPath(workingDir, expandUser(volume.Source)) |
|
| 379 |
+ volumes[i] = volume |
|
| 385 | 380 |
} |
| 386 |
- |
|
| 387 |
- return nil |
|
| 388 | 381 |
} |
| 389 | 382 |
|
| 390 | 383 |
// TODO: make this more robust |
| ... | ... |
@@ -555,6 +547,20 @@ func transformServiceSecret(data interface{}) (interface{}, error) {
|
| 555 | 555 |
} |
| 556 | 556 |
} |
| 557 | 557 |
|
| 558 |
+func transformServiceVolumeConfig(data interface{}) (interface{}, error) {
|
|
| 559 |
+ switch value := data.(type) {
|
|
| 560 |
+ case string: |
|
| 561 |
+ return parseVolume(value) |
|
| 562 |
+ case types.Dict: |
|
| 563 |
+ return data, nil |
|
| 564 |
+ case map[string]interface{}:
|
|
| 565 |
+ return data, nil |
|
| 566 |
+ default: |
|
| 567 |
+ return data, fmt.Errorf("invalid type %T for service volume", value)
|
|
| 568 |
+ } |
|
| 569 |
+ |
|
| 570 |
+} |
|
| 571 |
+ |
|
| 558 | 572 |
func transformServiceNetworkMap(value interface{}) (interface{}, error) {
|
| 559 | 573 |
if list, ok := value.([]interface{}); ok {
|
| 560 | 574 |
mapValue := map[interface{}]interface{}{}
|
| ... | ... |
@@ -881,13 +881,13 @@ func TestFullExample(t *testing.T) {
|
| 881 | 881 |
}, |
| 882 | 882 |
}, |
| 883 | 883 |
User: "someone", |
| 884 |
- Volumes: []string{
|
|
| 885 |
- "/var/lib/mysql", |
|
| 886 |
- "/opt/data:/var/lib/mysql", |
|
| 887 |
- fmt.Sprintf("%s:/code", workingDir),
|
|
| 888 |
- fmt.Sprintf("%s/static:/var/www/html", workingDir),
|
|
| 889 |
- fmt.Sprintf("%s/configs:/etc/configs/:ro", homeDir),
|
|
| 890 |
- "datavolume:/var/lib/mysql", |
|
| 884 |
+ Volumes: []types.ServiceVolumeConfig{
|
|
| 885 |
+ {Target: "/var/lib/mysql", Type: "volume"},
|
|
| 886 |
+ {Source: "/opt/data", Target: "/var/lib/mysql", Type: "bind"},
|
|
| 887 |
+ {Source: workingDir, Target: "/code", Type: "bind"},
|
|
| 888 |
+ {Source: workingDir + "/static", Target: "/var/www/html", Type: "bind"},
|
|
| 889 |
+ {Source: homeDir + "/configs", Target: "/etc/configs/", Type: "bind", ReadOnly: true},
|
|
| 890 |
+ {Source: "datavolume", Target: "/var/lib/mysql", Type: "volume"},
|
|
| 891 | 891 |
}, |
| 892 | 892 |
WorkingDir: "/code", |
| 893 | 893 |
} |
| ... | ... |
@@ -1085,3 +1085,31 @@ services: |
| 1085 | 1085 |
assert.Equal(t, 1, len(config.Services)) |
| 1086 | 1086 |
assert.Equal(t, expected, config.Services[0].Ports) |
| 1087 | 1087 |
} |
| 1088 |
+ |
|
| 1089 |
+func TestLoadExpandedMountFormat(t *testing.T) {
|
|
| 1090 |
+ config, err := loadYAML(` |
|
| 1091 |
+version: "3.1" |
|
| 1092 |
+services: |
|
| 1093 |
+ web: |
|
| 1094 |
+ image: busybox |
|
| 1095 |
+ volumes: |
|
| 1096 |
+ - type: volume |
|
| 1097 |
+ source: foo |
|
| 1098 |
+ target: /target |
|
| 1099 |
+ read_only: true |
|
| 1100 |
+volumes: |
|
| 1101 |
+ foo: {}
|
|
| 1102 |
+`) |
|
| 1103 |
+ assert.NoError(t, err) |
|
| 1104 |
+ |
|
| 1105 |
+ expected := types.ServiceVolumeConfig{
|
|
| 1106 |
+ Type: "volume", |
|
| 1107 |
+ Source: "foo", |
|
| 1108 |
+ Target: "/target", |
|
| 1109 |
+ ReadOnly: true, |
|
| 1110 |
+ } |
|
| 1111 |
+ |
|
| 1112 |
+ assert.Equal(t, 1, len(config.Services)) |
|
| 1113 |
+ assert.Equal(t, 1, len(config.Services[0].Volumes)) |
|
| 1114 |
+ assert.Equal(t, expected, config.Services[0].Volumes[0]) |
|
| 1115 |
+} |
| 1088 | 1116 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,119 @@ |
| 0 |
+package loader |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "strings" |
|
| 4 |
+ "unicode" |
|
| 5 |
+ "unicode/utf8" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/docker/api/types/mount" |
|
| 8 |
+ "github.com/docker/docker/cli/compose/types" |
|
| 9 |
+ "github.com/pkg/errors" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+func parseVolume(spec string) (types.ServiceVolumeConfig, error) {
|
|
| 13 |
+ volume := types.ServiceVolumeConfig{}
|
|
| 14 |
+ |
|
| 15 |
+ switch len(spec) {
|
|
| 16 |
+ case 0: |
|
| 17 |
+ return volume, errors.New("invalid empty volume spec")
|
|
| 18 |
+ case 1, 2: |
|
| 19 |
+ volume.Target = spec |
|
| 20 |
+ volume.Type = string(mount.TypeVolume) |
|
| 21 |
+ return volume, nil |
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ buffer := []rune{}
|
|
| 25 |
+ for _, char := range spec {
|
|
| 26 |
+ switch {
|
|
| 27 |
+ case isWindowsDrive(char, buffer, volume): |
|
| 28 |
+ buffer = append(buffer, char) |
|
| 29 |
+ case char == ':': |
|
| 30 |
+ if err := populateFieldFromBuffer(char, buffer, &volume); err != nil {
|
|
| 31 |
+ return volume, errors.Wrapf(err, "invalid spec: %s", spec) |
|
| 32 |
+ } |
|
| 33 |
+ buffer = []rune{}
|
|
| 34 |
+ default: |
|
| 35 |
+ buffer = append(buffer, char) |
|
| 36 |
+ } |
|
| 37 |
+ } |
|
| 38 |
+ |
|
| 39 |
+ if err := populateFieldFromBuffer(rune(0), buffer, &volume); err != nil {
|
|
| 40 |
+ return volume, errors.Wrapf(err, "invalid spec: %s", spec) |
|
| 41 |
+ } |
|
| 42 |
+ populateType(&volume) |
|
| 43 |
+ return volume, nil |
|
| 44 |
+} |
|
| 45 |
+ |
|
| 46 |
+func isWindowsDrive(char rune, buffer []rune, volume types.ServiceVolumeConfig) bool {
|
|
| 47 |
+ return char == ':' && len(buffer) == 1 && unicode.IsLetter(buffer[0]) |
|
| 48 |
+} |
|
| 49 |
+ |
|
| 50 |
+func populateFieldFromBuffer(char rune, buffer []rune, volume *types.ServiceVolumeConfig) error {
|
|
| 51 |
+ strBuffer := string(buffer) |
|
| 52 |
+ switch {
|
|
| 53 |
+ case len(buffer) == 0: |
|
| 54 |
+ return errors.New("empty section between colons")
|
|
| 55 |
+ // Anonymous volume |
|
| 56 |
+ case volume.Source == "" && char == rune(0): |
|
| 57 |
+ volume.Target = strBuffer |
|
| 58 |
+ return nil |
|
| 59 |
+ case volume.Source == "": |
|
| 60 |
+ volume.Source = strBuffer |
|
| 61 |
+ return nil |
|
| 62 |
+ case volume.Target == "": |
|
| 63 |
+ volume.Target = strBuffer |
|
| 64 |
+ return nil |
|
| 65 |
+ case char == ':': |
|
| 66 |
+ return errors.New("too many colons")
|
|
| 67 |
+ } |
|
| 68 |
+ for _, option := range strings.Split(strBuffer, ",") {
|
|
| 69 |
+ switch option {
|
|
| 70 |
+ case "ro": |
|
| 71 |
+ volume.ReadOnly = true |
|
| 72 |
+ case "nocopy": |
|
| 73 |
+ volume.Volume = &types.ServiceVolumeVolume{NoCopy: true}
|
|
| 74 |
+ default: |
|
| 75 |
+ if isBindOption(option) {
|
|
| 76 |
+ volume.Bind = &types.ServiceVolumeBind{Propagation: option}
|
|
| 77 |
+ } else {
|
|
| 78 |
+ return errors.Errorf("unknown option: %s", option)
|
|
| 79 |
+ } |
|
| 80 |
+ } |
|
| 81 |
+ } |
|
| 82 |
+ return nil |
|
| 83 |
+} |
|
| 84 |
+ |
|
| 85 |
+func isBindOption(option string) bool {
|
|
| 86 |
+ for _, propagation := range mount.Propagations {
|
|
| 87 |
+ if mount.Propagation(option) == propagation {
|
|
| 88 |
+ return true |
|
| 89 |
+ } |
|
| 90 |
+ } |
|
| 91 |
+ return false |
|
| 92 |
+} |
|
| 93 |
+ |
|
| 94 |
+func populateType(volume *types.ServiceVolumeConfig) {
|
|
| 95 |
+ switch {
|
|
| 96 |
+ // Anonymous volume |
|
| 97 |
+ case volume.Source == "": |
|
| 98 |
+ volume.Type = string(mount.TypeVolume) |
|
| 99 |
+ case isFilePath(volume.Source): |
|
| 100 |
+ volume.Type = string(mount.TypeBind) |
|
| 101 |
+ default: |
|
| 102 |
+ volume.Type = string(mount.TypeVolume) |
|
| 103 |
+ } |
|
| 104 |
+} |
|
| 105 |
+ |
|
| 106 |
+func isFilePath(source string) bool {
|
|
| 107 |
+ switch source[0] {
|
|
| 108 |
+ case '.', '/', '~': |
|
| 109 |
+ return true |
|
| 110 |
+ } |
|
| 111 |
+ |
|
| 112 |
+ // Windows absolute path |
|
| 113 |
+ first, next := utf8.DecodeRuneInString(source) |
|
| 114 |
+ if unicode.IsLetter(first) && source[next] == ':' {
|
|
| 115 |
+ return true |
|
| 116 |
+ } |
|
| 117 |
+ return false |
|
| 118 |
+} |
| 0 | 119 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,134 @@ |
| 0 |
+package loader |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "testing" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/docker/docker/cli/compose/types" |
|
| 6 |
+ "github.com/docker/docker/pkg/testutil/assert" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+func TestParseVolumeAnonymousVolume(t *testing.T) {
|
|
| 10 |
+ for _, path := range []string{"/path", "/path/foo"} {
|
|
| 11 |
+ volume, err := parseVolume(path) |
|
| 12 |
+ expected := types.ServiceVolumeConfig{Type: "volume", Target: path}
|
|
| 13 |
+ assert.NilError(t, err) |
|
| 14 |
+ assert.DeepEqual(t, volume, expected) |
|
| 15 |
+ } |
|
| 16 |
+} |
|
| 17 |
+ |
|
| 18 |
+func TestParseVolumeAnonymousVolumeWindows(t *testing.T) {
|
|
| 19 |
+ for _, path := range []string{"C:\\path", "Z:\\path\\foo"} {
|
|
| 20 |
+ volume, err := parseVolume(path) |
|
| 21 |
+ expected := types.ServiceVolumeConfig{Type: "volume", Target: path}
|
|
| 22 |
+ assert.NilError(t, err) |
|
| 23 |
+ assert.DeepEqual(t, volume, expected) |
|
| 24 |
+ } |
|
| 25 |
+} |
|
| 26 |
+ |
|
| 27 |
+func TestParseVolumeTooManyColons(t *testing.T) {
|
|
| 28 |
+ _, err := parseVolume("/foo:/foo:ro:foo")
|
|
| 29 |
+ assert.Error(t, err, "too many colons") |
|
| 30 |
+} |
|
| 31 |
+ |
|
| 32 |
+func TestParseVolumeShortVolumes(t *testing.T) {
|
|
| 33 |
+ for _, path := range []string{".", "/a"} {
|
|
| 34 |
+ volume, err := parseVolume(path) |
|
| 35 |
+ expected := types.ServiceVolumeConfig{Type: "volume", Target: path}
|
|
| 36 |
+ assert.NilError(t, err) |
|
| 37 |
+ assert.DeepEqual(t, volume, expected) |
|
| 38 |
+ } |
|
| 39 |
+} |
|
| 40 |
+ |
|
| 41 |
+func TestParseVolumeMissingSource(t *testing.T) {
|
|
| 42 |
+ for _, spec := range []string{":foo", "/foo::ro"} {
|
|
| 43 |
+ _, err := parseVolume(spec) |
|
| 44 |
+ assert.Error(t, err, "empty section between colons") |
|
| 45 |
+ } |
|
| 46 |
+} |
|
| 47 |
+ |
|
| 48 |
+func TestParseVolumeBindMount(t *testing.T) {
|
|
| 49 |
+ for _, path := range []string{"./foo", "~/thing", "../other", "/foo", "/home/user"} {
|
|
| 50 |
+ volume, err := parseVolume(path + ":/target") |
|
| 51 |
+ expected := types.ServiceVolumeConfig{
|
|
| 52 |
+ Type: "bind", |
|
| 53 |
+ Source: path, |
|
| 54 |
+ Target: "/target", |
|
| 55 |
+ } |
|
| 56 |
+ assert.NilError(t, err) |
|
| 57 |
+ assert.DeepEqual(t, volume, expected) |
|
| 58 |
+ } |
|
| 59 |
+} |
|
| 60 |
+ |
|
| 61 |
+func TestParseVolumeRelativeBindMountWindows(t *testing.T) {
|
|
| 62 |
+ for _, path := range []string{
|
|
| 63 |
+ "./foo", |
|
| 64 |
+ "~/thing", |
|
| 65 |
+ "../other", |
|
| 66 |
+ "D:\\path", "/home/user", |
|
| 67 |
+ } {
|
|
| 68 |
+ volume, err := parseVolume(path + ":d:\\target") |
|
| 69 |
+ expected := types.ServiceVolumeConfig{
|
|
| 70 |
+ Type: "bind", |
|
| 71 |
+ Source: path, |
|
| 72 |
+ Target: "d:\\target", |
|
| 73 |
+ } |
|
| 74 |
+ assert.NilError(t, err) |
|
| 75 |
+ assert.DeepEqual(t, volume, expected) |
|
| 76 |
+ } |
|
| 77 |
+} |
|
| 78 |
+ |
|
| 79 |
+func TestParseVolumeWithBindOptions(t *testing.T) {
|
|
| 80 |
+ volume, err := parseVolume("/source:/target:slave")
|
|
| 81 |
+ expected := types.ServiceVolumeConfig{
|
|
| 82 |
+ Type: "bind", |
|
| 83 |
+ Source: "/source", |
|
| 84 |
+ Target: "/target", |
|
| 85 |
+ Bind: &types.ServiceVolumeBind{Propagation: "slave"},
|
|
| 86 |
+ } |
|
| 87 |
+ assert.NilError(t, err) |
|
| 88 |
+ assert.DeepEqual(t, volume, expected) |
|
| 89 |
+} |
|
| 90 |
+ |
|
| 91 |
+func TestParseVolumeWithBindOptionsWindows(t *testing.T) {
|
|
| 92 |
+ volume, err := parseVolume("C:\\source\\foo:D:\\target:ro,rprivate")
|
|
| 93 |
+ expected := types.ServiceVolumeConfig{
|
|
| 94 |
+ Type: "bind", |
|
| 95 |
+ Source: "C:\\source\\foo", |
|
| 96 |
+ Target: "D:\\target", |
|
| 97 |
+ ReadOnly: true, |
|
| 98 |
+ Bind: &types.ServiceVolumeBind{Propagation: "rprivate"},
|
|
| 99 |
+ } |
|
| 100 |
+ assert.NilError(t, err) |
|
| 101 |
+ assert.DeepEqual(t, volume, expected) |
|
| 102 |
+} |
|
| 103 |
+ |
|
| 104 |
+func TestParseVolumeWithInvalidVolumeOptions(t *testing.T) {
|
|
| 105 |
+ _, err := parseVolume("name:/target:bogus")
|
|
| 106 |
+ assert.Error(t, err, "invalid spec: name:/target:bogus: unknown option: bogus") |
|
| 107 |
+} |
|
| 108 |
+ |
|
| 109 |
+func TestParseVolumeWithVolumeOptions(t *testing.T) {
|
|
| 110 |
+ volume, err := parseVolume("name:/target:nocopy")
|
|
| 111 |
+ expected := types.ServiceVolumeConfig{
|
|
| 112 |
+ Type: "volume", |
|
| 113 |
+ Source: "name", |
|
| 114 |
+ Target: "/target", |
|
| 115 |
+ Volume: &types.ServiceVolumeVolume{NoCopy: true},
|
|
| 116 |
+ } |
|
| 117 |
+ assert.NilError(t, err) |
|
| 118 |
+ assert.DeepEqual(t, volume, expected) |
|
| 119 |
+} |
|
| 120 |
+ |
|
| 121 |
+func TestParseVolumeWithReadOnly(t *testing.T) {
|
|
| 122 |
+ for _, path := range []string{"./foo", "/home/user"} {
|
|
| 123 |
+ volume, err := parseVolume(path + ":/target:ro") |
|
| 124 |
+ expected := types.ServiceVolumeConfig{
|
|
| 125 |
+ Type: "bind", |
|
| 126 |
+ Source: path, |
|
| 127 |
+ Target: "/target", |
|
| 128 |
+ ReadOnly: true, |
|
| 129 |
+ } |
|
| 130 |
+ assert.NilError(t, err) |
|
| 131 |
+ assert.DeepEqual(t, volume, expected) |
|
| 132 |
+ } |
|
| 133 |
+} |
| ... | ... |
@@ -89,7 +89,7 @@ func dataConfig_schema_v30Json() (*asset, error) {
|
| 89 | 89 |
return a, nil |
| 90 | 90 |
} |
| 91 | 91 |
|
| 92 |
-var _dataConfig_schema_v31Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5a\xcd\x8f\xdc\x28\x16\xbf\xd7\x5f\x61\x39\xb9\xa5\x3f\xb2\xda\x68\xa5\xcd\x6d\x8f\x7b\x9a\x39\x4f\xcb\xb1\x28\xfb\x95\x8b\x34\x06\x02\xb8\xd2\x95\xa8\xff\xf7\x11\xfe\x2a\x8c\xc1\xe0\x2e\xf7\x74\x34\x9a\x53\x77\x99\xdf\x03\xde\xf7\xe3\xc1\xcf\x5d\x92\xa4\xef\x65\x71\x84\x1a\xa5\x9f\x93\xf4\xa8\x14\xff\x7c\x7f\xff\x55\x32\x7a\xdb\x7d\xbd\x63\xa2\xba\x2f\x05\x3a\xa8\xdb\x8f\x9f\xee\xbb\x6f\xef\xd2\x1b\x4d\x87\x4b\x4d\x52\x30\x7a\xc0\x55\xde\x8d\xe4\xa7\x7f\xdf\xfd\xeb\x4e\x93\x77\x10\x75\xe6\xa0\x41\x6c\xff\x15\x0a\xd5\x7d\x13\xf0\xad\xc1\x02\x34\xf1\x43\x7a\x02\x21\x31\xa3\x69\x76\xb3\xd3\x63\x5c\x30\x0e\x42\x61\x90\xe9\xe7\x44\x6f\x2e\x49\x46\xc8\xf0\xc1\x98\x56\x2a\x81\x69\x95\xb6\x9f\x9f\xdb\x19\x92\x24\x95\x20\x4e\xb8\x30\x66\x18\xb7\xfa\xee\xfe\x32\xff\xfd\x08\xbb\xb1\x67\x35\x36\xdb\x7e\xe7\x48\x29\x10\xf4\xf7\xf9\xde\xda\xe1\x2f\x0f\xe8\xf6\xc7\xff\x6e\xff\xf8\x78\xfb\xdf\xbb\xfc\x36\xfb\xf0\x7e\x32\xac\xe5\x2b\xe0\xd0\x2d\x5f\xc2\x01\x53\xac\x30\xa3\xe3\xfa\xe9\x88\x7c\xee\xff\x7b\x1e\x17\x46\x65\xd9\x82\x11\x99\xac\x7d\x40\x44\xc2\x94\x67\x0a\xea\x3b\x13\x8f\x21\x9e\x47\xd8\x1b\xf1\xdc\xaf\xef\xe0\x79\xca\xce\x89\x91\xa6\x0e\x6a\x70\x40\xbd\x11\x33\xdd\xf2\xdb\xe8\x4f\x42\x21\x40\x85\x4d\xb6\x43\xbd\x99\xc5\xea\xe5\xaf\x63\x78\x37\x30\xbd\x88\xed\x10\xc6\xda\xed\x06\x27\xee\xed\x12\x95\xcb\xbd\xfc\xb2\x1a\x85\xe5\x91\x52\x09\x9c\xb0\xb3\xfe\xe6\x91\x47\x07\xa8\x81\xaa\x74\x14\x41\x92\xa4\xfb\x06\x93\xd2\x96\x28\xa3\xf0\x9b\x9e\xe2\xc1\xf8\x98\x24\x3f\xed\x48\x66\xcc\xd3\x8e\x4f\x7e\xf9\x15\x3e\x8e\x7b\x78\x19\xc7\x0b\x46\x15\x3c\xa9\x96\xa9\xe5\xa5\x3b\x11\xb0\xe2\x11\xc4\x01\x13\x88\xa5\x40\xa2\x92\x0b\x22\x23\x58\xaa\x9c\x89\xbc\xc4\x85\x4a\x9f\x2d\xf2\xd9\x7c\x61\x7b\x1a\x49\x8d\x5f\xd9\xce\x31\x61\x5a\x20\x9e\xa3\xb2\x9c\xf0\x81\x84\x40\xe7\xf4\x26\x49\xb1\x82\x5a\xba\x59\x4c\xd2\x86\xe2\x6f\x0d\xfc\xbf\x87\x28\xd1\x80\x3d\x6f\x29\x18\xdf\x7e\xe2\x4a\xb0\x86\xe7\x1c\x09\x6d\x60\xcb\xe2\x4f\x0b\x56\xd7\x88\x6e\x65\x75\x6b\xf8\x88\x90\x3c\xa3\x0a\x61\x0a\x22\xa7\xa8\x0e\x19\x92\xf6\x3a\xa0\xa5\xcc\xbb\x84\xbf\x68\x46\x87\xbc\xa3\x97\xd6\x04\x63\xf6\xdf\x54\x1f\x25\x5d\x32\xec\x6e\x1a\x6d\xda\x7a\x6f\xa9\x45\x98\x4b\x40\xa2\x38\xbe\x90\x9e\xd5\x08\xd3\x18\xd9\x01\x55\xe2\xcc\x19\xee\xec\xe5\x97\x33\x04\xa0\xa7\x7c\x8c\x25\xab\xc5\x00\xf4\x84\x05\xa3\xf5\xe0\x0d\x31\x01\x66\x0c\xf2\x9a\xfe\x89\x33\x09\xb6\x60\x2c\x06\xcd\xa1\x91\xd5\x89\x4c\x06\x8a\x87\x81\xf1\x9b\x24\xa5\x4d\xbd\x07\xa1\x6b\xd8\x09\xf2\xc0\x44\x8d\xf4\x66\x87\xb5\x8d\xe1\x89\xa4\x1d\x96\x67\x0a\xd0\xe4\x41\xa7\x75\x44\x72\x82\xe9\xe3\xf6\x26\x0e\x4f\x4a\xa0\xfc\xc8\xa4\x8a\x8f\xe1\x06\xf9\x11\x10\x51\xc7\xe2\x08\xc5\xe3\x02\xb9\x89\x9a\x50\x33\xa9\x62\x8c\x1c\xd7\xa8\x0a\x83\x78\x11\x82\x10\xb4\x07\xf2\x22\x3e\x37\x15\xbe\x31\x2d\xab\x2a\x0d\xf5\x59\xdc\xac\x72\xe9\x87\x43\x39\xbf\x14\xf8\x04\x22\x36\x81\x33\x7e\x29\xb8\xec\xc1\x70\x01\x92\x84\xab\xcf\x09\xf4\xcb\x5d\x57\x7c\x2e\x78\x55\xfb\x1f\x21\x69\x66\x97\x0b\x89\x95\xf7\x5d\x5f\x2c\x0e\xe3\x0a\x8a\x89\x56\x6a\x54\xe8\xba\x41\x80\xf4\xe8\xf5\x02\xed\x4f\x37\x79\xcd\x4a\x9f\x81\xce\xc0\xb6\x6c\xbc\x91\x7a\x75\x22\x4c\x5e\x54\x3f\x46\xa9\x2e\x78\x80\x08\x70\xe3\xdb\x5e\xec\x36\x2f\xdb\x0d\x9b\x58\x8b\x43\x04\x23\x09\x61\x67\xf7\x0a\x72\x32\x1b\xe6\xa7\x4f\x91\x36\xe1\xa2\xfd\xcf\x22\xad\x87\xd4\x3b\x67\x7c\x8d\x1c\x98\xea\xb2\x95\xd6\xdd\x5c\x1b\xc9\x02\xde\xf6\xca\x25\x3c\xc7\xa5\x3f\x56\xb4\x11\xc2\x74\x30\xce\x84\x9a\x79\xd7\xfa\x74\xef\xb3\x60\x53\x5c\x43\x9c\xba\x24\xfc\x6e\xf1\x99\x34\x66\xea\x8e\x22\x9a\xfb\x5f\xd0\x3f\xc2\x9e\x91\x2e\x44\x29\x07\x5a\x21\x51\xc1\xf4\x18\x82\xa9\x82\x0a\x84\x87\x80\x37\x7b\x82\xe5\x11\xca\x35\x34\x82\x29\x56\x30\x12\xe7\x18\xce\xe3\x67\xbc\x33\x4c\x27\xcc\xae\xae\xcd\xb8\xc0\x27\x4c\xa0\xb2\x38\xde\x33\x46\x00\xd1\x49\xa2\x10\x80\xca\x9c\x51\x72\x8e\x40\x4a\x85\x44\xf0\xf8\x27\xa1\x68\x04\x56\xe7\x9c\x71\xb5\x79\x55\x28\x8f\x75\x2e\xf1\x0f\x98\xfa\xde\xc5\xea\xfb\x89\x32\x6b\x43\x56\x3f\x2b\x79\x2d\xf7\xf3\x99\xed\x2b\xb9\x8d\x64\x8d\x28\xae\x73\x9c\x45\x7c\x33\x0d\x72\xcb\xe0\x6a\x0d\x78\xe6\xf0\xbd\x0a\x43\x35\xd4\xa2\xab\x38\x03\xb5\x3c\xcb\x42\xbd\xac\xb6\x96\xaa\xc4\x34\x67\x1c\x68\xd0\x37\xa4\x62\x3c\xaf\x04\x2a\x20\xe7\x20\x30\x73\x8a\x62\x12\x60\xcb\x46\x20\xbd\xfe\x7c\x1a\x89\x2b\x8a\xdc\x71\xc7\x80\xaa\x9a\x1f\x5e\xd8\x04\x50\x2a\xec\xec\x0d\xc1\x35\xf6\x3b\x8d\xc3\x6a\x23\xea\xb5\xae\x56\x73\x97\x68\x0b\xe5\x59\x54\xc8\x5e\x38\x21\x2c\x1f\x10\x22\x4e\x06\x47\x24\x56\xa4\x8e\xd6\x31\x0f\x9e\xfc\xe4\x3a\x37\x38\xf7\x35\xb9\x99\x6a\xe7\xbb\xe9\x37\x92\x39\xf1\xab\x4a\x2f\x7b\x1b\x99\xb7\xfa\x71\x3b\x55\x23\x83\x87\xb8\x16\x43\xe5\xd2\x01\x64\x84\x1a\x57\x2c\x9b\x66\x0b\x7d\xa8\xd1\x4e\x50\x62\xf7\x6e\x77\x16\x67\x2b\x2e\x49\xac\xfe\xc2\x30\x81\xab\xfb\x6f\x42\x83\xb7\x25\xcb\x37\x11\x3d\xc8\x7b\x4b\x80\x25\xda\x5b\xfd\x71\x97\x73\x6b\x6b\x14\xa7\x70\x8c\x11\xa0\x04\xb6\xf4\x32\x04\x6a\x33\x9e\x80\xfc\x35\x9b\x7c\x0a\xd7\xc0\x1a\x77\xc2\xdb\x99\xf6\xdd\x13\xa5\xc6\x2d\x4a\x40\xa9\x06\xd2\xd6\xe9\xc3\xa8\xd4\xe1\x2c\x10\x54\x5c\x8c\x93\x08\xe0\x04\x17\x48\x86\x02\xd1\x15\xcd\xa4\x86\x97\x48\x41\xde\xdd\xa2\xaf\x0a\xfd\x0b\x31\x9f\x23\x81\x08\x01\x82\x65\x1d\x13\x43\xd3\x12\x08\x3a\xbf\x28\x7d\xb6\xe4\x07\x84\x49\x23\x20\x47\x85\xea\x2f\xea\x03\x36\x97\xd6\x8c\x62\xc5\x9c\x11\x22\x6e\xc9\x1a\x3d\xe5\xc3\xb2\x2d\x24\x54\xd9\x4c\x8b\xfa\xd8\x3e\x90\x61\x09\x5d\xe1\xb7\x2e\x3b\x2f\xa8\xe8\x92\xeb\x3d\x16\x33\xac\x38\x63\x5d\x80\xd4\x91\x64\x6c\xd3\x05\xe9\x83\xa9\xa5\x3f\x65\xe4\x9c\x11\x5c\x9c\xb7\xe2\xb0\x60\xb4\x13\x72\x8c\x41\x5c\x69\x81\xda\x1c\x74\x29\x54\x73\x15\x74\xd6\x96\xe0\x3b\xa6\x25\xfb\xbe\x62\xc1\xed\x4c\x89\x13\x54\x80\x15\xef\xae\x15\xb4\x54\x02\x61\xaa\x56\xa7\xf3\x6b\xd9\xba\x22\x9b\x8f\xf6\x19\x88\xfa\x23\x2e\xfc\xea\xc1\x13\xe9\x0b\xde\x04\x7b\xb7\x35\xd4\x4c\x38\x0d\x70\x83\x67\x39\x21\x16\x07\xd8\x06\x59\x2d\xaa\xd9\xdf\xa3\x72\xc6\xb7\x3f\x6d\x84\x1b\xfa\x59\x38\x20\x61\x8e\xea\xad\xbc\x23\xfa\xfa\x23\x75\xe6\xe0\x64\xb9\x6f\x91\xf8\x7b\x17\xa1\x5d\x87\xf7\xde\x23\x64\xb3\xa7\x9e\x16\xc2\xfc\x94\xb1\x65\x53\x6c\xc3\xa0\x37\xdc\x5c\x7a\xb4\xfa\x30\xd6\xcc\x37\xa3\xac\xb2\x68\x15\x7b\xaf\x0d\xb7\xdb\x7f\x5b\xbe\xdb\x2d\x02\x57\x9d\x8f\x94\x42\xc5\x31\xea\x48\xb0\xb2\x68\xbc\x22\x0e\xf5\x4f\xd5\x02\x61\xa8\x47\xfd\x13\x85\xfe\x26\x36\xfb\xd7\xd9\x57\xff\x32\x30\xf8\x24\xaf\x45\xbd\x38\x8f\x47\xbc\x43\xfb\x05\x74\xf6\xd6\xaa\x98\xf6\x20\x0d\x95\xcc\xdb\x03\x4b\x92\x8c\xbe\x28\xed\x29\xb2\xe9\x36\x6c\x98\xe3\xf1\xf6\x34\x99\x2e\xf5\x9c\x06\x88\xe7\x2a\xc6\x5a\xb4\x17\xe2\x32\xe7\x1b\x06\x9b\xbb\x0f\x0b\x25\xc3\xd2\x83\x86\x57\xca\xb5\x1b\xf4\xf3\xdc\x3a\xb5\xce\x19\x83\x74\xe7\x0f\x72\x3d\xfe\x6f\xd0\xcf\x9e\xe7\x6a\x3e\xe9\x79\xd6\xbe\xfa\x39\xed\xc9\x76\x4f\x6b\xb3\x89\x7c\x2c\x48\xf7\x3c\xc8\x88\xee\x99\x79\xf4\xf2\xa9\xd1\xf9\x68\xd7\xee\x08\x0f\x8f\x67\x3d\x17\x20\x3b\xf3\x6f\xfb\xd0\x79\xf7\xbc\xfb\x33\x00\x00\xff\xff\xfa\xcc\x57\x15\x61\x31\x00\x00")
|
|
| 92 |
+var _dataConfig_schema_v31Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5b\xcd\x73\xdc\x28\x16\xbf\xf7\x5f\xa1\x52\x72\x8b\x3f\xb2\xb5\xa9\xad\xda\xdc\xf6\xb8\xa7\x99\xf3\xb8\x3a\x2a\x5a\x7a\xad\x26\x46\x40\x00\xb5\xdd\x49\xf9\x7f\x9f\xd2\x67\x03\x02\x81\xba\xe5\x38\x33\x35\x27\xdb\xe2\xf7\x80\xf7\xfd\x1e\xe0\x1f\x9b\x24\x49\xdf\xcb\xfc\x00\x15\x4a\x3f\x27\xe9\x41\x29\xfe\xf9\xfe\xfe\xab\x64\xf4\xb6\xfb\x7a\xc7\x44\x79\x5f\x08\xb4\x57\xb7\x1f\x3f\xdd\x77\xdf\xde\xa5\x37\x0d\x1d\x2e\x1a\x92\x9c\xd1\x3d\x2e\xb3\x6e\x24\x3b\xfe\xfb\xee\x5f\x77\x0d\x79\x07\x51\x27\x0e\x0d\x88\xed\xbe\x42\xae\xba\x6f\x02\xbe\xd5\x58\x40\x43\xfc\x90\x1e\x41\x48\xcc\x68\xba\xbd\xd9\x34\x63\x5c\x30\x0e\x42\x61\x90\xe9\xe7\xa4\xd9\x5c\x92\x8c\x90\xe1\x83\x36\xad\x54\x02\xd3\x32\x6d\x3f\xbf\xb4\x33\x24\x49\x2a\x41\x1c\x71\xae\xcd\x30\x6e\xf5\xdd\xfd\x79\xfe\xfb\x11\x76\x63\xcf\xaa\x6d\xb6\xfd\xce\x91\x52\x20\xe8\xef\xd3\xbd\xb5\xc3\x5f\x1e\xd0\xed\xf7\xff\xdd\xfe\xf1\xf1\xf6\xbf\x77\xd9\xed\xf6\xc3\x7b\x63\xb8\x91\xaf\x80\x7d\xb7\x7c\x01\x7b\x4c\xb1\xc2\x8c\x8e\xeb\xa7\x23\xf2\xa5\xff\xed\x65\x5c\x18\x15\x45\x0b\x46\xc4\x58\x7b\x8f\x88\x04\x93\x67\x0a\xea\x89\x89\xc7\x10\xcf\x23\xec\x8d\x78\xee\xd7\x77\xf0\x6c\xb2\x73\x64\xa4\xae\x82\x1a\x1c\x50\x6f\xc4\x4c\xb7\xfc\x3a\xfa\x93\x90\x0b\x50\x61\x93\xed\x50\x6f\x66\xb1\xcd\xf2\xd7\x31\xbc\x19\x98\x9e\xc5\x76\x08\x6d\xed\x76\x83\x86\x7b\xbb\x44\xe5\x72\x2f\xbf\xac\x46\x61\x79\xa4\x54\x00\x27\xec\xd4\x7c\xf3\xc8\xa3\x03\x54\x40\x55\x3a\x8a\x20\x49\xd2\x5d\x8d\x49\x61\x4b\x94\x51\xf8\xad\x99\xe2\x41\xfb\x98\x24\x3f\xec\x48\xa6\xcd\xd3\x8e\x1b\x7f\xf9\x15\x3e\x8e\x7b\x78\x19\xc7\x73\x46\x15\x3c\xab\x96\xa9\xf9\xa5\x3b\x11\xb0\xfc\x11\xc4\x1e\x13\x88\xa5\x40\xa2\x94\x33\x22\x23\x58\xaa\x8c\x89\xac\xc0\xb9\x4a\x5f\x2c\xf2\xc9\x7c\x61\x7b\x1a\x49\xb5\xbf\xb6\x1b\xc7\x84\x69\x8e\x78\x86\x8a\xc2\xe0\x03\x09\x81\x4e\xe9\x4d\x92\x62\x05\x95\x74\xb3\x98\xa4\x35\xc5\xdf\x6a\xf8\x7f\x0f\x51\xa2\x06\x7b\xde\x42\x30\xbe\xfe\xc4\xa5\x60\x35\xcf\x38\x12\x8d\x81\xcd\x8b\x3f\xcd\x59\x55\x21\xba\x96\xd5\x2d\xe1\x23\x42\xf2\x8c\x2a\x84\x29\x88\x8c\xa2\x2a\x64\x48\x8d\xd7\x01\x2d\x64\xd6\x25\xfc\x59\x33\xda\x67\x1d\xbd\xb4\x26\x18\xb3\xff\xaa\xfa\x28\xe8\x9c\x61\x77\xd3\x34\xa6\xdd\xec\x2d\xb5\x08\x33\x09\x48\xe4\x87\x0b\xe9\x59\x85\x30\x8d\x91\x1d\x50\x25\x4e\x9c\xe1\xce\x5e\x7e\x39\x43\x00\x7a\xcc\xc6\x58\xb2\x58\x0c\x40\x8f\x58\x30\x5a\x0d\xde\x10\x13\x60\xc6\x20\xdf\xd0\x3f\x73\x26\xc1\x16\x8c\xc5\xa0\x3e\x34\xb2\x6a\xc8\x64\xa0\x78\x18\x18\xbf\x49\x52\x5a\x57\x3b\x10\x4d\x0d\x6b\x20\xf7\x4c\x54\xa8\xd9\xec\xb0\xb6\x36\x6c\x48\xda\x61\x79\xba\x00\x75\x1e\x9a\xb4\x8e\x48\x46\x30\x7d\x5c\xdf\xc4\xe1\x59\x09\x94\x1d\x98\x54\xf1\x31\x5c\x23\x3f\x00\x22\xea\x90\x1f\x20\x7f\x9c\x21\xd7\x51\x06\x35\x93\x2a\xc6\xc8\x71\x85\xca\x30\x88\xe7\x21\x08\x41\x3b\x20\x17\xf1\xb9\xaa\xf0\xb5\x69\x59\x59\x36\x50\x9f\xc5\x4d\x2a\x97\x7e\x38\x94\xf3\x0b\x81\x8f\x20\x62\x13\x38\xe3\xe7\x82\xcb\x1e\x0c\x17\x20\x49\xb8\xfa\x34\xa0\x5f\xee\xba\xe2\x73\xc6\xab\xda\xdf\x08\x49\xb7\x76\xb9\x90\x58\x79\xdf\xf5\xc5\xe2\x30\xae\xa0\x30\xb4\x52\xa1\xbc\xa9\x1b\x04\x48\x8f\x5e\xcf\xd0\xbe\xbb\xc9\x2a\x56\xf8\x0c\x74\x02\xb6\x65\xe3\x8d\xd4\x8b\x13\x61\x72\x51\xfd\x18\xa5\xba\x60\x03\x11\xe0\xc6\xb7\xbd\xd8\x6d\x9e\xb7\x1b\x36\xb1\x16\x87\x08\x46\x12\xc2\xce\xee\x15\xa4\x31\x1b\xe6\xc7\x4f\x91\x36\xe1\xa2\xfd\xcf\x2c\xad\x87\xd4\x3b\x67\x7c\x8d\x1c\x98\xea\xbc\x95\xd6\xdd\x5c\x1b\xd9\x06\xbc\xed\x95\x4b\x78\x8e\x0b\x7f\xac\x68\x23\x84\xee\x60\x9c\x09\x35\xf1\xae\xe5\xe9\xde\x67\xc1\xba\xb8\x86\x38\x75\x4e\xf8\xdd\xe2\x13\x69\x4c\xd4\x1d\x45\x34\xf5\xbf\xa0\x7f\x84\x3d\x23\x9d\x89\x52\x0e\xb4\x42\xa2\x04\xb3\x0d\xc1\x54\x41\x09\xc2\x43\xc0\xeb\x1d\xc1\xf2\x00\xc5\x12\x1a\xc1\x14\xcb\x19\x89\x73\x0c\x67\xfb\x19\xef\x0c\xe6\x84\xdb\xab\x6b\x33\x2e\xf0\x11\x13\x28\x2d\x8e\x77\x8c\x11\x40\xd4\x48\x14\x02\x50\x91\x31\x4a\x4e\x11\x48\xa9\x90\x08\xb6\x7f\x12\xf2\x5a\x60\x75\xca\x18\x57\xab\x57\x85\xf2\x50\x65\x12\x7f\x07\xd3\xf7\xce\x56\xdf\x4f\xb4\xb5\x36\x64\x9d\x67\x25\xaf\xe5\x7e\x3e\xb3\x7d\x25\xb7\x91\xac\x16\xf9\x75\x8e\x33\x8b\xaf\xcd\x20\x37\x0f\x2e\x97\x80\x27\x0e\xdf\xab\x30\x54\x43\xcd\xba\x8a\x33\x50\xcb\x93\xcc\xd5\x65\xb5\xb5\x54\x05\xa6\x19\xe3\x40\x83\xbe\x21\x15\xe3\x59\x29\x50\x0e\x19\x07\x81\x99\x53\x14\x46\x80\x2d\x6a\x81\x9a\xf5\xa7\xd3\x48\x5c\x52\xe4\x8e\x3b\x1a\x54\x55\x7c\x7f\xe1\x21\x80\x52\x61\x67\xaf\x09\xae\xb0\xdf\x69\x1c\x56\x1b\x51\xaf\x75\xb5\x9a\xbb\x44\x9b\x29\xcf\xa2\x42\xf6\x4c\x87\x30\xdf\x20\x44\x74\x06\x07\x24\x16\xa4\x8e\xd6\x31\xf7\x9e\xfc\xe4\xea\x1b\x9c\xfb\x32\x6e\xa6\xda\xf9\x6e\xfa\x8d\x6c\x9d\xf8\x45\xa5\x97\xbd\x8d\xad\xb7\xfa\x71\x3b\x55\x2d\x83\x4d\x5c\x8b\xa1\x72\xae\x01\x19\xa1\xd3\x2b\x96\xe4\x2f\x11\xa1\x0d\x1d\xb5\x70\x87\x6e\x22\xe2\x78\xbf\x52\x64\xec\x7c\xed\xa8\x1f\x5d\x11\x68\x34\x3b\x3c\x39\xf0\x5d\x22\xc9\x38\x39\x8d\x28\x54\x76\xa1\x33\xba\x67\x89\x77\xbb\xfe\x22\xed\xa7\xb0\x42\x59\xce\xb8\x47\xca\xf1\x6c\x2c\xcd\x98\xd6\x29\xc4\x4c\x49\xe9\xf3\xfe\x27\x26\x1e\x9b\xdc\x52\x60\x77\x10\xd8\x58\x24\x0b\xee\x1e\xad\x63\xbb\x61\x02\xd7\xa5\x9a\x0e\x0d\x5e\x42\xce\x5f\xf0\xf5\x20\xef\xe5\x1b\x96\x68\x67\x5d\x3b\xb9\x72\x66\x13\xe4\xc5\x31\x9c\xba\x05\x28\x81\xad\x5b\x81\xa1\xfe\xd1\xd3\x34\xc8\x5f\xf3\xec\x5c\xe1\x0a\x58\xed\x8e\x28\x1b\xdd\x70\x7a\xa2\x54\xbb\x9c\x0c\x28\x55\x43\xda\x3a\x7d\x18\x95\x3a\xb4\xd8\x41\xc5\xc5\xe4\x1e\x01\x9c\xe0\x1c\xc9\x50\x7e\xbf\xe2\x8c\xb6\xe6\x05\x52\x90\x75\x8f\x53\x16\x55\x54\x33\xa5\x14\x47\x02\x11\x02\x04\xcb\x2a\xa6\x34\x49\x0b\x20\xe8\x74\x51\x55\xda\x92\xef\x11\x26\xb5\x80\x0c\xe5\xde\xc8\x6b\x51\x54\x8c\x62\xc5\x9c\x11\x22\x6e\xc9\x0a\x3d\x67\xc3\xb2\x2d\x24\xd4\x30\x98\xbd\x72\xec\xf1\xaa\x66\x09\x5d\x66\x5d\x56\xf4\xce\xa8\xe8\x5c\x42\x7b\x2c\x66\x58\x71\xc2\xba\x00\xd9\x44\x92\xf1\xf4\x3b\x48\x1f\x8c\xd9\x7d\xf3\x9e\x71\x46\x70\x7e\x5a\x8b\xc3\x9c\xd1\x4e\xc8\x31\x06\x71\xa5\x05\x36\xe6\xd0\x74\x18\x15\x57\x41\x67\x6d\x09\x9e\x30\x2d\xd8\xd3\x82\x05\xd7\x33\x25\x4e\x50\x0e\x56\xbc\xbb\x56\xd0\x52\x09\x84\xa9\x5a\x7c\xd9\x73\x2d\x5b\x57\x64\xf3\xd1\x3e\x03\x51\x7f\xc4\x05\xf3\xb8\x2f\xd2\xe7\xbc\x0e\x5e\x89\x54\x50\x31\xe1\x34\xc0\x15\x5e\xbb\x85\x58\x1c\x60\x2b\x64\xb5\xa8\x3b\xb4\x1e\x95\x31\xbe\x7e\x13\x1f\xbe\x27\xdb\x86\x03\x12\xe6\xa8\x5a\xcb\x3b\xa2\x6f\x15\x53\x67\x0e\x4e\xe6\x9b\xcd\xc4\xdf\x70\x86\x76\x1d\xde\x7b\x8f\x90\xf5\x8e\x7a\x7a\xb4\x69\x7d\xbf\xe6\x59\xf3\x8a\x41\x6f\x78\x10\xe0\xd1\xea\xc3\x58\x33\xdf\x8c\xb2\xda\x46\xab\xd8\x7b\x1b\xbf\xde\xfe\xdb\xf2\xdd\x3e\x79\x73\xd5\xf9\x48\x29\x94\x1f\xa2\x5a\x82\x85\x45\xe3\x15\x71\x68\xd2\xb8\x3a\xc3\x50\x8f\xfa\x27\x0a\xfd\x4d\x6c\xf6\xe7\xd9\x57\xff\xe0\x36\xf8\xd2\xb5\x45\x5d\x9c\xc7\x23\x9e\x77\xfe\x02\x3a\x7b\x6b\x55\x98\x47\xfb\x9a\x4a\xa6\xc7\x03\x73\x92\x8c\x7e\x7f\xd0\x53\x6c\xcd\x6d\xd8\x30\xc7\xff\x44\x98\xc9\x74\xee\xe2\x6f\x80\x78\x8e\xa3\xac\x45\x7b\x21\xce\x73\xbe\x62\xb0\xb9\xfb\x30\x53\x32\xcc\xbd\x13\x7a\xa5\x5c\xbb\xc2\xa5\xaa\x5b\xa7\x56\x9f\x31\x48\x77\xfa\xce\xdd\xe3\xff\x1a\xfd\xe4\xd5\x7b\xc3\x27\x3d\x4d\x8e\xaf\x7e\x98\xc7\xe8\xdd\x8b\xf5\xad\x21\x1f\x0b\xd2\xbd\xba\xd3\xa2\xfb\x56\x6f\xbd\x7c\x6a\x74\xbe\x85\xb7\x0f\xf1\x87\x37\xe9\x9e\x7b\xc5\x8d\xfe\xb3\xfd\xff\x81\xcd\xcb\xe6\xcf\x00\x00\x00\xff\xff\xea\x87\x24\xae\xb8\x34\x00\x00")
|
|
| 93 | 93 |
|
| 94 | 94 |
func dataConfig_schema_v31JsonBytes() ([]byte, error) {
|
| 95 | 95 |
return bindataRead( |
| ... | ... |
@@ -235,7 +235,37 @@ |
| 235 | 235 |
}, |
| 236 | 236 |
"user": {"type": "string"},
|
| 237 | 237 |
"userns_mode": {"type": "string"},
|
| 238 |
- "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
|
| 238 |
+ "volumes": {
|
|
| 239 |
+ "type": "array", |
|
| 240 |
+ "items": {
|
|
| 241 |
+ "oneOf": [ |
|
| 242 |
+ {"type": "string"},
|
|
| 243 |
+ {
|
|
| 244 |
+ "type": "object", |
|
| 245 |
+ "required": ["type"], |
|
| 246 |
+ "properties": {
|
|
| 247 |
+ "type": {"type": "string"},
|
|
| 248 |
+ "source": {"type": "string"},
|
|
| 249 |
+ "target": {"type": "string"},
|
|
| 250 |
+ "read_only": {"type": "boolean"},
|
|
| 251 |
+ "bind": {
|
|
| 252 |
+ "type": "object", |
|
| 253 |
+ "properties": {
|
|
| 254 |
+ "propagation": {"type": "string"}
|
|
| 255 |
+ } |
|
| 256 |
+ }, |
|
| 257 |
+ "volume": {
|
|
| 258 |
+ "type": "object", |
|
| 259 |
+ "properties": {
|
|
| 260 |
+ "nocopy": {"type": "boolean"}
|
|
| 261 |
+ } |
|
| 262 |
+ } |
|
| 263 |
+ } |
|
| 264 |
+ } |
|
| 265 |
+ ], |
|
| 266 |
+ "uniqueItems": true |
|
| 267 |
+ } |
|
| 268 |
+ }, |
|
| 239 | 269 |
"working_dir": {"type": "string"}
|
| 240 | 270 |
}, |
| 241 | 271 |
"additionalProperties": false |
| ... | ... |
@@ -78,18 +78,27 @@ func Validate(config map[string]interface{}, version string) error {
|
| 78 | 78 |
|
| 79 | 79 |
func toError(result *gojsonschema.Result) error {
|
| 80 | 80 |
err := getMostSpecificError(result.Errors()) |
| 81 |
- description := getDescription(err) |
|
| 82 |
- return fmt.Errorf("%s %s", err.Field(), description)
|
|
| 81 |
+ return err |
|
| 83 | 82 |
} |
| 84 | 83 |
|
| 85 |
-func getDescription(err gojsonschema.ResultError) string {
|
|
| 86 |
- if err.Type() == "invalid_type" {
|
|
| 87 |
- if expectedType, ok := err.Details()["expected"].(string); ok {
|
|
| 84 |
+const ( |
|
| 85 |
+ jsonschemaOneOf = "number_one_of" |
|
| 86 |
+ jsonschemaAnyOf = "number_any_of" |
|
| 87 |
+) |
|
| 88 |
+ |
|
| 89 |
+func getDescription(err validationError) string {
|
|
| 90 |
+ switch err.parent.Type() {
|
|
| 91 |
+ case "invalid_type": |
|
| 92 |
+ if expectedType, ok := err.parent.Details()["expected"].(string); ok {
|
|
| 88 | 93 |
return fmt.Sprintf("must be a %s", humanReadableType(expectedType))
|
| 89 | 94 |
} |
| 95 |
+ case jsonschemaOneOf, jsonschemaAnyOf: |
|
| 96 |
+ if err.child == nil {
|
|
| 97 |
+ return err.parent.Description() |
|
| 98 |
+ } |
|
| 99 |
+ return err.child.Description() |
|
| 90 | 100 |
} |
| 91 |
- |
|
| 92 |
- return err.Description() |
|
| 101 |
+ return err.parent.Description() |
|
| 93 | 102 |
} |
| 94 | 103 |
|
| 95 | 104 |
func humanReadableType(definition string) string {
|
| ... | ... |
@@ -113,23 +122,45 @@ func humanReadableType(definition string) string {
|
| 113 | 113 |
return definition |
| 114 | 114 |
} |
| 115 | 115 |
|
| 116 |
-func getMostSpecificError(errors []gojsonschema.ResultError) gojsonschema.ResultError {
|
|
| 117 |
- var mostSpecificError gojsonschema.ResultError |
|
| 116 |
+type validationError struct {
|
|
| 117 |
+ parent gojsonschema.ResultError |
|
| 118 |
+ child gojsonschema.ResultError |
|
| 119 |
+} |
|
| 118 | 120 |
|
| 119 |
- for _, err := range errors {
|
|
| 120 |
- if mostSpecificError == nil {
|
|
| 121 |
- mostSpecificError = err |
|
| 122 |
- } else if specificity(err) > specificity(mostSpecificError) {
|
|
| 123 |
- mostSpecificError = err |
|
| 124 |
- } else if specificity(err) == specificity(mostSpecificError) {
|
|
| 121 |
+func (err validationError) Error() string {
|
|
| 122 |
+ description := getDescription(err) |
|
| 123 |
+ return fmt.Sprintf("%s %s", err.parent.Field(), description)
|
|
| 124 |
+} |
|
| 125 |
+ |
|
| 126 |
+func getMostSpecificError(errors []gojsonschema.ResultError) validationError {
|
|
| 127 |
+ mostSpecificError := 0 |
|
| 128 |
+ for i, err := range errors {
|
|
| 129 |
+ if specificity(err) > specificity(errors[mostSpecificError]) {
|
|
| 130 |
+ mostSpecificError = i |
|
| 131 |
+ continue |
|
| 132 |
+ } |
|
| 133 |
+ |
|
| 134 |
+ if specificity(err) == specificity(errors[mostSpecificError]) {
|
|
| 125 | 135 |
// Invalid type errors win in a tie-breaker for most specific field name |
| 126 |
- if err.Type() == "invalid_type" && mostSpecificError.Type() != "invalid_type" {
|
|
| 127 |
- mostSpecificError = err |
|
| 136 |
+ if err.Type() == "invalid_type" && errors[mostSpecificError].Type() != "invalid_type" {
|
|
| 137 |
+ mostSpecificError = i |
|
| 128 | 138 |
} |
| 129 | 139 |
} |
| 130 | 140 |
} |
| 131 | 141 |
|
| 132 |
- return mostSpecificError |
|
| 142 |
+ if mostSpecificError+1 == len(errors) {
|
|
| 143 |
+ return validationError{parent: errors[mostSpecificError]}
|
|
| 144 |
+ } |
|
| 145 |
+ |
|
| 146 |
+ switch errors[mostSpecificError].Type() {
|
|
| 147 |
+ case "number_one_of", "number_any_of": |
|
| 148 |
+ return validationError{
|
|
| 149 |
+ parent: errors[mostSpecificError], |
|
| 150 |
+ child: errors[mostSpecificError+1], |
|
| 151 |
+ } |
|
| 152 |
+ default: |
|
| 153 |
+ return validationError{parent: errors[mostSpecificError]}
|
|
| 154 |
+ } |
|
| 133 | 155 |
} |
| 134 | 156 |
|
| 135 | 157 |
func specificity(err gojsonschema.ResultError) int {
|
| ... | ... |
@@ -119,7 +119,7 @@ type ServiceConfig struct {
|
| 119 | 119 |
Tty bool `mapstructure:"tty"` |
| 120 | 120 |
Ulimits map[string]*UlimitsConfig |
| 121 | 121 |
User string |
| 122 |
- Volumes []string |
|
| 122 |
+ Volumes []ServiceVolumeConfig |
|
| 123 | 123 |
WorkingDir string `mapstructure:"working_dir"` |
| 124 | 124 |
} |
| 125 | 125 |
|
| ... | ... |
@@ -223,6 +223,26 @@ type ServicePortConfig struct {
|
| 223 | 223 |
Protocol string |
| 224 | 224 |
} |
| 225 | 225 |
|
| 226 |
+// ServiceVolumeConfig are references to a volume used by a service |
|
| 227 |
+type ServiceVolumeConfig struct {
|
|
| 228 |
+ Type string |
|
| 229 |
+ Source string |
|
| 230 |
+ Target string |
|
| 231 |
+ ReadOnly bool `mapstructure:"read_only"` |
|
| 232 |
+ Bind *ServiceVolumeBind |
|
| 233 |
+ Volume *ServiceVolumeVolume |
|
| 234 |
+} |
|
| 235 |
+ |
|
| 236 |
+// ServiceVolumeBind are options for a service volume of type bind |
|
| 237 |
+type ServiceVolumeBind struct {
|
|
| 238 |
+ Propagation string |
|
| 239 |
+} |
|
| 240 |
+ |
|
| 241 |
+// ServiceVolumeVolume are options for a service volume of type volume |
|
| 242 |
+type ServiceVolumeVolume struct {
|
|
| 243 |
+ NoCopy bool `mapstructure:"nocopy"` |
|
| 244 |
+} |
|
| 245 |
+ |
|
| 226 | 246 |
// ServiceSecretConfig is the secret configuration for a service |
| 227 | 247 |
type ServiceSecretConfig struct {
|
| 228 | 248 |
Source string |
| ... | ... |
@@ -102,7 +102,7 @@ func (m *MountOpt) Set(value string) error {
|
| 102 | 102 |
case "volume-nocopy": |
| 103 | 103 |
volumeOptions().NoCopy, err = strconv.ParseBool(value) |
| 104 | 104 |
if err != nil {
|
| 105 |
- return fmt.Errorf("invalid value for populate: %s", value)
|
|
| 105 |
+ return fmt.Errorf("invalid value for volume-nocopy: %s", value)
|
|
| 106 | 106 |
} |
| 107 | 107 |
case "volume-label": |
| 108 | 108 |
setValueOnMap(volumeOptions().Labels, value) |