Merge pull request #30515 from vieux/1.13.1-rc1-changelog
bump to 1.13.1-rc1
diff --git a/api/swagger.yaml b/api/swagger.yaml
index ad97bb2..2f5b975 100644
--- a/api/swagger.yaml
+++ b/api/swagger.yaml
@@ -7678,3 +7678,36 @@
type: "string"
description: "ID of the secret"
tags: ["Secret"]
+ /secrets/{id}/update:
+ post:
+ summary: "Update a Secret"
+ operationId: "SecretUpdate"
+ responses:
+ 200:
+ description: "no error"
+ 404:
+ description: "no such secret"
+ schema:
+ $ref: "#/definitions/ErrorResponse"
+ 500:
+ description: "server error"
+ schema:
+ $ref: "#/definitions/ErrorResponse"
+ parameters:
+ - name: "id"
+ in: "path"
+ description: "The ID of the secret"
+ type: "string"
+ required: true
+ - name: "body"
+ in: "body"
+ schema:
+ $ref: "#/definitions/SecretSpec"
+ description: "The spec of the secret to update. Currently, only the Labels field can be updated. All other fields must remain unchanged from the [SecretInspect endpoint](#operation/SecretInspect) response values."
+ - name: "version"
+ in: "query"
+ description: "The version number of the secret object being updated. This is required to avoid conflicting writes."
+ type: "integer"
+ format: "int64"
+ required: true
+ tags: ["Secret"]
diff --git a/cli/command/plugin/disable.go b/cli/command/plugin/disable.go
index c3d36e2..07b0ec2 100644
--- a/cli/command/plugin/disable.go
+++ b/cli/command/plugin/disable.go
@@ -14,7 +14,7 @@
var force bool
cmd := &cobra.Command{
- Use: "disable PLUGIN",
+ Use: "disable [OPTIONS] PLUGIN",
Short: "Disable a plugin",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
diff --git a/cli/command/plugin/push.go b/cli/command/plugin/push.go
index b076630..9abb38e 100644
--- a/cli/command/plugin/push.go
+++ b/cli/command/plugin/push.go
@@ -16,7 +16,7 @@
func newPushCommand(dockerCli *command.DockerCli) *cobra.Command {
cmd := &cobra.Command{
- Use: "push PLUGIN[:TAG]",
+ Use: "push [OPTIONS] PLUGIN[:TAG]",
Short: "Push a plugin to a registry",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
diff --git a/cli/command/secret/utils.go b/cli/command/secret/utils.go
index 4249389..11d31ff 100644
--- a/cli/command/secret/utils.go
+++ b/cli/command/secret/utils.go
@@ -11,7 +11,8 @@
"golang.org/x/net/context"
)
-func getSecretsByNameOrIDPrefixes(ctx context.Context, client client.APIClient, terms []string) ([]swarm.Secret, error) {
+// GetSecretsByNameOrIDPrefixes returns secrets given a list of ids or names
+func GetSecretsByNameOrIDPrefixes(ctx context.Context, client client.APIClient, terms []string) ([]swarm.Secret, error) {
args := filters.NewArgs()
for _, n := range terms {
args.Add("names", n)
@@ -24,7 +25,7 @@
}
func getCliRequestedSecretIDs(ctx context.Context, client client.APIClient, terms []string) ([]string, error) {
- secrets, err := getSecretsByNameOrIDPrefixes(ctx, client, terms)
+ secrets, err := GetSecretsByNameOrIDPrefixes(ctx, client, terms)
if err != nil {
return nil, err
}
diff --git a/cli/command/service/create.go b/cli/command/service/create.go
index ca2bb08..1355c19 100644
--- a/cli/command/service/create.go
+++ b/cli/command/service/create.go
@@ -62,7 +62,7 @@
specifiedSecrets := opts.secrets.Value()
if len(specifiedSecrets) > 0 {
// parse and validate secrets
- secrets, err := parseSecrets(apiClient, specifiedSecrets)
+ secrets, err := ParseSecrets(apiClient, specifiedSecrets)
if err != nil {
return err
}
diff --git a/cli/command/service/parse.go b/cli/command/service/parse.go
index ff3249e..ce9b454 100644
--- a/cli/command/service/parse.go
+++ b/cli/command/service/parse.go
@@ -10,9 +10,9 @@
"golang.org/x/net/context"
)
-// parseSecrets retrieves the secrets from the requested names and converts
+// ParseSecrets retrieves the secrets from the requested names and converts
// them to secret references to use with the spec
-func parseSecrets(client client.APIClient, requestedSecrets []*types.SecretRequestOption) ([]*swarmtypes.SecretReference, error) {
+func ParseSecrets(client client.SecretAPIClient, requestedSecrets []*types.SecretRequestOption) ([]*swarmtypes.SecretReference, error) {
secretRefs := make(map[string]*swarmtypes.SecretReference)
ctx := context.Background()
diff --git a/cli/command/service/update.go b/cli/command/service/update.go
index 514b1bd..d56de10 100644
--- a/cli/command/service/update.go
+++ b/cli/command/service/update.go
@@ -431,11 +431,11 @@
*field = removeItems(*field, toRemove, envKey)
}
-func getUpdatedSecrets(apiClient client.APIClient, flags *pflag.FlagSet, secrets []*swarm.SecretReference) ([]*swarm.SecretReference, error) {
+func getUpdatedSecrets(apiClient client.SecretAPIClient, flags *pflag.FlagSet, secrets []*swarm.SecretReference) ([]*swarm.SecretReference, error) {
if flags.Changed(flagSecretAdd) {
values := flags.Lookup(flagSecretAdd).Value.(*opts.SecretOpt).Value()
- addSecrets, err := parseSecrets(apiClient, values)
+ addSecrets, err := ParseSecrets(apiClient, values)
if err != nil {
return nil, err
}
diff --git a/cli/command/stack/common.go b/cli/command/stack/common.go
index 5c4996d..72719f9 100644
--- a/cli/command/stack/common.go
+++ b/cli/command/stack/common.go
@@ -48,3 +48,13 @@
ctx,
types.NetworkListOptions{Filters: getStackFilter(namespace)})
}
+
+func getStackSecrets(
+ ctx context.Context,
+ apiclient client.APIClient,
+ namespace string,
+) ([]swarm.Secret, error) {
+ return apiclient.SecretList(
+ ctx,
+ types.SecretListOptions{Filters: getStackFilter(namespace)})
+}
diff --git a/cli/command/stack/deploy.go b/cli/command/stack/deploy.go
index ec50ff1..980876a 100644
--- a/cli/command/stack/deploy.go
+++ b/cli/command/stack/deploy.go
@@ -1,7 +1,6 @@
package stack
import (
- "errors"
"fmt"
"io/ioutil"
"os"
@@ -12,10 +11,12 @@
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
+ secretcli "github.com/docker/docker/cli/command/secret"
"github.com/docker/docker/cli/compose/convert"
"github.com/docker/docker/cli/compose/loader"
composetypes "github.com/docker/docker/cli/compose/types"
dockerclient "github.com/docker/docker/client"
+ "github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
@@ -124,7 +125,16 @@
if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil {
return err
}
- services, err := convert.Services(namespace, config)
+
+ secrets, err := convert.Secrets(namespace, config.Secrets)
+ if err != nil {
+ return err
+ }
+ if err := createSecrets(ctx, dockerCli, namespace, secrets); err != nil {
+ return err
+ }
+
+ services, err := convert.Services(namespace, config, dockerCli.Client())
if err != nil {
return err
}
@@ -208,6 +218,37 @@
return nil
}
+func createSecrets(
+ ctx context.Context,
+ dockerCli *command.DockerCli,
+ namespace convert.Namespace,
+ secrets []swarm.SecretSpec,
+) error {
+ client := dockerCli.Client()
+
+ for _, secretSpec := range secrets {
+ // TODO: fix this after https://github.com/docker/docker/pull/29218
+ secrets, err := secretcli.GetSecretsByNameOrIDPrefixes(ctx, client, []string{secretSpec.Name})
+ switch {
+ case err != nil:
+ return err
+ case len(secrets) > 1:
+ return errors.Errorf("ambiguous secret name: %s", secretSpec.Name)
+ case len(secrets) == 0:
+ fmt.Fprintf(dockerCli.Out(), "Creating secret %s\n", secretSpec.Name)
+ _, err = client.SecretCreate(ctx, secretSpec)
+ default:
+ secret := secrets[0]
+ // Update secret to ensure that the local data hasn't changed
+ err = client.SecretUpdate(ctx, secret.ID, secret.Meta.Version, secretSpec)
+ }
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
func createNetworks(
ctx context.Context,
dockerCli *command.DockerCli,
diff --git a/cli/command/stack/remove.go b/cli/command/stack/remove.go
index 734ff92..966c1aa 100644
--- a/cli/command/stack/remove.go
+++ b/cli/command/stack/remove.go
@@ -3,11 +3,12 @@
import (
"fmt"
- "golang.org/x/net/context"
-
+ "github.com/docker/docker/api/types"
+ "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/spf13/cobra"
+ "golang.org/x/net/context"
)
type removeOptions struct {
@@ -33,41 +34,79 @@
func runRemove(dockerCli *command.DockerCli, opts removeOptions) error {
namespace := opts.namespace
client := dockerCli.Client()
- stderr := dockerCli.Err()
ctx := context.Background()
- hasError := false
services, err := getServices(ctx, client, namespace)
if err != nil {
return err
}
- for _, service := range services {
- fmt.Fprintf(stderr, "Removing service %s\n", service.Spec.Name)
- if err := client.ServiceRemove(ctx, service.ID); err != nil {
- hasError = true
- fmt.Fprintf(stderr, "Failed to remove service %s: %s", service.ID, err)
- }
- }
networks, err := getStackNetworks(ctx, client, namespace)
if err != nil {
return err
}
- for _, network := range networks {
- fmt.Fprintf(stderr, "Removing network %s\n", network.Name)
- if err := client.NetworkRemove(ctx, network.ID); err != nil {
- hasError = true
- fmt.Fprintf(stderr, "Failed to remove network %s: %s", network.ID, err)
- }
+
+ secrets, err := getStackSecrets(ctx, client, namespace)
+ if err != nil {
+ return err
}
- if len(services) == 0 && len(networks) == 0 {
+ if len(services)+len(networks)+len(secrets) == 0 {
fmt.Fprintf(dockerCli.Out(), "Nothing found in stack: %s\n", namespace)
return nil
}
+ hasError := removeServices(ctx, dockerCli, services)
+ hasError = removeSecrets(ctx, dockerCli, secrets) || hasError
+ hasError = removeNetworks(ctx, dockerCli, networks) || hasError
+
if hasError {
return fmt.Errorf("Failed to remove some resources")
}
return nil
}
+
+func removeServices(
+ ctx context.Context,
+ dockerCli *command.DockerCli,
+ services []swarm.Service,
+) bool {
+ var err error
+ for _, service := range services {
+ fmt.Fprintf(dockerCli.Err(), "Removing service %s\n", service.Spec.Name)
+ if err = dockerCli.Client().ServiceRemove(ctx, service.ID); err != nil {
+ fmt.Fprintf(dockerCli.Err(), "Failed to remove service %s: %s", service.ID, err)
+ }
+ }
+ return err != nil
+}
+
+func removeNetworks(
+ ctx context.Context,
+ dockerCli *command.DockerCli,
+ networks []types.NetworkResource,
+) bool {
+ var err error
+ for _, network := range networks {
+ fmt.Fprintf(dockerCli.Err(), "Removing network %s\n", network.Name)
+ if err = dockerCli.Client().NetworkRemove(ctx, network.ID); err != nil {
+ fmt.Fprintf(dockerCli.Err(), "Failed to remove network %s: %s", network.ID, err)
+ }
+ }
+ return err != nil
+}
+
+func removeSecrets(
+ ctx context.Context,
+ dockerCli *command.DockerCli,
+ secrets []swarm.Secret,
+) bool {
+ var err error
+ for _, secret := range secrets {
+ fmt.Fprintf(dockerCli.Err(), "Removing secret %s\n", secret.Spec.Name)
+ if err = dockerCli.Client().SecretRemove(ctx, secret.ID); err != nil {
+ fmt.Fprintf(dockerCli.Err(), "Failed to remove secret %s: %s", secret.ID, err)
+ }
+ }
+ return err != nil
+}
diff --git a/cli/compose/convert/compose.go b/cli/compose/convert/compose.go
index 7091a09..8122326 100644
--- a/cli/compose/convert/compose.go
+++ b/cli/compose/convert/compose.go
@@ -1,8 +1,11 @@
package convert
import (
+ "io/ioutil"
+
"github.com/docker/docker/api/types"
networktypes "github.com/docker/docker/api/types/network"
+ "github.com/docker/docker/api/types/swarm"
composetypes "github.com/docker/docker/cli/compose/types"
)
@@ -87,3 +90,27 @@
return result, externalNetworks
}
+
+// Secrets converts secrets from the Compose type to the engine API type
+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)
+ 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,
+ })
+ }
+ return result, nil
+}
diff --git a/cli/compose/convert/compose_test.go b/cli/compose/convert/compose_test.go
index 5417d2d..f333d73f 100644
--- a/cli/compose/convert/compose_test.go
+++ b/cli/compose/convert/compose_test.go
@@ -7,6 +7,7 @@
"github.com/docker/docker/api/types/network"
composetypes "github.com/docker/docker/cli/compose/types"
"github.com/docker/docker/pkg/testutil/assert"
+ "github.com/docker/docker/pkg/testutil/tempfile"
)
func TestNamespaceScope(t *testing.T) {
@@ -88,3 +89,34 @@
assert.DeepEqual(t, networks, expected)
assert.DeepEqual(t, externals, []string{"special"})
}
+
+func TestSecrets(t *testing.T) {
+ namespace := Namespace{name: "foo"}
+
+ secretText := "this is the first secret"
+ secretFile := tempfile.NewTempFile(t, "convert-secrets", secretText)
+ defer secretFile.Remove()
+
+ source := map[string]composetypes.SecretConfig{
+ "one": {
+ File: secretFile.Name(),
+ Labels: map[string]string{"monster": "mash"},
+ },
+ "ext": {
+ External: composetypes.External{
+ External: true,
+ },
+ },
+ }
+
+ specs, err := Secrets(namespace, source)
+ assert.NilError(t, err)
+ assert.Equal(t, len(specs), 1)
+ secret := specs[0]
+ assert.Equal(t, secret.Name, "foo_one")
+ assert.DeepEqual(t, secret.Labels, map[string]string{
+ "monster": "mash",
+ LabelNamespace: "foo",
+ })
+ assert.DeepEqual(t, secret.Data, []byte(secretText))
+}
diff --git a/cli/compose/convert/service.go b/cli/compose/convert/service.go
index 05e4fa4..4a54895 100644
--- a/cli/compose/convert/service.go
+++ b/cli/compose/convert/service.go
@@ -2,21 +2,27 @@
import (
"fmt"
+ "os"
"sort"
"time"
+ "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/swarm"
+ servicecli "github.com/docker/docker/cli/command/service"
composetypes "github.com/docker/docker/cli/compose/types"
+ "github.com/docker/docker/client"
"github.com/docker/docker/opts"
runconfigopts "github.com/docker/docker/runconfig/opts"
"github.com/docker/go-connections/nat"
)
// Services from compose-file types to engine API types
+// TODO: fix secrets API so that SecretAPIClient is not required here
func Services(
namespace Namespace,
config *composetypes.Config,
+ client client.SecretAPIClient,
) (map[string]swarm.ServiceSpec, error) {
result := make(map[string]swarm.ServiceSpec)
@@ -25,7 +31,12 @@
networks := config.Networks
for _, service := range services {
- serviceSpec, err := convertService(namespace, service, networks, volumes)
+
+ secrets, err := convertServiceSecrets(client, namespace, service.Secrets, config.Secrets)
+ if err != nil {
+ return nil, err
+ }
+ serviceSpec, err := convertService(namespace, service, networks, volumes, secrets)
if err != nil {
return nil, err
}
@@ -40,6 +51,7 @@
service composetypes.ServiceConfig,
networkConfigs map[string]composetypes.NetworkConfig,
volumes map[string]composetypes.VolumeConfig,
+ secrets []*swarm.SecretReference,
) (swarm.ServiceSpec, error) {
name := namespace.Scope(service.Name)
@@ -109,6 +121,7 @@
StopGracePeriod: service.StopGracePeriod,
TTY: service.Tty,
OpenStdin: service.StdinOpen,
+ Secrets: secrets,
},
LogDriver: logDriver,
Resources: resources,
@@ -178,6 +191,47 @@
return nets, nil
}
+// TODO: fix secrets API so that SecretAPIClient is not required here
+func convertServiceSecrets(
+ client client.SecretAPIClient,
+ namespace Namespace,
+ secrets []composetypes.ServiceSecretConfig,
+ secretSpecs map[string]composetypes.SecretConfig,
+) ([]*swarm.SecretReference, error) {
+ opts := []*types.SecretRequestOption{}
+ for _, secret := range secrets {
+ target := secret.Target
+ if target == "" {
+ target = secret.Source
+ }
+
+ source := namespace.Scope(secret.Source)
+ secretSpec := secretSpecs[secret.Source]
+ if secretSpec.External.External {
+ source = secretSpec.External.Name
+ }
+
+ uid := secret.UID
+ gid := secret.GID
+ if uid == "" {
+ uid = "0"
+ }
+ if gid == "" {
+ gid = "0"
+ }
+
+ opts = append(opts, &types.SecretRequestOption{
+ Source: source,
+ Target: target,
+ UID: uid,
+ GID: gid,
+ Mode: os.FileMode(secret.Mode),
+ })
+ }
+
+ return servicecli.ParseSecrets(client, opts)
+}
+
func convertExtraHosts(extraHosts map[string]string) []string {
hosts := []string{}
for host, ip := range extraHosts {
diff --git a/cli/compose/loader/loader.go b/cli/compose/loader/loader.go
index 9e46b97..39f69a0 100644
--- a/cli/compose/loader/loader.go
+++ b/cli/compose/loader/loader.go
@@ -62,16 +62,11 @@
}
}
- if err := schema.Validate(configDict); err != nil {
+ if err := schema.Validate(configDict, schema.Version(configDict)); err != nil {
return nil, err
}
cfg := types.Config{}
- version := configDict["version"].(string)
- if version != "3" && version != "3.0" {
- return nil, fmt.Errorf(`Unsupported Compose file version: %#v. The only version supported is "3" (or "3.0")`, version)
- }
-
if services, ok := configDict["services"]; ok {
servicesConfig, err := interpolation.Interpolate(services.(types.Dict), "service", os.LookupEnv)
if err != nil {
@@ -114,6 +109,20 @@
cfg.Volumes = volumesMapping
}
+ if secrets, ok := configDict["secrets"]; ok {
+ secretsConfig, err := interpolation.Interpolate(secrets.(types.Dict), "secret", os.LookupEnv)
+ if err != nil {
+ return nil, err
+ }
+
+ secretsMapping, err := loadSecrets(secretsConfig, configDetails.WorkingDir)
+ if err != nil {
+ return nil, err
+ }
+
+ cfg.Secrets = secretsMapping
+ }
+
return &cfg, nil
}
@@ -215,13 +224,15 @@
) (interface{}, error) {
switch target {
case reflect.TypeOf(types.External{}):
- return transformExternal(source, target, data)
+ return transformExternal(data)
case reflect.TypeOf(make(map[string]string, 0)):
return transformMapStringString(source, target, data)
case reflect.TypeOf(types.UlimitsConfig{}):
- return transformUlimits(source, target, data)
+ return transformUlimits(data)
case reflect.TypeOf(types.UnitBytes(0)):
return loadSize(data)
+ case reflect.TypeOf(types.ServiceSecretConfig{}):
+ return transformServiceSecret(data)
}
switch target.Kind() {
case reflect.Struct:
@@ -316,7 +327,7 @@
var envVars []string
for _, file := range envFiles {
- filePath := path.Join(workingDir, file)
+ filePath := absPath(workingDir, file)
fileVars, err := opts.ParseEnvFile(filePath)
if err != nil {
return err
@@ -346,7 +357,7 @@
}
if strings.HasPrefix(parts[0], ".") {
- parts[0] = path.Join(workingDir, parts[0])
+ parts[0] = absPath(workingDir, parts[0])
}
parts[0] = expandUser(parts[0])
@@ -364,11 +375,7 @@
return path
}
-func transformUlimits(
- source reflect.Type,
- target reflect.Type,
- data interface{},
-) (interface{}, error) {
+func transformUlimits(data interface{}) (interface{}, error) {
switch value := data.(type) {
case int:
return types.UlimitsConfig{Single: value}, nil
@@ -412,6 +419,31 @@
return volumes, nil
}
+// TODO: remove duplicate with networks/volumes
+func loadSecrets(source types.Dict, workingDir string) (map[string]types.SecretConfig, error) {
+ secrets := make(map[string]types.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)
+ }
+ }
+ return secrets, nil
+}
+
+func absPath(workingDir string, filepath string) string {
+ if path.IsAbs(filepath) {
+ return filepath
+ }
+ return path.Join(workingDir, filepath)
+}
+
func transformStruct(
source reflect.Type,
target reflect.Type,
@@ -495,11 +527,7 @@
return data, nil
}
-func transformExternal(
- source reflect.Type,
- target reflect.Type,
- data interface{},
-) (interface{}, error) {
+func transformExternal(data interface{}) (interface{}, error) {
switch value := data.(type) {
case bool:
return map[string]interface{}{"external": value}, nil
@@ -512,6 +540,20 @@
}
}
+func transformServiceSecret(data interface{}) (interface{}, error) {
+ switch value := data.(type) {
+ case string:
+ return map[string]interface{}{"source": value}, nil
+ case types.Dict:
+ return data, nil
+ case map[string]interface{}:
+ return data, nil
+ default:
+ return data, fmt.Errorf("invalid type %T for external", value)
+ }
+
+}
+
func toYAMLName(name string) string {
nameParts := fieldNameRegexp.FindAllString(name, -1)
for i, p := range nameParts {
diff --git a/cli/compose/loader/loader_test.go b/cli/compose/loader/loader_test.go
index e15be7c..f7fee89 100644
--- a/cli/compose/loader/loader_test.go
+++ b/cli/compose/loader/loader_test.go
@@ -163,6 +163,24 @@
assert.Equal(t, sampleConfig.Volumes, actual.Volumes)
}
+func TestLoadV31(t *testing.T) {
+ actual, err := loadYAML(`
+version: "3.1"
+services:
+ foo:
+ image: busybox
+ secrets: [super]
+secrets:
+ super:
+ external: true
+`)
+ if !assert.NoError(t, err) {
+ return
+ }
+ assert.Equal(t, len(actual.Services), 1)
+ assert.Equal(t, len(actual.Secrets), 1)
+}
+
func TestParseAndLoad(t *testing.T) {
actual, err := loadYAML(sampleYAML)
if !assert.NoError(t, err) {
diff --git a/cli/compose/schema/bindata.go b/cli/compose/schema/bindata.go
index c976509..9486e91 100644
--- a/cli/compose/schema/bindata.go
+++ b/cli/compose/schema/bindata.go
@@ -1,6 +1,7 @@
// Code generated by go-bindata.
// sources:
// data/config_schema_v3.0.json
+// data/config_schema_v3.1.json
// DO NOT EDIT!
package schema
@@ -88,6 +89,26 @@
return a, nil
}
+var _dataConfig_schema_v31Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x1a\xcb\x8e\xdb\x36\xf0\xee\xaf\x10\x94\xdc\xe2\xdd\x4d\xd1\xa0\x40\x73\xeb\xb1\xa7\xf6\xdc\x85\x23\xd0\xd2\x58\x66\x96\x22\x19\x92\x72\xd6\x09\xfc\xef\x05\xf5\x32\x45\x91\x22\x6d\x2b\xd9\x45\xd1\xd3\xae\xc5\x99\xe1\xbc\x67\x38\xe4\xf7\x55\x92\xa4\x6f\x65\xbe\x87\x0a\xa5\x1f\x93\x74\xaf\x14\xff\xf8\xf0\xf0\x59\x32\x7a\xd7\x7e\xbd\x67\xa2\x7c\x28\x04\xda\xa9\xbb\xf7\x1f\x1e\xda\x6f\x6f\xd2\xb5\xc6\xc3\x85\x46\xc9\x19\xdd\xe1\x32\x6b\x57\xb2\xc3\xaf\xf7\xbf\xdc\x6b\xf4\x16\x44\x1d\x39\x68\x20\xb6\xfd\x0c\xb9\x6a\xbf\x09\xf8\x52\x63\x01\x1a\xf9\x31\x3d\x80\x90\x98\xd1\x74\xb3\x5e\xe9\x35\x2e\x18\x07\xa1\x30\xc8\xf4\x63\xa2\x99\x4b\x92\x01\xa4\xff\x60\x90\x95\x4a\x60\x5a\xa6\xcd\xe7\x53\x43\x21\x49\x52\x09\xe2\x80\x73\x83\xc2\xc0\xea\x9b\x87\x33\xfd\x87\x01\x6c\x6d\x53\x35\x98\x6d\xbe\x73\xa4\x14\x08\xfa\xf7\x94\xb7\x66\xf9\xd3\x23\xba\xfb\xf6\xc7\xdd\x3f\xef\xef\x7e\xbf\xcf\xee\x36\xef\xde\x8e\x96\xb5\x7e\x05\xec\xda\xed\x0b\xd8\x61\x8a\x15\x66\x74\xd8\x3f\x1d\x20\x4f\xdd\x7f\xa7\x61\x63\x54\x14\x0d\x30\x22\xa3\xbd\x77\x88\x48\x18\xcb\x4c\x41\x7d\x65\xe2\x29\x24\xf3\x00\xf6\x42\x32\x77\xfb\x3b\x64\x1e\x8b\x73\x60\xa4\xae\x82\x16\xec\xa1\x5e\x48\x98\x76\xfb\x65\xec\x27\x21\x17\xa0\xc2\x2e\xdb\x42\xbd\x98\xc7\xea\xed\x6f\x13\x78\xd5\x0b\x3d\x0b\xdb\x42\x18\x7b\x37\x0c\x8e\xc2\xdb\xa5\x2a\x57\x78\xf9\x75\x35\x28\xcb\xa3\xa5\x02\x38\x61\x47\xfd\xcd\xa3\x8f\x16\xa0\x02\xaa\xd2\x41\x05\x49\x92\x6e\x6b\x4c\x0a\x5b\xa3\x8c\xc2\x5f\x9a\xc4\xa3\xf1\x31\x49\xbe\xdb\x99\xcc\xa0\xd3\xac\x8f\x7e\xf9\x0d\x3e\xac\x7b\x64\x19\xd6\x73\x46\x15\x3c\xab\x46\xa8\xf9\xad\x5b\x15\xb0\xfc\x09\xc4\x0e\x13\x88\xc5\x40\xa2\x94\x33\x2a\x23\x58\xaa\x8c\x89\xac\xc0\xb9\x4a\x4f\x16\xfa\x84\x5e\xd8\x9f\x06\x54\xe3\xd7\x66\xe5\x20\x98\xe6\x88\x67\xa8\x28\x46\x72\x20\x21\xd0\x31\x5d\x27\x29\x56\x50\x49\xb7\x88\x49\x5a\x53\xfc\xa5\x86\x3f\x3b\x10\x25\x6a\xb0\xe9\x16\x82\xf1\xe5\x09\x97\x82\xd5\x3c\xe3\x48\x68\x07\x9b\x57\x7f\x9a\xb3\xaa\x42\x74\x29\xaf\xbb\x44\x8e\x08\xcd\x33\xaa\x10\xa6\x20\x32\x8a\xaa\x90\x23\xe9\xa8\x03\x5a\xc8\xac\x2d\xf8\xb3\x6e\xb4\xcb\x5a\x7c\x69\x11\x18\xaa\xff\xa2\xf6\x28\xe8\x9c\x63\xb7\x64\xb4\x6b\x6b\xde\x52\x0b\x31\x93\x80\x44\xbe\xbf\x12\x9f\x55\x08\xd3\x18\xdd\x01\x55\xe2\xc8\x19\x6e\xfd\xe5\xd5\x39\x02\xd0\x43\x36\xe4\x92\x8b\xd5\x00\xf4\x80\x05\xa3\x55\x1f\x0d\x31\x09\x66\x48\xf2\x1a\xff\x99\x33\x09\xb6\x62\x2c\x01\xcd\xa5\x41\xd4\x91\x4e\x7a\x8c\xc7\x5e\xf0\x75\x92\xd2\xba\xda\x82\xd0\x3d\xec\x08\x72\xc7\x44\x85\x34\xb3\xfd\xde\xc6\xf2\x48\xd3\x0e\xcf\x33\x15\x68\xca\xa0\xcb\x3a\x22\x19\xc1\xf4\x69\x79\x17\x87\x67\x25\x50\xb6\x67\x52\xc5\xe7\x70\x03\x7d\x0f\x88\xa8\x7d\xbe\x87\xfc\x69\x06\xdd\x84\x1a\x61\x33\xa9\x62\x9c\x1c\x57\xa8\x0c\x03\xf1\x3c\x04\x42\xd0\x16\xc8\x55\x72\x2e\xaa\x7c\x83\x2c\x2b\x4b\x0d\xea\xf3\xb8\x49\xe7\xd2\x2d\x87\x6a\x7e\x21\xf0\x01\x44\x6c\x01\x67\xfc\xdc\x70\xd9\x8b\xe1\x06\x24\x09\x77\x9f\x23\xd0\x4f\xf7\x6d\xf3\x39\x13\x55\xcd\x7f\x84\xa4\x1b\xbb\x5d\x48\xac\xba\xef\xfa\x62\x49\x18\xd7\x50\x8c\xac\x52\xa1\x5c\xf7\x0d\x02\xa4\xc7\xae\x67\xd0\xee\x74\x93\x55\xac\xf0\x39\xe8\x04\xd8\xd6\x8d\x37\x53\x5f\x5c\x08\x93\xab\xfa\xc7\x28\xd3\x05\x0f\x10\x01\x69\x7c\xec\xc5\xb2\x79\x66\x37\xec\x62\x0d\x1c\x22\x18\x49\x08\x07\xbb\x57\x91\x23\x6a\x98\x1f\x3e\x44\xfa\x84\x0b\xf7\xb7\x59\x5c\x0f\xaa\x97\x66\x7c\x8f\x1c\x20\x75\x66\xa5\x09\x37\x17\x23\x9b\x40\xb4\xfd\xe0\x16\x9e\xe3\xc2\x9f\x2b\x9a\x0c\x61\x06\x18\x67\x42\x4d\xa2\xeb\xe7\x94\xfb\x76\xeb\x9b\xab\x3d\x17\xf8\x80\x09\x94\x30\x3e\xb5\x6c\x19\x23\x80\xe8\x28\xf5\x08\x40\x45\xc6\x28\x39\x46\x40\x4a\x85\x44\xf0\x40\x21\x21\xaf\x05\x56\xc7\x8c\x71\xb5\x78\x9f\x21\xf7\x55\x26\xf1\x37\x18\x5b\xf3\x9c\xef\x3b\x42\x1b\x8b\x21\x6b\x42\x72\xa5\x41\x7d\x29\x29\x1c\xc6\x8e\x44\x18\x4c\x54\xe1\x14\x95\x4a\x56\x8b\x3c\xf6\x80\xad\xf7\x44\xa2\x84\xd8\x23\xbc\x76\xb7\x71\xd8\xcc\x03\x97\x97\x00\x4f\x0a\x5d\x67\xc2\x50\x55\xb6\x7f\x9b\x79\xe5\xe4\x0c\x7d\x79\x94\xb9\xba\xae\x5b\x93\xaa\xc0\x34\x63\x1c\x68\x30\x36\xa4\x62\x3c\x2b\x05\xca\x21\xe3\x20\x30\x73\xaa\x62\x6d\x46\x7a\x51\x0b\xa4\xf7\x9f\x92\x91\xb8\xa4\x88\x84\xc2\x4c\x55\x7c\x77\xe5\xb1\x52\xa9\x70\xb0\xd7\x04\x57\xd8\x1f\x34\x0e\xaf\x8d\xe8\x00\xda\xea\xef\x2e\xfa\x33\x05\xff\xcc\x29\xa6\x0a\x4a\xed\x26\x53\xa7\x9a\xe9\x39\xe7\x5b\xce\x88\x5e\x73\x8f\xc4\xd8\xa0\x33\x7c\x24\x6d\x60\xee\x94\x1b\xc1\xd5\x89\x3a\xf9\x1a\xdd\x75\x34\xf4\xd6\x1d\x23\x1b\x27\xfc\x45\xc5\xdc\x66\x63\xe3\xad\xa7\xee\xa0\xaa\x65\xf0\x58\xd0\xc0\x50\x39\xd7\xd2\x0e\xa0\xc6\xd0\x7e\xd1\x6a\xa1\xdb\x64\x1d\x04\x05\x76\x73\xbb\xb2\x24\xbb\x60\xec\x6e\x9d\x58\x7b\x02\xae\x79\xb2\x09\x1a\x9c\xbf\xcf\xcf\xb6\x3b\x20\xef\xdc\x19\x4b\xb4\xb5\x26\xae\xae\xe0\xd6\xde\x28\x0e\xe1\x1c\x23\x40\x09\x6c\xd9\xa5\x4f\xd4\x66\x3e\x01\xf9\x3a\xc7\x46\x0a\x57\xc0\x6a\x77\xc1\x5b\x99\xfe\xdd\x21\xa5\xc6\x5c\x3e\x60\x54\x03\xd2\xb6\xe9\xe3\x60\xd4\xbe\xbb\x0c\x1a\x2e\x26\x48\x04\x70\x82\x73\x24\x43\x89\xe8\x86\xf1\x44\xcd\x0b\xa4\x20\x6b\xef\x65\x2f\x4a\xfd\x33\x39\x9f\x23\x81\x08\x01\x82\x65\x15\x93\x43\xd3\x02\x08\x3a\x5e\x55\x3e\x1b\xf4\x1d\xc2\xa4\x16\x90\xa1\x5c\x75\x57\xbf\x01\x9f\x4b\x2b\x46\xb1\x62\xce\x0c\x11\xb7\x65\x85\x9e\xb3\x7e\xdb\x06\x24\xd4\xd9\x8c\x9b\xfa\xd8\xc9\x82\xe1\x09\x6d\xe3\x77\x59\x75\x9e\x31\xd1\xb9\xd6\x7b\x3c\xa6\xdf\x71\x22\xba\x00\xa9\x33\xc9\x30\xf8\x09\xe2\x07\x4b\x4b\x77\xca\xc8\x38\x23\x38\x3f\x2e\x25\x61\xce\x68\xab\xe4\x18\x87\xb8\xd1\x03\xb5\x3b\xe8\x56\xa8\xe2\x2a\x18\xac\x0d\xc2\x57\x4c\x0b\xf6\xf5\x82\x0d\x97\x73\x25\x4e\x50\x0e\x56\xbe\xbb\x55\xd1\x52\x09\x84\xa9\xba\xb8\x9c\xdf\x2a\xd6\x0d\xd5\x7c\xf0\xcf\x40\xd6\x1f\xe0\xc2\xf7\xe8\x9e\x4c\x9f\xf3\x3a\x38\x0d\xac\xa0\x62\xc2\xe9\x80\x0b\x3c\xf4\x08\x89\xd8\x83\x2d\x50\xd5\xa2\xc6\xc7\x1d\x54\xc6\xf8\xf2\xa7\x8d\xf0\x88\x78\x13\x4e\x48\x98\xa3\x6a\xa9\xe8\x88\x1e\xa8\xa7\xce\x1a\x9c\xcc\xcf\x2d\x12\xff\xec\x22\xc4\x75\x98\xf7\x0e\x42\xd6\x5b\xea\x19\x21\x4c\x4f\x19\xae\x5b\xfe\xf8\x63\xca\xc9\x7f\x28\xb9\x2d\xe9\xf5\x77\x61\x1e\xab\x3e\x0e\x3d\xf3\x7a\xd0\xd5\x26\xda\xc4\xde\x8b\xa8\xe5\xf8\x6f\xda\x77\x7b\x44\xe0\xea\xf3\x2f\xec\x04\x6f\x48\x2e\xdd\x8b\xa6\x40\x6e\xe9\xa0\xfe\x4f\x2d\xff\x11\x47\xfc\x79\xfe\xd5\x3d\x20\x0b\xbe\xdc\x6a\xa0\xae\x2e\xce\x11\xcf\x95\x5e\x81\xcd\x5e\xda\x14\xe3\xc1\xa2\x61\x92\xe9\x99\x7f\x4e\x93\xd1\xf7\x69\x1d\xc6\x66\xcc\x86\x0d\xe6\x78\xe3\x3b\xae\x90\x73\x83\xa4\x1e\xc4\x73\xbf\x62\x6d\xda\x29\x71\x5e\xf2\x05\x93\xcd\xfd\xbb\x99\x3e\x60\xee\xde\xfb\x07\x15\xd0\x05\x86\x74\x6e\x9b\x5a\x87\x87\x5e\xbb\xd3\x77\x9b\x9e\xf8\x37\xf0\x27\xaf\x38\xb5\x9c\xf4\x38\x99\x49\x7d\x1f\x0f\x5a\xdb\x17\x98\x9b\x91\x7e\x2c\x90\xf6\x15\x89\x91\xdd\x37\xe6\x79\xca\x67\x46\xe7\xdb\x4e\x7b\xcc\xdb\xbf\xb1\xf4\xdc\x6a\xac\xcc\xbf\xcd\x7b\xd8\xd5\x69\xf5\x6f\x00\x00\x00\xff\xff\xfc\xf3\x11\x6a\x88\x2f\x00\x00")
+
+func dataConfig_schema_v31JsonBytes() ([]byte, error) {
+ return bindataRead(
+ _dataConfig_schema_v31Json,
+ "data/config_schema_v3.1.json",
+ )
+}
+
+func dataConfig_schema_v31Json() (*asset, error) {
+ bytes, err := dataConfig_schema_v31JsonBytes()
+ if err != nil {
+ return nil, err
+ }
+
+ info := bindataFileInfo{name: "data/config_schema_v3.1.json", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)}
+ a := &asset{bytes: bytes, info: info}
+ return a, nil
+}
+
// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
@@ -141,6 +162,7 @@
// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() (*asset, error){
"data/config_schema_v3.0.json": dataConfig_schema_v30Json,
+ "data/config_schema_v3.1.json": dataConfig_schema_v31Json,
}
// AssetDir returns the file names below a certain
@@ -185,6 +207,7 @@
var _bintree = &bintree{nil, map[string]*bintree{
"data": &bintree{nil, map[string]*bintree{
"config_schema_v3.0.json": &bintree{dataConfig_schema_v30Json, map[string]*bintree{}},
+ "config_schema_v3.1.json": &bintree{dataConfig_schema_v31Json, map[string]*bintree{}},
}},
}}
diff --git a/cli/compose/schema/data/config_schema_v3.1.json b/cli/compose/schema/data/config_schema_v3.1.json
new file mode 100644
index 0000000..b703748
--- /dev/null
+++ b/cli/compose/schema/data/config_schema_v3.1.json
@@ -0,0 +1,428 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "id": "config_schema_v3.1.json",
+ "type": "object",
+ "required": ["version"],
+
+ "properties": {
+ "version": {
+ "type": "string"
+ },
+
+ "services": {
+ "id": "#/properties/services",
+ "type": "object",
+ "patternProperties": {
+ "^[a-zA-Z0-9._-]+$": {
+ "$ref": "#/definitions/service"
+ }
+ },
+ "additionalProperties": false
+ },
+
+ "networks": {
+ "id": "#/properties/networks",
+ "type": "object",
+ "patternProperties": {
+ "^[a-zA-Z0-9._-]+$": {
+ "$ref": "#/definitions/network"
+ }
+ }
+ },
+
+ "volumes": {
+ "id": "#/properties/volumes",
+ "type": "object",
+ "patternProperties": {
+ "^[a-zA-Z0-9._-]+$": {
+ "$ref": "#/definitions/volume"
+ }
+ },
+ "additionalProperties": false
+ },
+
+ "secrets": {
+ "id": "#/properties/secrets",
+ "type": "object",
+ "patternProperties": {
+ "^[a-zA-Z0-9._-]+$": {
+ "$ref": "#/definitions/secret"
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+
+ "additionalProperties": false,
+
+ "definitions": {
+
+ "service": {
+ "id": "#/definitions/service",
+ "type": "object",
+
+ "properties": {
+ "deploy": {"$ref": "#/definitions/deployment"},
+ "build": {
+ "oneOf": [
+ {"type": "string"},
+ {
+ "type": "object",
+ "properties": {
+ "context": {"type": "string"},
+ "dockerfile": {"type": "string"},
+ "args": {"$ref": "#/definitions/list_or_dict"}
+ },
+ "additionalProperties": false
+ }
+ ]
+ },
+ "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+ "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+ "cgroup_parent": {"type": "string"},
+ "command": {
+ "oneOf": [
+ {"type": "string"},
+ {"type": "array", "items": {"type": "string"}}
+ ]
+ },
+ "container_name": {"type": "string"},
+ "depends_on": {"$ref": "#/definitions/list_of_strings"},
+ "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+ "dns": {"$ref": "#/definitions/string_or_list"},
+ "dns_search": {"$ref": "#/definitions/string_or_list"},
+ "domainname": {"type": "string"},
+ "entrypoint": {
+ "oneOf": [
+ {"type": "string"},
+ {"type": "array", "items": {"type": "string"}}
+ ]
+ },
+ "env_file": {"$ref": "#/definitions/string_or_list"},
+ "environment": {"$ref": "#/definitions/list_or_dict"},
+
+ "expose": {
+ "type": "array",
+ "items": {
+ "type": ["string", "number"],
+ "format": "expose"
+ },
+ "uniqueItems": true
+ },
+
+ "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+ "extra_hosts": {"$ref": "#/definitions/list_or_dict"},
+ "healthcheck": {"$ref": "#/definitions/healthcheck"},
+ "hostname": {"type": "string"},
+ "image": {"type": "string"},
+ "ipc": {"type": "string"},
+ "labels": {"$ref": "#/definitions/list_or_dict"},
+ "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+
+ "logging": {
+ "type": "object",
+
+ "properties": {
+ "driver": {"type": "string"},
+ "options": {
+ "type": "object",
+ "patternProperties": {
+ "^.+$": {"type": ["string", "number", "null"]}
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+
+ "mac_address": {"type": "string"},
+ "network_mode": {"type": "string"},
+
+ "networks": {
+ "oneOf": [
+ {"$ref": "#/definitions/list_of_strings"},
+ {
+ "type": "object",
+ "patternProperties": {
+ "^[a-zA-Z0-9._-]+$": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "aliases": {"$ref": "#/definitions/list_of_strings"},
+ "ipv4_address": {"type": "string"},
+ "ipv6_address": {"type": "string"}
+ },
+ "additionalProperties": false
+ },
+ {"type": "null"}
+ ]
+ }
+ },
+ "additionalProperties": false
+ }
+ ]
+ },
+ "pid": {"type": ["string", "null"]},
+
+ "ports": {
+ "type": "array",
+ "items": {
+ "type": ["string", "number"],
+ "format": "ports"
+ },
+ "uniqueItems": true
+ },
+
+ "privileged": {"type": "boolean"},
+ "read_only": {"type": "boolean"},
+ "restart": {"type": "string"},
+ "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+ "shm_size": {"type": ["number", "string"]},
+ "secrets": {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ {"type": "string"},
+ {
+ "type": "object",
+ "properties": {
+ "source": {"type": "string"},
+ "target": {"type": "string"},
+ "uid": {"type": "string"},
+ "gid": {"type": "string"},
+ "mode": {"type": "number"}
+ }
+ }
+ ]
+ }
+ },
+ "sysctls": {"$ref": "#/definitions/list_or_dict"},
+ "stdin_open": {"type": "boolean"},
+ "stop_grace_period": {"type": "string", "format": "duration"},
+ "stop_signal": {"type": "string"},
+ "tmpfs": {"$ref": "#/definitions/string_or_list"},
+ "tty": {"type": "boolean"},
+ "ulimits": {
+ "type": "object",
+ "patternProperties": {
+ "^[a-z]+$": {
+ "oneOf": [
+ {"type": "integer"},
+ {
+ "type":"object",
+ "properties": {
+ "hard": {"type": "integer"},
+ "soft": {"type": "integer"}
+ },
+ "required": ["soft", "hard"],
+ "additionalProperties": false
+ }
+ ]
+ }
+ }
+ },
+ "user": {"type": "string"},
+ "userns_mode": {"type": "string"},
+ "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+ "working_dir": {"type": "string"}
+ },
+ "additionalProperties": false
+ },
+
+ "healthcheck": {
+ "id": "#/definitions/healthcheck",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "disable": {"type": "boolean"},
+ "interval": {"type": "string"},
+ "retries": {"type": "number"},
+ "test": {
+ "oneOf": [
+ {"type": "string"},
+ {"type": "array", "items": {"type": "string"}}
+ ]
+ },
+ "timeout": {"type": "string"}
+ }
+ },
+ "deployment": {
+ "id": "#/definitions/deployment",
+ "type": ["object", "null"],
+ "properties": {
+ "mode": {"type": "string"},
+ "replicas": {"type": "integer"},
+ "labels": {"$ref": "#/definitions/list_or_dict"},
+ "update_config": {
+ "type": "object",
+ "properties": {
+ "parallelism": {"type": "integer"},
+ "delay": {"type": "string", "format": "duration"},
+ "failure_action": {"type": "string"},
+ "monitor": {"type": "string", "format": "duration"},
+ "max_failure_ratio": {"type": "number"}
+ },
+ "additionalProperties": false
+ },
+ "resources": {
+ "type": "object",
+ "properties": {
+ "limits": {"$ref": "#/definitions/resource"},
+ "reservations": {"$ref": "#/definitions/resource"}
+ }
+ },
+ "restart_policy": {
+ "type": "object",
+ "properties": {
+ "condition": {"type": "string"},
+ "delay": {"type": "string", "format": "duration"},
+ "max_attempts": {"type": "integer"},
+ "window": {"type": "string", "format": "duration"}
+ },
+ "additionalProperties": false
+ },
+ "placement": {
+ "type": "object",
+ "properties": {
+ "constraints": {"type": "array", "items": {"type": "string"}}
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ },
+
+ "resource": {
+ "id": "#/definitions/resource",
+ "type": "object",
+ "properties": {
+ "cpus": {"type": "string"},
+ "memory": {"type": "string"}
+ },
+ "additionalProperties": false
+ },
+
+ "network": {
+ "id": "#/definitions/network",
+ "type": ["object", "null"],
+ "properties": {
+ "driver": {"type": "string"},
+ "driver_opts": {
+ "type": "object",
+ "patternProperties": {
+ "^.+$": {"type": ["string", "number"]}
+ }
+ },
+ "ipam": {
+ "type": "object",
+ "properties": {
+ "driver": {"type": "string"},
+ "config": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "subnet": {"type": "string"}
+ },
+ "additionalProperties": false
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "external": {
+ "type": ["boolean", "object"],
+ "properties": {
+ "name": {"type": "string"}
+ },
+ "additionalProperties": false
+ },
+ "internal": {"type": "boolean"},
+ "labels": {"$ref": "#/definitions/list_or_dict"}
+ },
+ "additionalProperties": false
+ },
+
+ "volume": {
+ "id": "#/definitions/volume",
+ "type": ["object", "null"],
+ "properties": {
+ "driver": {"type": "string"},
+ "driver_opts": {
+ "type": "object",
+ "patternProperties": {
+ "^.+$": {"type": ["string", "number"]}
+ }
+ },
+ "external": {
+ "type": ["boolean", "object"],
+ "properties": {
+ "name": {"type": "string"}
+ },
+ "additionalProperties": false
+ },
+ "labels": {"$ref": "#/definitions/list_or_dict"}
+ },
+ "additionalProperties": false
+ },
+
+ "secret": {
+ "id": "#/definitions/secret",
+ "type": "object",
+ "properties": {
+ "file": {"type": "string"},
+ "external": {
+ "type": ["boolean", "object"],
+ "properties": {
+ "name": {"type": "string"}
+ }
+ },
+ "labels": {"$ref": "#/definitions/list_or_dict"}
+ },
+ "additionalProperties": false
+ },
+
+ "string_or_list": {
+ "oneOf": [
+ {"type": "string"},
+ {"$ref": "#/definitions/list_of_strings"}
+ ]
+ },
+
+ "list_of_strings": {
+ "type": "array",
+ "items": {"type": "string"},
+ "uniqueItems": true
+ },
+
+ "list_or_dict": {
+ "oneOf": [
+ {
+ "type": "object",
+ "patternProperties": {
+ ".+": {
+ "type": ["string", "number", "null"]
+ }
+ },
+ "additionalProperties": false
+ },
+ {"type": "array", "items": {"type": "string"}, "uniqueItems": true}
+ ]
+ },
+
+ "constraints": {
+ "service": {
+ "id": "#/definitions/constraints/service",
+ "anyOf": [
+ {"required": ["build"]},
+ {"required": ["image"]}
+ ],
+ "properties": {
+ "build": {
+ "required": ["context"]
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/cli/compose/schema/schema.go b/cli/compose/schema/schema.go
index 6366cab..ae33c77 100644
--- a/cli/compose/schema/schema.go
+++ b/cli/compose/schema/schema.go
@@ -7,9 +7,15 @@
"strings"
"time"
+ "github.com/pkg/errors"
"github.com/xeipuuv/gojsonschema"
)
+const (
+ defaultVersion = "1.0"
+ versionField = "version"
+)
+
type portsFormatChecker struct{}
func (checker portsFormatChecker) IsFormat(input string) bool {
@@ -30,11 +36,29 @@
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{}) error {
- schemaData, err := Asset("data/config_schema_v3.0.json")
+func Validate(config map[string]interface{}, version string) error {
+ schemaData, err := Asset(fmt.Sprintf("data/config_schema_v%s.json", version))
if err != nil {
- return err
+ return errors.Errorf("unsupported Compose file version: %s", version)
}
schemaLoader := gojsonschema.NewStringLoader(string(schemaData))
diff --git a/cli/compose/schema/schema_test.go b/cli/compose/schema/schema_test.go
index be98f80..0935d40 100644
--- a/cli/compose/schema/schema_test.go
+++ b/cli/compose/schema/schema_test.go
@@ -8,7 +8,35 @@
type dict map[string]interface{}
-func TestValid(t *testing.T) {
+func TestValidate(t *testing.T) {
+ config := dict{
+ "version": "3.0",
+ "services": dict{
+ "foo": dict{
+ "image": "busybox",
+ },
+ },
+ }
+
+ assert.NoError(t, Validate(config, "3.0"))
+}
+
+func TestValidateUndefinedTopLevelOption(t *testing.T) {
+ config := dict{
+ "version": "3.0",
+ "helicopters": dict{
+ "foo": dict{
+ "image": "busybox",
+ },
+ },
+ }
+
+ err := Validate(config, "3.0")
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "Additional property helicopters is not allowed")
+}
+
+func TestValidateInvalidVersion(t *testing.T) {
config := dict{
"version": "2.1",
"services": dict{
@@ -18,18 +46,7 @@
},
}
- assert.NoError(t, Validate(config))
-}
-
-func TestUndefinedTopLevelOption(t *testing.T) {
- config := dict{
- "version": "2.1",
- "helicopters": dict{
- "foo": dict{
- "image": "busybox",
- },
- },
- }
-
- assert.Error(t, Validate(config))
+ err := Validate(config, "2.1")
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "unsupported Compose file version: 2.1")
}
diff --git a/cli/compose/types/types.go b/cli/compose/types/types.go
index aac1a10..cae7b4a 100644
--- a/cli/compose/types/types.go
+++ b/cli/compose/types/types.go
@@ -71,6 +71,7 @@
Services []ServiceConfig
Networks map[string]NetworkConfig
Volumes map[string]VolumeConfig
+ Secrets map[string]SecretConfig
}
// ServiceConfig is the configuration of one service
@@ -108,6 +109,7 @@
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"`
@@ -191,6 +193,15 @@
Ipv6Address string `mapstructure:"ipv6_address"`
}
+// ServiceSecretConfig is the secret configuration for a service
+type ServiceSecretConfig struct {
+ Source string
+ Target string
+ UID string
+ GID string
+ Mode uint32
+}
+
// UlimitsConfig the ulimit configuration
type UlimitsConfig struct {
Single int
@@ -233,3 +244,10 @@
Name string
External bool
}
+
+// SecretConfig for a secret
+type SecretConfig struct {
+ File string
+ External External
+ Labels map[string]string `compose:"list_or_dict_equals"`
+}
diff --git a/client/interface.go b/client/interface.go
index 00b9ade..924b22b 100644
--- a/client/interface.go
+++ b/client/interface.go
@@ -166,4 +166,5 @@
SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error)
SecretRemove(ctx context.Context, id string) error
SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error)
+ SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error
}
diff --git a/client/secret_update.go b/client/secret_update.go
new file mode 100644
index 0000000..b94e24a
--- /dev/null
+++ b/client/secret_update.go
@@ -0,0 +1,19 @@
+package client
+
+import (
+ "net/url"
+ "strconv"
+
+ "github.com/docker/docker/api/types/swarm"
+ "golang.org/x/net/context"
+)
+
+// SecretUpdate updates a Secret. Currently, the only part of a secret spec
+// which can be updated is Labels.
+func (cli *Client) SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error {
+ query := url.Values{}
+ query.Set("version", strconv.FormatUint(version.Index, 10))
+ resp, err := cli.post(ctx, "/secrets/"+id+"/update", query, secret, nil)
+ ensureReaderClosed(resp)
+ return err
+}
diff --git a/client/secret_update_test.go b/client/secret_update_test.go
new file mode 100644
index 0000000..c620985
--- /dev/null
+++ b/client/secret_update_test.go
@@ -0,0 +1,49 @@
+package client
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "strings"
+ "testing"
+
+ "golang.org/x/net/context"
+
+ "github.com/docker/docker/api/types/swarm"
+)
+
+func TestSecretUpdateError(t *testing.T) {
+ client := &Client{
+ client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
+ }
+
+ err := client.SecretUpdate(context.Background(), "secret_id", swarm.Version{}, swarm.SecretSpec{})
+ if err == nil || err.Error() != "Error response from daemon: Server error" {
+ t.Fatalf("expected a Server Error, got %v", err)
+ }
+}
+
+func TestSecretUpdate(t *testing.T) {
+ expectedURL := "/secrets/secret_id/update"
+
+ client := &Client{
+ client: newMockClient(func(req *http.Request) (*http.Response, error) {
+ if !strings.HasPrefix(req.URL.Path, expectedURL) {
+ return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+ }
+ if req.Method != "POST" {
+ return nil, fmt.Errorf("expected POST method, got %s", req.Method)
+ }
+ return &http.Response{
+ StatusCode: http.StatusOK,
+ Body: ioutil.NopCloser(bytes.NewReader([]byte("body"))),
+ }, nil
+ }),
+ }
+
+ err := client.SecretUpdate(context.Background(), "secret_id", swarm.Version{}, swarm.SecretSpec{})
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/docs/api/v1.24.md b/docs/api/v1.24.md
index a5e3efa..0cf4e2a 100644
--- a/docs/api/v1.24.md
+++ b/docs/api/v1.24.md
@@ -4459,7 +4459,11 @@
"Condition": "any",
"MaxAttempts": 0
},
- "Placement": {}
+ "Placement": {
+ "Constraints": [
+ "node.role == worker"
+ ]
+ }
},
"Mode": {
"Replicated": {
@@ -4575,7 +4579,11 @@
"max-size": "10M"
}
},
- "Placement": {},
+ "Placement": {
+ "Constraints": [
+ "node.role == worker"
+ ]
+ },
"Resources": {
"Limits": {
"MemoryBytes": 104857600
@@ -4682,7 +4690,8 @@
is 0, which is ignored).
- **Window** – Windows is the time window used to evaluate the restart policy (default value is
0, which is unbounded).
- - **Placement** – An array of constraints.
+ - **Placement** – Restrictions on where a service can run.
+ - **Constraints** – An array of constraints, e.g. `[ "node.role == manager" ]`.
- **Mode** – Scheduling mode for the service (`replicated` or `global`, defaults to `replicated`).
- **UpdateConfig** – Specification for the update strategy of the service.
- **Parallelism** – Maximum number of tasks to be updated in one iteration (0 means unlimited
@@ -4921,7 +4930,8 @@
is 0, which is ignored).
- **Window** – Windows is the time window used to evaluate the restart policy (default value is
0, which is unbounded).
- - **Placement** – An array of constraints.
+ - **Placement** – Restrictions on where a service can run.
+ - **Constraints** – An array of constraints, e.g. `[ "node.role == manager" ]`.
- **Mode** – Scheduling mode for the service (`replicated` or `global`, defaults to `replicated`).
- **UpdateConfig** – Specification for the update strategy of the service.
- **Parallelism** – Maximum number of tasks to be updated in one iteration (0 means unlimited
diff --git a/docs/extend/plugin_api.md b/docs/extend/plugin_api.md
index e299a09..693b77a 100644
--- a/docs/extend/plugin_api.md
+++ b/docs/extend/plugin_api.md
@@ -79,7 +79,7 @@
"InsecureSkipVerify": false,
"CAFile": "/usr/shared/docker/certs/example-ca.pem",
"CertFile": "/usr/shared/docker/certs/example-cert.pem",
- "KeyFile": "/usr/shared/docker/certs/example-key.pem",
+ "KeyFile": "/usr/shared/docker/certs/example-key.pem"
}
}
```
diff --git a/docs/reference/commandline/cli.md b/docs/reference/commandline/cli.md
index 4128baf..e56fb9f 100644
--- a/docs/reference/commandline/cli.md
+++ b/docs/reference/commandline/cli.md
@@ -69,6 +69,9 @@
Equates to `--disable-content-trust=false` for build, create, pull, push, run.
* `DOCKER_CONTENT_TRUST_SERVER` The URL of the Notary server to use. This defaults
to the same URL as the registry.
+* `DOCKER_HIDE_LEGACY_COMMANDS` When set, Docker hides "legacy" top-level commands (such as `docker rm`, and
+ `docker pull`) in `docker help` output, and only `Management commands` per object-type (e.g., `docker container`) are
+ printed. This may become the default in a future release, at which point this environment-variable is removed.
* `DOCKER_TMPDIR` Location for temporary Docker files.
Because Docker is developed using Go, you can also use any environment
diff --git a/docs/reference/commandline/plugin_disable.md b/docs/reference/commandline/plugin_disable.md
index f054fd0..e8d16c4 100644
--- a/docs/reference/commandline/plugin_disable.md
+++ b/docs/reference/commandline/plugin_disable.md
@@ -16,7 +16,7 @@
# plugin disable
```markdown
-Usage: docker plugin disable PLUGIN
+Usage: docker plugin disable [OPTIONS] PLUGIN
Disable a plugin
diff --git a/docs/reference/commandline/plugin_push.md b/docs/reference/commandline/plugin_push.md
index 2747f4c..f869e43 100644
--- a/docs/reference/commandline/plugin_push.md
+++ b/docs/reference/commandline/plugin_push.md
@@ -14,7 +14,7 @@
-->
```markdown
-Usage: docker plugin push PLUGIN[:TAG]
+Usage: docker plugin push [OPTIONS] PLUGIN[:TAG]
Push a plugin to a registry
diff --git a/docs/reference/index.md b/docs/reference/index.md
index 01ac341..f24c342 100644
--- a/docs/reference/index.md
+++ b/docs/reference/index.md
@@ -18,4 +18,4 @@
* [Dockerfile reference](builder.md)
* [Docker run reference](run.md)
* [Command line reference](commandline/index.md)
-* [API Reference](api/index.md)
+* [API Reference](https://docs.docker.com/engine/api/)
diff --git a/integration-cli/docker_cli_stack_test.go b/integration-cli/docker_cli_stack_test.go
index 1ff791b..fd9b154 100644
--- a/integration-cli/docker_cli_stack_test.go
+++ b/integration-cli/docker_cli_stack_test.go
@@ -1,15 +1,18 @@
package main
import (
+ "encoding/json"
"io/ioutil"
"os"
+ "sort"
+ "strings"
+ "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/pkg/integration/checker"
"github.com/go-check/check"
)
-func (s *DockerSwarmSuite) TestStackRemove(c *check.C) {
- testRequires(c, ExperimentalDaemon)
+func (s *DockerSwarmSuite) TestStackRemoveUnknown(c *check.C) {
d := s.AddDaemon(c, true, true)
stackArgs := append([]string{"stack", "remove", "UNKNOWN_STACK"})
@@ -19,8 +22,7 @@
c.Assert(out, check.Equals, "Nothing found in stack: UNKNOWN_STACK\n")
}
-func (s *DockerSwarmSuite) TestStackTasks(c *check.C) {
- testRequires(c, ExperimentalDaemon)
+func (s *DockerSwarmSuite) TestStackPSUnknown(c *check.C) {
d := s.AddDaemon(c, true, true)
stackArgs := append([]string{"stack", "ps", "UNKNOWN_STACK"})
@@ -30,8 +32,7 @@
c.Assert(out, check.Equals, "Nothing found in stack: UNKNOWN_STACK\n")
}
-func (s *DockerSwarmSuite) TestStackServices(c *check.C) {
- testRequires(c, ExperimentalDaemon)
+func (s *DockerSwarmSuite) TestStackServicesUnknown(c *check.C) {
d := s.AddDaemon(c, true, true)
stackArgs := append([]string{"stack", "services", "UNKNOWN_STACK"})
@@ -42,7 +43,6 @@
}
func (s *DockerSwarmSuite) TestStackDeployComposeFile(c *check.C) {
- testRequires(c, ExperimentalDaemon)
d := s.AddDaemon(c, true, true)
testStackName := "testdeploy"
@@ -54,17 +54,81 @@
out, err := d.Cmd(stackArgs...)
c.Assert(err, checker.IsNil, check.Commentf(out))
- out, err = d.Cmd([]string{"stack", "ls"}...)
+ out, err = d.Cmd("stack", "ls")
c.Assert(err, checker.IsNil)
c.Assert(out, check.Equals, "NAME SERVICES\n"+"testdeploy 2\n")
- out, err = d.Cmd([]string{"stack", "rm", testStackName}...)
+ out, err = d.Cmd("stack", "rm", testStackName)
c.Assert(err, checker.IsNil)
- out, err = d.Cmd([]string{"stack", "ls"}...)
+ out, err = d.Cmd("stack", "ls")
c.Assert(err, checker.IsNil)
c.Assert(out, check.Equals, "NAME SERVICES\n")
}
+func (s *DockerSwarmSuite) TestStackDeployWithSecretsTwice(c *check.C) {
+ d := s.AddDaemon(c, true, true)
+
+ out, err := d.Cmd("secret", "create", "outside", "fixtures/secrets/default")
+ c.Assert(err, checker.IsNil, check.Commentf(out))
+
+ testStackName := "testdeploy"
+ stackArgs := []string{
+ "stack", "deploy",
+ "--compose-file", "fixtures/deploy/secrets.yaml",
+ testStackName,
+ }
+ out, err = d.Cmd(stackArgs...)
+ c.Assert(err, checker.IsNil, check.Commentf(out))
+
+ out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Secrets }}", "testdeploy_web")
+ c.Assert(err, checker.IsNil)
+
+ var refs []swarm.SecretReference
+ c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil)
+ c.Assert(refs, checker.HasLen, 3)
+
+ sort.Sort(sortSecrets(refs))
+ c.Assert(refs[0].SecretName, checker.Equals, "outside")
+ c.Assert(refs[1].SecretName, checker.Equals, "testdeploy_special")
+ c.Assert(refs[1].File.Name, checker.Equals, "special")
+ c.Assert(refs[2].SecretName, checker.Equals, "testdeploy_super")
+ c.Assert(refs[2].File.Name, checker.Equals, "foo.txt")
+ c.Assert(refs[2].File.Mode, checker.Equals, os.FileMode(0400))
+
+ // Deploy again to ensure there are no errors when secret hasn't changed
+ out, err = d.Cmd(stackArgs...)
+ c.Assert(err, checker.IsNil, check.Commentf(out))
+}
+
+func (s *DockerSwarmSuite) TestStackRemove(c *check.C) {
+ d := s.AddDaemon(c, true, true)
+
+ stackName := "testdeploy"
+ stackArgs := []string{
+ "stack", "deploy",
+ "--compose-file", "fixtures/deploy/remove.yaml",
+ stackName,
+ }
+ out, err := d.Cmd(stackArgs...)
+ c.Assert(err, checker.IsNil, check.Commentf(out))
+
+ out, err = d.Cmd("stack", "ps", stackName)
+ c.Assert(err, checker.IsNil)
+ c.Assert(strings.Split(strings.TrimSpace(out), "\n"), checker.HasLen, 2)
+
+ out, err = d.Cmd("stack", "rm", stackName)
+ c.Assert(err, checker.IsNil, check.Commentf(out))
+ c.Assert(out, checker.Contains, "Removing service testdeploy_web")
+ c.Assert(out, checker.Contains, "Removing network testdeploy_default")
+ c.Assert(out, checker.Contains, "Removing secret testdeploy_special")
+}
+
+type sortSecrets []swarm.SecretReference
+
+func (s sortSecrets) Len() int { return len(s) }
+func (s sortSecrets) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s sortSecrets) Less(i, j int) bool { return s[i].SecretName < s[j].SecretName }
+
// testDAB is the DAB JSON used for testing.
// TODO: Use template/text and substitute "Image" with the result of
// `docker inspect --format '{{index .RepoDigests 0}}' busybox:latest`
diff --git a/integration-cli/fixtures/deploy/remove.yaml b/integration-cli/fixtures/deploy/remove.yaml
new file mode 100644
index 0000000..4ec8cac
--- /dev/null
+++ b/integration-cli/fixtures/deploy/remove.yaml
@@ -0,0 +1,11 @@
+
+version: "3.1"
+services:
+ web:
+ image: busybox@sha256:e4f93f6ed15a0cdd342f5aae387886fba0ab98af0a102da6276eaf24d6e6ade0
+ command: top
+ secrets:
+ - special
+secrets:
+ special:
+ file: fixtures/secrets/default
diff --git a/integration-cli/fixtures/deploy/secrets.yaml b/integration-cli/fixtures/deploy/secrets.yaml
new file mode 100644
index 0000000..6ac92cd
--- /dev/null
+++ b/integration-cli/fixtures/deploy/secrets.yaml
@@ -0,0 +1,20 @@
+
+version: "3.1"
+services:
+ web:
+ image: busybox@sha256:e4f93f6ed15a0cdd342f5aae387886fba0ab98af0a102da6276eaf24d6e6ade0
+ command: top
+ secrets:
+ - special
+ - source: super
+ target: foo.txt
+ mode: 0400
+ - star
+secrets:
+ special:
+ file: fixtures/secrets/default
+ super:
+ file: fixtures/secrets/default
+ star:
+ external:
+ name: outside
diff --git a/integration-cli/fixtures/secrets/default b/integration-cli/fixtures/secrets/default
new file mode 100644
index 0000000..e8ca7d8
--- /dev/null
+++ b/integration-cli/fixtures/secrets/default
@@ -0,0 +1 @@
+this is the secret