зеркало из https://github.com/microsoft/docker.git
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:
Родитель
1310dadf4a
Коммит
3716ec25b4
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package container
|
||||
|
||||
import "os"
|
||||
|
||||
type ContainerSecret struct {
|
||||
Name string
|
||||
Target string
|
||||
Data []byte
|
||||
Uid int
|
||||
Gid int
|
||||
Mode os.FileMode
|
||||
}
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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:"-"`
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ func newTestControllerWithMount(m api.Mount) (*controller, error) {
|
|||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func TestControllerValidateMountBind(t *testing.T) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
// +build linux
|
||||
|
||||
package daemon
|
||||
|
||||
func secretsSupported() bool {
|
||||
return true
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
// +build !linux
|
||||
|
||||
package daemon
|
||||
|
||||
func secretsSupported() bool {
|
||||
return false
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче