secrets: secret management for swarm

Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>

wip: use tmpfs for swarm secrets

Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>

wip: inject secrets from swarm secret store

Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>

secrets: use secret names in cli for service create

Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>

switch to use mounts instead of volumes

Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>

vendor: use ehazlett swarmkit

Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>

secrets: finish secret update

Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>
This commit is contained in:
Evan Hazlett 2016-10-19 12:22:02 -04:00
parent 1310dadf4a
commit 3716ec25b4
46 changed files with 1292 additions and 13 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/create", sr.createSecret),
router.NewDeleteRoute("/secrets/{id:.*}", sr.removeSecret),
router.NewGetRoute("/secrets/{id:.*}", sr.getSecret),
router.NewPostRoute("/secrets/{id:.*}/update", sr.updateSecret),
}
}

View File

@ -261,3 +261,77 @@ 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
}
filter, err := filters.FromParam(r.Form.Get("filters"))
if err != nil {
return err
}
secrets, err := sr.backend.GetSecrets(basictypes.SecretListOptions{Filter: filter})
if err != nil {
logrus.Errorf("Error getting secrets: %v", err)
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 {
logrus.Errorf("Error creating secret %s: %v", id, err)
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 {
logrus.Errorf("Error removing secret %s: %v", vars["id"], err)
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 {
logrus.Errorf("Error getting secret %s: %v", vars["id"], err)
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 err
}
rawVersion := r.URL.Query().Get("version")
version, err := strconv.ParseUint(rawVersion, 10, 64)
if err != nil {
return fmt.Errorf("Invalid secret version '%s': %s", rawVersion, err.Error())
}
id := vars["id"]
if err := sr.backend.UpdateSecret(id, version, secret); err != nil {
return fmt.Errorf("Error updating secret: %s", err)
}
return nil
}

View File

@ -0,0 +1,12 @@
package container
import "os"
type ContainerSecret struct {
Name string
Target string
Data []byte
Uid int
Gid int
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"`
}

30
api/types/swarm/secret.go Normal file
View File

@ -0,0 +1,30 @@
package swarm
// Secret represents a secret.
type Secret struct {
ID string
Meta
Spec *SecretSpec `json:",omitempty"`
Digest string `json:",omitempty"`
SecretSize int64 `json:",omitempty"`
}
type SecretSpec struct {
Annotations
Data []byte `json",omitempty"`
}
type SecretReferenceMode int
const (
SecretReferenceSystem SecretReferenceMode = 0
SecretReferenceFile SecretReferenceMode = 1
SecretReferenceEnv SecretReferenceMode = 2
)
type SecretReference struct {
SecretID string `json:",omitempty"`
Mode SecretReferenceMode `json:",omitempty"`
Target string `json:",omitempty"`
SecretName string `json:",omitempty"`
}

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"
@ -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 {
Filter 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),

29
cli/command/secret/cmd.go Normal file
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,57 @@
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/spf13/cobra"
)
type createOptions struct {
name string
}
func newSecretCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
return &cobra.Command{
Use: "create [name]",
Short: "Create a secret using stdin as content",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts := createOptions{
name: args[0],
}
return runSecretCreate(dockerCli, opts)
},
}
}
func runSecretCreate(dockerCli *command.DockerCli, opts 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: opts.name,
},
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,42 @@
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()
getRef := func(name string) (interface{}, []byte, error) {
return client.SecretInspectWithRaw(ctx, name)
}
return inspect.Inspect(dockerCli.Out(), []string{opts.name}, opts.format, getRef)
}

62
cli/command/secret/ls.go Normal file
View File

@ -0,0 +1,62 @@
package secret
import (
"context"
"fmt"
"text/tabwriter"
"github.com/docker/docker/api/types"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"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 {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\n", s.ID, s.Spec.Annotations.Name, s.Meta.CreatedAt, s.Meta.UpdatedAt, s.SecretSize)
}
}
w.Flush()
return nil
}

View File

@ -0,0 +1,43 @@
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()
for _, id := range opts.ids {
if err := client.SecretRemove(ctx, id); err != nil {
return err
}
fmt.Fprintln(dockerCli.Out(), id)
}
return nil
}

