1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00

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

Templated secrets and configs
This commit is contained in:
Sebastiaan van Stijn 2018-02-21 13:39:10 +01:00 committed by GitHub
commit 0076343b29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 594 additions and 154 deletions

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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)
}

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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
}

View file

@ -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")
if err := daemon.createSecretsDir(c); err != nil {
return err
}
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")
}
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()

View file

@ -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")

View file

@ -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)

View file

@ -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...)
}

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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)
}
}