Merge pull request #27794 from ehazlett/secrets

Secret Management
Upstream-commit: 5e143b5f8d82537635a57f3d9eda8126caf68a6f
Component: engine
This commit is contained in:
Sebastiaan van Stijn
2016-11-09 22:26:53 +01:00
committed by GitHub
66 changed files with 2466 additions and 15 deletions

View File

@ -23,4 +23,9 @@ type Backend interface {
RemoveNode(string, bool) error
GetTasks(basictypes.TaskListOptions) ([]types.Task, error)
GetTask(string) (types.Task, error)
GetSecrets(opts basictypes.SecretListOptions) ([]types.Secret, error)
CreateSecret(s types.SecretSpec) (string, error)
RemoveSecret(id string) error
GetSecret(id string) (types.Secret, error)
UpdateSecret(id string, version uint64, spec types.SecretSpec) error
}

View File

@ -40,5 +40,10 @@ func (sr *swarmRouter) initRoutes() {
router.NewPostRoute("/nodes/{id:.*}/update", sr.updateNode),
router.NewGetRoute("/tasks", sr.getTasks),
router.NewGetRoute("/tasks/{id:.*}", sr.getTask),
router.NewGetRoute("/secrets", sr.getSecrets),
router.NewPostRoute("/secrets", sr.createSecret),
router.NewDeleteRoute("/secrets/{id:.*}", sr.removeSecret),
router.NewGetRoute("/secrets/{id:.*}", sr.getSecret),
router.NewPostRoute("/secrets/{id:.*}/update", sr.updateSecret),
}
}

View File

@ -7,6 +7,7 @@ import (
"strconv"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/api/errors"
"github.com/docker/docker/api/server/httputils"
basictypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
@ -261,3 +262,73 @@ func (sr *swarmRouter) getTask(ctx context.Context, w http.ResponseWriter, r *ht
return httputils.WriteJSON(w, http.StatusOK, task)
}
func (sr *swarmRouter) getSecrets(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
if err := httputils.ParseForm(r); err != nil {
return err
}
filters, err := filters.FromParam(r.Form.Get("filters"))
if err != nil {
return err
}
secrets, err := sr.backend.GetSecrets(basictypes.SecretListOptions{Filters: filters})
if err != nil {
return err
}
return httputils.WriteJSON(w, http.StatusOK, secrets)
}
func (sr *swarmRouter) createSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
var secret types.SecretSpec
if err := json.NewDecoder(r.Body).Decode(&secret); err != nil {
return err
}
id, err := sr.backend.CreateSecret(secret)
if err != nil {
return err
}
return httputils.WriteJSON(w, http.StatusCreated, &basictypes.SecretCreateResponse{
ID: id,
})
}
func (sr *swarmRouter) removeSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
if err := sr.backend.RemoveSecret(vars["id"]); err != nil {
return err
}
return nil
}
func (sr *swarmRouter) getSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
secret, err := sr.backend.GetSecret(vars["id"])
if err != nil {
return err
}
return httputils.WriteJSON(w, http.StatusOK, secret)
}
func (sr *swarmRouter) updateSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
var secret types.SecretSpec
if err := json.NewDecoder(r.Body).Decode(&secret); err != nil {
return errors.NewBadRequestError(err)
}
rawVersion := r.URL.Query().Get("version")
version, err := strconv.ParseUint(rawVersion, 10, 64)
if err != nil {
return errors.NewBadRequestError(fmt.Errorf("invalid secret version"))
}
id := vars["id"]
if err := sr.backend.UpdateSecret(id, version, secret); err != nil {
return err
}
return nil
}

View File

@ -4,6 +4,7 @@ import (
"bufio"
"io"
"net"
"os"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
@ -339,3 +340,12 @@ type PluginInstallOptions struct {
AcceptPermissionsFunc func(PluginPrivileges) (bool, error)
Args []string
}
// SecretRequestOption is a type for requesting secrets
type SecretRequestOption struct {
Source string
Target string
UID string
GID string
Mode os.FileMode
}

View File

@ -0,0 +1,14 @@
package container
import "os"
// ContainerSecret represents a secret in a container. This gets realized
// in the container tmpfs
type ContainerSecret struct {
Name string
Target string
Data []byte
UID string
GID string
Mode os.FileMode
}

View File

@ -37,4 +37,5 @@ type ContainerSpec struct {
StopGracePeriod *time.Duration `json:",omitempty"`
Healthcheck *container.HealthConfig `json:",omitempty"`
DNSConfig *DNSConfig `json:",omitempty"`
Secrets []*SecretReference `json:",omitempty"`
}

View File

@ -0,0 +1,33 @@
package swarm
import "os"
// Secret represents a secret.
type Secret struct {
ID string
Meta
Spec SecretSpec
Digest string
SecretSize int64
}
// SecretSpec represents a secret specification from a secret in swarm
type SecretSpec struct {
Annotations
Data []byte `json:",omitempty"`
}
// SecretReferenceFileTarget is a file target in a secret reference
type SecretReferenceFileTarget struct {
Name string
UID string
GID string
Mode os.FileMode
}
// SecretReference is a reference to a secret in swarm
type SecretReference struct {
SecretID string
SecretName string
Target *SecretReferenceFileTarget
}

View File

@ -6,6 +6,7 @@ import (
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/registry"
@ -128,7 +129,7 @@ type ContainerProcessList struct {
Titles []string
}
// Info contains response of Remote API:
// Ping contains response of Remote API:
// GET "/_ping"
type Ping struct {
APIVersion string
@ -509,3 +510,15 @@ type ImagesPruneReport struct {
type NetworksPruneReport struct {
NetworksDeleted []string
}
// SecretCreateResponse contains the information returned to a client
// on the creation of a new secret.
type SecretCreateResponse struct {
// ID is the id of the created secret.
ID string
}
// SecretListOptions holds parameters to list secrets
type SecretListOptions struct {
Filters filters.Args
}

View File

@ -11,6 +11,7 @@ import (
"github.com/docker/docker/cli/command/node"
"github.com/docker/docker/cli/command/plugin"
"github.com/docker/docker/cli/command/registry"
"github.com/docker/docker/cli/command/secret"
"github.com/docker/docker/cli/command/service"
"github.com/docker/docker/cli/command/stack"
"github.com/docker/docker/cli/command/swarm"
@ -25,6 +26,7 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
node.NewNodeCommand(dockerCli),
service.NewServiceCommand(dockerCli),
swarm.NewSwarmCommand(dockerCli),
secret.NewSecretCommand(dockerCli),
container.NewContainerCommand(dockerCli),
image.NewImageCommand(dockerCli),
system.NewSystemCommand(dockerCli),

View File

@ -0,0 +1,29 @@
package secret
import (
"fmt"
"github.com/spf13/cobra"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
)
// NewSecretCommand returns a cobra command for `secret` subcommands
func NewSecretCommand(dockerCli *command.DockerCli) *cobra.Command {
cmd := &cobra.Command{
Use: "secret",
Short: "Manage Docker secrets",
Args: cli.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
},
}
cmd.AddCommand(
newSecretListCommand(dockerCli),
newSecretCreateCommand(dockerCli),
newSecretInspectCommand(dockerCli),
newSecretRemoveCommand(dockerCli),
)
return cmd
}

View File

@ -0,0 +1,66 @@
package secret
import (
"context"
"fmt"
"io/ioutil"
"os"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/opts"
runconfigopts "github.com/docker/docker/runconfig/opts"
"github.com/spf13/cobra"
)
type createOptions struct {
name string
labels opts.ListOpts
}
func newSecretCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
createOpts := createOptions{
labels: opts.NewListOpts(runconfigopts.ValidateEnv),
}
cmd := &cobra.Command{
Use: "create [name]",
Short: "Create a secret using stdin as content",
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
createOpts.name = args[0]
return runSecretCreate(dockerCli, createOpts)
},
}
flags := cmd.Flags()
flags.VarP(&createOpts.labels, "label", "l", "Secret labels")
return cmd
}
func runSecretCreate(dockerCli *command.DockerCli, options createOptions) error {
client := dockerCli.Client()
ctx := context.Background()
secretData, err := ioutil.ReadAll(os.Stdin)
if err != nil {
return fmt.Errorf("Error reading content from STDIN: %v", err)
}
spec := swarm.SecretSpec{
Annotations: swarm.Annotations{
Name: options.name,
Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()),
},
Data: secretData,
}
r, err := client.SecretCreate(ctx, spec)
if err != nil {
return err
}
fmt.Fprintln(dockerCli.Out(), r.ID)
return nil
}

View File

@ -0,0 +1,56 @@
package secret
import (
"context"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/cli/command/inspect"
"github.com/spf13/cobra"
)
type inspectOptions struct {
name string
format string
}
func newSecretInspectCommand(dockerCli *command.DockerCli) *cobra.Command {
opts := inspectOptions{}
cmd := &cobra.Command{
Use: "inspect [name]",
Short: "Inspect a secret",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.name = args[0]
return runSecretInspect(dockerCli, opts)
},
}
cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template")
return cmd
}
func runSecretInspect(dockerCli *command.DockerCli, opts inspectOptions) error {
client := dockerCli.Client()
ctx := context.Background()
// attempt to lookup secret by name
secrets, err := getSecretsByName(client, ctx, []string{opts.name})
if err != nil {
return err
}
id := opts.name
for _, s := range secrets {
if s.Spec.Annotations.Name == opts.name {
id = s.ID
break
}
}
getRef := func(name string) (interface{}, []byte, error) {
return client.SecretInspectWithRaw(ctx, id)
}
return inspect.Inspect(dockerCli.Out(), []string{id}, opts.format, getRef)
}

View File

