зеркало из 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
|
RemoveNode(string, bool) error
|
||||||
GetTasks(basictypes.TaskListOptions) ([]types.Task, error)
|
GetTasks(basictypes.TaskListOptions) ([]types.Task, error)
|
||||||
GetTask(string) (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.NewPostRoute("/nodes/{id:.*}/update", sr.updateNode),
|
||||||
router.NewGetRoute("/tasks", sr.getTasks),
|
router.NewGetRoute("/tasks", sr.getTasks),
|
||||||
router.NewGetRoute("/tasks/{id:.*}", sr.getTask),
|
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)
|
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"`
|
StopGracePeriod *time.Duration `json:",omitempty"`
|
||||||
Healthcheck *container.HealthConfig `json:",omitempty"`
|
Healthcheck *container.HealthConfig `json:",omitempty"`
|
||||||
DNSConfig *DNSConfig `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"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/container"
|
"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/mount"
|
||||||
"github.com/docker/docker/api/types/network"
|
"github.com/docker/docker/api/types/network"
|
||||||
"github.com/docker/docker/api/types/registry"
|
"github.com/docker/docker/api/types/registry"
|
||||||
|
@ -509,3 +510,15 @@ type ImagesPruneReport struct {
|
||||||
type NetworksPruneReport struct {
|
type NetworksPruneReport struct {
|
||||||
NetworksDeleted []string
|
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/node"
|
||||||
"github.com/docker/docker/cli/command/plugin"
|
"github.com/docker/docker/cli/command/plugin"
|
||||||
"github.com/docker/docker/cli/command/registry"
|
"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/service"
|
||||||
"github.com/docker/docker/cli/command/stack"
|
"github.com/docker/docker/cli/command/stack"
|
||||||
"github.com/docker/docker/cli/command/swarm"
|
"github.com/docker/docker/cli/command/swarm"
|
||||||
|
@ -25,6 +26,7 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
|
||||||
node.NewNodeCommand(dockerCli),
|
node.NewNodeCommand(dockerCli),
|
||||||
service.NewServiceCommand(dockerCli),
|
service.NewServiceCommand(dockerCli),
|
||||||
swarm.NewSwarmCommand(dockerCli),
|
swarm.NewSwarmCommand(dockerCli),
|
||||||
|
secret.NewSecretCommand(dockerCli),
|
||||||
container.NewContainerCommand(dockerCli),
|
container.NewContainerCommand(dockerCli),
|
||||||
image.NewImageCommand(dockerCli),
|
image.NewImageCommand(dockerCli),
|
||||||
system.NewSystemCommand(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
|
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()
|
ctx := context.Background()
|
||||||
|
|
||||||
// only send auth if flag was set
|
// only send auth if flag was set
|
||||||
|
|
|
@ -191,6 +191,19 @@ func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig {
|
||||||
return nets
|
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 {
|
type endpointOptions struct {
|
||||||
mode string
|
mode string
|
||||||
ports opts.ListOpts
|
ports opts.ListOpts
|
||||||
|
@ -337,6 +350,7 @@ type serviceOptions struct {
|
||||||
logDriver logDriverOptions
|
logDriver logDriverOptions
|
||||||
|
|
||||||
healthcheck healthCheckOptions
|
healthcheck healthCheckOptions
|
||||||
|
secrets []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newServiceOptions() *serviceOptions {
|
func newServiceOptions() *serviceOptions {
|
||||||
|
@ -403,6 +417,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
|
||||||
Options: opts.dnsOptions.GetAll(),
|
Options: opts.dnsOptions.GetAll(),
|
||||||
},
|
},
|
||||||
StopGracePeriod: opts.stopGrace.Value(),
|
StopGracePeriod: opts.stopGrace.Value(),
|
||||||
|
Secrets: convertSecrets(opts.secrets),
|
||||||
},
|
},
|
||||||
Networks: convertNetworks(opts.networks.GetAll()),
|
Networks: convertNetworks(opts.networks.GetAll()),
|
||||||
Resources: opts.resources.ToResourceRequirements(),
|
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.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK")
|
||||||
|
|
||||||
flags.BoolVarP(&opts.tty, flagTTY, "t", false, "Allocate a pseudo-TTY")
|
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 (
|
const (
|
||||||
|
@ -553,4 +569,5 @@ const (
|
||||||
flagHealthRetries = "health-retries"
|
flagHealthRetries = "health-retries"
|
||||||
flagHealthTimeout = "health-timeout"
|
flagHealthTimeout = "health-timeout"
|
||||||
flagNoHealthcheck = "no-healthcheck"
|
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
|
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
|
NetworkAPIClient
|
||||||
ServiceAPIClient
|
ServiceAPIClient
|
||||||
SwarmAPIClient
|
SwarmAPIClient
|
||||||
|
SecretAPIClient
|
||||||
SystemAPIClient
|
SystemAPIClient
|
||||||
VolumeAPIClient
|
VolumeAPIClient
|
||||||
ClientVersion() string
|
ClientVersion() string
|
||||||
|
@ -141,3 +142,11 @@ type VolumeAPIClient interface {
|
||||||
VolumeRemove(ctx context.Context, volumeID string, force bool) error
|
VolumeRemove(ctx context.Context, volumeID string, force bool) error
|
||||||
VolumesPrune(ctx context.Context, cfg types.VolumesPruneConfig) (types.VolumesPruneReport, 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
|
HasBeenStartedBefore bool
|
||||||
HasBeenManuallyStopped bool // used for unless-stopped restart policy
|
HasBeenManuallyStopped bool // used for unless-stopped restart policy
|
||||||
MountPoints map[string]*volume.MountPoint
|
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
|
HostConfig *containertypes.HostConfig `json:"-"` // do not serialize the host config in the json, otherwise we'll make the container unportable
|
||||||
ExecCommands *exec.Store `json:"-"`
|
ExecCommands *exec.Store `json:"-"`
|
||||||
|
Secrets []*containertypes.ContainerSecret `json:"-"` // do not serialize
|
||||||
// logDriver for closing
|
// logDriver for closing
|
||||||
LogDriver logger.Logger `json:"-"`
|
LogDriver logger.Logger `json:"-"`
|
||||||
LogCopier *logger.Copier `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
|
// 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.
|
// Container holds the fields specific to unixen implementations.
|
||||||
// See CommonContainer for standard fields common to all containers.
|
// See CommonContainer for standard fields common to all containers.
|
||||||
|
@ -175,6 +178,10 @@ func (container *Container) NetworkMounts() []Mount {
|
||||||
return mounts
|
return mounts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (container *Container) SecretMountPath() string {
|
||||||
|
return filepath.Join(container.Root, "secrets")
|
||||||
|
}
|
||||||
|
|
||||||
// CopyImagePathContent copies files in destination to the volume.
|
// CopyImagePathContent copies files in destination to the volume.
|
||||||
func (container *Container) CopyImagePathContent(v volume.Volume, destination string) error {
|
func (container *Container) CopyImagePathContent(v volume.Volume, destination string) error {
|
||||||
rootfs, err := symlink.FollowSymlinkInScope(filepath.Join(container.BaseFS, destination), container.BaseFS)
|
rootfs, err := symlink.FollowSymlinkInScope(filepath.Join(container.BaseFS, destination), container.BaseFS)
|
||||||
|
@ -260,6 +267,26 @@ func (container *Container) IpcMounts() []Mount {
|
||||||
return mounts
|
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.
|
// UpdateContainer updates configuration of a container.
|
||||||
func (container *Container) UpdateContainer(hostConfig *containertypes.HostConfig) error {
|
func (container *Container) UpdateContainer(hostConfig *containertypes.HostConfig) error {
|
||||||
container.Lock()
|
container.Lock()
|
||||||
|
|
|
@ -23,6 +23,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec {
|
||||||
User: c.User,
|
User: c.User,
|
||||||
Groups: c.Groups,
|
Groups: c.Groups,
|
||||||
TTY: c.TTY,
|
TTY: c.TTY,
|
||||||
|
Secrets: secretReferencesFromGRPC(c.Secrets),
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.DNSConfig != nil {
|
if c.DNSConfig != nil {
|
||||||
|
@ -75,6 +76,47 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec {
|
||||||
return 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) {
|
func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
|
||||||
containerSpec := &swarmapi.ContainerSpec{
|
containerSpec := &swarmapi.ContainerSpec{
|
||||||
Image: c.Image,
|
Image: c.Image,
|
||||||
|
@ -87,6 +129,7 @@ func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
|
||||||
User: c.User,
|
User: c.User,
|
||||||
Groups: c.Groups,
|
Groups: c.Groups,
|
||||||
TTY: c.TTY,
|
TTY: c.TTY,
|
||||||
|
Secrets: secretReferencesToGRPC(c.Secrets),
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.DNSConfig != nil {
|
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
|
ContainerWaitWithContext(ctx context.Context, name string) error
|
||||||
ContainerRm(name string, config *types.ContainerRmConfig) error
|
ContainerRm(name string, config *types.ContainerRmConfig) error
|
||||||
ContainerKill(name string, sig uint64) error
|
ContainerKill(name string, sig uint64) error
|
||||||
|
SetContainerSecrets(name string, secrets []*container.ContainerSecret) error
|
||||||
SystemInfo() (*types.Info, error)
|
SystemInfo() (*types.Info, error)
|
||||||
VolumeCreate(name, driverName string, opts, labels map[string]string) (*types.Volume, error)
|
VolumeCreate(name, driverName string, opts, labels map[string]string) (*types.Volume, error)
|
||||||
Containers(config *types.ContainerListOptions) ([]*types.Container, error)
|
Containers(config *types.ContainerListOptions) ([]*types.Container, error)
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"github.com/docker/docker/api/types/versions"
|
"github.com/docker/docker/api/types/versions"
|
||||||
executorpkg "github.com/docker/docker/daemon/cluster/executor"
|
executorpkg "github.com/docker/docker/daemon/cluster/executor"
|
||||||
"github.com/docker/libnetwork"
|
"github.com/docker/libnetwork"
|
||||||
|
"github.com/docker/swarmkit/agent/exec"
|
||||||
"github.com/docker/swarmkit/api"
|
"github.com/docker/swarmkit/api"
|
||||||
"github.com/docker/swarmkit/log"
|
"github.com/docker/swarmkit/log"
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
|
@ -29,9 +30,10 @@ import (
|
||||||
type containerAdapter struct {
|
type containerAdapter struct {
|
||||||
backend executorpkg.Backend
|
backend executorpkg.Backend
|
||||||
container *containerConfig
|
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)
|
ctnr, err := newContainerConfig(task)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -40,6 +42,7 @@ func newContainerAdapter(b executorpkg.Backend, task *api.Task) (*containerAdapt
|
||||||
return &containerAdapter{
|
return &containerAdapter{
|
||||||
container: ctnr,
|
container: ctnr,
|
||||||
backend: b,
|
backend: b,
|
||||||
|
secrets: secrets,
|
||||||
}, nil
|
}, 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 {
|
if err := c.backend.UpdateContainerServiceConfig(cr.ID, c.container.serviceConfig()); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
executorpkg "github.com/docker/docker/daemon/cluster/executor"
|
executorpkg "github.com/docker/docker/daemon/cluster/executor"
|
||||||
"github.com/docker/swarmkit/api"
|
"github.com/docker/swarmkit/api"
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
|
"src/github.com/docker/swarmkit/agent/exec"
|
||||||
)
|
)
|
||||||
|
|
||||||
// networkAttacherController implements agent.Controller against docker's API.
|
// networkAttacherController implements agent.Controller against docker's API.
|
||||||
|
@ -19,8 +20,8 @@ type networkAttacherController struct {
|
||||||
closed chan struct{}
|
closed chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newNetworkAttacherController(b executorpkg.Backend, task *api.Task) (*networkAttacherController, error) {
|
func newNetworkAttacherController(b executorpkg.Backend, task *api.Task, secrets exec.SecretProvider) (*networkAttacherController, error) {
|
||||||
adapter, err := newContainerAdapter(b, task)
|
adapter, err := newContainerAdapter(b, task, secrets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,8 +33,8 @@ type controller struct {
|
||||||
var _ exec.Controller = &controller{}
|
var _ exec.Controller = &controller{}
|
||||||
|
|
||||||
// NewController returns a docker exec runner for the provided task.
|
// NewController returns a docker exec runner for the provided task.
|
||||||
func newController(b executorpkg.Backend, task *api.Task) (*controller, error) {
|
func newController(b executorpkg.Backend, task *api.Task, secrets exec.SecretProvider) (*controller, error) {
|
||||||
adapter, err := newContainerAdapter(b, task)
|
adapter, err := newContainerAdapter(b, task, secrets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,10 @@ type executor struct {
|
||||||
backend executorpkg.Backend
|
backend executorpkg.Backend
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type secretProvider interface {
|
||||||
|
Get(secretID string) *api.Secret
|
||||||
|
}
|
||||||
|
|
||||||
// NewExecutor returns an executor from the docker client.
|
// NewExecutor returns an executor from the docker client.
|
||||||
func NewExecutor(b executorpkg.Backend) exec.Executor {
|
func NewExecutor(b executorpkg.Backend) exec.Executor {
|
||||||
return &executor{
|
return &executor{
|
||||||
|
@ -120,12 +124,12 @@ func (e *executor) Configure(ctx context.Context, node *api.Node) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Controller returns a docker container runner.
|
// 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 {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@ func TestHealthStates(t *testing.T) {
|
||||||
EventsService: e,
|
EventsService: e,
|
||||||
}
|
}
|
||||||
|
|
||||||
controller, err := newController(daemon, task)
|
controller, err := newController(daemon, task, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("create controller fail %v", err)
|
t.Fatalf("create controller fail %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ func newTestControllerWithMount(m api.Mount) (*controller, error) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestControllerValidateMountBind(t *testing.T) {
|
func TestControllerValidateMountBind(t *testing.T) {
|
||||||
|
|
|
@ -96,3 +96,21 @@ func newListTasksFilters(filter filters.Args, transformFunc func(filters.Args) e
|
||||||
|
|
||||||
return f, nil
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -18,6 +19,7 @@ import (
|
||||||
"github.com/docker/docker/pkg/idtools"
|
"github.com/docker/docker/pkg/idtools"
|
||||||
"github.com/docker/docker/pkg/stringid"
|
"github.com/docker/docker/pkg/stringid"
|
||||||
"github.com/docker/docker/runconfig"
|
"github.com/docker/docker/runconfig"
|
||||||
|
"github.com/docker/engine-api/types/mount"
|
||||||
"github.com/docker/libnetwork"
|
"github.com/docker/libnetwork"
|
||||||
"github.com/opencontainers/runc/libcontainer/configs"
|
"github.com/opencontainers/runc/libcontainer/configs"
|
||||||
"github.com/opencontainers/runc/libcontainer/devices"
|
"github.com/opencontainers/runc/libcontainer/devices"
|
||||||
|
@ -139,6 +141,43 @@ func (daemon *Daemon) setupIpcDirs(c *container.Container) error {
|
||||||
|
|
||||||
return nil
|
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 {
|
func killProcessDirectly(container *container.Container) error {
|
||||||
if _, err := container.WaitStop(10 * time.Second); err != nil {
|
if _, err := container.WaitStop(10 * time.Second); err != nil {
|
||||||
// Ensure that we don't kill ourselves
|
// 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)
|
logrus.Errorf("Error unmounting container %s: %s", container.ID, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -702,16 +702,23 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := daemon.setupSecretDir(c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
ms, err := daemon.setupMounts(c)
|
ms, err := daemon.setupMounts(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ms = append(ms, c.IpcMounts()...)
|
ms = append(ms, c.IpcMounts()...)
|
||||||
|
|
||||||
tmpfsMounts, err := c.TmpfsMounts()
|
tmpfsMounts, err := c.TmpfsMounts()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ms = append(ms, tmpfsMounts...)
|
ms = append(ms, tmpfsMounts...)
|
||||||
|
|
||||||
|
ms = append(ms, c.SecretMounts()...)
|
||||||
sort.Sort(mounts(ms))
|
sort.Sort(mounts(ms))
|
||||||
if err := setMounts(daemon, &s, c, ms); err != nil {
|
if err := setMounts(daemon, &s, c, ms); err != nil {
|
||||||
return nil, fmt.Errorf("linux mounts: %v", err)
|
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() {
|
for _, eConfig := range container.ExecCommands.Commands() {
|
||||||
daemon.unregisterExecCommand(container, eConfig)
|
daemon.unregisterExecCommand(container, eConfig)
|
||||||
}
|
}
|
||||||
|
|
Загрузка…
Ссылка в новой задаче