diff --git a/components/engine/api/server/router/swarm/cluster_routes.go b/components/engine/api/server/router/swarm/cluster_routes.go index 33e52a3af1..865ed6add6 100644 --- a/components/engine/api/server/router/swarm/cluster_routes.go +++ b/components/engine/api/server/router/swarm/cluster_routes.go @@ -372,6 +372,10 @@ func (sr *swarmRouter) createSecret(ctx context.Context, w http.ResponseWriter, if err := json.NewDecoder(r.Body).Decode(&secret); err != nil { return err } + version := httputils.VersionFromContext(ctx) + if secret.Templating != nil && versions.LessThan(version, "1.36") { + return errdefs.InvalidParameter(errors.Errorf("secret templating is not supported on the specified API version: %s", version)) + } id, err := sr.backend.CreateSecret(secret) if err != nil { @@ -440,6 +444,11 @@ func (sr *swarmRouter) createConfig(ctx context.Context, w http.ResponseWriter, return err } + version := httputils.VersionFromContext(ctx) + if config.Templating != nil && versions.LessThan(version, "1.36") { + return errdefs.InvalidParameter(errors.Errorf("config templating is not supported on the specified API version: %s", version)) + } + id, err := sr.backend.CreateConfig(config) if err != nil { return err diff --git a/components/engine/api/swagger.yaml b/components/engine/api/swagger.yaml index 031b9447c5..63cc5773bd 100644 --- a/components/engine/api/swagger.yaml +++ b/components/engine/api/swagger.yaml @@ -3339,6 +3339,13 @@ definitions: Driver: description: "Name of the secrets driver used to fetch the secret's value from an external secret store" $ref: "#/definitions/Driver" + Templating: + description: | + Templating driver, if applicable + + Templating controls whether and how to evaluate the config payload as + a template. If no driver is set, no templating is used. + $ref: "#/definitions/Driver" Secret: type: "object" @@ -3375,6 +3382,13 @@ definitions: Base64-url-safe-encoded ([RFC 4648](https://tools.ietf.org/html/rfc4648#section-3.2)) config data. type: "string" + Templating: + description: | + Templating driver, if applicable + + Templating controls whether and how to evaluate the config payload as + a template. If no driver is set, no templating is used. + $ref: "#/definitions/Driver" Config: type: "object" diff --git a/components/engine/api/types/swarm/config.go b/components/engine/api/types/swarm/config.go index c1fdf3b3e4..a1555cf43e 100644 --- a/components/engine/api/types/swarm/config.go +++ b/components/engine/api/types/swarm/config.go @@ -13,6 +13,10 @@ type Config struct { type ConfigSpec struct { Annotations Data []byte `json:",omitempty"` + + // Templating controls whether and how to evaluate the config payload as + // a template. If it is not set, no templating is used. + Templating *Driver `json:",omitempty"` } // ConfigReferenceFileTarget is a file target in a config reference diff --git a/components/engine/api/types/swarm/secret.go b/components/engine/api/types/swarm/secret.go index cfba1141d8..d5213ec981 100644 --- a/components/engine/api/types/swarm/secret.go +++ b/components/engine/api/types/swarm/secret.go @@ -14,6 +14,10 @@ type SecretSpec struct { Annotations Data []byte `json:",omitempty"` Driver *Driver `json:",omitempty"` // name of the secrets driver used to fetch the secret's value from an external secret store + + // Templating controls whether and how to evaluate the secret payload as + // a template. If it is not set, no templating is used. + Templating *Driver `json:",omitempty"` } // SecretReferenceFileTarget is a file target in a secret reference diff --git a/components/engine/container/container.go b/components/engine/container/container.go index 938d66f889..46e592ab45 100644 --- a/components/engine/container/container.go +++ b/components/engine/container/container.go @@ -1049,21 +1049,6 @@ func getSecretTargetPath(r *swarmtypes.SecretReference) string { return filepath.Join(containerSecretMountPath, r.File.Name) } -// ConfigsDirPath returns the path to the directory where configs are stored on -// disk. -func (container *Container) ConfigsDirPath() (string, error) { - return container.GetRootResourcePath("configs") -} - -// ConfigFilePath returns the path to the on-disk location of a config. -func (container *Container) ConfigFilePath(configRef swarmtypes.ConfigReference) (string, error) { - configs, err := container.ConfigsDirPath() - if err != nil { - return "", err - } - return filepath.Join(configs, configRef.ConfigID), nil -} - // CreateDaemonEnvironment creates a new environment variable slice for this container. func (container *Container) CreateDaemonEnvironment(tty bool, linkedEnv []string) []string { // Setup environment diff --git a/components/engine/container/container_unix.go b/components/engine/container/container_unix.go index 6f4d91b919..e5cecf3166 100644 --- a/components/engine/container/container_unix.go +++ b/components/engine/container/container_unix.go @@ -5,11 +5,13 @@ package container // import "github.com/docker/docker/container" import ( "io/ioutil" "os" + "path/filepath" "github.com/containerd/continuity/fs" "github.com/docker/docker/api/types" containertypes "github.com/docker/docker/api/types/container" mounttypes "github.com/docker/docker/api/types/mount" + swarmtypes "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/pkg/mount" "github.com/docker/docker/pkg/stringid" "github.com/docker/docker/volume" @@ -233,6 +235,17 @@ func (container *Container) SecretMounts() ([]Mount, error) { Writable: false, }) } + for _, r := range container.ConfigReferences { + fPath, err := container.ConfigFilePath(*r) + if err != nil { + return nil, err + } + mounts = append(mounts, Mount{ + Source: fPath, + Destination: r.File.Name, + Writable: false, + }) + } return mounts, nil } @@ -253,27 +266,6 @@ func (container *Container) UnmountSecrets() error { return mount.RecursiveUnmount(p) } -// ConfigMounts returns the mounts for configs. -func (container *Container) ConfigMounts() ([]Mount, error) { - var mounts []Mount - for _, configRef := range container.ConfigReferences { - if configRef.File == nil { - continue - } - src, err := container.ConfigFilePath(*configRef) - if err != nil { - return nil, err - } - mounts = append(mounts, Mount{ - Source: src, - Destination: configRef.File.Name, - Writable: false, - }) - } - - return mounts, nil -} - type conflictingUpdateOptions string func (e conflictingUpdateOptions) Error() string { @@ -457,3 +449,13 @@ func (container *Container) GetMountPoints() []types.MountPoint { } return mountPoints } + +// ConfigFilePath returns the path to the on-disk location of a config. +// On unix, configs are always considered secret +func (container *Container) ConfigFilePath(configRef swarmtypes.ConfigReference) (string, error) { + mounts, err := container.SecretMountPath() + if err != nil { + return "", err + } + return filepath.Join(mounts, configRef.ConfigID), nil +} diff --git a/components/engine/container/container_windows.go b/components/engine/container/container_windows.go index 44b646a1ad..b5bdb5bc34 100644 --- a/components/engine/container/container_windows.go +++ b/components/engine/container/container_windows.go @@ -7,6 +7,7 @@ import ( "github.com/docker/docker/api/types" containertypes "github.com/docker/docker/api/types/container" + swarmtypes "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/pkg/system" ) @@ -102,23 +103,20 @@ func (container *Container) CreateConfigSymlinks() error { } // ConfigMounts returns the mount for configs. -// All configs are stored in a single mount on Windows. Target symlinks are -// created for each config, pointing to the files in this mount. -func (container *Container) ConfigMounts() ([]Mount, error) { +// TODO: Right now Windows doesn't really have a "secure" storage for secrets, +// however some configs may contain secrets. Once secure storage is worked out, +// configs and secret handling should be merged. +func (container *Container) ConfigMounts() []Mount { var mounts []Mount if len(container.ConfigReferences) > 0 { - src, err := container.ConfigsDirPath() - if err != nil { - return nil, err - } mounts = append(mounts, Mount{ - Source: src, + Source: container.ConfigsDirPath(), Destination: containerInternalConfigsDirPath, Writable: false, }) } - return mounts, nil + return mounts } // DetachAndUnmount unmounts all volumes. @@ -204,3 +202,12 @@ func (container *Container) GetMountPoints() []types.MountPoint { } return mountPoints } + +func (container *Container) ConfigsDirPath() string { + return filepath.Join(container.Root, "configs") +} + +// ConfigFilePath returns the path to the on-disk location of a config. +func (container *Container) ConfigFilePath(configRef swarmtypes.ConfigReference) string { + return filepath.Join(container.ConfigsDirPath(), configRef.ConfigID) +} diff --git a/components/engine/daemon/cluster/convert/config.go b/components/engine/daemon/cluster/convert/config.go index ba7920ec94..16b3475af8 100644 --- a/components/engine/daemon/cluster/convert/config.go +++ b/components/engine/daemon/cluster/convert/config.go @@ -2,6 +2,7 @@ package convert // import "github.com/docker/docker/daemon/cluster/convert" import ( swarmtypes "github.com/docker/docker/api/types/swarm" + types "github.com/docker/docker/api/types/swarm" swarmapi "github.com/docker/swarmkit/api" gogotypes "github.com/gogo/protobuf/types" ) @@ -21,18 +22,34 @@ func ConfigFromGRPC(s *swarmapi.Config) swarmtypes.Config { config.CreatedAt, _ = gogotypes.TimestampFromProto(s.Meta.CreatedAt) config.UpdatedAt, _ = gogotypes.TimestampFromProto(s.Meta.UpdatedAt) + if s.Spec.Templating != nil { + config.Spec.Templating = &types.Driver{ + Name: s.Spec.Templating.Name, + Options: s.Spec.Templating.Options, + } + } + return config } // ConfigSpecToGRPC converts Config to a grpc Config. func ConfigSpecToGRPC(s swarmtypes.ConfigSpec) swarmapi.ConfigSpec { - return swarmapi.ConfigSpec{ + spec := swarmapi.ConfigSpec{ Annotations: swarmapi.Annotations{ Name: s.Name, Labels: s.Labels, }, Data: s.Data, } + + if s.Templating != nil { + spec.Templating = &swarmapi.Driver{ + Name: s.Templating.Name, + Options: s.Templating.Options, + } + } + + return spec } // ConfigReferencesFromGRPC converts a slice of grpc ConfigReference to ConfigReference diff --git a/components/engine/daemon/cluster/convert/secret.go b/components/engine/daemon/cluster/convert/secret.go index 3ec2a353dd..d0e5ac45d2 100644 --- a/components/engine/daemon/cluster/convert/secret.go +++ b/components/engine/daemon/cluster/convert/secret.go @@ -2,6 +2,7 @@ package convert // import "github.com/docker/docker/daemon/cluster/convert" import ( swarmtypes "github.com/docker/docker/api/types/swarm" + types "github.com/docker/docker/api/types/swarm" swarmapi "github.com/docker/swarmkit/api" gogotypes "github.com/gogo/protobuf/types" ) @@ -22,12 +23,19 @@ func SecretFromGRPC(s *swarmapi.Secret) swarmtypes.Secret { secret.CreatedAt, _ = gogotypes.TimestampFromProto(s.Meta.CreatedAt) secret.UpdatedAt, _ = gogotypes.TimestampFromProto(s.Meta.UpdatedAt) + if s.Spec.Templating != nil { + secret.Spec.Templating = &types.Driver{ + Name: s.Spec.Templating.Name, + Options: s.Spec.Templating.Options, + } + } + return secret } // SecretSpecToGRPC converts Secret to a grpc Secret. func SecretSpecToGRPC(s swarmtypes.SecretSpec) swarmapi.SecretSpec { - return swarmapi.SecretSpec{ + spec := swarmapi.SecretSpec{ Annotations: swarmapi.Annotations{ Name: s.Name, Labels: s.Labels, @@ -35,6 +43,15 @@ func SecretSpecToGRPC(s swarmtypes.SecretSpec) swarmapi.SecretSpec { Data: s.Data, Driver: driverToGRPC(s.Driver), } + + if s.Templating != nil { + spec.Templating = &swarmapi.Driver{ + Name: s.Templating.Name, + Options: s.Templating.Options, + } + } + + return spec } // SecretReferencesFromGRPC converts a slice of grpc SecretReference to SecretReference diff --git a/components/engine/daemon/cluster/executor/container/executor.go b/components/engine/daemon/cluster/executor/container/executor.go index 48ec7e0d25..2c4f619cf0 100644 --- a/components/engine/daemon/cluster/executor/container/executor.go +++ b/components/engine/daemon/cluster/executor/container/executor.go @@ -19,6 +19,7 @@ import ( "github.com/docker/swarmkit/agent/exec" "github.com/docker/swarmkit/api" "github.com/docker/swarmkit/api/naming" + "github.com/docker/swarmkit/template" "github.com/sirupsen/logrus" "golang.org/x/net/context" ) @@ -191,7 +192,7 @@ func (e *executor) Configure(ctx context.Context, node *api.Node) error { // Controller returns a docker container runner. func (e *executor) Controller(t *api.Task) (exec.Controller, error) { - dependencyGetter := agent.Restrict(e.dependencies, t) + dependencyGetter := template.NewTemplatedDependencyGetter(agent.Restrict(e.dependencies, t), t, nil) // Get the node description from the executor field e.mutex.Lock() diff --git a/components/engine/daemon/configs.go b/components/engine/daemon/configs.go index f85c51db2e..4fd0d2272c 100644 --- a/components/engine/daemon/configs.go +++ b/components/engine/daemon/configs.go @@ -16,8 +16,6 @@ func (daemon *Daemon) SetContainerConfigReferences(name string, refs []*swarmtyp if err != nil { return err } - - c.ConfigReferences = refs - + c.ConfigReferences = append(c.ConfigReferences, refs...) return nil } diff --git a/components/engine/daemon/container_operations_unix.go b/components/engine/daemon/container_operations_unix.go index abd47c807f..4e92b6392e 100644 --- a/components/engine/daemon/container_operations_unix.go +++ b/components/engine/daemon/container_operations_unix.go @@ -161,43 +161,26 @@ func (daemon *Daemon) setupIpcDirs(c *container.Container) error { } func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) { - if len(c.SecretReferences) == 0 { + if len(c.SecretReferences) == 0 && len(c.ConfigReferences) == 0 { return nil } - localMountPath, err := c.SecretMountPath() - if err != nil { - return errors.Wrap(err, "error getting secrets mount dir") + 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() diff --git a/components/engine/daemon/container_operations_windows.go b/components/engine/daemon/container_operations_windows.go index e3914f9410..0559b8ac3e 100644 --- a/components/engine/daemon/container_operations_windows.go +++ b/components/engine/daemon/container_operations_windows.go @@ -21,10 +21,7 @@ func (daemon *Daemon) setupConfigDir(c *container.Container) (setupErr error) { return nil } - localPath, err := c.ConfigsDirPath() - if err != nil { - return err - } + localPath := c.ConfigsDirPath() logrus.Debugf("configs: setting up config dir: %s", localPath) // create local config root @@ -51,11 +48,7 @@ func (daemon *Daemon) setupConfigDir(c *container.Container) (setupErr error) { continue } - fPath, err := c.ConfigFilePath(*configRef) - if err != nil { - return err - } - + fPath := c.ConfigFilePath(*configRef) log := logrus.WithFields(logrus.Fields{"name": configRef.File.Name, "path": fPath}) log.Debug("injecting config") diff --git a/components/engine/daemon/oci_linux.go b/components/engine/daemon/oci_linux.go index 41c0a71f55..15bcb705bf 100644 --- a/components/engine/daemon/oci_linux.go +++ b/components/engine/daemon/oci_linux.go @@ -755,7 +755,7 @@ func (daemon *Daemon) populateCommonSpec(s *specs.Spec, c *container.Container) return nil } -func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) { +func (daemon *Daemon) createSpec(c *container.Container) (retSpec *specs.Spec, err error) { s := oci.DefaultSpec() if err := daemon.populateCommonSpec(&s, c); err != nil { return nil, err @@ -837,11 +837,13 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) { return nil, err } - if err := daemon.setupSecretDir(c); err != nil { - return nil, err - } + defer func() { + if err != nil { + daemon.cleanupSecretDir(c) + } + }() - if err := daemon.setupConfigDir(c); err != nil { + if err := daemon.setupSecretDir(c); err != nil { return nil, err } @@ -866,12 +868,6 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) { } ms = append(ms, secretMounts...) - configMounts, err := c.ConfigMounts() - if err != nil { - return nil, err - } - ms = append(ms, configMounts...) - sort.Sort(mounts(ms)) if err := setMounts(daemon, &s, c, ms); err != nil { return nil, fmt.Errorf("linux mounts: %v", err) diff --git a/components/engine/daemon/oci_windows.go b/components/engine/daemon/oci_windows.go index 47b1301eee..64c651c4af 100644 --- a/components/engine/daemon/oci_windows.go +++ b/components/engine/daemon/oci_windows.go @@ -102,10 +102,7 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) { mounts = append(mounts, secretMounts...) } - configMounts, err := c.ConfigMounts() - if err != nil { - return nil, err - } + configMounts := c.ConfigMounts() if configMounts != nil { mounts = append(mounts, configMounts...) } diff --git a/components/engine/integration/config/config_test.go b/components/engine/integration/config/config_test.go index fa2a205953..c152be59bf 100644 --- a/components/engine/integration/config/config_test.go +++ b/components/engine/integration/config/config_test.go @@ -1,8 +1,10 @@ package config import ( + "bytes" "sort" "testing" + "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" @@ -10,6 +12,7 @@ import ( "github.com/docker/docker/client" "github.com/docker/docker/integration/internal/swarm" "github.com/docker/docker/internal/testutil" + "github.com/docker/docker/pkg/stdcopy" "github.com/gotestyourself/gotestyourself/skip" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -188,3 +191,139 @@ func TestConfigsUpdate(t *testing.T) { err = client.ConfigUpdate(ctx, configID, insp.Version, insp.Spec) testutil.ErrorContains(t, err, "only updates to Labels are allowed") } + +func TestTemplatedConfig(t *testing.T) { + d := swarm.NewSwarm(t, testEnv) + defer d.Stop(t) + + ctx := context.Background() + client := swarm.GetClient(t, d) + + referencedSecretSpec := swarmtypes.SecretSpec{ + Annotations: swarmtypes.Annotations{ + Name: "referencedsecret", + }, + Data: []byte("this is a secret"), + } + referencedSecret, err := client.SecretCreate(ctx, referencedSecretSpec) + assert.NoError(t, err) + + referencedConfigSpec := swarmtypes.ConfigSpec{ + Annotations: swarmtypes.Annotations{ + Name: "referencedconfig", + }, + Data: []byte("this is a config"), + } + referencedConfig, err := client.ConfigCreate(ctx, referencedConfigSpec) + assert.NoError(t, err) + + configSpec := swarmtypes.ConfigSpec{ + Annotations: swarmtypes.Annotations{ + Name: "templated_config", + }, + Templating: &swarmtypes.Driver{ + Name: "golang", + }, + Data: []byte("SERVICE_NAME={{.Service.Name}}\n" + + "{{secret \"referencedsecrettarget\"}}\n" + + "{{config \"referencedconfigtarget\"}}\n"), + } + + templatedConfig, err := client.ConfigCreate(ctx, configSpec) + assert.NoError(t, err) + + serviceID := swarm.CreateService(t, d, + swarm.ServiceWithConfig( + &swarmtypes.ConfigReference{ + File: &swarmtypes.ConfigReferenceFileTarget{ + Name: "/templated_config", + UID: "0", + GID: "0", + Mode: 0600, + }, + ConfigID: templatedConfig.ID, + ConfigName: "templated_config", + }, + ), + swarm.ServiceWithConfig( + &swarmtypes.ConfigReference{ + File: &swarmtypes.ConfigReferenceFileTarget{ + Name: "referencedconfigtarget", + UID: "0", + GID: "0", + Mode: 0600, + }, + ConfigID: referencedConfig.ID, + ConfigName: "referencedconfig", + }, + ), + swarm.ServiceWithSecret( + &swarmtypes.SecretReference{ + File: &swarmtypes.SecretReferenceFileTarget{ + Name: "referencedsecrettarget", + UID: "0", + GID: "0", + Mode: 0600, + }, + SecretID: referencedSecret.ID, + SecretName: "referencedsecret", + }, + ), + swarm.ServiceWithName("svc"), + ) + + var tasks []swarmtypes.Task + waitAndAssert(t, 60*time.Second, func(t *testing.T) bool { + tasks = swarm.GetRunningTasks(t, d, serviceID) + return len(tasks) > 0 + }) + + task := tasks[0] + waitAndAssert(t, 60*time.Second, func(t *testing.T) bool { + if task.NodeID == "" || (task.Status.ContainerStatus == nil || task.Status.ContainerStatus.ContainerID == "") { + task, _, _ = client.TaskInspectWithRaw(context.Background(), task.ID) + } + return task.NodeID != "" && task.Status.ContainerStatus != nil && task.Status.ContainerStatus.ContainerID != "" + }) + + attach := swarm.ExecTask(t, d, task, types.ExecConfig{ + Cmd: []string{"/bin/cat", "/templated_config"}, + AttachStdout: true, + AttachStderr: true, + }) + + expect := "SERVICE_NAME=svc\n" + + "this is a secret\n" + + "this is a config\n" + assertAttachedStream(t, attach, expect) + + attach = swarm.ExecTask(t, d, task, types.ExecConfig{ + Cmd: []string{"mount"}, + AttachStdout: true, + AttachStderr: true, + }) + assertAttachedStream(t, attach, "tmpfs on /templated_config type tmpfs") +} + +func assertAttachedStream(t *testing.T, attach types.HijackedResponse, expect string) { + buf := bytes.NewBuffer(nil) + _, err := stdcopy.StdCopy(buf, buf, attach.Reader) + require.NoError(t, err) + assert.Contains(t, buf.String(), expect) +} + +func waitAndAssert(t *testing.T, timeout time.Duration, f func(*testing.T) bool) { + t.Helper() + after := time.After(timeout) + for { + select { + case <-after: + t.Fatalf("timed out waiting for condition") + default: + } + if f(t) { + return + } + time.Sleep(100 * time.Millisecond) + } +} diff --git a/components/engine/integration/internal/swarm/service.go b/components/engine/integration/internal/swarm/service.go index bd001ba0b4..a46b02e146 100644 --- a/components/engine/integration/internal/swarm/service.go +++ b/components/engine/integration/internal/swarm/service.go @@ -1,10 +1,14 @@ package swarm import ( + "context" "fmt" "testing" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" swarmtypes "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/client" "github.com/docker/docker/integration-cli/daemon" "github.com/docker/docker/internal/test/environment" "github.com/stretchr/testify/require" @@ -34,3 +38,121 @@ func NewSwarm(t *testing.T, testEnv *environment.Execution) *daemon.Swarm { require.NoError(t, d.Init(swarmtypes.InitRequest{})) return d } + +// ServiceSpecOpt is used with `CreateService` to pass in service spec modifiers +type ServiceSpecOpt func(*swarmtypes.ServiceSpec) + +// CreateService creates a service on the passed in swarm daemon. +func CreateService(t *testing.T, d *daemon.Swarm, opts ...ServiceSpecOpt) string { + spec := defaultServiceSpec() + for _, o := range opts { + o(&spec) + } + + client := GetClient(t, d) + + resp, err := client.ServiceCreate(context.Background(), spec, types.ServiceCreateOptions{}) + require.NoError(t, err, "error creating service") + return resp.ID +} + +func defaultServiceSpec() swarmtypes.ServiceSpec { + var spec swarmtypes.ServiceSpec + ServiceWithImage("busybox:latest")(&spec) + ServiceWithCommand([]string{"/bin/top"})(&spec) + ServiceWithReplicas(1)(&spec) + return spec +} + +// ServiceWithImage sets the image to use for the service +func ServiceWithImage(image string) func(*swarmtypes.ServiceSpec) { + return func(spec *swarmtypes.ServiceSpec) { + ensureContainerSpec(spec) + spec.TaskTemplate.ContainerSpec.Image = image + } +} + +// ServiceWithCommand sets the command to use for the service +func ServiceWithCommand(cmd []string) ServiceSpecOpt { + return func(spec *swarmtypes.ServiceSpec) { + ensureContainerSpec(spec) + spec.TaskTemplate.ContainerSpec.Command = cmd + } +} + +// ServiceWithConfig adds the config reference to the service +func ServiceWithConfig(configRef *swarmtypes.ConfigReference) ServiceSpecOpt { + return func(spec *swarmtypes.ServiceSpec) { + ensureContainerSpec(spec) + spec.TaskTemplate.ContainerSpec.Configs = append(spec.TaskTemplate.ContainerSpec.Configs, configRef) + } +} + +// ServiceWithSecret adds the secret reference to the service +func ServiceWithSecret(secretRef *swarmtypes.SecretReference) ServiceSpecOpt { + return func(spec *swarmtypes.ServiceSpec) { + ensureContainerSpec(spec) + spec.TaskTemplate.ContainerSpec.Secrets = append(spec.TaskTemplate.ContainerSpec.Secrets, secretRef) + } +} + +// ServiceWithReplicas sets the replicas for the service +func ServiceWithReplicas(n uint64) ServiceSpecOpt { + return func(spec *swarmtypes.ServiceSpec) { + spec.Mode = swarmtypes.ServiceMode{ + Replicated: &swarmtypes.ReplicatedService{ + Replicas: &n, + }, + } + } +} + +// ServiceWithName sets the name of the service +func ServiceWithName(name string) ServiceSpecOpt { + return func(spec *swarmtypes.ServiceSpec) { + spec.Annotations.Name = name + } +} + +// GetRunningTasks gets the list of running tasks for a service +func GetRunningTasks(t *testing.T, d *daemon.Swarm, serviceID string) []swarmtypes.Task { + client := GetClient(t, d) + + filterArgs := filters.NewArgs() + filterArgs.Add("desired-state", "running") + filterArgs.Add("service", serviceID) + + options := types.TaskListOptions{ + Filters: filterArgs, + } + tasks, err := client.TaskList(context.Background(), options) + require.NoError(t, err) + return tasks +} + +// ExecTask runs the passed in exec config on the given task +func ExecTask(t *testing.T, d *daemon.Swarm, task swarmtypes.Task, config types.ExecConfig) types.HijackedResponse { + client := GetClient(t, d) + + ctx := context.Background() + resp, err := client.ContainerExecCreate(ctx, task.Status.ContainerStatus.ContainerID, config) + require.NoError(t, err, "error creating exec") + + startCheck := types.ExecStartCheck{} + attach, err := client.ContainerExecAttach(ctx, resp.ID, startCheck) + require.NoError(t, err, "error attaching to exec") + return attach +} + +func ensureContainerSpec(spec *swarmtypes.ServiceSpec) { + if spec.TaskTemplate.ContainerSpec == nil { + spec.TaskTemplate.ContainerSpec = &swarmtypes.ContainerSpec{} + } +} + +// GetClient creates a new client for the passed in swarm daemon. +func GetClient(t *testing.T, d *daemon.Swarm) client.APIClient { + client, err := client.NewClientWithOpts(client.WithHost((d.Sock()))) + require.NoError(t, err) + return client +} diff --git a/components/engine/integration/secret/secret_test.go b/components/engine/integration/secret/secret_test.go index a6e9983e23..3b5e66a5bf 100644 --- a/components/engine/integration/secret/secret_test.go +++ b/components/engine/integration/secret/secret_test.go @@ -1,8 +1,10 @@ package secret import ( + "bytes" "sort" "testing" + "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" @@ -10,6 +12,7 @@ import ( "github.com/docker/docker/client" "github.com/docker/docker/integration/internal/swarm" "github.com/docker/docker/internal/testutil" + "github.com/docker/docker/pkg/stdcopy" "github.com/gotestyourself/gotestyourself/skip" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -232,3 +235,139 @@ func TestSecretsUpdate(t *testing.T) { err = client.SecretUpdate(ctx, secretID, insp.Version, insp.Spec) testutil.ErrorContains(t, err, "only updates to Labels are allowed") } + +func TestTemplatedSecret(t *testing.T) { + d := swarm.NewSwarm(t, testEnv) + defer d.Stop(t) + + ctx := context.Background() + client := swarm.GetClient(t, d) + + referencedSecretSpec := swarmtypes.SecretSpec{ + Annotations: swarmtypes.Annotations{ + Name: "referencedsecret", + }, + Data: []byte("this is a secret"), + } + referencedSecret, err := client.SecretCreate(ctx, referencedSecretSpec) + assert.NoError(t, err) + + referencedConfigSpec := swarmtypes.ConfigSpec{ + Annotations: swarmtypes.Annotations{ + Name: "referencedconfig", + }, + Data: []byte("this is a config"), + } + referencedConfig, err := client.ConfigCreate(ctx, referencedConfigSpec) + assert.NoError(t, err) + + secretSpec := swarmtypes.SecretSpec{ + Annotations: swarmtypes.Annotations{ + Name: "templated_secret", + }, + Templating: &swarmtypes.Driver{ + Name: "golang", + }, + Data: []byte("SERVICE_NAME={{.Service.Name}}\n" + + "{{secret \"referencedsecrettarget\"}}\n" + + "{{config \"referencedconfigtarget\"}}\n"), + } + + templatedSecret, err := client.SecretCreate(ctx, secretSpec) + assert.NoError(t, err) + + serviceID := swarm.CreateService(t, d, + swarm.ServiceWithSecret( + &swarmtypes.SecretReference{ + File: &swarmtypes.SecretReferenceFileTarget{ + Name: "templated_secret", + UID: "0", + GID: "0", + Mode: 0600, + }, + SecretID: templatedSecret.ID, + SecretName: "templated_secret", + }, + ), + swarm.ServiceWithConfig( + &swarmtypes.ConfigReference{ + File: &swarmtypes.ConfigReferenceFileTarget{ + Name: "referencedconfigtarget", + UID: "0", + GID: "0", + Mode: 0600, + }, + ConfigID: referencedConfig.ID, + ConfigName: "referencedconfig", + }, + ), + swarm.ServiceWithSecret( + &swarmtypes.SecretReference{ + File: &swarmtypes.SecretReferenceFileTarget{ + Name: "referencedsecrettarget", + UID: "0", + GID: "0", + Mode: 0600, + }, + SecretID: referencedSecret.ID, + SecretName: "referencedsecret", + }, + ), + swarm.ServiceWithName("svc"), + ) + + var tasks []swarmtypes.Task + waitAndAssert(t, 60*time.Second, func(t *testing.T) bool { + tasks = swarm.GetRunningTasks(t, d, serviceID) + return len(tasks) > 0 + }) + + task := tasks[0] + waitAndAssert(t, 60*time.Second, func(t *testing.T) bool { + if task.NodeID == "" || (task.Status.ContainerStatus == nil || task.Status.ContainerStatus.ContainerID == "") { + task, _, _ = client.TaskInspectWithRaw(context.Background(), task.ID) + } + return task.NodeID != "" && task.Status.ContainerStatus != nil && task.Status.ContainerStatus.ContainerID != "" + }) + + attach := swarm.ExecTask(t, d, task, types.ExecConfig{ + Cmd: []string{"/bin/cat", "/run/secrets/templated_secret"}, + AttachStdout: true, + AttachStderr: true, + }) + + expect := "SERVICE_NAME=svc\n" + + "this is a secret\n" + + "this is a config\n" + assertAttachedStream(t, attach, expect) + + attach = swarm.ExecTask(t, d, task, types.ExecConfig{ + Cmd: []string{"mount"}, + AttachStdout: true, + AttachStderr: true, + }) + assertAttachedStream(t, attach, "tmpfs on /run/secrets/templated_secret type tmpfs") +} + +func assertAttachedStream(t *testing.T, attach types.HijackedResponse, expect string) { + buf := bytes.NewBuffer(nil) + _, err := stdcopy.StdCopy(buf, buf, attach.Reader) + require.NoError(t, err) + assert.Contains(t, buf.String(), expect) +} + +func waitAndAssert(t *testing.T, timeout time.Duration, f func(*testing.T) bool) { + t.Helper() + after := time.After(timeout) + for { + select { + case <-after: + t.Fatalf("timed out waiting for condition") + default: + } + if f(t) { + return + } + time.Sleep(100 * time.Millisecond) + } +}