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:
commit
0076343b29
18 changed files with 594 additions and 154 deletions
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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...)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue