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