View File

@ -58,6 +58,13 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error {
return err
}
// parse and validate secrets
secrets, err := parseSecrets(apiClient, opts.secrets)
if err != nil {
return err
}
service.TaskTemplate.ContainerSpec.Secrets = secrets
ctx := context.Background()
// only send auth if flag was set

View File

@ -191,6 +191,19 @@ func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig {
return nets
}
func convertSecrets(secrets []string) []*swarm.SecretReference {
sec := []*swarm.SecretReference{}
for _, s := range secrets {
sec = append(sec, &swarm.SecretReference{
SecretID: s,
Mode: swarm.SecretReferenceFile,
Target: "",
})
}
return sec
}
type endpointOptions struct {
mode string
ports opts.ListOpts
@ -337,6 +350,7 @@ type serviceOptions struct {
logDriver logDriverOptions
healthcheck healthCheckOptions
secrets []string
}
func newServiceOptions() *serviceOptions {
@ -403,6 +417,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
Options: opts.dnsOptions.GetAll(),
},
StopGracePeriod: opts.stopGrace.Value(),
Secrets: convertSecrets(opts.secrets),
},
Networks: convertNetworks(opts.networks.GetAll()),
Resources: opts.resources.ToResourceRequirements(),
@ -488,6 +503,7 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) {
flags.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK")
flags.BoolVarP(&opts.tty, flagTTY, "t", false, "Allocate a pseudo-TTY")
flags.StringSliceVar(&opts.secrets, flagSecret, []string{}, "Specify secrets to expose to the service")
}
const (
@ -553,4 +569,5 @@ const (
flagHealthRetries = "health-retries"
flagHealthTimeout = "health-timeout"
flagNoHealthcheck = "no-healthcheck"
flagSecret = "secret"
)

View File

@ -0,0 +1,92 @@
package service
import (
"context"
"fmt"
"strings"
"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"
)
// parseSecretString parses the requested secret and returns the secret name
// and target. Expects format SECRET_NAME:TARGET
func parseSecretString(secretString string) (string, string, error) {
tokens := strings.Split(secretString, ":")
secretName := strings.TrimSpace(tokens[0])
targetName := ""
if secretName == "" {
return "", "", fmt.Errorf("invalid secret name provided")
}
if len(tokens) > 1 {
targetName = strings.TrimSpace(tokens[1])
if targetName == "" {
return "", "", fmt.Errorf("invalid presentation name provided")
}
} else {
targetName = secretName
}
return secretName, targetName, nil
}
// 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 []string) ([]*swarmtypes.SecretReference, error) {
lookupSecretNames := []string{}
needSecrets := make(map[string]*swarmtypes.SecretReference)
ctx := context.Background()
for _, secret := range requestedSecrets {
n, t, err := parseSecretString(secret)
if err != nil {
return nil, err
}
secretRef := &swarmtypes.SecretReference{
SecretName: n,
Mode: swarmtypes.SecretReferenceFile,
Target: t,
}
lookupSecretNames = append(lookupSecretNames, n)
needSecrets[n] = secretRef
}
args := filters.NewArgs()
for _, s := range lookupSecretNames {
args.Add("names", s)
}
secrets, err := client.SecretList(ctx, types.SecretListOptions{
Filter: args,
})
if err != nil {
return nil, err
}
foundSecrets := make(map[string]*swarmtypes.Secret)
for _, secret := range secrets {
foundSecrets[secret.Spec.Annotations.Name] = &secret
}
addedSecrets := []*swarmtypes.SecretReference{}
for secretName, secretRef := range needSecrets {
s, ok := foundSecrets[secretName]
if !ok {
return nil, fmt.Errorf("secret not found: %s", secretName)
}
// set the id for the ref to properly assign in swarm
// since swarm needs the ID instead of the name
secretRef.SecretID = s.ID
addedSecrets = append(addedSecrets, secretRef)
}
return addedSecrets, nil
}

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

24
client/secret_create.go Normal file
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/create", 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/create"
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)
}
}

34
client/secret_inspect.go Normal file
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)
}
}