@ -0,0 +1,68 @@
package secret
import (
"context"
"fmt"
"text/tabwriter"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/docker/go-units"
"github.com/spf13/cobra"
)
type listOptions struct {
quiet bool
}
func newSecretListCommand(dockerCli *command.DockerCli) *cobra.Command {
opts := listOptions{}
cmd := &cobra.Command{
Use: "ls",
Short: "List secrets",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runSecretList(dockerCli, opts)
},
}
flags := cmd.Flags()
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs")
return cmd
}
func runSecretList(dockerCli *command.DockerCli, opts listOptions) error {
client := dockerCli.Client()
ctx := context.Background()
secrets, err := client.SecretList(ctx, types.SecretListOptions{})
if err != nil {
return err
}
w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
if opts.quiet {
for _, s := range secrets {
fmt.Fprintf(w, "%s\n", s.ID)
}
} else {
fmt.Fprintf(w, "ID\tNAME\tCREATED\tUPDATED\tSIZE")
fmt.Fprintf(w, "\n")
for _, s := range secrets {
created := units.HumanDuration(time.Now().UTC().Sub(s.Meta.CreatedAt)) + " ago"
updated := units.HumanDuration(time.Now().UTC().Sub(s.Meta.UpdatedAt)) + " ago"
size := units.HumanSizeWithPrecision(float64(s.SecretSize), 3)
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", s.ID, s.Spec.Annotations.Name, created, updated, size)
}
}
w.Flush()
return nil
}

View File

@ -0,0 +1,66 @@
package secret
import (
"context"
"fmt"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/spf13/cobra"
)
type removeOptions struct {
ids []string
}
func newSecretRemoveCommand(dockerCli *command.DockerCli) *cobra.Command {
return &cobra.Command{
Use: "rm [id]",
Short: "Remove a secret",
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts := removeOptions{
ids: args,
}
return runSecretRemove(dockerCli, opts)
},
}
}
func runSecretRemove(dockerCli *command.DockerCli, opts removeOptions) error {
client := dockerCli.Client()
ctx := context.Background()
// attempt to lookup secret by name
secrets, err := getSecretsByName(client, ctx, opts.ids)
if err != nil {
return err
}
ids := opts.ids
names := make(map[string]int)
for _, id := range ids {
names[id] = 1
}
if len(secrets) > 0 {
ids = []string{}
for _, s := range secrets {
if _, ok := names[s.Spec.Annotations.Name]; ok {
ids = append(ids, s.ID)
}
}
}
for _, id := range ids {
if err := client.SecretRemove(ctx, id); err != nil {
return err
}
fmt.Fprintln(dockerCli.Out(), id)
}
return nil
}

View File

@ -0,0 +1,21 @@
package secret
import (
"context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
)
func getSecretsByName(client client.APIClient, ctx context.Context, names []string) ([]swarm.Secret, error) {
args := filters.NewArgs()
for _, n := range names {
args.Add("names", n)
}
return client.SecretList(ctx, types.SecretListOptions{
Filters: args,
})
}

View File

@ -39,6 +39,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
flags.Var(&opts.mounts, flagMount, "Attach a filesystem mount to the service")
flags.Var(&opts.constraints, flagConstraint, "Placement constraints")
flags.Var(&opts.networks, flagNetwork, "Network attachments")
flags.Var(&opts.secrets, flagSecret, "Specify secrets to expose to the service")
flags.VarP(&opts.endpoint.ports, flagPublish, "p", "Publish a port as a node port")
flags.Var(&opts.groups, flagGroup, "Set one or more supplementary user groups for the container")
flags.Var(&opts.dns, flagDNS, "Set custom DNS servers")
@ -58,6 +59,13 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error {
return err
}
// parse and validate secrets
secrets, err := parseSecrets(apiClient, opts.secrets.Value())
if err != nil {
return err
}
service.TaskTemplate.ContainerSpec.Secrets = secrets
ctx := context.Background()
// only send auth if flag was set

View File

@ -1,7 +1,10 @@
package service
import (
"encoding/csv"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
@ -139,6 +142,98 @@ func (f *floatValue) Value() float32 {
return float32(*f)
}
// SecretRequestSpec is a type for requesting secrets
type SecretRequestSpec struct {
source string
target string
uid string
gid string
mode os.FileMode
}
// SecretOpt is a Value type for parsing secrets
type SecretOpt struct {
values []*SecretRequestSpec
}
// Set a new secret value
func (o *SecretOpt) Set(value string) error {
csvReader := csv.NewReader(strings.NewReader(value))
fields, err := csvReader.Read()
if err != nil {
return err
}
spec := &SecretRequestSpec{
source: "",
target: "",
uid: "0",
gid: "0",
mode: 0444,
}
for _, field := range fields {
parts := strings.SplitN(field, "=", 2)
key := strings.ToLower(parts[0])
if len(parts) != 2 {
return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
}
value := parts[1]
switch key {
case "source":
spec.source = value
case "target":
tDir, _ := filepath.Split(value)
if tDir != "" {
return fmt.Errorf("target must not have a path")
}
spec.target = value
case "uid":
spec.uid = value
case "gid":
spec.gid = value
case "mode":
m, err := strconv.ParseUint(value, 0, 32)
if err != nil {
return fmt.Errorf("invalid mode specified: %v", err)
}
spec.mode = os.FileMode(m)
default:
return fmt.Errorf("invalid field in secret request: %s", key)
}
}
if spec.source == "" {
return fmt.Errorf("source is required")
}
o.values = append(o.values, spec)
return nil
}
// Type returns the type of this option
func (o *SecretOpt) Type() string {
return "secret"
}
// String returns a string repr of this option
func (o *SecretOpt) String() string {
secrets := []string{}
for _, secret := range o.values {
repr := fmt.Sprintf("%s -> %s", secret.source, secret.target)
secrets = append(secrets, repr)
}
return strings.Join(secrets, ", ")
}
// Value returns the secret requests
func (o *SecretOpt) Value() []*SecretRequestSpec {
return o.values
}
type updateOptions struct {
parallelism uint64
delay time.Duration
@ -337,6 +432,7 @@ type serviceOptions struct {
logDriver logDriverOptions
healthcheck healthCheckOptions
secrets opts.SecretOpt
}
func newServiceOptions() *serviceOptions {
@ -403,6 +499,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
Options: opts.dnsOptions.GetAll(),
},
StopGracePeriod: opts.stopGrace.Value(),
Secrets: nil,
},
Networks: convertNetworks(opts.networks.GetAll()),
Resources: opts.resources.ToResourceRequirements(),
@ -553,4 +650,7 @@ const (
flagHealthRetries = "health-retries"
flagHealthTimeout = "health-timeout"
flagNoHealthcheck = "no-healthcheck"
flagSecret = "secret"
flagSecretAdd = "secret-add"
flagSecretRemove = "secret-rm"
)

View File

@ -0,0 +1,68 @@
package service
import (
"fmt"
"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"
"golang.org/x/net/context"
)
// parseSecrets retrieves the secrets from the requested names and converts
// them to secret references to use with the spec
func parseSecrets(client client.APIClient, requestedSecrets []*types.SecretRequestOption) ([]*swarmtypes.SecretReference, error) {
secretRefs := make(map[string]*swarmtypes.SecretReference)
ctx := context.Background()
for _, secret := range requestedSecrets {
secretRef := &swarmtypes.SecretReference{
SecretName: secret.Source,
Target: &swarmtypes.SecretReferenceFileTarget{
Name: secret.Target,
UID: secret.UID,
GID: secret.GID,
Mode: secret.Mode,
},
}
if _, exists := secretRefs[secret.Target]; exists {
return nil, fmt.Errorf("duplicate secret target for %s not allowed", secret.Source)
}
secretRefs[secret.Target] = secretRef
}
args := filters.NewArgs()
for _, s := range secretRefs {
args.Add("names", s.SecretName)
}
secrets, err := client.SecretList(ctx, types.SecretListOptions{
Filters: args,
})
if err != nil {
return nil, err
}
foundSecrets := make(map[string]string)
for _, secret := range secrets {
foundSecrets[secret.Spec.Annotations.Name] = secret.ID
}
addedSecrets := []*swarmtypes.SecretReference{}
for _, ref := range secretRefs {
id, ok := foundSecrets[ref.SecretName]
if !ok {
return nil, fmt.Errorf("secret not found: %s", ref.SecretName)
}
// set the id for the ref to properly assign in swarm
// since swarm needs the ID instead of the name
ref.SecretID = id
addedSecrets = append(addedSecrets, ref)
}
return addedSecrets, nil
}

View File

@ -14,6 +14,7 @@ import (
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/client"
"github.com/docker/docker/opts"
runconfigopts "github.com/docker/docker/runconfig/opts"
"github.com/docker/go-connections/nat"
@ -54,6 +55,8 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
flags.Var(&opts.labels, flagLabelAdd, "Add or update a service label")
flags.Var(&opts.containerLabels, flagContainerLabelAdd, "Add or update a container label")
flags.Var(&opts.env, flagEnvAdd, "Add or update an environment variable")
flags.Var(newListOptsVar(), flagSecretRemove, "Remove a secret")
flags.Var(&opts.secrets, flagSecretAdd, "Add or update a secret on a service")
flags.Var(&opts.mounts, flagMountAdd, "Add or update a mount on a service")
flags.Var(&opts.constraints, flagConstraintAdd, "Add or update a placement constraint")
flags.Var(&opts.endpoint.ports, flagPublishAdd, "Add or update a published port")
@ -97,6 +100,13 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str
return err
}
updatedSecrets, err := getUpdatedSecrets(apiClient, flags, spec.TaskTemplate.ContainerSpec.Secrets)
if err != nil {
return err
}
spec.TaskTemplate.ContainerSpec.Secrets = updatedSecrets
// only send auth if flag was set
sendAuth, err := flags.GetBool(flagRegistryAuth)
if err != nil {
@ -401,6 +411,27 @@ func updateEnvironment(flags *pflag.FlagSet, field *[]string) {
*field = removeItems(*field, toRemove, envKey)
}
func getUpdatedSecrets(apiClient client.APIClient, flags *pflag.FlagSet, secrets []*swarm.SecretReference) ([]*swarm.SecretReference, error) {
if flags.Changed(flagSecretAdd) {
values := flags.Lookup(flagSecretAdd).Value.(*opts.SecretOpt).Value()
addSecrets, err := parseSecrets(apiClient, values)
if err != nil {
return nil, err
}
secrets = append(secrets, addSecrets...)
}
toRemove := buildToRemoveSet(flags, flagSecretRemove)
newSecrets := []*swarm.SecretReference{}
for _, secret := range secrets {
if _, exists := toRemove[secret.SecretName]; !exists {
newSecrets = append(newSecrets, secret)
}
}
return newSecrets, nil
}
func envKey(value string) string {
kv := strings.SplitN(value, "=", 2)
return kv[0]

View File

@ -217,3 +217,25 @@ func (cli *Client) NewVersionError(APIrequired, feature string) error {
}
return nil
}
// secretNotFoundError implements an error returned when a secret is not found.
type secretNotFoundError struct {
name string
}
// Error returns a string representation of a secretNotFoundError
func (e secretNotFoundError) Error() string {
return fmt.Sprintf("Error: no such secret: %s", e.name)
}
// NoFound indicates that this error type is of NotFound
func (e secretNotFoundError) NotFound() bool {
return true
}
// IsErrSecretNotFound returns true if the error is caused
// when a secret is not found.
func IsErrSecretNotFound(err error) bool {
_, ok := err.(secretNotFoundError)
return ok
}

View File

@ -23,6 +23,7 @@ type CommonAPIClient interface {
NetworkAPIClient
ServiceAPIClient
SwarmAPIClient
SecretAPIClient
SystemAPIClient
VolumeAPIClient
ClientVersion() string
@ -141,3 +142,11 @@ type VolumeAPIClient interface {
VolumeRemove(ctx context.Context, volumeID string, force bool) error
VolumesPrune(ctx context.Context, cfg types.VolumesPruneConfig) (types.VolumesPruneReport, error)
}
// SecretAPIClient defines API client methods for secrets
type SecretAPIClient interface {
SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error)
SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error)
SecretRemove(ctx context.Context, id string) error
SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error)
}

