瀏覽代碼

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

Templated secrets and configs
Sebastiaan van Stijn 7 年之前
父節點
當前提交
0076343b29

+ 9 - 0
api/server/router/swarm/cluster_routes.go

@@ -372,6 +372,10 @@ func (sr *swarmRouter) createSecret(ctx context.Context, w http.ResponseWriter,
 	if err := json.NewDecoder(r.Body).Decode(&secret); err != nil {
 		return err
 	}
+	version := httputils.VersionFromContext(ctx)
+	if secret.Templating != nil && versions.LessThan(version, "1.36") {
+		return errdefs.InvalidParameter(errors.Errorf("secret templating is not supported on the specified API version: %s", version))
+	}
 
 	id, err := sr.backend.CreateSecret(secret)
 	if err != nil {
@@ -440,6 +444,11 @@ func (sr *swarmRouter) createConfig(ctx context.Context, w http.ResponseWriter,
 		return err
 	}
 
+	version := httputils.VersionFromContext(ctx)
+	if config.Templating != nil && versions.LessThan(version, "1.36") {
+		return errdefs.InvalidParameter(errors.Errorf("config templating is not supported on the specified API version: %s", version))
+	}
+
 	id, err := sr.backend.CreateConfig(config)
 	if err != nil {
 		return err

+ 14 - 0
api/swagger.yaml

@@ -3339,6 +3339,13 @@ definitions:
       Driver:
         description: "Name of the secrets driver used to fetch the secret's value from an external secret store"
         $ref: "#/definitions/Driver"
+      Templating:
+        description: |
+          Templating driver, if applicable
+
+          Templating controls whether and how to evaluate the config payload as
+          a template. If no driver is set, no templating is used.
+        $ref: "#/definitions/Driver"
 
   Secret:
     type: "object"
@@ -3375,6 +3382,13 @@ definitions:
           Base64-url-safe-encoded ([RFC 4648](https://tools.ietf.org/html/rfc4648#section-3.2))
           config data.
         type: "string"
+      Templating:
+        description: |
+          Templating driver, if applicable
+
+          Templating controls whether and how to evaluate the config payload as
+          a template. If no driver is set, no templating is used.
+        $ref: "#/definitions/Driver"
 
   Config:
     type: "object"

+ 4 - 0
api/types/swarm/config.go

@@ -13,6 +13,10 @@ type Config struct {
 type ConfigSpec struct {
 	Annotations
 	Data []byte `json:",omitempty"`
+
+	// Templating controls whether and how to evaluate the config payload as
+	// a template. If it is not set, no templating is used.
+	Templating *Driver `json:",omitempty"`
 }
 
 // ConfigReferenceFileTarget is a file target in a config reference

+ 4 - 0
api/types/swarm/secret.go

@@ -14,6 +14,10 @@ type SecretSpec struct {
 	Annotations
 	Data   []byte  `json:",omitempty"`
 	Driver *Driver `json:",omitempty"` // name of the secrets driver used to fetch the secret's value from an external secret store
+
+	// Templating controls whether and how to evaluate the secret payload as
+	// a template. If it is not set, no templating is used.
+	Templating *Driver `json:",omitempty"`
 }
 
 // SecretReferenceFileTarget is a file target in a secret reference

+ 0 - 15
container/container.go

@@ -1049,21 +1049,6 @@ func getSecretTargetPath(r *swarmtypes.SecretReference) string {
 	return filepath.Join(containerSecretMountPath, r.File.Name)
 }
 
-// ConfigsDirPath returns the path to the directory where configs are stored on
-// disk.
-func (container *Container) ConfigsDirPath() (string, error) {
-	return container.GetRootResourcePath("configs")
-}
-
-// ConfigFilePath returns the path to the on-disk location of a config.
-func (container *Container) ConfigFilePath(configRef swarmtypes.ConfigReference) (string, error) {
-	configs, err := container.ConfigsDirPath()
-	if err != nil {
-		return "", err
-	}
-	return filepath.Join(configs, configRef.ConfigID), nil
-}
-
 // CreateDaemonEnvironment creates a new environment variable slice for this container.
 func (container *Container) CreateDaemonEnvironment(tty bool, linkedEnv []string) []string {
 	// Setup environment

+ 23 - 21
container/container_unix.go

@@ -5,11 +5,13 @@ package container // import "github.com/docker/docker/container"
 import (
 	"io/ioutil"
 	"os"
+	"path/filepath"
 
 	"github.com/containerd/continuity/fs"
 	"github.com/docker/docker/api/types"
 	containertypes "github.com/docker/docker/api/types/container"
 	mounttypes "github.com/docker/docker/api/types/mount"
+	swarmtypes "github.com/docker/docker/api/types/swarm"
 	"github.com/docker/docker/pkg/mount"
 	"github.com/docker/docker/pkg/stringid"
 	"github.com/docker/docker/volume"
@@ -233,6 +235,17 @@ func (container *Container) SecretMounts() ([]Mount, error) {
 			Writable:    false,
 		})
 	}
+	for _, r := range container.ConfigReferences {
+		fPath, err := container.ConfigFilePath(*r)
+		if err != nil {
+			return nil, err
+		}
+		mounts = append(mounts, Mount{
+			Source:      fPath,
+			Destination: r.File.Name,
+			Writable:    false,
+		})
+	}
 
 	return mounts, nil
 }
@@ -253,27 +266,6 @@ func (container *Container) UnmountSecrets() error {
 	return mount.RecursiveUnmount(p)
 }
 
-// ConfigMounts returns the mounts for configs.
-func (container *Container) ConfigMounts() ([]Mount, error) {
-	var mounts []Mount
-	for _, configRef := range container.ConfigReferences {
-		if configRef.File == nil {
-			continue
-		}
-		src, err := container.ConfigFilePath(*configRef)
-		if err != nil {
-			return nil, err
-		}
-		mounts = append(mounts, Mount{
-			Source:      src,
-			Destination: configRef.File.Name,
-			Writable:    false,
-		})
-	}
-
-	return mounts, nil
-}
-
 type conflictingUpdateOptions string
 
 func (e conflictingUpdateOptions) Error() string {
@@ -457,3 +449,13 @@ func (container *Container) GetMountPoints() []types.MountPoint {
 	}
 	return mountPoints
 }
+
+// ConfigFilePath returns the path to the on-disk location of a config.
+// On unix, configs are always considered secret
+func (container *Container) ConfigFilePath(configRef swarmtypes.ConfigReference) (string, error) {
+	mounts, err := container.SecretMountPath()
+	if err != nil {
+		return "", err
+	}
+	return filepath.Join(mounts, configRef.ConfigID), nil
+}

+ 16 - 9
container/container_windows.go

@@ -7,6 +7,7 @@ import (
 
 	"github.com/docker/docker/api/types"
 	containertypes "github.com/docker/docker/api/types/container"
+	swarmtypes "github.com/docker/docker/api/types/swarm"
 	"github.com/docker/docker/pkg/system"
 )
 
@@ -102,23 +103,20 @@ func (container *Container) CreateConfigSymlinks() error {
 }
 
 // ConfigMounts returns the mount for configs.
-// All configs are stored in a single mount on Windows. Target symlinks are
-// created for each config, pointing to the files in this mount.
-func (container *Container) ConfigMounts() ([]Mount, error) {
+// TODO: Right now Windows doesn't really have a "secure" storage for secrets,
+// however some configs may contain secrets. Once secure storage is worked out,
+// configs and secret handling should be merged.
+func (container *Container) ConfigMounts() []Mount {
 	var mounts []Mount
 	if len(container.ConfigReferences) > 0 {
-		src, err := container.ConfigsDirPath()
-		if err != nil {
-			return nil, err
-		}
 		mounts = append(mounts, Mount{
-			Source:      src,
+			Source:      container.ConfigsDirPath(),
 			Destination: containerInternalConfigsDirPath,
 			Writable:    false,
 		})
 	}
 
-	return mounts, nil
+	return mounts
 }
 
 // DetachAndUnmount unmounts all volumes.
@@ -204,3 +202,12 @@ func (container *Container) GetMountPoints() []types.MountPoint {
 	}
 	return mountPoints
 }
+
+func (container *Container) ConfigsDirPath() string {
+	return filepath.Join(container.Root, "configs")
+}
+
+// ConfigFilePath returns the path to the on-disk location of a config.
+func (container *Container) ConfigFilePath(configRef swarmtypes.ConfigReference) string {
+	return filepath.Join(container.ConfigsDirPath(), configRef.ConfigID)
+}

+ 18 - 1
daemon/cluster/convert/config.go

@@ -2,6 +2,7 @@ package convert // import "github.com/docker/docker/daemon/cluster/convert"
 
 import (
 	swarmtypes "github.com/docker/docker/api/types/swarm"
+	types "github.com/docker/docker/api/types/swarm"
 	swarmapi "github.com/docker/swarmkit/api"
 	gogotypes "github.com/gogo/protobuf/types"
 )
@@ -21,18 +22,34 @@ func ConfigFromGRPC(s *swarmapi.Config) swarmtypes.Config {
 	config.CreatedAt, _ = gogotypes.TimestampFromProto(s.Meta.CreatedAt)
 	config.UpdatedAt, _ = gogotypes.TimestampFromProto(s.Meta.UpdatedAt)
 
+	if s.Spec.Templating != nil {
+		config.Spec.Templating = &types.Driver{
+			Name:    s.Spec.Templating.Name,
+			Options: s.Spec.Templating.Options,
+		}
+	}
+
 	return config
 }
 
 // ConfigSpecToGRPC converts Config to a grpc Config.
 func ConfigSpecToGRPC(s swarmtypes.ConfigSpec) swarmapi.ConfigSpec {
-	return swarmapi.ConfigSpec{
+	spec := swarmapi.ConfigSpec{
 		Annotations: swarmapi.Annotations{
 			Name:   s.Name,
 			Labels: s.Labels,
 		},
 		Data: s.Data,
 	}
+
+	if s.Templating != nil {
+		spec.Templating = &swarmapi.Driver{
+			Name:    s.Templating.Name,
+			Options: s.Templating.Options,
+		}
+	}
+
+	return spec
 }
 
 // ConfigReferencesFromGRPC converts a slice of grpc ConfigReference to ConfigReference

+ 18 - 1
daemon/cluster/convert/secret.go

@@ -2,6 +2,7 @@ package convert // import "github.com/docker/docker/daemon/cluster/convert"
 
 import (
 	swarmtypes "github.com/docker/docker/api/types/swarm"
+	types "github.com/docker/docker/api/types/swarm"
 	swarmapi "github.com/docker/swarmkit/api"
 	gogotypes "github.com/gogo/protobuf/types"
 )
@@ -22,12 +23,19 @@ func SecretFromGRPC(s *swarmapi.Secret) swarmtypes.Secret {
 	secret.CreatedAt, _ = gogotypes.TimestampFromProto(s.Meta.CreatedAt)
 	secret.UpdatedAt, _ = gogotypes.TimestampFromProto(s.Meta.UpdatedAt)
 
+	if s.Spec.Templating != nil {
+		secret.Spec.Templating = &types.Driver{
+			Name:    s.Spec.Templating.Name,
+			Options: s.Spec.Templating.Options,
+		}
+	}
+
 	return secret
 }
 
 // SecretSpecToGRPC converts Secret to a grpc Secret.
 func SecretSpecToGRPC(s swarmtypes.SecretSpec) swarmapi.SecretSpec {
-	return swarmapi.SecretSpec{
+	spec := swarmapi.SecretSpec{
 		Annotations: swarmapi.Annotations{
 			Name:   s.Name,
 			Labels: s.Labels,
@@ -35,6 +43,15 @@ func SecretSpecToGRPC(s swarmtypes.SecretSpec) swarmapi.SecretSpec {
 		Data:   s.Data,
 		Driver: driverToGRPC(s.Driver),
 	}
+
+	if s.Templating != nil {
+		spec.Templating = &swarmapi.Driver{
+			Name:    s.Templating.Name,
+			Options: s.Templating.Options,
+		}
+	}
+
+	return spec
 }
 
 // SecretReferencesFromGRPC converts a slice of grpc SecretReference to SecretReference

+ 2 - 1
daemon/cluster/executor/container/executor.go

@@ -19,6 +19,7 @@ import (
 	"github.com/docker/swarmkit/agent/exec"
 	"github.com/docker/swarmkit/api"
 	"github.com/docker/swarmkit/api/naming"
+	"github.com/docker/swarmkit/template"
 	"github.com/sirupsen/logrus"
 	"golang.org/x/net/context"
 )
@@ -191,7 +192,7 @@ func (e *executor) Configure(ctx context.Context, node *api.Node) error {
 
 // Controller returns a docker container runner.
 func (e *executor) Controller(t *api.Task) (exec.Controller, error) {
-	dependencyGetter := agent.Restrict(e.dependencies, t)
+	dependencyGetter := template.NewTemplatedDependencyGetter(agent.Restrict(e.dependencies, t), t, nil)
 
 	// Get the node description from the executor field
 	e.mutex.Lock()

+ 1 - 3
daemon/configs.go

@@ -16,8 +16,6 @@ func (daemon *Daemon) SetContainerConfigReferences(name string, refs []*swarmtyp
 	if err != nil {
 		return err
 	}
-
-	c.ConfigReferences = refs
-
+	c.ConfigReferences = append(c.ConfigReferences, refs...)
 	return nil
 }

+ 75 - 79
daemon/container_operations_unix.go

@@ -161,43 +161,26 @@ func (daemon *Daemon) setupIpcDirs(c *container.Container) error {
 }
 
 func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) {
-	if len(c.SecretReferences) == 0 {
+	if len(c.SecretReferences) == 0 && len(c.ConfigReferences) == 0 {
 		return nil
 	}
 
-	localMountPath, err := c.SecretMountPath()
-	if err != nil {
-		return errors.Wrap(err, "error getting secrets mount dir")
-	}
-	logrus.Debugf("secrets: setting up secret dir: %s", localMountPath)
-
-	// retrieve possible remapped range start for root UID, GID
-	rootIDs := daemon.idMappings.RootPair()
-	// create tmpfs
-	if err := idtools.MkdirAllAndChown(localMountPath, 0700, rootIDs); err != nil {
-		return errors.Wrap(err, "error creating secret local mount path")
+	if err := daemon.createSecretsDir(c); err != nil {
+		return err
 	}
-
 	defer func() {
 		if setupErr != nil {
-			// cleanup
-			_ = detachMounted(localMountPath)
-
-			if err := os.RemoveAll(localMountPath); err != nil {
-				logrus.Errorf("error cleaning up secret mount: %s", err)
-			}
+			daemon.cleanupSecretDir(c)
 		}
 	}()
 
-	tmpfsOwnership := fmt.Sprintf("uid=%d,gid=%d", rootIDs.UID, rootIDs.GID)
-	if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "nodev,nosuid,noexec,"+tmpfsOwnership); err != nil {
-		return errors.Wrap(err, "unable to setup secret mount")
-	}
-
 	if c.DependencyStore == nil {
 		return fmt.Errorf("secret store is not initialized")
 	}
 
+	// retrieve possible remapped range start for root UID, GID
+	rootIDs := daemon.idMappings.RootPair()
+
 	for _, s := range c.SecretReferences {
 		// TODO (ehazlett): use type switch when more are supported
 		if s.File == nil {
@@ -244,78 +227,38 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) {
 		}
 	}
 
-	label.Relabel(localMountPath, c.MountLabel, false)
-
-	// remount secrets ro
-	if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "remount,ro,"+tmpfsOwnership); err != nil {
-		return errors.Wrap(err, "unable to remount secret dir as readonly")
-	}
-
-	return nil
-}
-
-func (daemon *Daemon) setupConfigDir(c *container.Container) (setupErr error) {
-	if len(c.ConfigReferences) == 0 {
-		return nil
-	}
-
-	localPath, err := c.ConfigsDirPath()
-	if err != nil {
-		return err
-	}
-	logrus.Debugf("configs: setting up config dir: %s", localPath)
-
-	// retrieve possible remapped range start for root UID, GID
-	rootIDs := daemon.idMappings.RootPair()
-	// create tmpfs
-	if err := idtools.MkdirAllAndChown(localPath, 0700, rootIDs); err != nil {
-		return errors.Wrap(err, "error creating config dir")
-	}
-
-	defer func() {
-		if setupErr != nil {
-			if err := os.RemoveAll(localPath); err != nil {
-				logrus.Errorf("error cleaning up config dir: %s", err)
-			}
-		}
-	}()
-
-	if c.DependencyStore == nil {
-		return fmt.Errorf("config store is not initialized")
-	}
-
-	for _, configRef := range c.ConfigReferences {
+	for _, ref := range c.ConfigReferences {
 		// TODO (ehazlett): use type switch when more are supported
-		if configRef.File == nil {
+		if ref.File == nil {
 			logrus.Error("config target type is not a file target")
 			continue
 		}
 
-		fPath, err := c.ConfigFilePath(*configRef)
+		fPath, err := c.ConfigFilePath(*ref)
 		if err != nil {
-			return err
+			return errors.Wrap(err, "error getting config file path for container")
 		}
-
-		log := logrus.WithFields(logrus.Fields{"name": configRef.File.Name, "path": fPath})
-
 		if err := idtools.MkdirAllAndChown(filepath.Dir(fPath), 0700, rootIDs); err != nil {
-			return errors.Wrap(err, "error creating config path")
+			return errors.Wrap(err, "error creating config mount path")
 		}
 
-		log.Debug("injecting config")
-		config, err := c.DependencyStore.Configs().Get(configRef.ConfigID)
+		logrus.WithFields(logrus.Fields{
+			"name": ref.File.Name,
+			"path": fPath,
+		}).Debug("injecting config")
+		config, err := c.DependencyStore.Configs().Get(ref.ConfigID)
 		if err != nil {
 			return errors.Wrap(err, "unable to get config from config store")
 		}
-		if err := ioutil.WriteFile(fPath, config.Spec.Data, configRef.File.Mode); err != nil {
+		if err := ioutil.WriteFile(fPath, config.Spec.Data, ref.File.Mode); err != nil {
 			return errors.Wrap(err, "error injecting config")
 		}
 
-		uid, err := strconv.Atoi(configRef.File.UID)
+		uid, err := strconv.Atoi(ref.File.UID)
 		if err != nil {
 			return err
 		}
-		gid, err := strconv.Atoi(configRef.File.GID)
+		gid, err := strconv.Atoi(ref.File.GID)
 		if err != nil {
 			return err
 		}
@@ -323,16 +266,69 @@ func (daemon *Daemon) setupConfigDir(c *container.Container) (setupErr error) {
 		if err := os.Chown(fPath, rootIDs.UID+uid, rootIDs.GID+gid); err != nil {
 			return errors.Wrap(err, "error setting ownership for config")
 		}
-		if err := os.Chmod(fPath, configRef.File.Mode); err != nil {
+		if err := os.Chmod(fPath, ref.File.Mode); err != nil {
 			return errors.Wrap(err, "error setting file mode for config")
 		}
+	}
 
-		label.Relabel(fPath, c.MountLabel, false)
+	return daemon.remountSecretDir(c)
+}
+
+// createSecretsDir is used to create a dir suitable for storing container secrets.
+// In practice this is using a tmpfs mount and is used for both "configs" and "secrets"
+func (daemon *Daemon) createSecretsDir(c *container.Container) error {
+	// retrieve possible remapped range start for root UID, GID
+	rootIDs := daemon.idMappings.RootPair()
+	dir, err := c.SecretMountPath()
+	if err != nil {
+		return errors.Wrap(err, "error getting container secrets dir")
+	}
+
+	// create tmpfs
+	if err := idtools.MkdirAllAndChown(dir, 0700, rootIDs); err != nil {
+		return errors.Wrap(err, "error creating secret local mount path")
+	}
+
+	tmpfsOwnership := fmt.Sprintf("uid=%d,gid=%d", rootIDs.UID, rootIDs.GID)
+	if err := mount.Mount("tmpfs", dir, "tmpfs", "nodev,nosuid,noexec,"+tmpfsOwnership); err != nil {
+		return errors.Wrap(err, "unable to setup secret mount")
+	}
+
+	return nil
+}
+
+func (daemon *Daemon) remountSecretDir(c *container.Container) error {
+	dir, err := c.SecretMountPath()
+	if err != nil {
+		return errors.Wrap(err, "error getting container secrets path")
+	}
+	if err := label.Relabel(dir, c.MountLabel, false); err != nil {
+		logrus.WithError(err).WithField("dir", dir).Warn("Error while attempting to set selinux label")
+	}
+	rootIDs := daemon.idMappings.RootPair()
+	tmpfsOwnership := fmt.Sprintf("uid=%d,gid=%d", rootIDs.UID, rootIDs.GID)
+
+	// remount secrets ro
+	if err := mount.Mount("tmpfs", dir, "tmpfs", "remount,ro,"+tmpfsOwnership); err != nil {
+		return errors.Wrap(err, "unable to remount dir as readonly")
 	}
 
 	return nil
 }
 
+func (daemon *Daemon) cleanupSecretDir(c *container.Container) {
+	dir, err := c.SecretMountPath()
+	if err != nil {
+		logrus.WithError(err).WithField("container", c.ID).Warn("error getting secrets mount path for container")
+	}
+	if err := mount.RecursiveUnmount(dir); err != nil {
+		logrus.WithField("dir", dir).WithError(err).Warn("Error while attmepting to unmount dir, this may prevent removal of container.")
+	}
+	if err := os.RemoveAll(dir); err != nil && !os.IsNotExist(err) {
+		logrus.WithField("dir", dir).WithError(err).Error("Error removing dir.")
+	}
+}
+
 func killProcessDirectly(cntr *container.Container) error {
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 	defer cancel()

+ 2 - 9
daemon/container_operations_windows.go

@@ -21,10 +21,7 @@ func (daemon *Daemon) setupConfigDir(c *container.Container) (setupErr error) {
 		return nil
 	}
 
-	localPath, err := c.ConfigsDirPath()
-	if err != nil {
-		return err
-	}
+	localPath := c.ConfigsDirPath()
 	logrus.Debugf("configs: setting up config dir: %s", localPath)
 
 	// create local config root
@@ -51,11 +48,7 @@ func (daemon *Daemon) setupConfigDir(c *container.Container) (setupErr error) {
 			continue
 		}
 
-		fPath, err := c.ConfigFilePath(*configRef)
-		if err != nil {
-			return err
-		}
-
+		fPath := c.ConfigFilePath(*configRef)
 		log := logrus.WithFields(logrus.Fields{"name": configRef.File.Name, "path": fPath})
 
 		log.Debug("injecting config")

+ 7 - 11
daemon/oci_linux.go

@@ -755,7 +755,7 @@ func (daemon *Daemon) populateCommonSpec(s *specs.Spec, c *container.Container)
 	return nil
 }
 
-func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
+func (daemon *Daemon) createSpec(c *container.Container) (retSpec *specs.Spec, err error) {
 	s := oci.DefaultSpec()
 	if err := daemon.populateCommonSpec(&s, c); err != nil {
 		return nil, err
@@ -837,11 +837,13 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
 		return nil, err
 	}
 
-	if err := daemon.setupSecretDir(c); err != nil {
-		return nil, err
-	}
+	defer func() {
+		if err != nil {
+			daemon.cleanupSecretDir(c)
+		}
+	}()
 
-	if err := daemon.setupConfigDir(c); err != nil {
+	if err := daemon.setupSecretDir(c); err != nil {
 		return nil, err
 	}
 
@@ -866,12 +868,6 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
 	}
 	ms = append(ms, secretMounts...)
 
-	configMounts, err := c.ConfigMounts()
-	if err != nil {
-		return nil, err
-	}
-	ms = append(ms, configMounts...)
-
 	sort.Sort(mounts(ms))
 	if err := setMounts(daemon, &s, c, ms); err != nil {
 		return nil, fmt.Errorf("linux mounts: %v", err)

+ 1 - 4
daemon/oci_windows.go

@@ -102,10 +102,7 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
 		mounts = append(mounts, secretMounts...)
 	}
 
-	configMounts, err := c.ConfigMounts()
-	if err != nil {
-		return nil, err
-	}
+	configMounts := c.ConfigMounts()
 	if configMounts != nil {
 		mounts = append(mounts, configMounts...)
 	}

+ 139 - 0
integration/config/config_test.go

@@ -1,8 +1,10 @@
 package config
 
 import (
+	"bytes"
 	"sort"
 	"testing"
+	"time"
 
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/filters"
@@ -10,6 +12,7 @@ import (
 	"github.com/docker/docker/client"
 	"github.com/docker/docker/integration/internal/swarm"
 	"github.com/docker/docker/internal/testutil"
+	"github.com/docker/docker/pkg/stdcopy"
 	"github.com/gotestyourself/gotestyourself/skip"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -188,3 +191,139 @@ func TestConfigsUpdate(t *testing.T) {
 	err = client.ConfigUpdate(ctx, configID, insp.Version, insp.Spec)
 	testutil.ErrorContains(t, err, "only updates to Labels are allowed")
 }
+
+func TestTemplatedConfig(t *testing.T) {
+	d := swarm.NewSwarm(t, testEnv)
+	defer d.Stop(t)
+
+	ctx := context.Background()
+	client := swarm.GetClient(t, d)
+
+	referencedSecretSpec := swarmtypes.SecretSpec{
+		Annotations: swarmtypes.Annotations{
+			Name: "referencedsecret",
+		},
+		Data: []byte("this is a secret"),
+	}
+	referencedSecret, err := client.SecretCreate(ctx, referencedSecretSpec)
+	assert.NoError(t, err)
+
+	referencedConfigSpec := swarmtypes.ConfigSpec{
+		Annotations: swarmtypes.Annotations{
+			Name: "referencedconfig",
+		},
+		Data: []byte("this is a config"),
+	}
+	referencedConfig, err := client.ConfigCreate(ctx, referencedConfigSpec)
+	assert.NoError(t, err)
+
+	configSpec := swarmtypes.ConfigSpec{
+		Annotations: swarmtypes.Annotations{
+			Name: "templated_config",
+		},
+		Templating: &swarmtypes.Driver{
+			Name: "golang",
+		},
+		Data: []byte("SERVICE_NAME={{.Service.Name}}\n" +
+			"{{secret \"referencedsecrettarget\"}}\n" +
+			"{{config \"referencedconfigtarget\"}}\n"),
+	}
+
+	templatedConfig, err := client.ConfigCreate(ctx, configSpec)
+	assert.NoError(t, err)
+
+	serviceID := swarm.CreateService(t, d,
+		swarm.ServiceWithConfig(
+			&swarmtypes.ConfigReference{
+				File: &swarmtypes.ConfigReferenceFileTarget{
+					Name: "/templated_config",
+					UID:  "0",
+					GID:  "0",
+					Mode: 0600,
+				},
+				ConfigID:   templatedConfig.ID,
+				ConfigName: "templated_config",
+			},
+		),
+		swarm.ServiceWithConfig(
+			&swarmtypes.ConfigReference{
+				File: &swarmtypes.ConfigReferenceFileTarget{
+					Name: "referencedconfigtarget",
+					UID:  "0",
+					GID:  "0",
+					Mode: 0600,
+				},
+				ConfigID:   referencedConfig.ID,
+				ConfigName: "referencedconfig",
+			},
+		),
+		swarm.ServiceWithSecret(
+			&swarmtypes.SecretReference{
+				File: &swarmtypes.SecretReferenceFileTarget{
+					Name: "referencedsecrettarget",
+					UID:  "0",
+					GID:  "0",
+					Mode: 0600,
+				},
+				SecretID:   referencedSecret.ID,
+				SecretName: "referencedsecret",
+			},
+		),
+		swarm.ServiceWithName("svc"),
+	)
+
+	var tasks []swarmtypes.Task
+	waitAndAssert(t, 60*time.Second, func(t *testing.T) bool {
+		tasks = swarm.GetRunningTasks(t, d, serviceID)
+		return len(tasks) > 0
+	})
+
+	task := tasks[0]
+	waitAndAssert(t, 60*time.Second, func(t *testing.T) bool {
+		if task.NodeID == "" || (task.Status.ContainerStatus == nil || task.Status.ContainerStatus.ContainerID == "") {
+			task, _, _ = client.TaskInspectWithRaw(context.Background(), task.ID)
+		}
+		return task.NodeID != "" && task.Status.ContainerStatus != nil && task.Status.ContainerStatus.ContainerID != ""
+	})
+
+	attach := swarm.ExecTask(t, d, task, types.ExecConfig{
+		Cmd:          []string{"/bin/cat", "/templated_config"},
+		AttachStdout: true,
+		AttachStderr: true,
+	})
+
+	expect := "SERVICE_NAME=svc\n" +
+		"this is a secret\n" +
+		"this is a config\n"
+	assertAttachedStream(t, attach, expect)
+
+	attach = swarm.ExecTask(t, d, task, types.ExecConfig{
+		Cmd:          []string{"mount"},
+		AttachStdout: true,
+		AttachStderr: true,
+	})
+	assertAttachedStream(t, attach, "tmpfs on /templated_config type tmpfs")
+}
+
+func assertAttachedStream(t *testing.T, attach types.HijackedResponse, expect string) {
+	buf := bytes.NewBuffer(nil)
+	_, err := stdcopy.StdCopy(buf, buf, attach.Reader)
+	require.NoError(t, err)
+	assert.Contains(t, buf.String(), expect)
+}
+
+func waitAndAssert(t *testing.T, timeout time.Duration, f func(*testing.T) bool) {
+	t.Helper()
+	after := time.After(timeout)
+	for {
+		select {
+		case <-after:
+			t.Fatalf("timed out waiting for condition")
+		default:
+		}
+		if f(t) {
+			return
+		}
+		time.Sleep(100 * time.Millisecond)
+	}
+}

+ 122 - 0
integration/internal/swarm/service.go

@@ -1,10 +1,14 @@
 package swarm
 
 import (
+	"context"
 	"fmt"
 	"testing"
 
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
 	swarmtypes "github.com/docker/docker/api/types/swarm"
+	"github.com/docker/docker/client"
 	"github.com/docker/docker/integration-cli/daemon"
 	"github.com/docker/docker/internal/test/environment"
 	"github.com/stretchr/testify/require"
@@ -34,3 +38,121 @@ func NewSwarm(t *testing.T, testEnv *environment.Execution) *daemon.Swarm {
 	require.NoError(t, d.Init(swarmtypes.InitRequest{}))
 	return d
 }
+
+// ServiceSpecOpt is used with `CreateService` to pass in service spec modifiers
+type ServiceSpecOpt func(*swarmtypes.ServiceSpec)
+
+// CreateService creates a service on the passed in swarm daemon.
+func CreateService(t *testing.T, d *daemon.Swarm, opts ...ServiceSpecOpt) string {
+	spec := defaultServiceSpec()
+	for _, o := range opts {
+		o(&spec)
+	}
+
+	client := GetClient(t, d)
+
+	resp, err := client.ServiceCreate(context.Background(), spec, types.ServiceCreateOptions{})
+	require.NoError(t, err, "error creating service")
+	return resp.ID
+}
+
+func defaultServiceSpec() swarmtypes.ServiceSpec {
+	var spec swarmtypes.ServiceSpec
+	ServiceWithImage("busybox:latest")(&spec)
+	ServiceWithCommand([]string{"/bin/top"})(&spec)
+	ServiceWithReplicas(1)(&spec)
+	return spec
+}
+
+// ServiceWithImage sets the image to use for the service
+func ServiceWithImage(image string) func(*swarmtypes.ServiceSpec) {
+	return func(spec *swarmtypes.ServiceSpec) {
+		ensureContainerSpec(spec)
+		spec.TaskTemplate.ContainerSpec.Image = image
+	}
+}
+
+// ServiceWithCommand sets the command to use for the service
+func ServiceWithCommand(cmd []string) ServiceSpecOpt {
+	return func(spec *swarmtypes.ServiceSpec) {
+		ensureContainerSpec(spec)
+		spec.TaskTemplate.ContainerSpec.Command = cmd
+	}
+}
+
+// ServiceWithConfig adds the config reference to the service
+func ServiceWithConfig(configRef *swarmtypes.ConfigReference) ServiceSpecOpt {
+	return func(spec *swarmtypes.ServiceSpec) {
+		ensureContainerSpec(spec)
+		spec.TaskTemplate.ContainerSpec.Configs = append(spec.TaskTemplate.ContainerSpec.Configs, configRef)
+	}
+}
+
+// ServiceWithSecret adds the secret reference to the service
+func ServiceWithSecret(secretRef *swarmtypes.SecretReference) ServiceSpecOpt {
+	return func(spec *swarmtypes.ServiceSpec) {
+		ensureContainerSpec(spec)
+		spec.TaskTemplate.ContainerSpec.Secrets = append(spec.TaskTemplate.ContainerSpec.Secrets, secretRef)
+	}
+}
+
+// ServiceWithReplicas sets the replicas for the service
+func ServiceWithReplicas(n uint64) ServiceSpecOpt {
+	return func(spec *swarmtypes.ServiceSpec) {
+		spec.Mode = swarmtypes.ServiceMode{
+			Replicated: &swarmtypes.ReplicatedService{
+				Replicas: &n,
+			},
+		}
+	}
+}
+
+// ServiceWithName sets the name of the service
+func ServiceWithName(name string) ServiceSpecOpt {
+	return func(spec *swarmtypes.ServiceSpec) {
+		spec.Annotations.Name = name
+	}
+}
+
+// GetRunningTasks gets the list of running tasks for a service
+func GetRunningTasks(t *testing.T, d *daemon.Swarm, serviceID string) []swarmtypes.Task {
+	client := GetClient(t, d)
+
+	filterArgs := filters.NewArgs()
+	filterArgs.Add("desired-state", "running")
+	filterArgs.Add("service", serviceID)
+
+	options := types.TaskListOptions{
+		Filters: filterArgs,
+	}
+	tasks, err := client.TaskList(context.Background(), options)
+	require.NoError(t, err)
+	return tasks
+}
+
+// ExecTask runs the passed in exec config on the given task
+func ExecTask(t *testing.T, d *daemon.Swarm, task swarmtypes.Task, config types.ExecConfig) types.HijackedResponse {
+	client := GetClient(t, d)
+
+	ctx := context.Background()
+	resp, err := client.ContainerExecCreate(ctx, task.Status.ContainerStatus.ContainerID, config)
+	require.NoError(t, err, "error creating exec")
+
+	startCheck := types.ExecStartCheck{}
+	attach, err := client.ContainerExecAttach(ctx, resp.ID, startCheck)
+	require.NoError(t, err, "error attaching to exec")
+	return attach
+}
+
+func ensureContainerSpec(spec *swarmtypes.ServiceSpec) {
+	if spec.TaskTemplate.ContainerSpec == nil {
+		spec.TaskTemplate.ContainerSpec = &swarmtypes.ContainerSpec{}
+	}
+}
+
+// GetClient creates a new client for the passed in swarm daemon.
+func GetClient(t *testing.T, d *daemon.Swarm) client.APIClient {
+	client, err := client.NewClientWithOpts(client.WithHost((d.Sock())))
+	require.NoError(t, err)
+	return client
+}

+ 139 - 0
integration/secret/secret_test.go

@@ -1,8 +1,10 @@
 package secret
 
 import (
+	"bytes"
 	"sort"
 	"testing"
+	"time"
 
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/filters"
@@ -10,6 +12,7 @@ import (
 	"github.com/docker/docker/client"
 	"github.com/docker/docker/integration/internal/swarm"
 	"github.com/docker/docker/internal/testutil"
+	"github.com/docker/docker/pkg/stdcopy"
 	"github.com/gotestyourself/gotestyourself/skip"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -232,3 +235,139 @@ func TestSecretsUpdate(t *testing.T) {
 	err = client.SecretUpdate(ctx, secretID, insp.Version, insp.Spec)
 	testutil.ErrorContains(t, err, "only updates to Labels are allowed")
 }
+
+func TestTemplatedSecret(t *testing.T) {
+	d := swarm.NewSwarm(t, testEnv)
+	defer d.Stop(t)
+
+	ctx := context.Background()
+	client := swarm.GetClient(t, d)
+
+	referencedSecretSpec := swarmtypes.SecretSpec{
+		Annotations: swarmtypes.Annotations{
+			Name: "referencedsecret",
+		},
+		Data: []byte("this is a secret"),
+	}
+	referencedSecret, err := client.SecretCreate(ctx, referencedSecretSpec)
+	assert.NoError(t, err)
+
+	referencedConfigSpec := swarmtypes.ConfigSpec{
+		Annotations: swarmtypes.Annotations{
+			Name: "referencedconfig",
+		},
+		Data: []byte("this is a config"),
+	}
+	referencedConfig, err := client.ConfigCreate(ctx, referencedConfigSpec)
+	assert.NoError(t, err)
+
+	secretSpec := swarmtypes.SecretSpec{
+		Annotations: swarmtypes.Annotations{
+			Name: "templated_secret",
+		},
+		Templating: &swarmtypes.Driver{
+			Name: "golang",
+		},
+		Data: []byte("SERVICE_NAME={{.Service.Name}}\n" +
+			"{{secret \"referencedsecrettarget\"}}\n" +
+			"{{config \"referencedconfigtarget\"}}\n"),
+	}
+
+	templatedSecret, err := client.SecretCreate(ctx, secretSpec)
+	assert.NoError(t, err)
+
+	serviceID := swarm.CreateService(t, d,
+		swarm.ServiceWithSecret(
+			&swarmtypes.SecretReference{
+				File: &swarmtypes.SecretReferenceFileTarget{
+					Name: "templated_secret",
+					UID:  "0",
+					GID:  "0",
+					Mode: 0600,
+				},
+				SecretID:   templatedSecret.ID,
+				SecretName: "templated_secret",
+			},
+		),
+		swarm.ServiceWithConfig(
+			&swarmtypes.ConfigReference{
+				File: &swarmtypes.ConfigReferenceFileTarget{
+					Name: "referencedconfigtarget",
+					UID:  "0",
+					GID:  "0",
+					Mode: 0600,
+				},
+				ConfigID:   referencedConfig.ID,
+				ConfigName: "referencedconfig",
+			},
+		),
+		swarm.ServiceWithSecret(
+			&swarmtypes.SecretReference{
+				File: &swarmtypes.SecretReferenceFileTarget{
+					Name: "referencedsecrettarget",
+					UID:  "0",
+					GID:  "0",
+					Mode: 0600,
+				},
+				SecretID:   referencedSecret.ID,
+				SecretName: "referencedsecret",
+			},
+		),
+		swarm.ServiceWithName("svc"),
+	)
+
+	var tasks []swarmtypes.Task
+	waitAndAssert(t, 60*time.Second, func(t *testing.T) bool {
+		tasks = swarm.GetRunningTasks(t, d, serviceID)
+		return len(tasks) > 0
+	})
+
+	task := tasks[0]
+	waitAndAssert(t, 60*time.Second, func(t *testing.T) bool {
+		if task.NodeID == "" || (task.Status.ContainerStatus == nil || task.Status.ContainerStatus.ContainerID == "") {
+			task, _, _ = client.TaskInspectWithRaw(context.Background(), task.ID)
+		}
+		return task.NodeID != "" && task.Status.ContainerStatus != nil && task.Status.ContainerStatus.ContainerID != ""
+	})
+
+	attach := swarm.ExecTask(t, d, task, types.ExecConfig{
+		Cmd:          []string{"/bin/cat", "/run/secrets/templated_secret"},
+		AttachStdout: true,
+		AttachStderr: true,
+	})
+
+	expect := "SERVICE_NAME=svc\n" +
+		"this is a secret\n" +
+		"this is a config\n"
+	assertAttachedStream(t, attach, expect)
+
+	attach = swarm.ExecTask(t, d, task, types.ExecConfig{
+		Cmd:          []string{"mount"},
+		AttachStdout: true,
+		AttachStderr: true,
+	})
+	assertAttachedStream(t, attach, "tmpfs on /run/secrets/templated_secret type tmpfs")
+}
+
+func assertAttachedStream(t *testing.T, attach types.HijackedResponse, expect string) {
+	buf := bytes.NewBuffer(nil)
+	_, err := stdcopy.StdCopy(buf, buf, attach.Reader)
+	require.NoError(t, err)
+	assert.Contains(t, buf.String(), expect)
+}
+
+func waitAndAssert(t *testing.T, timeout time.Duration, f func(*testing.T) bool) {
+	t.Helper()
+	after := time.After(timeout)
+	for {
+		select {
+		case <-after:
+			t.Fatalf("timed out waiting for condition")
+		default:
+		}
+		if f(t) {
+			return
+		}
+		time.Sleep(100 * time.Millisecond)
+	}
+}