35
client/secret_list.go Normal file
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.Filter.Len() > 0 {
filterJSON, err := filters.ToParam(options.Filter)
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{
Filter: 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)
}
}
}

10
client/secret_remove.go Normal file
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

@ -23,7 +23,10 @@ import (
)
// DefaultSHMSize is the default size (64MB) of the SHM which will be mounted in the container
const DefaultSHMSize int64 = 67108864
const (
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,10 @@ func (container *Container) NetworkMounts() []Mount {
return mounts
}
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 +267,26 @@ func (container *Container) IpcMounts() []Mount {
return mounts
}
// SecretMounts returns the list of Secret mounts
func (container *Container) SecretMounts() []Mount {
var mounts []Mount
if len(container.Secrets) > 0 {
mounts = append(mounts, Mount{
Source: container.SecretMountPath(),
Destination: containerSecretMountPath,
Writable: false,
})
}
return mounts
}
// UnmountSecrets unmounts the local tmpfs for secrets
func (container *Container) UnmountSecrets() error {
return detachMounted(container.SecretMountPath())
}
// UpdateContainer updates configuration of a container.
func (container *Container) UpdateContainer(hostConfig *containertypes.HostConfig) error {
container.Lock()

View File

@ -23,6 +23,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 +76,47 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec {
return containerSpec
}
func secretReferencesToGRPC(sr []*types.SecretReference) []*swarmapi.SecretReference {
refs := []*swarmapi.SecretReference{}
for _, s := range sr {
var mode swarmapi.SecretReference_Mode
switch s.Mode {
case types.SecretReferenceSystem:
mode = swarmapi.SecretReference_SYSTEM
default:
mode = swarmapi.SecretReference_FILE
}
refs = append(refs, &swarmapi.SecretReference{
SecretID: s.SecretID,
SecretName: s.SecretName,
Target: s.Target,
Mode: mode,
})
}
return refs
}
func secretReferencesFromGRPC(sr []*swarmapi.SecretReference) []*types.SecretReference {
refs := []*types.SecretReference{}
for _, s := range sr {
var mode types.SecretReferenceMode
switch s.Mode {
case swarmapi.SecretReference_SYSTEM:
mode = types.SecretReferenceSystem
default:
mode = types.SecretReferenceFile
}
refs = append(refs, &types.SecretReference{
SecretID: s.SecretID,
SecretName: s.SecretName,
Target: s.Target,
Mode: mode,
})
}
return refs
}
func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
containerSpec := &swarmapi.ContainerSpec{
Image: c.Image,
@ -87,6 +129,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,46 @@
package convert
import (
"github.com/Sirupsen/logrus"
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 {
logrus.Debugf("%+v", s)
secret := swarmtypes.Secret{
ID: s.ID,
Digest: s.Digest,
SecretSize: s.SecretSize,
}
// Meta
secret.Version.Index = s.Meta.Version.Index
secret.CreatedAt, _ = ptypes.Timestamp(s.Meta.CreatedAt)
secret.UpdatedAt, _ = ptypes.Timestamp(s.Meta.UpdatedAt)
secret.Spec = &swarmtypes.SecretSpec{
Annotations: swarmtypes.Annotations{
Name: s.Spec.Annotations.Name,
Labels: s.Spec.Annotations.Labels,
},
Data: s.Spec.Data,
}
return secret
}
// SecretSpecToGRPC converts Secret to a grpc Secret.
func SecretSpecToGRPC(s swarmtypes.SecretSpec) (swarmapi.SecretSpec, error) {
spec := swarmapi.SecretSpec{
Annotations: swarmapi.Annotations{
Name: s.Name,
Labels: s.Labels,
},
Data: s.Data,
}
return spec, nil
}

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.SecretProvider
}
func newContainerAdapter(b executorpkg.Backend, task *api.Task) (*containerAdapter, error) {
func newContainerAdapter(b executorpkg.Backend, task *api.Task, secrets exec.SecretProvider) (*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,35 @@ func (c *containerAdapter) create(ctx context.Context) error {
}
}
secrets := []*containertypes.ContainerSecret{}
for _, s := range c.container.task.Spec.GetContainer().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.Target
if target == "" {
target = name
}
secrets = append(secrets, &containertypes.ContainerSecret{
Name: name,
Target: target,
Data: sec.Spec.Data,
// TODO (ehazlett): enable configurable uid, gid, mode
Uid: 0,
Gid: 0,
Mode: 0444,
})
}
// 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

@ -4,6 +4,7 @@ import (
executorpkg "github.com/docker/docker/daemon/cluster/executor"
"github.com/docker/swarmkit/api"
"golang.org/x/net/context"
"src/github.com/docker/swarmkit/agent/exec"
)
// networkAttacherController implements agent.Controller against docker's API.
@ -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.SecretProvider) (*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.SecretProvider) (*controller, error) {
adapter, err := newContainerAdapter(b, task, secrets)
if err != nil {
return nil, err
}

View File

@ -18,6 +18,10 @@ type executor struct {
backend executorpkg.Backend
}
type secretProvider interface {
Get(secretID string) *api.Secret
}
// NewExecutor returns an executor from the docker client.
func NewExecutor(b executorpkg.Backend) exec.Executor {
return &executor{
@ -120,12 +124,12 @@ 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) {
func (e *executor) Controller(t *api.Task, secrets exec.SecretProvider) (exec.Controller, error) {
if t.Spec.GetAttachment() != nil {
return newNetworkAttacherController(e.backend, t)
return newNetworkAttacherController(e.backend, t, secrets)
}
ctlr, err := newController(e.backend, t)
ctlr, err := newController(e.backend, t, secrets)
if err != nil {
return nil, err
}

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
}

131
daemon/cluster/secrets.go Normal file
View File

@ -0,0 +1,131 @@
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.Filter)
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, err := convert.SecretSpecToGRPC(s)
if err != nil {
return "", err
}
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.
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, err := convert.SecretSpecToGRPC(spec)
if err != nil {
return err
}
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"
@ -18,6 +19,7 @@ import (
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/runconfig"
"github.com/docker/engine-api/types/mount"
"github.com/docker/libnetwork"
"github.com/opencontainers/runc/libcontainer/configs"
"github.com/opencontainers/runc/libcontainer/devices"
@ -139,6 +141,43 @@ func (daemon *Daemon) setupIpcDirs(c *container.Container) error {
return nil
}
func (daemon *Daemon) setupSecretDir(c *container.Container) error {
localMountPath := c.SecretMountPath()
logrus.Debugf("secrets: setting up secret dir: %s", localMountPath)
// create tmpfs
if err := os.MkdirAll(localMountPath, 0700); err != nil {
return fmt.Errorf("error creating secret local mount path: %s", err)
}
if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "nodev"); err != nil {
return fmt.Errorf("unable to setup secret mount: %s", err)
}
for _, s := range c.Secrets {
fPath := filepath.Join(localMountPath, s.Target)
if err := os.MkdirAll(filepath.Dir(fPath), 0700); err != nil {
return fmt.Errorf("error creating secret mount path: %s", err)
}
logrus.Debugf("injecting secret: name=%s path=%s", s.Name, fPath)
if err := ioutil.WriteFile(fPath, s.Data, s.Mode); err != nil {
return fmt.Errorf("error injecting secret: %s", err)
}
if err := os.Chown(fPath, s.Uid, s.Gid); err != nil {
return fmt.Errorf("error setting ownership for secret: %s", err)
}
}
// remount secrets ro
if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "remount,ro"); err != nil {
return fmt.Errorf("unable to remount secret dir as readonly: %s", err)
}
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,23 @@ 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...)
ms = append(ms, c.SecretMounts()...)
sort.Sort(mounts(ms))
if err := setMounts(daemon, &s, c, ms); err != nil {
return nil, fmt.Errorf("linux mounts: %v", err)

22
daemon/secrets.go Normal file
View File

@ -0,0 +1,22 @@
package daemon
import (
"github.com/Sirupsen/logrus"
containertypes "github.com/docker/docker/api/types/container"
)
func (daemon *Daemon) SetContainerSecrets(name string, secrets []*containertypes.ContainerSecret) error {
if !secretsSupported() {
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
}

7
daemon/secrets_linux.go Normal file
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)
}