View File

@ -0,0 +1,24 @@
package client
import (
"encoding/json"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"golang.org/x/net/context"
)
// SecretCreate creates a new Secret.
func (cli *Client) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error) {
var headers map[string][]string
var response types.SecretCreateResponse
resp, err := cli.post(ctx, "/secrets", nil, secret, headers)
if err != nil {
return response, err
}
err = json.NewDecoder(resp.body).Decode(&response)
ensureReaderClosed(resp)
return response, err
}

View File

@ -0,0 +1,57 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"golang.org/x/net/context"
)
func TestSecretCreateError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.SecretCreate(context.Background(), swarm.SecretSpec{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestSecretCreate(t *testing.T) {
expectedURL := "/secrets"
client := &Client{
client: newMockClient(func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
if req.Method != "POST" {
return nil, fmt.Errorf("expected POST method, got %s", req.Method)
}
b, err := json.Marshal(types.SecretCreateResponse{
ID: "test_secret",
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(b)),
}, nil
}),
}
r, err := client.SecretCreate(context.Background(), swarm.SecretSpec{})
if err != nil {
t.Fatal(err)
}
if r.ID != "test_secret" {
t.Fatalf("expected `test_secret`, got %s", r.ID)
}
}

View File

@ -0,0 +1,34 @@
package client
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"github.com/docker/docker/api/types/swarm"
"golang.org/x/net/context"
)
// SecretInspectWithRaw returns the secret information with raw data
func (cli *Client) SecretInspectWithRaw(ctx context.Context, id string) (swarm.Secret, []byte, error) {
resp, err := cli.get(ctx, "/secrets/"+id, nil, nil)
if err != nil {
if resp.statusCode == http.StatusNotFound {
return swarm.Secret{}, nil, secretNotFoundError{id}
}
return swarm.Secret{}, nil, err
}
defer ensureReaderClosed(resp)
body, err := ioutil.ReadAll(resp.body)
if err != nil {
return swarm.Secret{}, nil, err
}
var secret swarm.Secret
rdr := bytes.NewReader(body)
err = json.NewDecoder(rdr).Decode(&secret)
return secret, body, err
}

View File

@ -0,0 +1,65 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/docker/docker/api/types/swarm"
"golang.org/x/net/context"
)
func TestSecretInspectError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}
_, _, err := client.SecretInspectWithRaw(context.Background(), "nothing")
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestSecretInspectSecretNotFound(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusNotFound, "Server error")),
}
_, _, err := client.SecretInspectWithRaw(context.Background(), "unknown")
if err == nil || !IsErrSecretNotFound(err) {
t.Fatalf("expected an secretNotFoundError error, got %v", err)
}
}
func TestSecretInspect(t *testing.T) {
expectedURL := "/secrets/secret_id"
client := &Client{
client: newMockClient(func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
content, err := json.Marshal(swarm.Secret{
ID: "secret_id",
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(content)),
}, nil
}),
}
secretInspect, _, err := client.SecretInspectWithRaw(context.Background(), "secret_id")
if err != nil {
t.Fatal(err)
}
if secretInspect.ID != "secret_id" {
t.Fatalf("expected `secret_id`, got %s", secretInspect.ID)
}
}

View File

@ -0,0 +1,35 @@
package client
import (
"encoding/json"
"net/url"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"golang.org/x/net/context"
)
// SecretList returns the list of secrets.
func (cli *Client) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) {
query := url.Values{}
if options.Filters.Len() > 0 {
filterJSON, err := filters.ToParam(options.Filters)
if err != nil {
return nil, err
}
query.Set("filters", filterJSON)
}
resp, err := cli.get(ctx, "/secrets", query, nil)
if err != nil {
return nil, err
}
var secrets []swarm.Secret
err = json.NewDecoder(resp.body).Decode(&secrets)
ensureReaderClosed(resp)
return secrets, err
}

View File

@ -0,0 +1,94 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"golang.org/x/net/context"
)
func TestSecretListError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.SecretList(context.Background(), types.SecretListOptions{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestSecretList(t *testing.T) {
expectedURL := "/secrets"
filters := filters.NewArgs()
filters.Add("label", "label1")
filters.Add("label", "label2")
listCases := []struct {
options types.SecretListOptions
expectedQueryParams map[string]string
}{
{
options: types.SecretListOptions{},
expectedQueryParams: map[string]string{
"filters": "",
},
},
{
options: types.SecretListOptions{
Filters: filters,
},
expectedQueryParams: map[string]string{
"filters": `{"label":{"label1":true,"label2":true}}`,
},
},
}
for _, listCase := range listCases {
client := &Client{
client: newMockClient(func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
query := req.URL.Query()
for key, expected := range listCase.expectedQueryParams {
actual := query.Get(key)
if actual != expected {
return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
}
}
content, err := json.Marshal([]swarm.Secret{
{
ID: "secret_id1",
},
{
ID: "secret_id2",
},
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(content)),
}, nil
}),
}
secrets, err := client.SecretList(context.Background(), listCase.options)
if err != nil {
t.Fatal(err)
}
if len(secrets) != 2 {
t.Fatalf("expected 2 secrets, got %v", secrets)
}
}
}

View File

@ -0,0 +1,10 @@
package client
import "golang.org/x/net/context"
// SecretRemove removes a Secret.
func (cli *Client) SecretRemove(ctx context.Context, id string) error {
resp, err := cli.delete(ctx, "/secrets/"+id, nil, nil)
ensureReaderClosed(resp)
return err
}

View File

@ -0,0 +1,47 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"golang.org/x/net/context"
)
func TestSecretRemoveError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}
err := client.SecretRemove(context.Background(), "secret_id")
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestSecretRemove(t *testing.T) {
expectedURL := "/secrets/secret_id"
client := &Client{
client: newMockClient(func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
if req.Method != "DELETE" {
return nil, fmt.Errorf("expected DELETE method, got %s", req.Method)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte("body"))),
}, nil
}),
}
err := client.SecretRemove(context.Background(), "secret_id")
if err != nil {
t.Fatal(err)
}
}

View File

