Browse code

Merge pull request #33702 from aaronlehmann/templated-secrets-and-configs

Templated secrets and configs

Sebastiaan van Stijn authored on 2018/02/21 21:39:10
Showing 18 changed files
... ...
@@ -372,6 +372,10 @@ func (sr *swarmRouter) createSecret(ctx context.Context, w http.ResponseWriter,
372 372
 	if err := json.NewDecoder(r.Body).Decode(&secret); err != nil {
373 373
 		return err
374 374
 	}
375
+	version := httputils.VersionFromContext(ctx)
376
+	if secret.Templating != nil && versions.LessThan(version, "1.36") {
377
+		return errdefs.InvalidParameter(errors.Errorf("secret templating is not supported on the specified API version: %s", version))
378
+	}
375 379
 
376 380
 	id, err := sr.backend.CreateSecret(secret)
377 381
 	if err != nil {
... ...
@@ -440,6 +444,11 @@ func (sr *swarmRouter) createConfig(ctx context.Context, w http.ResponseWriter,
440 440
 		return err
441 441
 	}
442 442
 
443
+	version := httputils.VersionFromContext(ctx)
444
+	if config.Templating != nil && versions.LessThan(version, "1.36") {
445
+		return errdefs.InvalidParameter(errors.Errorf("config templating is not supported on the specified API version: %s", version))
446
+	}
447
+
443 448
 	id, err := sr.backend.CreateConfig(config)
444 449
 	if err != nil {
445 450
 		return err
... ...
@@ -3339,6 +3339,13 @@ definitions:
3339 3339
       Driver:
3340 3340
         description: "Name of the secrets driver used to fetch the secret's value from an external secret store"
3341 3341
         $ref: "#/definitions/Driver"
3342
+      Templating:
3343
+        description: |
3344
+          Templating driver, if applicable
3345
+
3346
+          Templating controls whether and how to evaluate the config payload as
3347
+          a template. If no driver is set, no templating is used.
3348
+        $ref: "#/definitions/Driver"
3342 3349
 
3343 3350
   Secret:
3344 3351
     type: "object"
... ...
@@ -3375,6 +3382,13 @@ definitions:
3375 3375
           Base64-url-safe-encoded ([RFC 4648](https://tools.ietf.org/html/rfc4648#section-3.2))
3376 3376
           config data.
3377 3377
         type: "string"
3378
+      Templating:
3379
+        description: |
3380
+          Templating driver, if applicable
3381
+
3382
+          Templating controls whether and how to evaluate the config payload as
3383
+          a template. If no driver is set, no templating is used.
3384
+        $ref: "#/definitions/Driver"
3378 3385
 
3379 3386
   Config:
3380 3387
     type: "object"
... ...
@@ -13,6 +13,10 @@ type Config struct {
13 13
 type ConfigSpec struct {
14 14
 	Annotations
15 15
 	Data []byte `json:",omitempty"`
16
+
17
+	// Templating controls whether and how to evaluate the config payload as
18
+	// a template. If it is not set, no templating is used.
19
+	Templating *Driver `json:",omitempty"`
16 20
 }
17 21
 
18 22
 // ConfigReferenceFileTarget is a file target in a config reference
... ...
@@ -14,6 +14,10 @@ type SecretSpec struct {
14 14
 	Annotations
15 15
 	Data   []byte  `json:",omitempty"`
16 16
 	Driver *Driver `json:",omitempty"` // name of the secrets driver used to fetch the secret's value from an external secret store
17
+
18
+	// Templating controls whether and how to evaluate the secret payload as
19
+	// a template. If it is not set, no templating is used.
20
+	Templating *Driver `json:",omitempty"`
17 21
 }
18 22
 
19 23
 // SecretReferenceFileTarget is a file target in a secret reference
... ...
@@ -1049,21 +1049,6 @@ func getSecretTargetPath(r *swarmtypes.SecretReference) string {
1049 1049
 	return filepath.Join(containerSecretMountPath, r.File.Name)
1050 1050
 }
1051 1051
 
1052
-// ConfigsDirPath returns the path to the directory where configs are stored on
1053
-// disk.
1054
-func (container *Container) ConfigsDirPath() (string, error) {
1055
-	return container.GetRootResourcePath("configs")
1056
-}
1057
-
1058
-// ConfigFilePath returns the path to the on-disk location of a config.
1059
-func (container *Container) ConfigFilePath(configRef swarmtypes.ConfigReference) (string, error) {
1060
-	configs, err := container.ConfigsDirPath()
1061
-	if err != nil {
1062
-		return "", err
1063
-	}
1064
-	return filepath.Join(configs, configRef.ConfigID), nil
1065
-}
1066
-
1067 1052
 // CreateDaemonEnvironment creates a new environment variable slice for this container.
1068 1053
 func (container *Container) CreateDaemonEnvironment(tty bool, linkedEnv []string) []string {
1069 1054
 	// Setup environment
... ...
@@ -5,11 +5,13 @@ package container // import "github.com/docker/docker/container"
5 5
 import (
6 6
 	"io/ioutil"
7 7
 	"os"
8
+	"path/filepath"
8 9
 
9 10
 	"github.com/containerd/continuity/fs"
10 11
 	"github.com/docker/docker/api/types"
11 12
 	containertypes "github.com/docker/docker/api/types/container"
12 13
 	mounttypes "github.com/docker/docker/api/types/mount"
14
+	swarmtypes "github.com/docker/docker/api/types/swarm"
13 15
 	"github.com/docker/docker/pkg/mount"
14 16
 	"github.com/docker/docker/pkg/stringid"
15 17
 	"github.com/docker/docker/volume"
... ...
@@ -233,6 +235,17 @@ func (container *Container) SecretMounts() ([]Mount, error) {
233 233
 			Writable:    false,
234 234
 		})
235 235
 	}
236
+	for _, r := range container.ConfigReferences {
237
+		fPath, err := container.ConfigFilePath(*r)
238
+		if err != nil {
239
+			return nil, err
240
+		}
241
+		mounts = append(mounts, Mount{
242
+			Source:      fPath,
243
+			Destination: r.File.Name,
244
+			Writable:    false,
245
+		})
246
+	}
236 247
 
237 248
 	return mounts, nil
238 249
 }
... ...
@@ -253,27 +266,6 @@ func (container *Container) UnmountSecrets() error {
253 253
 	return mount.RecursiveUnmount(p)
254 254
 }
255 255
 
256
-// ConfigMounts returns the mounts for configs.
257
-func (container *Container) ConfigMounts() ([]Mount, error) {
258
-	var mounts []Mount
259
-	for _, configRef := range container.ConfigReferences {
260
-		if configRef.File == nil {
261
-			continue
262
-		}
263
-		src, err := container.ConfigFilePath(*configRef)
264
-		if err != nil {
265
-			return nil, err
266
-		}
267
-		mounts = append(mounts, Mount{
268
-			Source:      src,
269
-			Destination: configRef.File.Name,
270
-			Writable:    false,
271
-		})
272
-	}
273
-
274
-	return mounts, nil
275
-}
276
-
277 256
 type conflictingUpdateOptions string
278 257
 
279 258
 func (e conflictingUpdateOptions) Error() string {
... ...
@@ -457,3 +449,13 @@ func (container *Container) GetMountPoints() []types.MountPoint {
457 457
 	}
458 458
 	return mountPoints
459 459
 }
460
+
461
+// ConfigFilePath returns the path to the on-disk location of a config.
462
+// On unix, configs are always considered secret
463
+func (container *Container) ConfigFilePath(configRef swarmtypes.ConfigReference) (string, error) {
464
+	mounts, err := container.SecretMountPath()
465
+	if err != nil {
466
+		return "", err
467
+	}
468
+	return filepath.Join(mounts, configRef.ConfigID), nil
469
+}
... ...
@@ -7,6 +7,7 @@ import (
7 7
 
8 8
 	"github.com/docker/docker/api/types"
9 9
 	containertypes "github.com/docker/docker/api/types/container"
10
+	swarmtypes "github.com/docker/docker/api/types/swarm"
10 11
 	"github.com/docker/docker/pkg/system"
11 12
 )
12 13
 
... ...
@@ -102,23 +103,20 @@ func (container *Container) CreateConfigSymlinks() error {
102 102
 }
103 103
 
104 104
 // ConfigMounts returns the mount for configs.
105
-// All configs are stored in a single mount on Windows. Target symlinks are
106
-// created for each config, pointing to the files in this mount.
107
-func (container *Container) ConfigMounts() ([]Mount, error) {
105
+// TODO: Right now Windows doesn't really have a "secure" storage for secrets,
106
+// however some configs may contain secrets. Once secure storage is worked out,
107
+// configs and secret handling should be merged.
108
+func (container *Container) ConfigMounts() []Mount {
108 109
 	var mounts []Mount
109 110
 	if len(container.ConfigReferences) > 0 {
110
-		src, err := container.ConfigsDirPath()
111
-		if err != nil {
112
-			return nil, err
113
-		}
114 111
 		mounts = append(mounts, Mount{
115
-			Source:      src,
112
+			Source:      container.ConfigsDirPath(),
116 113
 			Destination: containerInternalConfigsDirPath,
117 114
 			Writable:    false,
118 115
 		})
119 116
 	}
120 117
 
121
-	return mounts, nil
118
+	return mounts
122 119
 }
123 120
 
124 121
 // DetachAndUnmount unmounts all volumes.
... ...
@@ -204,3 +202,12 @@ func (container *Container) GetMountPoints() []types.MountPoint {
204 204
 	}
205 205
 	return mountPoints
206 206
 }
207
+
208
+func (container *Container) ConfigsDirPath() string {
209
+	return filepath.Join(container.Root, "configs")
210
+}
211
+
212
+// ConfigFilePath returns the path to the on-disk location of a config.
213
+func (container *Container) ConfigFilePath(configRef swarmtypes.ConfigReference) string {
214
+	return filepath.Join(container.ConfigsDirPath(), configRef.ConfigID)
215
+}
... ...
@@ -2,6 +2,7 @@ package convert // import "github.com/docker/docker/daemon/cluster/convert"
2 2
 
3 3
 import (
4 4
 	swarmtypes "github.com/docker/docker/api/types/swarm"
5
+	types "github.com/docker/docker/api/types/swarm"
5 6
 	swarmapi "github.com/docker/swarmkit/api"
6 7
 	gogotypes "github.com/gogo/protobuf/types"
7 8
 )
... ...
@@ -21,18 +22,34 @@ func ConfigFromGRPC(s *swarmapi.Config) swarmtypes.Config {
21 21
 	config.CreatedAt, _ = gogotypes.TimestampFromProto(s.Meta.CreatedAt)
22 22
 	config.UpdatedAt, _ = gogotypes.TimestampFromProto(s.Meta.UpdatedAt)
23 23
 
24
+	if s.Spec.Templating != nil {
25
+		config.Spec.Templating = &types.Driver{
26
+			Name:    s.Spec.Templating.Name,
27
+			Options: s.Spec.Templating.Options,
28
+		}
29
+	}
30
+
24 31
 	return config
25 32
 }
26 33
 
27 34
 // ConfigSpecToGRPC converts Config to a grpc Config.
28 35
 func ConfigSpecToGRPC(s swarmtypes.ConfigSpec) swarmapi.ConfigSpec {
29
-	return swarmapi.ConfigSpec{
36
+	spec := swarmapi.ConfigSpec{
30 37
 		Annotations: swarmapi.Annotations{
31 38
 			Name:   s.Name,
32 39
 			Labels: s.Labels,
33 40
 		},
34 41
 		Data: s.Data,
35 42
 	}
43
+
44
+	if s.Templating != nil {
45
+		spec.Templating = &swarmapi.Driver{
46
+			Name:    s.Templating.Name,
47
+			Options: s.Templating.Options,
48
+		}
49
+	}
50
+
51
+	return spec
36 52
 }
37 53
 
38 54
 // ConfigReferencesFromGRPC converts a slice of grpc ConfigReference to ConfigReference
... ...
@@ -2,6 +2,7 @@ package convert // import "github.com/docker/docker/daemon/cluster/convert"
2 2
 
3 3
 import (
4 4
 	swarmtypes "github.com/docker/docker/api/types/swarm"
5
+	types "github.com/docker/docker/api/types/swarm"
5 6
 	swarmapi "github.com/docker/swarmkit/api"
6 7
 	gogotypes "github.com/gogo/protobuf/types"
7 8
 )
... ...
@@ -22,12 +23,19 @@ func SecretFromGRPC(s *swarmapi.Secret) swarmtypes.Secret {
22 22
 	secret.CreatedAt, _ = gogotypes.TimestampFromProto(s.Meta.CreatedAt)
23 23
 	secret.UpdatedAt, _ = gogotypes.TimestampFromProto(s.Meta.UpdatedAt)
24 24
 
25
+	if s.Spec.Templating != nil {
26
+		secret.Spec.Templating = &types.Driver{
27
+			Name:    s.Spec.Templating.Name,
28
+			Options: s.Spec.Templating.Options,
29
+		}
30
+	}
31
+
25 32
 	return secret
26 33
 }
27 34
 
28 35
 // SecretSpecToGRPC converts Secret to a grpc Secret.
29 36
 func SecretSpecToGRPC(s swarmtypes.SecretSpec) swarmapi.SecretSpec {
30
-	return swarmapi.SecretSpec{
37
+	spec := swarmapi.SecretSpec{
31 38
 		Annotations: swarmapi.Annotations{
32 39
 			Name:   s.Name,
33 40
 			Labels: s.Labels,
... ...
@@ -35,6 +43,15 @@ func SecretSpecToGRPC(s swarmtypes.SecretSpec) swarmapi.SecretSpec {
35 35
 		Data:   s.Data,
36 36
 		Driver: driverToGRPC(s.Driver),
37 37
 	}
38
+
39
+	if s.Templating != nil {
40
+		spec.Templating = &swarmapi.Driver{
41
+			Name:    s.Templating.Name,
42
+			Options: s.Templating.Options,
43
+		}
44
+	}
45
+
46
+	return spec
38 47
 }
39 48
 
40 49
 // SecretReferencesFromGRPC converts a slice of grpc SecretReference to SecretReference
... ...
@@ -19,6 +19,7 @@ import (
19 19
 	"github.com/docker/swarmkit/agent/exec"
20 20
 	"github.com/docker/swarmkit/api"
21 21
 	"github.com/docker/swarmkit/api/naming"
22
+	"github.com/docker/swarmkit/template"
22 23
 	"github.com/sirupsen/logrus"
23 24
 	"golang.org/x/net/context"
24 25
 )
... ...
@@ -191,7 +192,7 @@ func (e *executor) Configure(ctx context.Context, node *api.Node) error {
191 191
 
192 192
 // Controller returns a docker container runner.
193 193
 func (e *executor) Controller(t *api.Task) (exec.Controller, error) {
194
-	dependencyGetter := agent.Restrict(e.dependencies, t)
194
+	dependencyGetter := template.NewTemplatedDependencyGetter(agent.Restrict(e.dependencies, t), t, nil)
195 195
 
196 196
 	// Get the node description from the executor field
197 197
 	e.mutex.Lock()
... ...
@@ -16,8 +16,6 @@ func (daemon *Daemon) SetContainerConfigReferences(name string, refs []*swarmtyp
16 16
 	if err != nil {
17 17
 		return err
18 18
 	}
19
-
20
-	c.ConfigReferences = refs
21
-
19
+	c.ConfigReferences = append(c.ConfigReferences, refs...)
22 20
 	return nil
23 21
 }
... ...
@@ -161,43 +161,26 @@ func (daemon *Daemon) setupIpcDirs(c *container.Container) error {
161 161
 }
162 162
 
163 163
 func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) {
164
-	if len(c.SecretReferences) == 0 {
164
+	if len(c.SecretReferences) == 0 && len(c.ConfigReferences) == 0 {
165 165
 		return nil
166 166
 	}
167 167
 
168
-	localMountPath, err := c.SecretMountPath()
169
-	if err != nil {
170
-		return errors.Wrap(err, "error getting secrets mount dir")
171
-	}
172
-	logrus.Debugf("secrets: setting up secret dir: %s", localMountPath)
173
-
174
-	// retrieve possible remapped range start for root UID, GID
175
-	rootIDs := daemon.idMappings.RootPair()
176
-	// create tmpfs
177
-	if err := idtools.MkdirAllAndChown(localMountPath, 0700, rootIDs); err != nil {
178
-		return errors.Wrap(err, "error creating secret local mount path")
168
+	if err := daemon.createSecretsDir(c); err != nil {
169
+		return err
179 170
 	}
180
-
181 171
 	defer func() {
182 172
 		if setupErr != nil {
183
-			// cleanup
184
-			_ = detachMounted(localMountPath)
185
-
186
-			if err := os.RemoveAll(localMountPath); err != nil {
187
-				logrus.Errorf("error cleaning up secret mount: %s", err)
188
-			}
173
+			daemon.cleanupSecretDir(c)
189 174
 		}
190 175
 	}()
191 176
 
192
-	tmpfsOwnership := fmt.Sprintf("uid=%d,gid=%d", rootIDs.UID, rootIDs.GID)
193
-	if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "nodev,nosuid,noexec,"+tmpfsOwnership); err != nil {
194
-		return errors.Wrap(err, "unable to setup secret mount")
195
-	}
196
-
197 177
 	if c.DependencyStore == nil {
198 178
 		return fmt.Errorf("secret store is not initialized")
199 179
 	}
200 180
 
181
+	// retrieve possible remapped range start for root UID, GID
182
+	rootIDs := daemon.idMappings.RootPair()
183
+
201 184
 	for _, s := range c.SecretReferences {
202 185
 		// TODO (ehazlett): use type switch when more are supported
203 186
 		if s.File == nil {
... ...
@@ -244,78 +227,38 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) {
244 244
 		}
245 245
 	}
246 246
 
247
-	label.Relabel(localMountPath, c.MountLabel, false)
248
-
249
-	// remount secrets ro
250
-	if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "remount,ro,"+tmpfsOwnership); err != nil {
251
-		return errors.Wrap(err, "unable to remount secret dir as readonly")
252
-	}
253
-
254
-	return nil
255
-}
256
-
257
-func (daemon *Daemon) setupConfigDir(c *container.Container) (setupErr error) {
258
-	if len(c.ConfigReferences) == 0 {
259
-		return nil
260
-	}
261
-
262
-	localPath, err := c.ConfigsDirPath()
263
-	if err != nil {
264
-		return err
265
-	}
266
-	logrus.Debugf("configs: setting up config dir: %s", localPath)
267
-
268
-	// retrieve possible remapped range start for root UID, GID
269
-	rootIDs := daemon.idMappings.RootPair()
270
-	// create tmpfs
271
-	if err := idtools.MkdirAllAndChown(localPath, 0700, rootIDs); err != nil {
272
-		return errors.Wrap(err, "error creating config dir")
273
-	}
274
-
275
-	defer func() {
276
-		if setupErr != nil {
277
-			if err := os.RemoveAll(localPath); err != nil {
278
-				logrus.Errorf("error cleaning up config dir: %s", err)
279
-			}
280
-		}
281
-	}()
282
-
283
-	if c.DependencyStore == nil {
284
-		return fmt.Errorf("config store is not initialized")
285
-	}
286
-
287
-	for _, configRef := range c.ConfigReferences {
247
+	for _, ref := range c.ConfigReferences {
288 248
 		// TODO (ehazlett): use type switch when more are supported
289
-		if configRef.File == nil {
249
+		if ref.File == nil {
290 250
 			logrus.Error("config target type is not a file target")
291 251
 			continue
292 252
 		}
293 253
 
294
-		fPath, err := c.ConfigFilePath(*configRef)
254
+		fPath, err := c.ConfigFilePath(*ref)
295 255
 		if err != nil {
296
-			return err
256
+			return errors.Wrap(err, "error getting config file path for container")
297 257
 		}
298
-
299
-		log := logrus.WithFields(logrus.Fields{"name": configRef.File.Name, "path": fPath})
300
-
301 258
 		if err := idtools.MkdirAllAndChown(filepath.Dir(fPath), 0700, rootIDs); err != nil {
302
-			return errors.Wrap(err, "error creating config path")
259
+			return errors.Wrap(err, "error creating config mount path")
303 260
 		}
304 261
 
305
-		log.Debug("injecting config")
306
-		config, err := c.DependencyStore.Configs().Get(configRef.ConfigID)
262
+		logrus.WithFields(logrus.Fields{
263
+			"name": ref.File.Name,
264
+			"path": fPath,
265
+		}).Debug("injecting config")
266
+		config, err := c.DependencyStore.Configs().Get(ref.ConfigID)
307 267
 		if err != nil {
308 268
 			return errors.Wrap(err, "unable to get config from config store")
309 269
 		}
310
-		if err := ioutil.WriteFile(fPath, config.Spec.Data, configRef.File.Mode); err != nil {
270
+		if err := ioutil.WriteFile(fPath, config.Spec.Data, ref.File.Mode); err != nil {
311 271
 			return errors.Wrap(err, "error injecting config")
312 272
 		}
313 273
 
314
-		uid, err := strconv.Atoi(configRef.File.UID)
274
+		uid, err := strconv.Atoi(ref.File.UID)
315 275
 		if err != nil {
316 276
 			return err
317 277
 		}
318
-		gid, err := strconv.Atoi(configRef.File.GID)
278
+		gid, err := strconv.Atoi(ref.File.GID)
319 279
 		if err != nil {
320 280
 			return err
321 281
 		}
... ...
@@ -323,16 +266,69 @@ func (daemon *Daemon) setupConfigDir(c *container.Container) (setupErr error) {
323 323
 		if err := os.Chown(fPath, rootIDs.UID+uid, rootIDs.GID+gid); err != nil {
324 324
 			return errors.Wrap(err, "error setting ownership for config")
325 325
 		}
326
-		if err := os.Chmod(fPath, configRef.File.Mode); err != nil {
326
+		if err := os.Chmod(fPath, ref.File.Mode); err != nil {
327 327
 			return errors.Wrap(err, "error setting file mode for config")
328 328
 		}
329
+	}
329 330
 
330
-		label.Relabel(fPath, c.MountLabel, false)
331
+	return daemon.remountSecretDir(c)
332
+}
333
+
334
+// createSecretsDir is used to create a dir suitable for storing container secrets.
335
+// In practice this is using a tmpfs mount and is used for both "configs" and "secrets"
336
+func (daemon *Daemon) createSecretsDir(c *container.Container) error {
337
+	// retrieve possible remapped range start for root UID, GID
338
+	rootIDs := daemon.idMappings.RootPair()
339
+	dir, err := c.SecretMountPath()
340
+	if err != nil {
341
+		return errors.Wrap(err, "error getting container secrets dir")
342
+	}
343
+
344
+	// create tmpfs
345
+	if err := idtools.MkdirAllAndChown(dir, 0700, rootIDs); err != nil {
346
+		return errors.Wrap(err, "error creating secret local mount path")
347
+	}
348
+
349
+	tmpfsOwnership := fmt.Sprintf("uid=%d,gid=%d", rootIDs.UID, rootIDs.GID)
350
+	if err := mount.Mount("tmpfs", dir, "tmpfs", "nodev,nosuid,noexec,"+tmpfsOwnership); err != nil {
351
+		return errors.Wrap(err, "unable to setup secret mount")
352
+	}
353
+
354
+	return nil
355
+}
356
+
357
+func (daemon *Daemon) remountSecretDir(c *container.Container) error {
358
+	dir, err := c.SecretMountPath()
359
+	if err != nil {
360
+		return errors.Wrap(err, "error getting container secrets path")
361
+	}
362
+	if err := label.Relabel(dir, c.MountLabel, false); err != nil {
363
+		logrus.WithError(err).WithField("dir", dir).Warn("Error while attempting to set selinux label")
364
+	}
365
+	rootIDs := daemon.idMappings.RootPair()
366
+	tmpfsOwnership := fmt.Sprintf("uid=%d,gid=%d", rootIDs.UID, rootIDs.GID)
367
+
368
+	// remount secrets ro
369
+	if err := mount.Mount("tmpfs", dir, "tmpfs", "remount,ro,"+tmpfsOwnership); err != nil {
370
+		return errors.Wrap(err, "unable to remount dir as readonly")
331 371
 	}
332 372
 
333 373
 	return nil
334 374
 }
335 375
 
376
+func (daemon *Daemon) cleanupSecretDir(c *container.Container) {
377
+	dir, err := c.SecretMountPath()
378
+	if err != nil {
379
+		logrus.WithError(err).WithField("container", c.ID).Warn("error getting secrets mount path for container")
380
+	}
381
+	if err := mount.RecursiveUnmount(dir); err != nil {
382
+		logrus.WithField("dir", dir).WithError(err).Warn("Error while attmepting to unmount dir, this may prevent removal of container.")
383
+	}
384
+	if err := os.RemoveAll(dir); err != nil && !os.IsNotExist(err) {
385
+		logrus.WithField("dir", dir).WithError(err).Error("Error removing dir.")
386
+	}
387
+}
388
+
336 389
 func killProcessDirectly(cntr *container.Container) error {
337 390
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
338 391
 	defer cancel()
... ...
@@ -21,10 +21,7 @@ func (daemon *Daemon) setupConfigDir(c *container.Container) (setupErr error) {
21 21
 		return nil
22 22
 	}
23 23
 
24
-	localPath, err := c.ConfigsDirPath()
25
-	if err != nil {
26
-		return err
27
-	}
24
+	localPath := c.ConfigsDirPath()
28 25
 	logrus.Debugf("configs: setting up config dir: %s", localPath)
29 26
 
30 27
 	// create local config root
... ...
@@ -51,11 +48,7 @@ func (daemon *Daemon) setupConfigDir(c *container.Container) (setupErr error) {
51 51
 			continue
52 52
 		}
53 53
 
54
-		fPath, err := c.ConfigFilePath(*configRef)
55
-		if err != nil {
56
-			return err
57
-		}
58
-
54
+		fPath := c.ConfigFilePath(*configRef)
59 55
 		log := logrus.WithFields(logrus.Fields{"name": configRef.File.Name, "path": fPath})
60 56
 
61 57
 		log.Debug("injecting config")
... ...
@@ -755,7 +755,7 @@ func (daemon *Daemon) populateCommonSpec(s *specs.Spec, c *container.Container)
755 755
 	return nil
756 756
 }
757 757
 
758
-func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
758
+func (daemon *Daemon) createSpec(c *container.Container) (retSpec *specs.Spec, err error) {
759 759
 	s := oci.DefaultSpec()
760 760
 	if err := daemon.populateCommonSpec(&s, c); err != nil {
761 761
 		return nil, err
... ...
@@ -837,11 +837,13 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
837 837
 		return nil, err
838 838
 	}
839 839
 
840
-	if err := daemon.setupSecretDir(c); err != nil {
841
-		return nil, err
842
-	}
840
+	defer func() {
841
+		if err != nil {
842
+			daemon.cleanupSecretDir(c)
843
+		}
844
+	}()
843 845
 
844
-	if err := daemon.setupConfigDir(c); err != nil {
846
+	if err := daemon.setupSecretDir(c); err != nil {
845 847
 		return nil, err
846 848
 	}
847 849
 
... ...
@@ -866,12 +868,6 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
866 866
 	}
867 867
 	ms = append(ms, secretMounts...)
868 868
 
869
-	configMounts, err := c.ConfigMounts()
870
-	if err != nil {
871
-		return nil, err
872
-	}
873
-	ms = append(ms, configMounts...)
874
-
875 869
 	sort.Sort(mounts(ms))
876 870
 	if err := setMounts(daemon, &s, c, ms); err != nil {
877 871
 		return nil, fmt.Errorf("linux mounts: %v", err)
... ...
@@ -102,10 +102,7 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
102 102
 		mounts = append(mounts, secretMounts...)
103 103
 	}
104 104
 
105
-	configMounts, err := c.ConfigMounts()
106
-	if err != nil {
107
-		return nil, err
108
-	}
105
+	configMounts := c.ConfigMounts()
109 106
 	if configMounts != nil {
110 107
 		mounts = append(mounts, configMounts...)
111 108
 	}
... ...
@@ -1,8 +1,10 @@
1 1
 package config
2 2
 
3 3
 import (
4
+	"bytes"
4 5
 	"sort"
5 6
 	"testing"
7
+	"time"
6 8
 
7 9
 	"github.com/docker/docker/api/types"
8 10
 	"github.com/docker/docker/api/types/filters"
... ...
@@ -10,6 +12,7 @@ import (
10 10
 	"github.com/docker/docker/client"
11 11
 	"github.com/docker/docker/integration/internal/swarm"
12 12
 	"github.com/docker/docker/internal/testutil"
13
+	"github.com/docker/docker/pkg/stdcopy"
13 14
 	"github.com/gotestyourself/gotestyourself/skip"
14 15
 	"github.com/stretchr/testify/assert"
15 16
 	"github.com/stretchr/testify/require"
... ...
@@ -188,3 +191,139 @@ func TestConfigsUpdate(t *testing.T) {
188 188
 	err = client.ConfigUpdate(ctx, configID, insp.Version, insp.Spec)
189 189
 	testutil.ErrorContains(t, err, "only updates to Labels are allowed")
190 190
 }
191
+
192
+func TestTemplatedConfig(t *testing.T) {
193
+	d := swarm.NewSwarm(t, testEnv)
194
+	defer d.Stop(t)
195
+
196
+	ctx := context.Background()
197
+	client := swarm.GetClient(t, d)
198
+
199
+	referencedSecretSpec := swarmtypes.SecretSpec{
200
+		Annotations: swarmtypes.Annotations{
201
+			Name: "referencedsecret",
202
+		},
203
+		Data: []byte("this is a secret"),
204
+	}
205
+	referencedSecret, err := client.SecretCreate(ctx, referencedSecretSpec)
206
+	assert.NoError(t, err)
207
+
208
+	referencedConfigSpec := swarmtypes.ConfigSpec{
209
+		Annotations: swarmtypes.Annotations{
210
+			Name: "referencedconfig",
211
+		},
212
+		Data: []byte("this is a config"),
213
+	}
214
+	referencedConfig, err := client.ConfigCreate(ctx, referencedConfigSpec)
215
+	assert.NoError(t, err)
216
+
217
+	configSpec := swarmtypes.ConfigSpec{
218
+		Annotations: swarmtypes.Annotations{
219
+			Name: "templated_config",
220
+		},
221
+		Templating: &swarmtypes.Driver{
222
+			Name: "golang",
223
+		},
224
+		Data: []byte("SERVICE_NAME={{.Service.Name}}\n" +
225
+			"{{secret \"referencedsecrettarget\"}}\n" +
226
+			"{{config \"referencedconfigtarget\"}}\n"),
227
+	}
228
+
229
+	templatedConfig, err := client.ConfigCreate(ctx, configSpec)
230
+	assert.NoError(t, err)
231
+
232
+	serviceID := swarm.CreateService(t, d,
233
+		swarm.ServiceWithConfig(
234
+			&swarmtypes.ConfigReference{
235
+				File: &swarmtypes.ConfigReferenceFileTarget{
236
+					Name: "/templated_config",
237
+					UID:  "0",
238
+					GID:  "0",
239
+					Mode: 0600,
240
+				},
241
+				ConfigID:   templatedConfig.ID,
242
+				ConfigName: "templated_config",
243
+			},
244
+		),
245
+		swarm.ServiceWithConfig(
246
+			&swarmtypes.ConfigReference{
247
+				File: &swarmtypes.ConfigReferenceFileTarget{
248
+					Name: "referencedconfigtarget",
249
+					UID:  "0",
250
+					GID:  "0",
251
+					Mode: 0600,
252
+				},
253
+				ConfigID:   referencedConfig.ID,
254
+				ConfigName: "referencedconfig",
255
+			},
256
+		),
257
+		swarm.ServiceWithSecret(
258
+			&swarmtypes.SecretReference{
259
+				File: &swarmtypes.SecretReferenceFileTarget{
260
+					Name: "referencedsecrettarget",
261
+					UID:  "0",
262
+					GID:  "0",
263
+					Mode: 0600,
264
+				},
265
+				SecretID:   referencedSecret.ID,
266
+				SecretName: "referencedsecret",
267
+			},
268
+		),
269
+		swarm.ServiceWithName("svc"),
270
+	)
271
+
272
+	var tasks []swarmtypes.Task
273
+	waitAndAssert(t, 60*time.Second, func(t *testing.T) bool {
274
+		tasks = swarm.GetRunningTasks(t, d, serviceID)
275
+		return len(tasks) > 0
276
+	})
277
+
278
+	task := tasks[0]
279
+	waitAndAssert(t, 60*time.Second, func(t *testing.T) bool {
280
+		if task.NodeID == "" || (task.Status.ContainerStatus == nil || task.Status.ContainerStatus.ContainerID == "") {
281
+			task, _, _ = client.TaskInspectWithRaw(context.Background(), task.ID)
282
+		}
283
+		return task.NodeID != "" && task.Status.ContainerStatus != nil && task.Status.ContainerStatus.ContainerID != ""
284
+	})
285
+
286
+	attach := swarm.ExecTask(t, d, task, types.ExecConfig{
287
+		Cmd:          []string{"/bin/cat", "/templated_config"},
288
+		AttachStdout: true,
289
+		AttachStderr: true,
290
+	})
291
+
292
+	expect := "SERVICE_NAME=svc\n" +
293
+		"this is a secret\n" +
294
+		"this is a config\n"
295
+	assertAttachedStream(t, attach, expect)
296
+
297
+	attach = swarm.ExecTask(t, d, task, types.ExecConfig{
298
+		Cmd:          []string{"mount"},
299
+		AttachStdout: true,
300
+		AttachStderr: true,
301
+	})
302
+	assertAttachedStream(t, attach, "tmpfs on /templated_config type tmpfs")
303
+}
304
+
305
+func assertAttachedStream(t *testing.T, attach types.HijackedResponse, expect string) {
306
+	buf := bytes.NewBuffer(nil)
307
+	_, err := stdcopy.StdCopy(buf, buf, attach.Reader)
308
+	require.NoError(t, err)
309
+	assert.Contains(t, buf.String(), expect)
310
+}
311
+
312
+func waitAndAssert(t *testing.T, timeout time.Duration, f func(*testing.T) bool) {
313
+	t.Helper()
314
+	after := time.After(timeout)
315
+	for {
316
+		select {
317
+		case <-after:
318
+			t.Fatalf("timed out waiting for condition")
319
+		default:
320
+		}
321
+		if f(t) {
322
+			return
323
+		}
324
+		time.Sleep(100 * time.Millisecond)
325
+	}
326
+}
... ...
@@ -1,10 +1,14 @@
1 1
 package swarm
2 2
 
3 3
 import (
4
+	"context"
4 5
 	"fmt"
5 6
 	"testing"
6 7
 
8
+	"github.com/docker/docker/api/types"
9
+	"github.com/docker/docker/api/types/filters"
7 10
 	swarmtypes "github.com/docker/docker/api/types/swarm"
11
+	"github.com/docker/docker/client"
8 12
 	"github.com/docker/docker/integration-cli/daemon"
9 13
 	"github.com/docker/docker/internal/test/environment"
10 14
 	"github.com/stretchr/testify/require"
... ...
@@ -34,3 +38,121 @@ func NewSwarm(t *testing.T, testEnv *environment.Execution) *daemon.Swarm {
34 34
 	require.NoError(t, d.Init(swarmtypes.InitRequest{}))
35 35
 	return d
36 36
 }
37
+
38
+// ServiceSpecOpt is used with `CreateService` to pass in service spec modifiers
39
+type ServiceSpecOpt func(*swarmtypes.ServiceSpec)
40
+
41
+// CreateService creates a service on the passed in swarm daemon.
42
+func CreateService(t *testing.T, d *daemon.Swarm, opts ...ServiceSpecOpt) string {
43
+	spec := defaultServiceSpec()
44
+	for _, o := range opts {
45
+		o(&spec)
46
+	}
47
+
48
+	client := GetClient(t, d)
49
+
50
+	resp, err := client.ServiceCreate(context.Background(), spec, types.ServiceCreateOptions{})
51
+	require.NoError(t, err, "error creating service")
52
+	return resp.ID
53
+}
54
+
55
+func defaultServiceSpec() swarmtypes.ServiceSpec {
56
+	var spec swarmtypes.ServiceSpec
57
+	ServiceWithImage("busybox:latest")(&spec)
58
+	ServiceWithCommand([]string{"/bin/top"})(&spec)
59
+	ServiceWithReplicas(1)(&spec)
60
+	return spec
61
+}
62
+
63
+// ServiceWithImage sets the image to use for the service
64
+func ServiceWithImage(image string) func(*swarmtypes.ServiceSpec) {
65
+	return func(spec *swarmtypes.ServiceSpec) {
66
+		ensureContainerSpec(spec)
67
+		spec.TaskTemplate.ContainerSpec.Image = image
68
+	}
69
+}
70
+
71
+// ServiceWithCommand sets the command to use for the service
72
+func ServiceWithCommand(cmd []string) ServiceSpecOpt {
73
+	return func(spec *swarmtypes.ServiceSpec) {
74
+		ensureContainerSpec(spec)
75
+		spec.TaskTemplate.ContainerSpec.Command = cmd
76
+	}
77
+}
78
+
79
+// ServiceWithConfig adds the config reference to the service
80
+func ServiceWithConfig(configRef *swarmtypes.ConfigReference) ServiceSpecOpt {
81
+	return func(spec *swarmtypes.ServiceSpec) {
82
+		ensureContainerSpec(spec)
83
+		spec.TaskTemplate.ContainerSpec.Configs = append(spec.TaskTemplate.ContainerSpec.Configs, configRef)
84
+	}
85
+}
86
+
87
+// ServiceWithSecret adds the secret reference to the service
88
+func ServiceWithSecret(secretRef *swarmtypes.SecretReference) ServiceSpecOpt {
89
+	return func(spec *swarmtypes.ServiceSpec) {
90
+		ensureContainerSpec(spec)
91
+		spec.TaskTemplate.ContainerSpec.Secrets = append(spec.TaskTemplate.ContainerSpec.Secrets, secretRef)
92
+	}
93
+}
94
+
95
+// ServiceWithReplicas sets the replicas for the service
96
+func ServiceWithReplicas(n uint64) ServiceSpecOpt {
97
+	return func(spec *swarmtypes.ServiceSpec) {
98
+		spec.Mode = swarmtypes.ServiceMode{
99
+			Replicated: &swarmtypes.ReplicatedService{
100
+				Replicas: &n,
101
+			},
102
+		}
103
+	}
104
+}
105
+
106
+// ServiceWithName sets the name of the service
107
+func ServiceWithName(name string) ServiceSpecOpt {
108
+	return func(spec *swarmtypes.ServiceSpec) {
109
+		spec.Annotations.Name = name
110
+	}
111
+}
112
+
113
+// GetRunningTasks gets the list of running tasks for a service
114
+func GetRunningTasks(t *testing.T, d *daemon.Swarm, serviceID string) []swarmtypes.Task {
115
+	client := GetClient(t, d)
116
+
117
+	filterArgs := filters.NewArgs()
118
+	filterArgs.Add("desired-state", "running")
119
+	filterArgs.Add("service", serviceID)
120
+
121
+	options := types.TaskListOptions{
122
+		Filters: filterArgs,
123
+	}
124
+	tasks, err := client.TaskList(context.Background(), options)
125
+	require.NoError(t, err)
126
+	return tasks
127
+}
128
+
129
+// ExecTask runs the passed in exec config on the given task
130
+func ExecTask(t *testing.T, d *daemon.Swarm, task swarmtypes.Task, config types.ExecConfig) types.HijackedResponse {
131
+	client := GetClient(t, d)
132
+
133
+	ctx := context.Background()
134
+	resp, err := client.ContainerExecCreate(ctx, task.Status.ContainerStatus.ContainerID, config)
135
+	require.NoError(t, err, "error creating exec")
136
+
137
+	startCheck := types.ExecStartCheck{}
138
+	attach, err := client.ContainerExecAttach(ctx, resp.ID, startCheck)
139
+	require.NoError(t, err, "error attaching to exec")
140
+	return attach
141
+}
142
+
143
+func ensureContainerSpec(spec *swarmtypes.ServiceSpec) {
144
+	if spec.TaskTemplate.ContainerSpec == nil {
145
+		spec.TaskTemplate.ContainerSpec = &swarmtypes.ContainerSpec{}
146
+	}
147
+}
148
+
149
+// GetClient creates a new client for the passed in swarm daemon.
150
+func GetClient(t *testing.T, d *daemon.Swarm) client.APIClient {
151
+	client, err := client.NewClientWithOpts(client.WithHost((d.Sock())))
152
+	require.NoError(t, err)
153
+	return client
154
+}
... ...
@@ -1,8 +1,10 @@
1 1
 package secret
2 2
 
3 3
 import (
4
+	"bytes"
4 5
 	"sort"
5 6
 	"testing"
7
+	"time"
6 8
 
7 9
 	"github.com/docker/docker/api/types"
8 10
 	"github.com/docker/docker/api/types/filters"
... ...
@@ -10,6 +12,7 @@ import (
10 10
 	"github.com/docker/docker/client"
11 11
 	"github.com/docker/docker/integration/internal/swarm"
12 12
 	"github.com/docker/docker/internal/testutil"
13
+	"github.com/docker/docker/pkg/stdcopy"
13 14
 	"github.com/gotestyourself/gotestyourself/skip"
14 15
 	"github.com/stretchr/testify/assert"
15 16
 	"github.com/stretchr/testify/require"
... ...
@@ -232,3 +235,139 @@ func TestSecretsUpdate(t *testing.T) {
232 232
 	err = client.SecretUpdate(ctx, secretID, insp.Version, insp.Spec)
233 233
 	testutil.ErrorContains(t, err, "only updates to Labels are allowed")
234 234
 }
235
+
236
+func TestTemplatedSecret(t *testing.T) {
237
+	d := swarm.NewSwarm(t, testEnv)
238
+	defer d.Stop(t)
239
+
240
+	ctx := context.Background()
241
+	client := swarm.GetClient(t, d)
242
+
243
+	referencedSecretSpec := swarmtypes.SecretSpec{
244
+		Annotations: swarmtypes.Annotations{
245
+			Name: "referencedsecret",
246
+		},
247
+		Data: []byte("this is a secret"),
248
+	}
249
+	referencedSecret, err := client.SecretCreate(ctx, referencedSecretSpec)
250
+	assert.NoError(t, err)
251
+
252
+	referencedConfigSpec := swarmtypes.ConfigSpec{
253
+		Annotations: swarmtypes.Annotations{
254
+			Name: "referencedconfig",
255
+		},
256
+		Data: []byte("this is a config"),
257
+	}
258
+	referencedConfig, err := client.ConfigCreate(ctx, referencedConfigSpec)
259
+	assert.NoError(t, err)
260
+
261
+	secretSpec := swarmtypes.SecretSpec{
262
+		Annotations: swarmtypes.Annotations{
263
+			Name: "templated_secret",
264
+		},
265
+		Templating: &swarmtypes.Driver{
266
+			Name: "golang",
267
+		},
268
+		Data: []byte("SERVICE_NAME={{.Service.Name}}\n" +
269
+			"{{secret \"referencedsecrettarget\"}}\n" +
270
+			"{{config \"referencedconfigtarget\"}}\n"),
271
+	}
272
+
273
+	templatedSecret, err := client.SecretCreate(ctx, secretSpec)
274
+	assert.NoError(t, err)
275
+
276
+	serviceID := swarm.CreateService(t, d,
277
+		swarm.ServiceWithSecret(
278
+			&swarmtypes.SecretReference{
279
+				File: &swarmtypes.SecretReferenceFileTarget{
280
+					Name: "templated_secret",
281
+					UID:  "0",
282
+					GID:  "0",
283
+					Mode: 0600,
284
+				},
285
+				SecretID:   templatedSecret.ID,
286
+				SecretName: "templated_secret",
287
+			},
288
+		),
289
+		swarm.ServiceWithConfig(
290
+			&swarmtypes.ConfigReference{
291
+				File: &swarmtypes.ConfigReferenceFileTarget{
292
+					Name: "referencedconfigtarget",
293
+					UID:  "0",
294
+					GID:  "0",
295
+					Mode: 0600,
296
+				},
297
+				ConfigID:   referencedConfig.ID,
298
+				ConfigName: "referencedconfig",
299
+			},
300
+		),
301
+		swarm.ServiceWithSecret(
302
+			&swarmtypes.SecretReference{
303
+				File: &swarmtypes.SecretReferenceFileTarget{
304
+					Name: "referencedsecrettarget",
305
+					UID:  "0",
306
+					GID:  "0",
307
+					Mode: 0600,
308
+				},
309
+				SecretID:   referencedSecret.ID,
310
+				SecretName: "referencedsecret",
311
+			},
312
+		),
313
+		swarm.ServiceWithName("svc"),
314
+	)
315
+
316
+	var tasks []swarmtypes.Task
317
+	waitAndAssert(t, 60*time.Second, func(t *testing.T) bool {
318
+		tasks = swarm.GetRunningTasks(t, d, serviceID)
319
+		return len(tasks) > 0
320
+	})
321
+
322
+	task := tasks[0]
323
+	waitAndAssert(t, 60*time.Second, func(t *testing.T) bool {
324
+		if task.NodeID == "" || (task.Status.ContainerStatus == nil || task.Status.ContainerStatus.ContainerID == "") {
325
+			task, _, _ = client.TaskInspectWithRaw(context.Background(), task.ID)
326
+		}
327
+		return task.NodeID != "" && task.Status.ContainerStatus != nil && task.Status.ContainerStatus.ContainerID != ""
328
+	})
329
+
330
+	attach := swarm.ExecTask(t, d, task, types.ExecConfig{
331
+		Cmd:          []string{"/bin/cat", "/run/secrets/templated_secret"},
332
+		AttachStdout: true,
333
+		AttachStderr: true,
334
+	})
335
+
336
+	expect := "SERVICE_NAME=svc\n" +
337
+		"this is a secret\n" +
338
+		"this is a config\n"
339
+	assertAttachedStream(t, attach, expect)
340
+
341
+	attach = swarm.ExecTask(t, d, task, types.ExecConfig{
342
+		Cmd:          []string{"mount"},
343
+		AttachStdout: true,
344
+		AttachStderr: true,
345
+	})
346
+	assertAttachedStream(t, attach, "tmpfs on /run/secrets/templated_secret type tmpfs")
347
+}
348
+
349
+func assertAttachedStream(t *testing.T, attach types.HijackedResponse, expect string) {
350
+	buf := bytes.NewBuffer(nil)
351
+	_, err := stdcopy.StdCopy(buf, buf, attach.Reader)
352
+	require.NoError(t, err)
353
+	assert.Contains(t, buf.String(), expect)
354
+}
355
+
356
+func waitAndAssert(t *testing.T, timeout time.Duration, f func(*testing.T) bool) {
357
+	t.Helper()
358
+	after := time.After(timeout)
359
+	for {
360
+		select {
361
+		case <-after:
362
+			t.Fatalf("timed out waiting for condition")
363
+		default:
364
+		}
365
+		if f(t) {
366
+			return
367
+		}
368
+		time.Sleep(100 * time.Millisecond)
369
+	}
370
+}