Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>
| ... | ... |
@@ -39,6 +39,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 39 | 39 |
flags.Var(&opts.mounts, flagMount, "Attach a filesystem mount to the service") |
| 40 | 40 |
flags.Var(&opts.constraints, flagConstraint, "Placement constraints") |
| 41 | 41 |
flags.Var(&opts.networks, flagNetwork, "Network attachments") |
| 42 |
+ flags.Var(&opts.secrets, flagSecret, "Specify secrets to expose to the service") |
|
| 42 | 43 |
flags.VarP(&opts.endpoint.ports, flagPublish, "p", "Publish a port as a node port") |
| 43 | 44 |
flags.Var(&opts.groups, flagGroup, "Set one or more supplementary user groups for the container") |
| 44 | 45 |
flags.Var(&opts.dns, flagDNS, "Set custom DNS servers") |
| ... | ... |
@@ -59,7 +60,7 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error {
|
| 59 | 59 |
} |
| 60 | 60 |
|
| 61 | 61 |
// parse and validate secrets |
| 62 |
- secrets, err := parseSecrets(apiClient, opts.secrets) |
|
| 62 |
+ secrets, err := parseSecrets(apiClient, opts.secrets.Value()) |
|
| 63 | 63 |
if err != nil {
|
| 64 | 64 |
return err |
| 65 | 65 |
} |
| ... | ... |
@@ -1,7 +1,10 @@ |
| 1 | 1 |
package service |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "encoding/csv" |
|
| 4 | 5 |
"fmt" |
| 6 |
+ "os" |
|
| 7 |
+ "path/filepath" |
|
| 5 | 8 |
"strconv" |
| 6 | 9 |
"strings" |
| 7 | 10 |
"time" |
| ... | ... |
@@ -139,6 +142,98 @@ func (f *floatValue) Value() float32 {
|
| 139 | 139 |
return float32(*f) |
| 140 | 140 |
} |
| 141 | 141 |
|
| 142 |
+// SecretRequestSpec is a type for requesting secrets |
|
| 143 |
+type SecretRequestSpec struct {
|
|
| 144 |
+ source string |
|
| 145 |
+ target string |
|
| 146 |
+ uid string |
|
| 147 |
+ gid string |
|
| 148 |
+ mode os.FileMode |
|
| 149 |
+} |
|
| 150 |
+ |
|
| 151 |
+// SecretOpt is a Value type for parsing secrets |
|
| 152 |
+type SecretOpt struct {
|
|
| 153 |
+ values []*SecretRequestSpec |
|
| 154 |
+} |
|
| 155 |
+ |
|
| 156 |
+// Set a new secret value |
|
| 157 |
+func (o *SecretOpt) Set(value string) error {
|
|
| 158 |
+ csvReader := csv.NewReader(strings.NewReader(value)) |
|
| 159 |
+ fields, err := csvReader.Read() |
|
| 160 |
+ if err != nil {
|
|
| 161 |
+ return err |
|
| 162 |
+ } |
|
| 163 |
+ |
|
| 164 |
+ spec := &SecretRequestSpec{
|
|
| 165 |
+ source: "", |
|
| 166 |
+ target: "", |
|
| 167 |
+ uid: "0", |
|
| 168 |
+ gid: "0", |
|
| 169 |
+ mode: 0444, |
|
| 170 |
+ } |
|
| 171 |
+ |
|
| 172 |
+ for _, field := range fields {
|
|
| 173 |
+ parts := strings.SplitN(field, "=", 2) |
|
| 174 |
+ key := strings.ToLower(parts[0]) |
|
| 175 |
+ |
|
| 176 |
+ if len(parts) != 2 {
|
|
| 177 |
+ return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
|
|
| 178 |
+ } |
|
| 179 |
+ |
|
| 180 |
+ value := parts[1] |
|
| 181 |
+ switch key {
|
|
| 182 |
+ case "source": |
|
| 183 |
+ spec.source = value |
|
| 184 |
+ case "target": |
|
| 185 |
+ tDir, _ := filepath.Split(value) |
|
| 186 |
+ if tDir != "" {
|
|
| 187 |
+ return fmt.Errorf("target must not have a path")
|
|
| 188 |
+ } |
|
| 189 |
+ spec.target = value |
|
| 190 |
+ case "uid": |
|
| 191 |
+ spec.uid = value |
|
| 192 |
+ case "gid": |
|
| 193 |
+ spec.gid = value |
|
| 194 |
+ case "mode": |
|
| 195 |
+ m, err := strconv.ParseUint(value, 0, 32) |
|
| 196 |
+ if err != nil {
|
|
| 197 |
+ return fmt.Errorf("invalid mode specified: %v", err)
|
|
| 198 |
+ } |
|
| 199 |
+ |
|
| 200 |
+ spec.mode = os.FileMode(m) |
|
| 201 |
+ default: |
|
| 202 |
+ return fmt.Errorf("invalid field in secret request: %s", key)
|
|
| 203 |
+ } |
|
| 204 |
+ } |
|
| 205 |
+ |
|
| 206 |
+ if spec.source == "" {
|
|
| 207 |
+ return fmt.Errorf("source is required")
|
|
| 208 |
+ } |
|
| 209 |
+ |
|
| 210 |
+ o.values = append(o.values, spec) |
|
| 211 |
+ return nil |
|
| 212 |
+} |
|
| 213 |
+ |
|
| 214 |
+// Type returns the type of this option |
|
| 215 |
+func (o *SecretOpt) Type() string {
|
|
| 216 |
+ return "secret" |
|
| 217 |
+} |
|
| 218 |
+ |
|
| 219 |
+// String returns a string repr of this option |
|
| 220 |
+func (o *SecretOpt) String() string {
|
|
| 221 |
+ secrets := []string{}
|
|
| 222 |
+ for _, secret := range o.values {
|
|
| 223 |
+ repr := fmt.Sprintf("%s -> %s", secret.source, secret.target)
|
|
| 224 |
+ secrets = append(secrets, repr) |
|
| 225 |
+ } |
|
| 226 |
+ return strings.Join(secrets, ", ") |
|
| 227 |
+} |
|
| 228 |
+ |
|
| 229 |
+// Value returns the secret requests |
|
| 230 |
+func (o *SecretOpt) Value() []*SecretRequestSpec {
|
|
| 231 |
+ return o.values |
|
| 232 |
+} |
|
| 233 |
+ |
|
| 142 | 234 |
type updateOptions struct {
|
| 143 | 235 |
parallelism uint64 |
| 144 | 236 |
delay time.Duration |
| ... | ... |
@@ -337,7 +432,7 @@ type serviceOptions struct {
|
| 337 | 337 |
logDriver logDriverOptions |
| 338 | 338 |
|
| 339 | 339 |
healthcheck healthCheckOptions |
| 340 |
- secrets []string |
|
| 340 |
+ secrets SecretOpt |
|
| 341 | 341 |
} |
| 342 | 342 |
|
| 343 | 343 |
func newServiceOptions() *serviceOptions {
|
| ... | ... |
@@ -1,6 +1,7 @@ |
| 1 | 1 |
package service |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "os" |
|
| 4 | 5 |
"reflect" |
| 5 | 6 |
"testing" |
| 6 | 7 |
"time" |
| ... | ... |
@@ -105,3 +106,47 @@ func TestHealthCheckOptionsToHealthConfigConflict(t *testing.T) {
|
| 105 | 105 |
_, err := opt.toHealthConfig() |
| 106 | 106 |
assert.Error(t, err, "--no-healthcheck conflicts with --health-* options") |
| 107 | 107 |
} |
| 108 |
+ |
|
| 109 |
+func TestSecretOptionsSimple(t *testing.T) {
|
|
| 110 |
+ var opt SecretOpt |
|
| 111 |
+ |
|
| 112 |
+ testCase := "source=/foo,target=testing" |
|
| 113 |
+ assert.NilError(t, opt.Set(testCase)) |
|
| 114 |
+ |
|
| 115 |
+ reqs := opt.Value() |
|
| 116 |
+ assert.Equal(t, len(reqs), 1) |
|
| 117 |
+ req := reqs[0] |
|
| 118 |
+ assert.Equal(t, req.source, "/foo") |
|
| 119 |
+ assert.Equal(t, req.target, "testing") |
|
| 120 |
+} |
|
| 121 |
+ |
|
| 122 |
+func TestSecretOptionsCustomUidGid(t *testing.T) {
|
|
| 123 |
+ var opt SecretOpt |
|
| 124 |
+ |
|
| 125 |
+ testCase := "source=/foo,target=testing,uid=1000,gid=1001" |
|
| 126 |
+ assert.NilError(t, opt.Set(testCase)) |
|
| 127 |
+ |
|
| 128 |
+ reqs := opt.Value() |
|
| 129 |
+ assert.Equal(t, len(reqs), 1) |
|
| 130 |
+ req := reqs[0] |
|
| 131 |
+ assert.Equal(t, req.source, "/foo") |
|
| 132 |
+ assert.Equal(t, req.target, "testing") |
|
| 133 |
+ assert.Equal(t, req.uid, "1000") |
|
| 134 |
+ assert.Equal(t, req.gid, "1001") |
|
| 135 |
+} |
|
| 136 |
+ |
|
| 137 |
+func TestSecretOptionsCustomMode(t *testing.T) {
|
|
| 138 |
+ var opt SecretOpt |
|
| 139 |
+ |
|
| 140 |
+ testCase := "source=/foo,target=testing,uid=1000,gid=1001,mode=0444" |
|
| 141 |
+ assert.NilError(t, opt.Set(testCase)) |
|
| 142 |
+ |
|
| 143 |
+ reqs := opt.Value() |
|
| 144 |
+ assert.Equal(t, len(reqs), 1) |
|
| 145 |
+ req := reqs[0] |
|
| 146 |
+ assert.Equal(t, req.source, "/foo") |
|
| 147 |
+ assert.Equal(t, req.target, "testing") |
|
| 148 |
+ assert.Equal(t, req.uid, "1000") |
|
| 149 |
+ assert.Equal(t, req.gid, "1001") |
|
| 150 |
+ assert.Equal(t, req.mode, os.FileMode(0444)) |
|
| 151 |
+} |
| ... | ... |
@@ -3,8 +3,6 @@ package service |
| 3 | 3 |
import ( |
| 4 | 4 |
"context" |
| 5 | 5 |
"fmt" |
| 6 |
- "path/filepath" |
|
| 7 |
- "strings" |
|
| 8 | 6 |
|
| 9 | 7 |
"github.com/docker/docker/api/types" |
| 10 | 8 |
"github.com/docker/docker/api/types/filters" |
| ... | ... |
@@ -12,61 +10,27 @@ import ( |
| 12 | 12 |
"github.com/docker/docker/client" |
| 13 | 13 |
) |
| 14 | 14 |
|
| 15 |
-// parseSecretString parses the requested secret and returns the secret name |
|
| 16 |
-// and target. Expects format SECRET_NAME:TARGET |
|
| 17 |
-func parseSecretString(secretString string) (string, string, error) {
|
|
| 18 |
- tokens := strings.Split(secretString, ":") |
|
| 19 |
- |
|
| 20 |
- secretName := strings.TrimSpace(tokens[0]) |
|
| 21 |
- targetName := secretName |
|
| 22 |
- |
|
| 23 |
- if secretName == "" {
|
|
| 24 |
- return "", "", fmt.Errorf("invalid secret name provided")
|
|
| 25 |
- } |
|
| 26 |
- |
|
| 27 |
- if len(tokens) > 1 {
|
|
| 28 |
- targetName = strings.TrimSpace(tokens[1]) |
|
| 29 |
- if targetName == "" {
|
|
| 30 |
- return "", "", fmt.Errorf("invalid presentation name provided")
|
|
| 31 |
- } |
|
| 32 |
- } |
|
| 33 |
- |
|
| 34 |
- // ensure target is a filename only; no paths allowed |
|
| 35 |
- tDir, _ := filepath.Split(targetName) |
|
| 36 |
- if tDir != "" {
|
|
| 37 |
- return "", "", fmt.Errorf("target must not have a path")
|
|
| 38 |
- } |
|
| 39 |
- |
|
| 40 |
- return secretName, targetName, nil |
|
| 41 |
-} |
|
| 42 |
- |
|
| 43 | 15 |
// parseSecrets retrieves the secrets from the requested names and converts |
| 44 | 16 |
// them to secret references to use with the spec |
| 45 |
-func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmtypes.SecretReference, error) {
|
|
| 17 |
+func parseSecrets(client client.APIClient, requestedSecrets []*SecretRequestSpec) ([]*swarmtypes.SecretReference, error) {
|
|
| 46 | 18 |
secretRefs := make(map[string]*swarmtypes.SecretReference) |
| 47 | 19 |
ctx := context.Background() |
| 48 | 20 |
|
| 49 | 21 |
for _, secret := range requestedSecrets {
|
| 50 |
- n, t, err := parseSecretString(secret) |
|
| 51 |
- if err != nil {
|
|
| 52 |
- return nil, err |
|
| 53 |
- } |
|
| 54 |
- |
|
| 55 | 22 |
secretRef := &swarmtypes.SecretReference{
|
| 56 |
- SecretName: n, |
|
| 57 |
- // TODO (ehazlett): parse these from cli request |
|
| 23 |
+ SecretName: secret.source, |
|
| 58 | 24 |
Target: swarmtypes.SecretReferenceFileTarget{
|
| 59 |
- Name: t, |
|
| 60 |
- UID: "0", |
|
| 61 |
- GID: "0", |
|
| 62 |
- Mode: 0444, |
|
| 25 |
+ Name: secret.target, |
|
| 26 |
+ UID: secret.uid, |
|
| 27 |
+ GID: secret.gid, |
|
| 28 |
+ Mode: secret.mode, |
|
| 63 | 29 |
}, |
| 64 | 30 |
} |
| 65 | 31 |
|
| 66 |
- if _, exists := secretRefs[t]; exists {
|
|
| 67 |
- return nil, fmt.Errorf("duplicate secret target for %s not allowed", n)
|
|
| 32 |
+ if _, exists := secretRefs[secret.target]; exists {
|
|
| 33 |
+ return nil, fmt.Errorf("duplicate secret target for %s not allowed", secret.source)
|
|
| 68 | 34 |
} |
| 69 |
- secretRefs[t] = secretRef |
|
| 35 |
+ secretRefs[secret.target] = secretRef |
|
| 70 | 36 |
} |
| 71 | 37 |
|
| 72 | 38 |
args := filters.NewArgs() |
| ... | ... |
@@ -56,7 +56,7 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 56 | 56 |
flags.Var(&opts.containerLabels, flagContainerLabelAdd, "Add or update a container label") |
| 57 | 57 |
flags.Var(&opts.env, flagEnvAdd, "Add or update an environment variable") |
| 58 | 58 |
flags.Var(newListOptsVar(), flagSecretRemove, "Remove a secret") |
| 59 |
- flags.StringSliceVar(&opts.secrets, flagSecretAdd, []string{}, "Add a secret")
|
|
| 59 |
+ flags.Var(&opts.secrets, flagSecretAdd, "Add or update a secret on a service") |
|
| 60 | 60 |
flags.Var(&opts.mounts, flagMountAdd, "Add or update a mount on a service") |
| 61 | 61 |
flags.Var(&opts.constraints, flagConstraintAdd, "Add or update a placement constraint") |
| 62 | 62 |
flags.Var(&opts.endpoint.ports, flagPublishAdd, "Add or update a published port") |
| ... | ... |
@@ -413,10 +413,7 @@ func updateEnvironment(flags *pflag.FlagSet, field *[]string) {
|
| 413 | 413 |
|
| 414 | 414 |
func getUpdatedSecrets(apiClient client.APIClient, flags *pflag.FlagSet, secrets []*swarm.SecretReference) ([]*swarm.SecretReference, error) {
|
| 415 | 415 |
if flags.Changed(flagSecretAdd) {
|
| 416 |
- values, err := flags.GetStringSlice(flagSecretAdd) |
|
| 417 |
- if err != nil {
|
|
| 418 |
- return nil, err |
|
| 419 |
- } |
|
| 416 |
+ values := flags.Lookup(flagSecretAdd).Value.(*SecretOpt).Value() |
|
| 420 | 417 |
|
| 421 | 418 |
addSecrets, err := parseSecrets(apiClient, values) |
| 422 | 419 |
if err != nil {
|
| ... | ... |
@@ -126,7 +126,7 @@ Use the `--secret` flag to give a container access to a |
| 126 | 126 |
with two secrets named `ssh-key` and `app-key`: |
| 127 | 127 |
|
| 128 | 128 |
```bash |
| 129 |
-$ docker service create --name redis --secret ssh-key:ssh --secret app-key:app redis:3.0.6 |
|
| 129 |
+$ docker service create --name redis --secret source=ssh-key,target=ssh --secret source=app-key,target=app,uid=1000,gid=1001,mode=0400 redis:3.0.6 |
|
| 130 | 130 |
4cdgfyky7ozwh3htjfw0d12qv |
| 131 | 131 |
``` |
| 132 | 132 |
|
| ... | ... |
@@ -59,7 +59,7 @@ func (s *DockerSwarmSuite) TestServiceCreateWithSecret(c *check.C) {
|
| 59 | 59 |
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id))
|
| 60 | 60 |
testTarget := "testing" |
| 61 | 61 |
|
| 62 |
- out, err := d.Cmd("service", "create", "--name", serviceName, "--secret", fmt.Sprintf("%s:%s", testName, testTarget), "busybox", "top")
|
|
| 62 |
+ out, err := d.Cmd("service", "create", "--name", serviceName, "--secret", fmt.Sprintf("source=%s,target=%s", testName, testTarget), "busybox", "top")
|
|
| 63 | 63 |
c.Assert(err, checker.IsNil, check.Commentf(out)) |
| 64 | 64 |
|
| 65 | 65 |
out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Secrets }}", serviceName)
|
| ... | ... |
@@ -103,7 +103,7 @@ func (s *DockerSwarmSuite) TestServiceUpdateSecrets(c *check.C) {
|
| 103 | 103 |
c.Assert(err, checker.IsNil, check.Commentf(out)) |
| 104 | 104 |
|
| 105 | 105 |
// add secret |
| 106 |
- out, err = d.Cmd("service", "update", "test", "--secret-add", fmt.Sprintf("%s:%s", testName, testTarget))
|
|
| 106 |
+ out, err = d.Cmd("service", "update", "test", "--secret-add", fmt.Sprintf("source=%s,target=%s", testName, testTarget))
|
|
| 107 | 107 |
c.Assert(err, checker.IsNil, check.Commentf(out)) |
| 108 | 108 |
|
| 109 | 109 |
out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Secrets }}", serviceName)
|