@ -89,8 +89,9 @@ type CommonContainer struct {
HasBeenStartedBefore bool
HasBeenManuallyStopped bool // used for unless-stopped restart policy
MountPoints map[string]*volume.MountPoint
HostConfig *containertypes.HostConfig `json:"-"` // do not serialize the host config in the json, otherwise we'll make the container unportable
ExecCommands *exec.Store `json:"-"`
HostConfig *containertypes.HostConfig `json:"-"` // do not serialize the host config in the json, otherwise we'll make the container unportable
ExecCommands *exec.Store `json:"-"`
Secrets []*containertypes.ContainerSecret `json:"-"` // do not serialize
// logDriver for closing
LogDriver logger.Logger `json:"-"`
LogCopier *logger.Copier `json:"-"`

View File

@ -11,3 +11,13 @@ func detachMounted(path string) error {
// Therefore there are separate definitions for this.
return unix.Unmount(path, 0)
}
// SecretMount returns the mount for the secret path
func (container *Container) SecretMount() *Mount {
return nil
}
// UnmountSecrets unmounts the fs for secrets
func (container *Container) UnmountSecrets() error {
return nil
}

View File

@ -22,8 +22,11 @@ import (
"golang.org/x/sys/unix"
)
// DefaultSHMSize is the default size (64MB) of the SHM which will be mounted in the container
const DefaultSHMSize int64 = 67108864
const (
// DefaultSHMSize is the default size (64MB) of the SHM which will be mounted in the container
DefaultSHMSize int64 = 67108864
containerSecretMountPath = "/run/secrets"
)
// Container holds the fields specific to unixen implementations.
// See CommonContainer for standard fields common to all containers.
@ -175,6 +178,11 @@ func (container *Container) NetworkMounts() []Mount {
return mounts
}
// SecretMountPath returns the path of the secret mount for the container
func (container *Container) SecretMountPath() string {
return filepath.Join(container.Root, "secrets")
}
// CopyImagePathContent copies files in destination to the volume.
func (container *Container) CopyImagePathContent(v volume.Volume, destination string) error {
rootfs, err := symlink.FollowSymlinkInScope(filepath.Join(container.BaseFS, destination), container.BaseFS)
@ -260,6 +268,31 @@ func (container *Container) IpcMounts() []Mount {
return mounts
}
// SecretMount returns the mount for the secret path
func (container *Container) SecretMount() *Mount {
if len(container.Secrets) > 0 {
return &Mount{
Source: container.SecretMountPath(),
Destination: containerSecretMountPath,
Writable: false,
}
}
return nil
}
// UnmountSecrets unmounts the local tmpfs for secrets
func (container *Container) UnmountSecrets() error {
if _, err := os.Stat(container.SecretMountPath()); err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
return detachMounted(container.SecretMountPath())
}
// UpdateContainer updates configuration of a container.
func (container *Container) UpdateContainer(hostConfig *containertypes.HostConfig) error {
container.Lock()

View File

@ -44,6 +44,16 @@ func (container *Container) IpcMounts() []Mount {
return nil
}
// SecretMount returns the mount for the secret path
func (container *Container) SecretMount() *Mount {
return nil
}
// UnmountSecrets unmounts the fs for secrets
func (container *Container) UnmountSecrets() error {
return nil
}
// UnmountVolumes explicitly unmounts volumes from the container.
func (container *Container) UnmountVolumes(forceSyscall bool, volumeEventLog func(name, action string, attributes map[string]string)) error {
var (

View File

@ -4,6 +4,7 @@ import (
"fmt"
"strings"
"github.com/Sirupsen/logrus"
container "github.com/docker/docker/api/types/container"
mounttypes "github.com/docker/docker/api/types/mount"
types "github.com/docker/docker/api/types/swarm"
@ -23,6 +24,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec {
User: c.User,
Groups: c.Groups,
TTY: c.TTY,
Secrets: secretReferencesFromGRPC(c.Secrets),
}
if c.DNSConfig != nil {
@ -75,6 +77,49 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec {
return containerSpec
}
func secretReferencesToGRPC(sr []*types.SecretReference) []*swarmapi.SecretReference {
refs := make([]*swarmapi.SecretReference, 0, len(sr))
for _, s := range sr {
refs = append(refs, &swarmapi.SecretReference{
SecretID: s.SecretID,
SecretName: s.SecretName,
Target: &swarmapi.SecretReference_File{
File: &swarmapi.SecretReference_FileTarget{
Name: s.Target.Name,
UID: s.Target.UID,
GID: s.Target.GID,
Mode: s.Target.Mode,
},
},
})
}
return refs
}
func secretReferencesFromGRPC(sr []*swarmapi.SecretReference) []*types.SecretReference {
refs := make([]*types.SecretReference, 0, len(sr))
for _, s := range sr {
target := s.GetFile()
if target == nil {
// not a file target
logrus.Warnf("secret target not a file: secret=%s", s.SecretID)
continue
}
refs = append(refs, &types.SecretReference{
SecretID: s.SecretID,
SecretName: s.SecretName,
Target: &types.SecretReferenceFileTarget{
Name: target.Name,
UID: target.UID,
GID: target.GID,
Mode: target.Mode,
},
})
}
return refs
}
func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
containerSpec := &swarmapi.ContainerSpec{
Image: c.Image,
@ -87,6 +132,7 @@ func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
User: c.User,
Groups: c.Groups,
TTY: c.TTY,
Secrets: secretReferencesToGRPC(c.Secrets),
}
if c.DNSConfig != nil {

View File

@ -0,0 +1,41 @@
package convert
import (
swarmtypes "github.com/docker/docker/api/types/swarm"
swarmapi "github.com/docker/swarmkit/api"
"github.com/docker/swarmkit/protobuf/ptypes"
)
// SecretFromGRPC converts a grpc Secret to a Secret.
func SecretFromGRPC(s *swarmapi.Secret) swarmtypes.Secret {
secret := swarmtypes.Secret{
ID: s.ID,
Digest: s.Digest,
SecretSize: s.SecretSize,
Spec: swarmtypes.SecretSpec{
Annotations: swarmtypes.Annotations{
Name: s.Spec.Annotations.Name,
Labels: s.Spec.Annotations.Labels,
},
Data: s.Spec.Data,
},
}
secret.Version.Index = s.Meta.Version.Index
// Meta
secret.CreatedAt, _ = ptypes.Timestamp(s.Meta.CreatedAt)
secret.UpdatedAt, _ = ptypes.Timestamp(s.Meta.UpdatedAt)
return secret
}
// SecretSpecToGRPC converts Secret to a grpc Secret.
func SecretSpecToGRPC(s swarmtypes.SecretSpec) swarmapi.SecretSpec {
return swarmapi.SecretSpec{
Annotations: swarmapi.Annotations{
Name: s.Name,
Labels: s.Labels,
},
Data: s.Data,
}
}

View File

@ -34,6 +34,7 @@ type Backend interface {
ContainerWaitWithContext(ctx context.Context, name string) error
ContainerRm(name string, config *types.ContainerRmConfig) error
ContainerKill(name string, sig uint64) error
SetContainerSecrets(name string, secrets []*container.ContainerSecret) error
SystemInfo() (*types.Info, error)
VolumeCreate(name, driverName string, opts, labels map[string]string) (*types.Volume, error)
Containers(config *types.ContainerListOptions) ([]*types.Container, error)

View File

@ -17,6 +17,7 @@ import (
"github.com/docker/docker/api/types/versions"
executorpkg "github.com/docker/docker/daemon/cluster/executor"
"github.com/docker/libnetwork"
"github.com/docker/swarmkit/agent/exec"
"github.com/docker/swarmkit/api"
"github.com/docker/swarmkit/log"
"golang.org/x/net/context"
@ -29,9 +30,10 @@ import (
type containerAdapter struct {
backend executorpkg.Backend
container *containerConfig
secrets exec.SecretGetter
}
func newContainerAdapter(b executorpkg.Backend, task *api.Task) (*containerAdapter, error) {
func newContainerAdapter(b executorpkg.Backend, task *api.Task, secrets exec.SecretGetter) (*containerAdapter, error) {
ctnr, err := newContainerConfig(task)
if err != nil {
return nil, err
@ -40,6 +42,7 @@ func newContainerAdapter(b executorpkg.Backend, task *api.Task) (*containerAdapt
return &containerAdapter{
container: ctnr,
backend: b,
secrets: secrets,
}, nil
}
@ -215,6 +218,40 @@ func (c *containerAdapter) create(ctx context.Context) error {
}
}
container := c.container.task.Spec.GetContainer()
if container == nil {
return fmt.Errorf("unable to get container from task spec")
}
secrets := make([]*containertypes.ContainerSecret, 0, len(container.Secrets))
for _, s := range container.Secrets {
sec := c.secrets.Get(s.SecretID)
if sec == nil {
logrus.Warnf("unable to get secret %s from provider", s.SecretID)
continue
}
name := sec.Spec.Annotations.Name
target := s.GetFile()
if target == nil {
logrus.Warnf("secret target was not a file: secret=%s", s.SecretID)
continue
}
secrets = append(secrets, &containertypes.ContainerSecret{
Name: name,
Target: target.Name,
Data: sec.Spec.Data,
UID: target.UID,
GID: target.GID,
Mode: target.Mode,
})
}
// configure secrets
if err := c.backend.SetContainerSecrets(cr.ID, secrets); err != nil {
return err
}
if err := c.backend.UpdateContainerServiceConfig(cr.ID, c.container.serviceConfig()); err != nil {
return err
}

View File

@ -2,6 +2,7 @@ package container
import (
executorpkg "github.com/docker/docker/daemon/cluster/executor"
"github.com/docker/swarmkit/agent/exec"
"github.com/docker/swarmkit/api"
"golang.org/x/net/context"
)
@ -19,8 +20,8 @@ type networkAttacherController struct {
closed chan struct{}
}
func newNetworkAttacherController(b executorpkg.Backend, task *api.Task) (*networkAttacherController, error) {
adapter, err := newContainerAdapter(b, task)
func newNetworkAttacherController(b executorpkg.Backend, task *api.Task, secrets exec.SecretGetter) (*networkAttacherController, error) {
adapter, err := newContainerAdapter(b, task, secrets)
if err != nil {
return nil, err
}

View File

@ -33,8 +33,8 @@ type controller struct {
var _ exec.Controller = &controller{}
// NewController returns a docker exec runner for the provided task.
func newController(b executorpkg.Backend, task *api.Task) (*controller, error) {
adapter, err := newContainerAdapter(b, task)
func newController(b executorpkg.Backend, task *api.Task, secrets exec.SecretGetter) (*controller, error) {
adapter, err := newContainerAdapter(b, task, secrets)
if err != nil {
return nil, err
}

View File

@ -10,18 +10,21 @@ import (
clustertypes "github.com/docker/docker/daemon/cluster/provider"
networktypes "github.com/docker/libnetwork/types"
"github.com/docker/swarmkit/agent/exec"
"github.com/docker/swarmkit/agent/secrets"
"github.com/docker/swarmkit/api"
"golang.org/x/net/context"
)
type executor struct {
backend executorpkg.Backend
secrets exec.SecretsManager
}
// NewExecutor returns an executor from the docker client.
func NewExecutor(b executorpkg.Backend) exec.Executor {
return &executor{
backend: b,
secrets: secrets.NewManager(),
}
}
@ -122,10 +125,10 @@ 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) {
if t.Spec.GetAttachment() != nil {
return newNetworkAttacherController(e.backend, t)
return newNetworkAttacherController(e.backend, t, e.secrets)
}
ctlr, err := newController(e.backend, t)
ctlr, err := newController(e.backend, t, e.secrets)
if err != nil {
return nil, err
}
@ -150,6 +153,10 @@ func (e *executor) SetNetworkBootstrapKeys(keys []*api.EncryptionKey) error {
return nil
}
func (e *executor) Secrets() exec.SecretsManager {
return e.secrets
}
type sortedPlugins []api.PluginDescription
func (sp sortedPlugins) Len() int { return len(sp) }

View File

@ -54,7 +54,7 @@ func TestHealthStates(t *testing.T) {
EventsService: e,
}
controller, err := newController(daemon, task)
controller, err := newController(daemon, task, nil)
if err != nil {
t.Fatalf("create controller fail %v", err)
}

View File

@ -26,7 +26,7 @@ func newTestControllerWithMount(m api.Mount) (*controller, error) {
},
},
},
})
}, nil)
}
func TestControllerValidateMountBind(t *testing.T) {

View File

@ -96,3 +96,21 @@ func newListTasksFilters(filter filters.Args, transformFunc func(filters.Args) e
return f, nil
}
func newListSecretsFilters(filter filters.Args) (*swarmapi.ListSecretsRequest_Filters, error) {
accepted := map[string]bool{
"names": true,
"name": true,
"id": true,
"label": true,
}
if err := filter.Validate(accepted); err != nil {
return nil, err
}
return &swarmapi.ListSecretsRequest_Filters{
Names: filter.Get("names"),
NamePrefixes: filter.Get("name"),
IDPrefixes: filter.Get("id"),
Labels: runconfigopts.ConvertKVStringsToMap(filter.Get("label")),
}, nil
}

View File

@ -0,0 +1,126 @@
package cluster
import (
apitypes "github.com/docker/docker/api/types"
types "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/daemon/cluster/convert"
swarmapi "github.com/docker/swarmkit/api"
)
// GetSecret returns a secret from a managed swarm cluster
func (c *Cluster) GetSecret(id string) (types.Secret, error) {
ctx, cancel := c.getRequestContext()
defer cancel()
r, err := c.node.client.GetSecret(ctx, &swarmapi.GetSecretRequest{SecretID: id})
if err != nil {
return types.Secret{}, err
}
return convert.SecretFromGRPC(r.Secret), nil
}
// GetSecrets returns all secrets of a managed swarm cluster.
func (c *Cluster) GetSecrets(options apitypes.SecretListOptions) ([]types.Secret, error) {
c.RLock()
defer c.RUnlock()
if !c.isActiveManager() {
return nil, c.errNoManager()
}
filters, err := newListSecretsFilters(options.Filters)
if err != nil {
return nil, err
}
ctx, cancel := c.getRequestContext()
defer cancel()
r, err := c.node.client.ListSecrets(ctx,
&swarmapi.ListSecretsRequest{Filters: filters})
if err != nil {
return nil, err
}
secrets := []types.Secret{}
for _, secret := range r.Secrets {
secrets = append(secrets, convert.SecretFromGRPC(secret))
}
return secrets, nil
}
// CreateSecret creates a new secret in a managed swarm cluster.
func (c *Cluster) CreateSecret(s types.SecretSpec) (string, error) {
c.RLock()
defer c.RUnlock()
if !c.isActiveManager() {
return "", c.errNoManager()
}
ctx, cancel := c.getRequestContext()
defer cancel()
secretSpec := convert.SecretSpecToGRPC(s)
r, err := c.node.client.CreateSecret(ctx,
&swarmapi.CreateSecretRequest{Spec: &secretSpec})
if err != nil {
return "", err
}
return r.Secret.ID, nil
}
// RemoveSecret removes a secret from a managed swarm cluster.
func (c *Cluster) RemoveSecret(id string) error {
c.RLock()
defer c.RUnlock()
if !c.isActiveManager() {
return c.errNoManager()
}
ctx, cancel := c.getRequestContext()
defer cancel()
req := &swarmapi.RemoveSecretRequest{
SecretID: id,
}
if _, err := c.node.client.RemoveSecret(ctx, req); err != nil {
return err
}
return nil
}
// UpdateSecret updates a secret in a managed swarm cluster.
// Note: this is not exposed to the CLI but is available from the API only
func (c *Cluster) UpdateSecret(id string, version uint64, spec types.SecretSpec) error {
c.RLock()
defer c.RUnlock()
if !c.isActiveManager() {
return c.errNoManager()
}
ctx, cancel := c.getRequestContext()
defer cancel()
secretSpec := convert.SecretSpecToGRPC(spec)
if _, err := c.client.UpdateSecret(ctx,
&swarmapi.UpdateSecretRequest{
SecretID: id,
SecretVersion: &swarmapi.Version{
Index: version,
},
Spec: &secretSpec,
}); err != nil {
return err
}
return nil
}

View File

@ -4,6 +4,7 @@ package daemon
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
@ -12,10 +13,12 @@ import (
"time"
"github.com/Sirupsen/logrus"
"github.com/cloudflare/cfssl/log"
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/container"
"github.com/docker/docker/daemon/links"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/pkg/mount"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/runconfig"
"github.com/docker/libnetwork"
@ -23,6 +26,7 @@ import (
"github.com/opencontainers/runc/libcontainer/devices"
"github.com/opencontainers/runc/libcontainer/label"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
)
func u32Ptr(i int64) *uint32 { u := uint32(i); return &u }
@ -139,6 +143,79 @@ func (daemon *Daemon) setupIpcDirs(c *container.Container) error {
return nil
}
func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) {
if len(c.Secrets) == 0 {
return nil
}
localMountPath := c.SecretMountPath()
logrus.Debugf("secrets: setting up secret dir: %s", localMountPath)
defer func() {
if setupErr != nil {
// cleanup
_ = detachMounted(localMountPath)
if err := os.RemoveAll(localMountPath); err != nil {
log.Errorf("error cleaning up secret mount: %s", err)
}
}
}()
// retrieve possible remapped range start for root UID, GID
rootUID, rootGID := daemon.GetRemappedUIDGID()
// create tmpfs
if err := idtools.MkdirAllAs(localMountPath, 0700, rootUID, rootGID); err != nil {
return errors.Wrap(err, "error creating secret local mount path")
}
tmpfsOwnership := fmt.Sprintf("uid=%d,gid=%d", rootUID, rootGID)
if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "nodev,nosuid,noexec,"+tmpfsOwnership); err != nil {
return errors.Wrap(err, "unable to setup secret mount")
}
for _, s := range c.Secrets {
targetPath := filepath.Clean(s.Target)
// ensure that the target is a filename only; no paths allowed
if targetPath != filepath.Base(targetPath) {
return fmt.Errorf("error creating secret: secret must not be a path")
}
fPath := filepath.Join(localMountPath, targetPath)
if err := idtools.MkdirAllAs(filepath.Dir(fPath), 0700, rootUID, rootGID); err != nil {
return errors.Wrap(err, "error creating secret mount path")
}
logrus.WithFields(logrus.Fields{
"name": s.Name,
"path": fPath,
}).Debug("injecting secret")
if err := ioutil.WriteFile(fPath, s.Data, s.Mode); err != nil {
return errors.Wrap(err, "error injecting secret")
}
uid, err := strconv.Atoi(s.UID)
if err != nil {
return err
}
gid, err := strconv.Atoi(s.GID)
if err != nil {
return err
}
if err := os.Chown(fPath, rootUID+uid, rootGID+gid); err != nil {
return errors.Wrap(err, "error setting ownership for secret")
}
}
// 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 killProcessDirectly(container *container.Container) error {
if _, err := container.WaitStop(10 * time.Second); err != nil {
// Ensure that we don't kill ourselves

View File

@ -854,6 +854,7 @@ func (daemon *Daemon) Unmount(container *container.Container) error {
logrus.Errorf("Error unmounting container %s: %s", container.ID, err)
return err
}
return nil
}

View File

@ -702,16 +702,27 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
return nil, err
}
if err := daemon.setupSecretDir(c); err != nil {
return nil, err
}
ms, err := daemon.setupMounts(c)
if err != nil {
return nil, err
}
ms = append(ms, c.IpcMounts()...)
tmpfsMounts, err := c.TmpfsMounts()
if err != nil {
return nil, err
}
ms = append(ms, tmpfsMounts...)
if m := c.SecretMount(); m != nil {
ms = append(ms, *m)
}
sort.Sort(mounts(ms))
if err := setMounts(daemon, &s, c, ms); err != nil {
return nil, fmt.Errorf("linux mounts: %v", err)

View File

@ -0,0 +1,23 @@
package daemon
import (
"github.com/Sirupsen/logrus"
containertypes "github.com/docker/docker/api/types/container"
)
// SetContainerSecrets sets the container secrets needed
func (daemon *Daemon) SetContainerSecrets(name string, secrets []*containertypes.ContainerSecret) error {
if !secretsSupported() && len(secrets) > 0 {
logrus.Warn("secrets are not supported on this platform")
return nil
}
c, err := daemon.GetContainer(name)
if err != nil {
return err
}
c.Secrets = secrets
return nil
}

View File

@ -0,0 +1,7 @@
// +build linux
package daemon
func secretsSupported() bool {
return true
}

View File

@ -0,0 +1,7 @@
// +build !linux
package daemon
func secretsSupported() bool {
return false
}

View File

@ -212,6 +212,10 @@ func (daemon *Daemon) Cleanup(container *container.Container) {
}
}
if err := container.UnmountSecrets(); err != nil {
logrus.Warnf("%s cleanup: failed to unmount secrets: %s", container.ID, err)
}
for _, eConfig := range container.ExecCommands.Commands() {
daemon.unregisterExecCommand(container, eConfig)
}

View File

@ -240,7 +240,7 @@ List containers
- `volume`=(`<volume name>` or `<mount point destination>`)
- `network`=(`<network id>` or `<network name>`)
- `health`=(`starting`|`healthy`|`unhealthy`|`none`)
**Status codes**:
- **200** no error
@ -5842,6 +5842,134 @@ Get details on a task
- **404** unknown task
- **500** server error
## 3.11 Secrets
**Note**: Secret operations require the engine to be part of a swarm.
### List secrets
`GET /secrets`
List secrets
**Example request**:
GET /secrets HTTP/1.1
**Example response**:
[
{
"ID": "ktnbjxoalbkvbvedmg1urrz8h",
"Version": {
"Index": 11
},
"CreatedAt": "2016-11-05T01:20:17.327670065Z",
"UpdatedAt": "2016-11-05T01:20:17.327670065Z",
"Spec": {
"Name": "app-dev.crt"
},
"Digest": "sha256:11d7c6f38253b73e608153c9f662a191ae605e1a3d9b756b0b3426388f91d3fa",
"SecretSize": 31
}
]
**Query parameters**:
- **filters** - a JSON encoded value of the filters (a `map[string][]string`) to process on the secrets list. Available filters:
- `names=<secret name>`
**Status codes**:
- **200** no error
### Create a secret
`POST /secrets/create`
Create a secret
**Example request**:
POST /secrets/create HTTP/1.1
Content-Type: application/json
{
"Name": "app-key.crt",
"Labels": {
"foo": "bar"
},
"Data": "VEhJUyBJUyBOT1QgQSBSRUFMIENFUlRJRklDQVRFCg=="
}
**Example response**:
HTTP/1.1 201 Created
Content-Type: application/json
{
"ID": "ktnbjxoalbkvbvedmg1urrz8h"
}
**Status codes**:
- **201** no error
- **406** server error or node is not part of a swarm
- **409** name conflicts with an existing object
**JSON Parameters**:
- **Name** User-defined name for the secret.
- **Labels** A map of labels to associate with the secret (e.g., `{"key":"value", "key2":"value2"}`).
- **Data** Base64-url-safe-encoded secret data
### Inspect a secret
`GET /secrets/(secret id)`
Get details on a secret
**Example request**:
GET /secrets/ktnbjxoalbkvbvedmg1urrz8h HTTP/1.1
**Example response**:
{
"ID": "ktnbjxoalbkvbvedmg1urrz8h",
"Version": {
"Index": 11
},
"CreatedAt": "2016-11-05T01:20:17.327670065Z",
"UpdatedAt": "2016-11-05T01:20:17.327670065Z",
"Spec": {
"Name": "app-dev.crt"
},
"Digest": "sha256:11d7c6f38253b73e608153c9f662a191ae605e1a3d9b756b0b3426388f91d3fa",
"SecretSize": 31
}
**Status codes**:
- **200** no error
- **404** unknown secret
- **500** server error
### Remove a secret
`DELETE /secrets/(id)`
Remove the secret `id` from the secret store
**Example request**:
DELETE /secrets/ktnbjxoalbkvbvedmg1urrz8h HTTP/1.1
**Example response**:
HTTP/1.1 204 No Content
# 4. Going further
## 4.1 Inside `docker run`

View File

@ -0,0 +1,80 @@
---
title: "secret create"
description: "The secret create command description and usage"
keywords: ["secret, create"]
---
<!-- This file is maintained within the docker/docker Github
repository at https://github.com/docker/docker/. Make all
pull requests against that repo. If you see this file in
another repository, consider it read-only there, as it will
periodically be overwritten by the definitive file. Pull
requests which include edits to this file in other repositories
will be rejected.
-->
# secret create
```Markdown
Usage: docker secret create [NAME]
Create a secret using stdin as content
Options:
--help Print usage
-l, --label list Secret labels (default [])
```
Creates a secret using standard input for the secret content. You must run this
command on a manager node.
## Examples
### Create a secret
```bash
$ cat secret.json | docker secret create secret.json
mhv17xfe3gh6xc4rij5orpfds
$ docker secret ls
ID NAME CREATED UPDATED SIZE
mhv17xfe3gh6xc4rij5orpfds secret.json 2016-10-27 23:25:43.909181089 +0000 UTC 2016-10-27 23:25:43.909181089 +0000 UTC 1679
```
### Create a secret with labels
```bash
$ cat secret.json | docker secret create secret.json --label env=dev --label rev=20161102
jtn7g6aukl5ky7nr9gvwafoxh
$ docker secret inspect secret.json
[
{
"ID": "jtn7g6aukl5ky7nr9gvwafoxh",
"Version": {
"Index": 541
},
"CreatedAt": "2016-11-03T20:54:12.924766548Z",
"UpdatedAt": "2016-11-03T20:54:12.924766548Z",
"Spec": {
"Name": "secret.json",
"Labels": {
"env": "dev",
"rev": "20161102"
},
"Data": null
},
"Digest": "sha256:4212a44b14e94154359569333d3fc6a80f6b9959dfdaff26412f4b2796b1f387",
"SecretSize": 1679
}
]
```
## Related information
* [secret inspect](secret_inspect.md)
* [secret ls](secret_ls.md)
* [secret rm](secret_rm.md)
<style>table tr > td:first-child { white-space: nowrap;}</style>

View File

@ -0,0 +1,88 @@
---
title: "secret inspect"
description: "The secret inspect command description and usage"
keywords: ["secret, inspect"]
---
<!-- This file is maintained within the docker/docker Github
repository at https://github.com/docker/docker/. Make all
pull requests against that repo. If you see this file in
another repository, consider it read-only there, as it will
periodically be overwritten by the definitive file. Pull
requests which include edits to this file in other repositories
will be rejected.
-->
# secret inspect
```Markdown
Usage: docker secret inspect [OPTIONS] SECRET [SECRET...]
Display detailed information on one or more secrets
Options:
-f, --format string Format the output using the given Go template
--help Print usage
```
Inspects the specified secret. This command has to be run targeting a manager
node.
By default, this renders all results in a JSON array. If a format is specified,
the given template will be executed for each result.
Go's [text/template](http://golang.org/pkg/text/template/) package
describes all the details of the format.
## Examples
### Inspecting a secret by name or ID
You can inspect a secret, either by its *name*, or *ID*
For example, given the following secret:
```bash
$ docker secret ls
ID NAME CREATED UPDATED SIZE
mhv17xfe3gh6xc4rij5orpfds secret.json 2016-10-27 23:25:43.909181089 +0000 UTC 2016-10-27 23:25:43.909181089 +0000 UTC 1679
```
```bash
$ docker secret inspect secret.json
[
{
"ID": "mhv17xfe3gh6xc4rij5orpfds",
"Version": {
"Index": 1198
},
"CreatedAt": "2016-10-27T23:25:43.909181089Z",
"UpdatedAt": "2016-10-27T23:25:43.909181089Z",
"Spec": {
"Name": "secret.json",
"Data": null
},
"Digest": "sha256:8281c6d924520986e3c6af23ed8926710a611c90339db582c2a9ac480ba622b7",
"SecretSize": 1679
}
]
```
### Formatting secret output
You can use the --format option to obtain specific information about a
secret. The following example command outputs the digest of the
secret.
```bash{% raw %}
$ docker secret inspect --format='{{.Digest}}' mhv17xfe3gh6xc4rij5orpfds
sha256:8281c6d924520986e3c6af23ed8926710a611c90339db582c2a9ac480ba622b7
{% endraw %}```
## Related information
* [secret create](secret_create.md)
* [secret ls](secret_ls.md)
* [secret rm](secret_rm.md)

View File

@ -0,0 +1,43 @@
---
title: "secret ls"
description: "The secret ls command description and usage"
keywords: ["secret, ls"]
---
<!-- This file is maintained within the docker/docker Github
repository at https://github.com/docker/docker/. Make all
pull requests against that repo. If you see this file in
another repository, consider it read-only there, as it will
periodically be overwritten by the definitive file. Pull
requests which include edits to this file in other repositories
will be rejected.
-->
# secret ls
```Markdown
Usage: docker secret ls [OPTIONS]
List secrets
Aliases:
ls, list
Options:
-q, --quiet Only display IDs
```
Run this command from a manager to list the secrets in the Swarm.
On a manager node:
```bash
$ docker secret ls
ID NAME CREATED UPDATED SIZE
mhv17xfe3gh6xc4rij5orpfds secret.json 2016-10-27 23:25:43.909181089 +0000 UTC 2016-10-27 23:25:43.909181089 +0000 UTC 1679
```
## Related information
* [secret create](secret_create.md)
* [secret inspect](secret_inspect.md)
* [secret rm](secret_rm.md)

View File

@ -0,0 +1,48 @@
---
title: "secret rm"
description: "The secret rm command description and usage"
keywords: ["secret, rm"]
---
<!-- This file is maintained within the docker/docker Github
repository at https://github.com/docker/docker/. Make all
pull requests against that repo. If you see this file in
another repository, consider it read-only there, as it will
periodically be overwritten by the definitive file. Pull
requests which include edits to this file in other repositories
will be rejected.
-->
# secret rm
```Markdown
Usage: docker secret rm SECRET [SECRET...]
Remove one or more secrets
Aliases:
rm, remove
Options:
--help Print usage
```
Removes the specified secrets from the swarm. This command has to be run
targeting a manager node.
This example removes a secret:
```bash
$ docker secret rm secret.json
sapth4csdo5b6wz2p5uimh5xg
```
> **Warning**: Unlike `docker rm`, this command does not ask for confirmation
> before removing a secret.
## Related information
* [secret create](secret_create.md)
* [secret inspect](secret_inspect.md)
* [secret ls](secret_ls.md)

View File

@ -54,6 +54,7 @@ Options:
--restart-delay duration Delay between restart attempts (default none)
--restart-max-attempts uint Maximum number of restarts before giving up (default none)
--restart-window duration Window used to evaluate the restart policy (default none)
--secret value Specify secrets to expose to the service (default [])
--stop-grace-period duration Time to wait before force killing a container (default none)
-t, --tty Allocate a pseudo-TTY
--update-delay duration Delay between updates (ns|us|ms|s|m|h) (default 0s)
@ -119,6 +120,33 @@ ID NAME MODE REPLICAS IMAGE
4cdgfyky7ozw redis replicated 5/5 redis:3.0.7
```
### Create a service with secrets
Use the `--secret` flag to give a container access to a
[secret](secret_create.md).
Create a service specifying a secret:
```bash
$ docker service create --name redis --secret secret.json redis:3.0.6
4cdgfyky7ozwh3htjfw0d12qv
```
Create a service specifying the secret, target, user/group ID and mode:
```bash
$ docker service create --name redis \
--secret source=ssh-key,target=ssh \
--secret source=app-key,target=app,uid=1000,gid=1001,mode=0400 \
redis:3.0.6
4cdgfyky7ozwh3htjfw0d12qv
```
Secrets are located in `/run/secrets` in the container. If no target is
specified, the name of the secret will be used as the in memory file in the
container. If a target is specified, that will be the filename. In the
example above, two files will be created: `/run/secrets/ssh` and
`/run/secrets/app` for each of the secret targets specified.
### Create a service with a rolling update policy
```bash

View File

@ -63,6 +63,8 @@ Options:
--restart-max-attempts uint Maximum number of restarts before giving up (default none)
--restart-window duration Window used to evaluate the restart policy (default none)
--rollback Rollback to previous specification
--secret-add list Add a secret (default [])
--secret-rm list Remove a secret (default [])
--stop-grace-period duration Time to wait before force killing a container (default none)
-t, --tty Allocate a pseudo-TTY
--update-delay duration Delay between updates (ns|us|ms|s|m|h) (default 0s)
@ -146,6 +148,20 @@ $ docker service update --mount-rm /somewhere myservice
myservice
```
### Adding and removing secrets
Use the `--secret-add` or `--secret-rm` options add or remove a service's
secrets.
The following example adds a secret named `ssh-2` and removes `ssh-1`:
```bash
$ docker service update \
--secret-add source=ssh-2,target=ssh-2 \
--secret-rm ssh-1 \
myservice
```
## Related information
* [service create](service_create.md)

View File

@ -284,6 +284,42 @@ func (d *SwarmDaemon) listServices(c *check.C) []swarm.Service {
return services
}
func (d *SwarmDaemon) createSecret(c *check.C, secretSpec swarm.SecretSpec) string {
status, out, err := d.SockRequest("POST", "/secrets", secretSpec)
c.Assert(err, checker.IsNil, check.Commentf(string(out)))
c.Assert(status, checker.Equals, http.StatusCreated, check.Commentf("output: %q", string(out)))
var scr types.SecretCreateResponse
c.Assert(json.Unmarshal(out, &scr), checker.IsNil)
return scr.ID
}
func (d *SwarmDaemon) listSecrets(c *check.C) []swarm.Secret {
status, out, err := d.SockRequest("GET", "/secrets", nil)
c.Assert(err, checker.IsNil, check.Commentf(string(out)))
c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out)))
secrets := []swarm.Secret{}
c.Assert(json.Unmarshal(out, &secrets), checker.IsNil)
return secrets
}
func (d *SwarmDaemon) getSecret(c *check.C, id string) *swarm.Secret {
var secret swarm.Secret
status, out, err := d.SockRequest("GET", "/secrets/"+id, nil)
c.Assert(err, checker.IsNil, check.Commentf(string(out)))
c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out)))
c.Assert(json.Unmarshal(out, &secret), checker.IsNil)
return &secret
}
func (d *SwarmDaemon) deleteSecret(c *check.C, id string) {
status, out, err := d.SockRequest("DELETE", "/secrets/"+id, nil)
c.Assert(err, checker.IsNil, check.Commentf(string(out)))
c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out)))
}
func (d *SwarmDaemon) getSwarm(c *check.C) swarm.Swarm {
var sw swarm.Swarm
status, out, err := d.SockRequest("GET", "/swarm", nil)

View File

@ -1263,3 +1263,50 @@ func (s *DockerSwarmSuite) TestAPISwarmServicesUpdateWithName(c *check.C) {
c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out)))
waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, instances)
}
func (s *DockerSwarmSuite) TestAPISwarmSecretsEmptyList(c *check.C) {
d := s.AddDaemon(c, true, true)
secrets := d.listSecrets(c)
c.Assert(secrets, checker.NotNil)
c.Assert(len(secrets), checker.Equals, 0, check.Commentf("secrets: %#v", secrets))
}
func (s *DockerSwarmSuite) TestAPISwarmSecretsCreate(c *check.C) {
d := s.AddDaemon(c, true, true)
testName := "test_secret"
id := d.createSecret(c, swarm.SecretSpec{
swarm.Annotations{
Name: testName,
},
[]byte("TESTINGDATA"),
})
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id))
secrets := d.listSecrets(c)
c.Assert(len(secrets), checker.Equals, 1, check.Commentf("secrets: %#v", secrets))
name := secrets[0].Spec.Annotations.Name
c.Assert(name, checker.Equals, testName, check.Commentf("secret: %s", name))
}
func (s *DockerSwarmSuite) TestAPISwarmSecretsDelete(c *check.C) {
d := s.AddDaemon(c, true, true)
testName := "test_secret"
id := d.createSecret(c, swarm.SecretSpec{
swarm.Annotations{
Name: testName,
},
[]byte("TESTINGDATA"),
})
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id))
secret := d.getSecret(c, id)
c.Assert(secret.ID, checker.Equals, id, check.Commentf("secret: %v", secret))
d.deleteSecret(c, secret.ID)
status, out, err := d.SockRequest("GET", "/secrets/"+id, nil)
c.Assert(err, checker.IsNil)
c.Assert(status, checker.Equals, http.StatusNotFound, check.Commentf("secret delete: %s", string(out)))
}

View File

@ -0,0 +1,48 @@
// +build !windows
package main
import (
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/pkg/integration/checker"
"github.com/go-check/check"
)
func (s *DockerSwarmSuite) TestSecretCreate(c *check.C) {
d := s.AddDaemon(c, true, true)
testName := "test_secret"
id := d.createSecret(c, swarm.SecretSpec{
swarm.Annotations{
Name: testName,
},
[]byte("TESTINGDATA"),
})
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id))
secret := d.getSecret(c, id)
c.Assert(secret.Spec.Name, checker.Equals, testName)
}
func (s *DockerSwarmSuite) TestSecretCreateWithLabels(c *check.C) {
d := s.AddDaemon(c, true, true)
testName := "test_secret"
id := d.createSecret(c, swarm.SecretSpec{
swarm.Annotations{
Name: testName,
Labels: map[string]string{
"key1": "value1",
"key2": "value2",
},
},
[]byte("TESTINGDATA"),
})
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id))
secret := d.getSecret(c, id)
c.Assert(secret.Spec.Name, checker.Equals, testName)
c.Assert(len(secret.Spec.Labels), checker.Equals, 2)
c.Assert(secret.Spec.Labels["key1"], checker.Equals, "value1")
c.Assert(secret.Spec.Labels["key2"], checker.Equals, "value2")
}

View File

@ -4,6 +4,7 @@ package main
import (
"encoding/json"
"fmt"
"strings"
"github.com/docker/docker/api/types"
@ -43,3 +44,62 @@ func (s *DockerSwarmSuite) TestServiceCreateMountVolume(c *check.C) {
c.Assert(mounts[0].Destination, checker.Equals, "/foo")
c.Assert(mounts[0].RW, checker.Equals, true)
}
func (s *DockerSwarmSuite) TestServiceCreateWithSecretSimple(c *check.C) {
d := s.AddDaemon(c, true, true)
serviceName := "test-service-secret"
testName := "test_secret"
id := d.createSecret(c, swarm.SecretSpec{
swarm.Annotations{
Name: testName,
},
[]byte("TESTINGDATA"),
})
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id))
out, err := d.Cmd("service", "create", "--name", serviceName, "--secret", testName, "busybox", "top")
c.Assert(err, checker.IsNil, check.Commentf(out))
out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Secrets }}", serviceName)
c.Assert(err, checker.IsNil)
var refs []swarm.SecretReference
c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil)
c.Assert(refs, checker.HasLen, 1)
c.Assert(refs[0].SecretName, checker.Equals, testName)
c.Assert(refs[0].Target, checker.Not(checker.IsNil))
c.Assert(refs[0].Target.Name, checker.Equals, testName)
c.Assert(refs[0].Target.UID, checker.Equals, "0")
c.Assert(refs[0].Target.GID, checker.Equals, "0")
}
func (s *DockerSwarmSuite) TestServiceCreateWithSecretSourceTarget(c *check.C) {
d := s.AddDaemon(c, true, true)
serviceName := "test-service-secret"
testName := "test_secret"
id := d.createSecret(c, swarm.SecretSpec{
swarm.Annotations{
Name: testName,
},
[]byte("TESTINGDATA"),
})
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id))
testTarget := "testing"
out, err := d.Cmd("service", "create", "--name", serviceName, "--secret", fmt.Sprintf("source=%s,target=%s", testName, testTarget), "busybox", "top")
c.Assert(err, checker.IsNil, check.Commentf(out))
out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Secrets }}", serviceName)
c.Assert(err, checker.IsNil)
var refs []swarm.SecretReference
c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil)
c.Assert(refs, checker.HasLen, 1)
c.Assert(refs[0].SecretName, checker.Equals, testName)
c.Assert(refs[0].Target, checker.Not(checker.IsNil))
c.Assert(refs[0].Target.Name, checker.Equals, testTarget)
}

View File

@ -4,6 +4,7 @@ package main
import (
"encoding/json"
"fmt"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/pkg/integration/checker"
@ -84,3 +85,45 @@ func (s *DockerSwarmSuite) TestServiceUpdateLabel(c *check.C) {
c.Assert(service.Spec.Labels, checker.HasLen, 1)
c.Assert(service.Spec.Labels["foo"], checker.Equals, "bar")
}
func (s *DockerSwarmSuite) TestServiceUpdateSecrets(c *check.C) {
d := s.AddDaemon(c, true, true)
testName := "test_secret"
id := d.createSecret(c, swarm.SecretSpec{
swarm.Annotations{
Name: testName,
},
[]byte("TESTINGDATA"),
})
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id))
testTarget := "testing"
serviceName := "test"
out, err := d.Cmd("service", "create", "--name", serviceName, "busybox", "top")
c.Assert(err, checker.IsNil, check.Commentf(out))
// add secret
out, err = d.Cmd("service", "update", "test", "--secret-add", fmt.Sprintf("source=%s,target=%s", testName, testTarget))
c.Assert(err, checker.IsNil, check.Commentf(out))
out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Secrets }}", serviceName)
c.Assert(err, checker.IsNil)
var refs []swarm.SecretReference
c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil)
c.Assert(refs, checker.HasLen, 1)
c.Assert(refs[0].SecretName, checker.Equals, testName)
c.Assert(refs[0].Target, checker.Not(checker.IsNil))
c.Assert(refs[0].Target.Name, checker.Equals, testTarget)
// remove
out, err = d.Cmd("service", "update", "test", "--secret-rm", testName)
c.Assert(err, checker.IsNil, check.Commentf(out))
out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Secrets }}", serviceName)
c.Assert(err, checker.IsNil)
c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil)
c.Assert(refs, checker.HasLen, 0)
}

View File

@ -0,0 +1,107 @@
package opts
import (
"encoding/csv"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/docker/docker/api/types"
)
// SecretOpt is a Value type for parsing secrets
type SecretOpt struct {
values []*types.SecretRequestOption
}
// Set a new secret value
func (o *SecretOpt) Set(value string) error {
csvReader := csv.NewReader(strings.NewReader(value))
fields, err := csvReader.Read()
if err != nil {
return err
}
options := &types.SecretRequestOption{
Source: "",
Target: "",
UID: "0",
GID: "0",
Mode: 0444,
}
// support a simple syntax of --secret foo
if len(fields) == 1 {
options.Source = fields[0]
options.Target = fields[0]
o.values = append(o.values, options)
return nil
}
for _, field := range fields {
parts := strings.SplitN(field, "=", 2)
key := strings.ToLower(parts[0])
if len(parts) != 2 {
return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
}
value := parts[1]
switch key {
case "source":
options.Source = value
case "target":
tDir, _ := filepath.Split(value)
if tDir != "" {
return fmt.Errorf("target must not be a path")
}
options.Target = value
case "uid":
options.UID = value
case "gid":
options.GID = value
case "mode":
m, err := strconv.ParseUint(value, 0, 32)
if err != nil {
return fmt.Errorf("invalid mode specified: %v", err)
}
options.Mode = os.FileMode(m)
default:
if len(fields) == 1 && value == "" {
} else {
return fmt.Errorf("invalid field in secret request: %s", key)
}
}
}
if options.Source == "" {
return fmt.Errorf("source is required")
}
o.values = append(o.values, options)
return nil
}
// Type returns the type of this option
func (o *SecretOpt) Type() string {
return "secret"
}
// String returns a string repr of this option
func (o *SecretOpt) String() string {
secrets := []string{}
for _, secret := range o.values {
repr := fmt.Sprintf("%s -> %s", secret.Source, secret.Target)
secrets = append(secrets, repr)
}
return strings.Join(secrets, ", ")
}
// Value returns the secret requests
func (o *SecretOpt) Value() []*types.SecretRequestOption {
return o.values
}

View File

@ -0,0 +1,67 @@
package opts
import (
"os"
"testing"
"github.com/docker/docker/pkg/testutil/assert"
)
func TestSecretOptionsSimple(t *testing.T) {
var opt SecretOpt
testCase := "app-secret"
assert.NilError(t, opt.Set(testCase))
reqs := opt.Value()
assert.Equal(t, len(reqs), 1)
req := reqs[0]
assert.Equal(t, req.Source, "app-secret")
assert.Equal(t, req.Target, "app-secret")
assert.Equal(t, req.UID, "0")
assert.Equal(t, req.GID, "0")
}
func TestSecretOptionsSourceTarget(t *testing.T) {
var opt SecretOpt
testCase := "source=foo,target=testing"
assert.NilError(t, opt.Set(testCase))
reqs := opt.Value()
assert.Equal(t, len(reqs), 1)
req := reqs[0]
assert.Equal(t, req.Source, "foo")
assert.Equal(t, req.Target, "testing")
}
func TestSecretOptionsCustomUidGid(t *testing.T) {
var opt SecretOpt
testCase := "source=foo,target=testing,uid=1000,gid=1001"
assert.NilError(t, opt.Set(testCase))
reqs := opt.Value()
assert.Equal(t, len(reqs), 1)
req := reqs[0]
assert.Equal(t, req.Source, "foo")
assert.Equal(t, req.Target, "testing")
assert.Equal(t, req.UID, "1000")
assert.Equal(t, req.GID, "1001")
}
func TestSecretOptionsCustomMode(t *testing.T) {
var opt SecretOpt
testCase := "source=foo,target=testing,uid=1000,gid=1001,mode=0444"
assert.NilError(t, opt.Set(testCase))
reqs := opt.Value()
assert.Equal(t, len(reqs), 1)
req := reqs[0]
assert.Equal(t, req.Source, "foo")
assert.Equal(t, req.Target, "testing")
assert.Equal(t, req.UID, "1000")
assert.Equal(t, req.GID, "1001")
assert.Equal(t, req.Mode, os.FileMode(0444))
}

View File

@ -0,0 +1,87 @@
package secrets
import (
"sync"
"github.com/docker/swarmkit/agent/exec"
"github.com/docker/swarmkit/api"
)
// secrets is a map that keeps all the currenty available secrets to the agent
// mapped by secret ID
type secrets struct {
mu sync.RWMutex
m map[string]*api.Secret
}
// NewManager returns a place to store secrets.
func NewManager() exec.SecretsManager {
return &secrets{
m: make(map[string]*api.Secret),
}
}
// Get returns a secret by ID. If the secret doesn't exist, returns nil.
func (s *secrets) Get(secretID string) *api.Secret {
s.mu.RLock()
defer s.mu.RUnlock()
if s, ok := s.m[secretID]; ok {
return s
}
return nil
}
// add adds one or more secrets to the secret map
func (s *secrets) Add(secrets ...api.Secret) {
s.mu.Lock()
defer s.mu.Unlock()
for _, secret := range secrets {
s.m[secret.ID] = secret.Copy()
}
}
// remove removes one or more secrets by ID from the secret map. Succeeds
// whether or not the given IDs are in the map.
func (s *secrets) Remove(secrets []string) {
s.mu.Lock()
defer s.mu.Unlock()
for _, secret := range secrets {
delete(s.m, secret)
}
}
// reset removes all the secrets
func (s *secrets) Reset() {
s.mu.Lock()
defer s.mu.Unlock()
s.m = make(map[string]*api.Secret)
}
// taskRestrictedSecretsProvider restricts the ids to the task.
type taskRestrictedSecretsProvider struct {
secrets exec.SecretGetter
secretIDs map[string]struct{} // allow list of secret ids
}
func (sp *taskRestrictedSecretsProvider) Get(secretID string) *api.Secret {
if _, ok := sp.secretIDs[secretID]; !ok {
return nil
}
return sp.secrets.Get(secretID)
}
// Restrict provides a getter that only allows access to the secrets
// referenced by the task.
func Restrict(secrets exec.SecretGetter, t *api.Task) exec.SecretGetter {
sids := map[string]struct{}{}
container := t.Spec.GetContainer()
if container != nil {
for _, ref := range container.Secrets {
sids[ref.SecretID] = struct{}{}
}
}
return &taskRestrictedSecretsProvider{secrets: secrets, secretIDs: sids}
}