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>
| ... | ... |
@@ -23,4 +23,9 @@ type Backend interface {
|
| 23 | 23 |
RemoveNode(string, bool) error |
| 24 | 24 |
GetTasks(basictypes.TaskListOptions) ([]types.Task, error) |
| 25 | 25 |
GetTask(string) (types.Task, error) |
| 26 |
+ GetSecrets(opts basictypes.SecretListOptions) ([]types.Secret, error) |
|
| 27 |
+ CreateSecret(s types.SecretSpec) (string, error) |
|
| 28 |
+ RemoveSecret(id string) error |
|
| 29 |
+ GetSecret(id string) (types.Secret, error) |
|
| 30 |
+ UpdateSecret(id string, version uint64, spec types.SecretSpec) error |
|
| 26 | 31 |
} |
| ... | ... |
@@ -40,5 +40,10 @@ func (sr *swarmRouter) initRoutes() {
|
| 40 | 40 |
router.NewPostRoute("/nodes/{id:.*}/update", sr.updateNode),
|
| 41 | 41 |
router.NewGetRoute("/tasks", sr.getTasks),
|
| 42 | 42 |
router.NewGetRoute("/tasks/{id:.*}", sr.getTask),
|
| 43 |
+ router.NewGetRoute("/secrets", sr.getSecrets),
|
|
| 44 |
+ router.NewPostRoute("/secrets/create", sr.createSecret),
|
|
| 45 |
+ router.NewDeleteRoute("/secrets/{id:.*}", sr.removeSecret),
|
|
| 46 |
+ router.NewGetRoute("/secrets/{id:.*}", sr.getSecret),
|
|
| 47 |
+ router.NewPostRoute("/secrets/{id:.*}/update", sr.updateSecret),
|
|
| 43 | 48 |
} |
| 44 | 49 |
} |
| ... | ... |
@@ -261,3 +261,77 @@ func (sr *swarmRouter) getTask(ctx context.Context, w http.ResponseWriter, r *ht |
| 261 | 261 |
|
| 262 | 262 |
return httputils.WriteJSON(w, http.StatusOK, task) |
| 263 | 263 |
} |
| 264 |
+ |
|
| 265 |
+func (sr *swarmRouter) getSecrets(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
| 266 |
+ if err := httputils.ParseForm(r); err != nil {
|
|
| 267 |
+ return err |
|
| 268 |
+ } |
|
| 269 |
+ filter, err := filters.FromParam(r.Form.Get("filters"))
|
|
| 270 |
+ if err != nil {
|
|
| 271 |
+ return err |
|
| 272 |
+ } |
|
| 273 |
+ |
|
| 274 |
+ secrets, err := sr.backend.GetSecrets(basictypes.SecretListOptions{Filter: filter})
|
|
| 275 |
+ if err != nil {
|
|
| 276 |
+ logrus.Errorf("Error getting secrets: %v", err)
|
|
| 277 |
+ return err |
|
| 278 |
+ } |
|
| 279 |
+ |
|
| 280 |
+ return httputils.WriteJSON(w, http.StatusOK, secrets) |
|
| 281 |
+} |
|
| 282 |
+ |
|
| 283 |
+func (sr *swarmRouter) createSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
| 284 |
+ var secret types.SecretSpec |
|
| 285 |
+ if err := json.NewDecoder(r.Body).Decode(&secret); err != nil {
|
|
| 286 |
+ return err |
|
| 287 |
+ } |
|
| 288 |
+ |
|
| 289 |
+ id, err := sr.backend.CreateSecret(secret) |
|
| 290 |
+ if err != nil {
|
|
| 291 |
+ logrus.Errorf("Error creating secret %s: %v", id, err)
|
|
| 292 |
+ return err |
|
| 293 |
+ } |
|
| 294 |
+ |
|
| 295 |
+ return httputils.WriteJSON(w, http.StatusCreated, &basictypes.SecretCreateResponse{
|
|
| 296 |
+ ID: id, |
|
| 297 |
+ }) |
|
| 298 |
+} |
|
| 299 |
+ |
|
| 300 |
+func (sr *swarmRouter) removeSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
| 301 |
+ if err := sr.backend.RemoveSecret(vars["id"]); err != nil {
|
|
| 302 |
+ logrus.Errorf("Error removing secret %s: %v", vars["id"], err)
|
|
| 303 |
+ return err |
|
| 304 |
+ } |
|
| 305 |
+ |
|
| 306 |
+ return nil |
|
| 307 |
+} |
|
| 308 |
+ |
|
| 309 |
+func (sr *swarmRouter) getSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
| 310 |
+ secret, err := sr.backend.GetSecret(vars["id"]) |
|
| 311 |
+ if err != nil {
|
|
| 312 |
+ logrus.Errorf("Error getting secret %s: %v", vars["id"], err)
|
|
| 313 |
+ return err |
|
| 314 |
+ } |
|
| 315 |
+ |
|
| 316 |
+ return httputils.WriteJSON(w, http.StatusOK, secret) |
|
| 317 |
+} |
|
| 318 |
+ |
|
| 319 |
+func (sr *swarmRouter) updateSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
| 320 |
+ var secret types.SecretSpec |
|
| 321 |
+ if err := json.NewDecoder(r.Body).Decode(&secret); err != nil {
|
|
| 322 |
+ return err |
|
| 323 |
+ } |
|
| 324 |
+ |
|
| 325 |
+ rawVersion := r.URL.Query().Get("version")
|
|
| 326 |
+ version, err := strconv.ParseUint(rawVersion, 10, 64) |
|
| 327 |
+ if err != nil {
|
|
| 328 |
+ return fmt.Errorf("Invalid secret version '%s': %s", rawVersion, err.Error())
|
|
| 329 |
+ } |
|
| 330 |
+ |
|
| 331 |
+ id := vars["id"] |
|
| 332 |
+ if err := sr.backend.UpdateSecret(id, version, secret); err != nil {
|
|
| 333 |
+ return fmt.Errorf("Error updating secret: %s", err)
|
|
| 334 |
+ } |
|
| 335 |
+ |
|
| 336 |
+ return nil |
|
| 337 |
+} |
| 41 | 42 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,30 @@ |
| 0 |
+package swarm |
|
| 1 |
+ |
|
| 2 |
+// Secret represents a secret. |
|
| 3 |
+type Secret struct {
|
|
| 4 |
+ ID string |
|
| 5 |
+ Meta |
|
| 6 |
+ Spec *SecretSpec `json:",omitempty"` |
|
| 7 |
+ Digest string `json:",omitempty"` |
|
| 8 |
+ SecretSize int64 `json:",omitempty"` |
|
| 9 |
+} |
|
| 10 |
+ |
|
| 11 |
+type SecretSpec struct {
|
|
| 12 |
+ Annotations |
|
| 13 |
+ Data []byte `json",omitempty"` |
|
| 14 |
+} |
|
| 15 |
+ |
|
| 16 |
+type SecretReferenceMode int |
|
| 17 |
+ |
|
| 18 |
+const ( |
|
| 19 |
+ SecretReferenceSystem SecretReferenceMode = 0 |
|
| 20 |
+ SecretReferenceFile SecretReferenceMode = 1 |
|
| 21 |
+ SecretReferenceEnv SecretReferenceMode = 2 |
|
| 22 |
+) |
|
| 23 |
+ |
|
| 24 |
+type SecretReference struct {
|
|
| 25 |
+ SecretID string `json:",omitempty"` |
|
| 26 |
+ Mode SecretReferenceMode `json:",omitempty"` |
|
| 27 |
+ Target string `json:",omitempty"` |
|
| 28 |
+ SecretName string `json:",omitempty"` |
|
| 29 |
+} |
| ... | ... |
@@ -6,6 +6,7 @@ import ( |
| 6 | 6 |
"time" |
| 7 | 7 |
|
| 8 | 8 |
"github.com/docker/docker/api/types/container" |
| 9 |
+ "github.com/docker/docker/api/types/filters" |
|
| 9 | 10 |
"github.com/docker/docker/api/types/mount" |
| 10 | 11 |
"github.com/docker/docker/api/types/network" |
| 11 | 12 |
"github.com/docker/docker/api/types/registry" |
| ... | ... |
@@ -509,3 +510,15 @@ type ImagesPruneReport struct {
|
| 509 | 509 |
type NetworksPruneReport struct {
|
| 510 | 510 |
NetworksDeleted []string |
| 511 | 511 |
} |
| 512 |
+ |
|
| 513 |
+// SecretCreateResponse contains the information returned to a client |
|
| 514 |
+// on the creation of a new secret. |
|
| 515 |
+type SecretCreateResponse struct {
|
|
| 516 |
+ // ID is the id of the created secret. |
|
| 517 |
+ ID string |
|
| 518 |
+} |
|
| 519 |
+ |
|
| 520 |
+// SecretListOptions holds parameters to list secrets |
|
| 521 |
+type SecretListOptions struct {
|
|
| 522 |
+ Filter filters.Args |
|
| 523 |
+} |
| ... | ... |
@@ -11,6 +11,7 @@ import ( |
| 11 | 11 |
"github.com/docker/docker/cli/command/node" |
| 12 | 12 |
"github.com/docker/docker/cli/command/plugin" |
| 13 | 13 |
"github.com/docker/docker/cli/command/registry" |
| 14 |
+ "github.com/docker/docker/cli/command/secret" |
|
| 14 | 15 |
"github.com/docker/docker/cli/command/service" |
| 15 | 16 |
"github.com/docker/docker/cli/command/stack" |
| 16 | 17 |
"github.com/docker/docker/cli/command/swarm" |
| ... | ... |
@@ -25,6 +26,7 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
|
| 25 | 25 |
node.NewNodeCommand(dockerCli), |
| 26 | 26 |
service.NewServiceCommand(dockerCli), |
| 27 | 27 |
swarm.NewSwarmCommand(dockerCli), |
| 28 |
+ secret.NewSecretCommand(dockerCli), |
|
| 28 | 29 |
container.NewContainerCommand(dockerCli), |
| 29 | 30 |
image.NewImageCommand(dockerCli), |
| 30 | 31 |
system.NewSystemCommand(dockerCli), |
| 31 | 32 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,29 @@ |
| 0 |
+package secret |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/spf13/cobra" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/docker/cli" |
|
| 8 |
+ "github.com/docker/docker/cli/command" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// NewSecretCommand returns a cobra command for `secret` subcommands |
|
| 12 |
+func NewSecretCommand(dockerCli *command.DockerCli) *cobra.Command {
|
|
| 13 |
+ cmd := &cobra.Command{
|
|
| 14 |
+ Use: "secret", |
|
| 15 |
+ Short: "Manage Docker secrets", |
|
| 16 |
+ Args: cli.NoArgs, |
|
| 17 |
+ Run: func(cmd *cobra.Command, args []string) {
|
|
| 18 |
+ fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) |
|
| 19 |
+ }, |
|
| 20 |
+ } |
|
| 21 |
+ cmd.AddCommand( |
|
| 22 |
+ newSecretListCommand(dockerCli), |
|
| 23 |
+ newSecretCreateCommand(dockerCli), |
|
| 24 |
+ newSecretInspectCommand(dockerCli), |
|
| 25 |
+ newSecretRemoveCommand(dockerCli), |
|
| 26 |
+ ) |
|
| 27 |
+ return cmd |
|
| 28 |
+} |
| 0 | 29 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,57 @@ |
| 0 |
+package secret |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "io/ioutil" |
|
| 6 |
+ "os" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/docker/docker/api/types/swarm" |
|
| 9 |
+ "github.com/docker/docker/cli" |
|
| 10 |
+ "github.com/docker/docker/cli/command" |
|
| 11 |
+ "github.com/spf13/cobra" |
|
| 12 |
+) |
|
| 13 |
+ |
|
| 14 |
+type createOptions struct {
|
|
| 15 |
+ name string |
|
| 16 |
+} |
|
| 17 |
+ |
|
| 18 |
+func newSecretCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
|
| 19 |
+ return &cobra.Command{
|
|
| 20 |
+ Use: "create [name]", |
|
| 21 |
+ Short: "Create a secret using stdin as content", |
|
| 22 |
+ Args: cli.ExactArgs(1), |
|
| 23 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 24 |
+ opts := createOptions{
|
|
| 25 |
+ name: args[0], |
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ return runSecretCreate(dockerCli, opts) |
|
| 29 |
+ }, |
|
| 30 |
+ } |
|
| 31 |
+} |
|
| 32 |
+ |
|
| 33 |
+func runSecretCreate(dockerCli *command.DockerCli, opts createOptions) error {
|
|
| 34 |
+ client := dockerCli.Client() |
|
| 35 |
+ ctx := context.Background() |
|
| 36 |
+ |
|
| 37 |
+ secretData, err := ioutil.ReadAll(os.Stdin) |
|
| 38 |
+ if err != nil {
|
|
| 39 |
+ return fmt.Errorf("Error reading content from STDIN: %v", err)
|
|
| 40 |
+ } |
|
| 41 |
+ |
|
| 42 |
+ spec := swarm.SecretSpec{
|
|
| 43 |
+ Annotations: swarm.Annotations{
|
|
| 44 |
+ Name: opts.name, |
|
| 45 |
+ }, |
|
| 46 |
+ Data: secretData, |
|
| 47 |
+ } |
|
| 48 |
+ |
|
| 49 |
+ r, err := client.SecretCreate(ctx, spec) |
|
| 50 |
+ if err != nil {
|
|
| 51 |
+ return err |
|
| 52 |
+ } |
|
| 53 |
+ |
|
| 54 |
+ fmt.Fprintln(dockerCli.Out(), r.ID) |
|
| 55 |
+ return nil |
|
| 56 |
+} |
| 0 | 57 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,42 @@ |
| 0 |
+package secret |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/docker/docker/cli" |
|
| 6 |
+ "github.com/docker/docker/cli/command" |
|
| 7 |
+ "github.com/docker/docker/cli/command/inspect" |
|
| 8 |
+ "github.com/spf13/cobra" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+type inspectOptions struct {
|
|
| 12 |
+ name string |
|
| 13 |
+ format string |
|
| 14 |
+} |
|
| 15 |
+ |
|
| 16 |
+func newSecretInspectCommand(dockerCli *command.DockerCli) *cobra.Command {
|
|
| 17 |
+ opts := inspectOptions{}
|
|
| 18 |
+ cmd := &cobra.Command{
|
|
| 19 |
+ Use: "inspect [name]", |
|
| 20 |
+ Short: "Inspect a secret", |
|
| 21 |
+ Args: cli.ExactArgs(1), |
|
| 22 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 23 |
+ opts.name = args[0] |
|
| 24 |
+ return runSecretInspect(dockerCli, opts) |
|
| 25 |
+ }, |
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") |
|
| 29 |
+ return cmd |
|
| 30 |
+} |
|
| 31 |
+ |
|
| 32 |
+func runSecretInspect(dockerCli *command.DockerCli, opts inspectOptions) error {
|
|
| 33 |
+ client := dockerCli.Client() |
|
| 34 |
+ ctx := context.Background() |
|
| 35 |
+ |
|
| 36 |
+ getRef := func(name string) (interface{}, []byte, error) {
|
|
| 37 |
+ return client.SecretInspectWithRaw(ctx, name) |
|
| 38 |
+ } |
|
| 39 |
+ |
|
| 40 |
+ return inspect.Inspect(dockerCli.Out(), []string{opts.name}, opts.format, getRef)
|
|
| 41 |
+} |
| 0 | 42 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,62 @@ |
| 0 |
+package secret |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "text/tabwriter" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/docker/api/types" |
|
| 8 |
+ "github.com/docker/docker/cli" |
|
| 9 |
+ "github.com/docker/docker/cli/command" |
|
| 10 |
+ "github.com/spf13/cobra" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+type listOptions struct {
|
|
| 14 |
+ quiet bool |
|
| 15 |
+} |
|
| 16 |
+ |
|
| 17 |
+func newSecretListCommand(dockerCli *command.DockerCli) *cobra.Command {
|
|
| 18 |
+ opts := listOptions{}
|
|
| 19 |
+ |
|
| 20 |
+ cmd := &cobra.Command{
|
|
| 21 |
+ Use: "ls", |
|
| 22 |
+ Short: "List secrets", |
|
| 23 |
+ Args: cli.NoArgs, |
|
| 24 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 25 |
+ return runSecretList(dockerCli, opts) |
|
| 26 |
+ }, |
|
| 27 |
+ } |
|
| 28 |
+ |
|
| 29 |
+ flags := cmd.Flags() |
|
| 30 |
+ flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") |
|
| 31 |
+ |
|
| 32 |
+ return cmd |
|
| 33 |
+} |
|
| 34 |
+ |
|
| 35 |
+func runSecretList(dockerCli *command.DockerCli, opts listOptions) error {
|
|
| 36 |
+ client := dockerCli.Client() |
|
| 37 |
+ ctx := context.Background() |
|
| 38 |
+ |
|
| 39 |
+ secrets, err := client.SecretList(ctx, types.SecretListOptions{})
|
|
| 40 |
+ if err != nil {
|
|
| 41 |
+ return err |
|
| 42 |
+ } |
|
| 43 |
+ |
|
| 44 |
+ w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) |
|
| 45 |
+ if opts.quiet {
|
|
| 46 |
+ for _, s := range secrets {
|
|
| 47 |
+ fmt.Fprintf(w, "%s\n", s.ID) |
|
| 48 |
+ } |
|
| 49 |
+ } else {
|
|
| 50 |
+ fmt.Fprintf(w, "ID\tNAME\tCREATED\tUPDATED\tSIZE") |
|
| 51 |
+ fmt.Fprintf(w, "\n") |
|
| 52 |
+ |
|
| 53 |
+ for _, s := range secrets {
|
|
| 54 |
+ 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) |
|
| 55 |
+ } |
|
| 56 |
+ } |
|
| 57 |
+ |
|
| 58 |
+ w.Flush() |
|
| 59 |
+ |
|
| 60 |
+ return nil |
|
| 61 |
+} |
| 0 | 62 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,43 @@ |
| 0 |
+package secret |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/docker/docker/cli" |
|
| 7 |
+ "github.com/docker/docker/cli/command" |
|
| 8 |
+ "github.com/spf13/cobra" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+type removeOptions struct {
|
|
| 12 |
+ ids []string |
|
| 13 |
+} |
|
| 14 |
+ |
|
| 15 |
+func newSecretRemoveCommand(dockerCli *command.DockerCli) *cobra.Command {
|
|
| 16 |
+ return &cobra.Command{
|
|
| 17 |
+ Use: "rm [id]", |
|
| 18 |
+ Short: "Remove a secret", |
|
| 19 |
+ Args: cli.RequiresMinArgs(1), |
|
| 20 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 21 |
+ opts := removeOptions{
|
|
| 22 |
+ ids: args, |
|
| 23 |
+ } |
|
| 24 |
+ return runSecretRemove(dockerCli, opts) |
|
| 25 |
+ }, |
|
| 26 |
+ } |
|
| 27 |
+} |
|
| 28 |
+ |
|
| 29 |
+func runSecretRemove(dockerCli *command.DockerCli, opts removeOptions) error {
|
|
| 30 |
+ client := dockerCli.Client() |
|
| 31 |
+ ctx := context.Background() |
|
| 32 |
+ |
|
| 33 |
+ for _, id := range opts.ids {
|
|
| 34 |
+ if err := client.SecretRemove(ctx, id); err != nil {
|
|
| 35 |
+ return err |
|
| 36 |
+ } |
|
| 37 |
+ |
|
| 38 |
+ fmt.Fprintln(dockerCli.Out(), id) |
|
| 39 |
+ } |
|
| 40 |
+ |
|
| 41 |
+ return nil |
|
| 42 |
+} |
| ... | ... |
@@ -58,6 +58,13 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error {
|
| 58 | 58 |
return err |
| 59 | 59 |
} |
| 60 | 60 |
|
| 61 |
+ // parse and validate secrets |
|
| 62 |
+ secrets, err := parseSecrets(apiClient, opts.secrets) |
|
| 63 |
+ if err != nil {
|
|
| 64 |
+ return err |
|
| 65 |
+ } |
|
| 66 |
+ service.TaskTemplate.ContainerSpec.Secrets = secrets |
|
| 67 |
+ |
|
| 61 | 68 |
ctx := context.Background() |
| 62 | 69 |
|
| 63 | 70 |
// only send auth if flag was set |
| ... | ... |
@@ -191,6 +191,19 @@ func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig {
|
| 191 | 191 |
return nets |
| 192 | 192 |
} |
| 193 | 193 |
|
| 194 |
+func convertSecrets(secrets []string) []*swarm.SecretReference {
|
|
| 195 |
+ sec := []*swarm.SecretReference{}
|
|
| 196 |
+ for _, s := range secrets {
|
|
| 197 |
+ sec = append(sec, &swarm.SecretReference{
|
|
| 198 |
+ SecretID: s, |
|
| 199 |
+ Mode: swarm.SecretReferenceFile, |
|
| 200 |
+ Target: "", |
|
| 201 |
+ }) |
|
| 202 |
+ } |
|
| 203 |
+ |
|
| 204 |
+ return sec |
|
| 205 |
+} |
|
| 206 |
+ |
|
| 194 | 207 |
type endpointOptions struct {
|
| 195 | 208 |
mode string |
| 196 | 209 |
ports opts.ListOpts |
| ... | ... |
@@ -337,6 +350,7 @@ type serviceOptions struct {
|
| 337 | 337 |
logDriver logDriverOptions |
| 338 | 338 |
|
| 339 | 339 |
healthcheck healthCheckOptions |
| 340 |
+ secrets []string |
|
| 340 | 341 |
} |
| 341 | 342 |
|
| 342 | 343 |
func newServiceOptions() *serviceOptions {
|
| ... | ... |
@@ -403,6 +417,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
|
| 403 | 403 |
Options: opts.dnsOptions.GetAll(), |
| 404 | 404 |
}, |
| 405 | 405 |
StopGracePeriod: opts.stopGrace.Value(), |
| 406 |
+ Secrets: convertSecrets(opts.secrets), |
|
| 406 | 407 |
}, |
| 407 | 408 |
Networks: convertNetworks(opts.networks.GetAll()), |
| 408 | 409 |
Resources: opts.resources.ToResourceRequirements(), |
| ... | ... |
@@ -488,6 +503,7 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) {
|
| 488 | 488 |
flags.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK") |
| 489 | 489 |
|
| 490 | 490 |
flags.BoolVarP(&opts.tty, flagTTY, "t", false, "Allocate a pseudo-TTY") |
| 491 |
+ flags.StringSliceVar(&opts.secrets, flagSecret, []string{}, "Specify secrets to expose to the service")
|
|
| 491 | 492 |
} |
| 492 | 493 |
|
| 493 | 494 |
const ( |
| ... | ... |
@@ -553,4 +569,5 @@ const ( |
| 553 | 553 |
flagHealthRetries = "health-retries" |
| 554 | 554 |
flagHealthTimeout = "health-timeout" |
| 555 | 555 |
flagNoHealthcheck = "no-healthcheck" |
| 556 |
+ flagSecret = "secret" |
|
| 556 | 557 |
) |
| 557 | 558 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,92 @@ |
| 0 |
+package service |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "strings" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/docker/api/types" |
|
| 8 |
+ "github.com/docker/docker/api/types/filters" |
|
| 9 |
+ swarmtypes "github.com/docker/docker/api/types/swarm" |
|
| 10 |
+ "github.com/docker/docker/client" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+// parseSecretString parses the requested secret and returns the secret name |
|
| 14 |
+// and target. Expects format SECRET_NAME:TARGET |
|
| 15 |
+func parseSecretString(secretString string) (string, string, error) {
|
|
| 16 |
+ tokens := strings.Split(secretString, ":") |
|
| 17 |
+ |
|
| 18 |
+ secretName := strings.TrimSpace(tokens[0]) |
|
| 19 |
+ targetName := "" |
|
| 20 |
+ |
|
| 21 |
+ if secretName == "" {
|
|
| 22 |
+ return "", "", fmt.Errorf("invalid secret name provided")
|
|
| 23 |
+ } |
|
| 24 |
+ |
|
| 25 |
+ if len(tokens) > 1 {
|
|
| 26 |
+ targetName = strings.TrimSpace(tokens[1]) |
|
| 27 |
+ if targetName == "" {
|
|
| 28 |
+ return "", "", fmt.Errorf("invalid presentation name provided")
|
|
| 29 |
+ } |
|
| 30 |
+ } else {
|
|
| 31 |
+ targetName = secretName |
|
| 32 |
+ } |
|
| 33 |
+ return secretName, targetName, nil |
|
| 34 |
+} |
|
| 35 |
+ |
|
| 36 |
+// parseSecrets retrieves the secrets from the requested names and converts |
|
| 37 |
+// them to secret references to use with the spec |
|
| 38 |
+func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmtypes.SecretReference, error) {
|
|
| 39 |
+ lookupSecretNames := []string{}
|
|
| 40 |
+ needSecrets := make(map[string]*swarmtypes.SecretReference) |
|
| 41 |
+ ctx := context.Background() |
|
| 42 |
+ |
|
| 43 |
+ for _, secret := range requestedSecrets {
|
|
| 44 |
+ n, t, err := parseSecretString(secret) |
|
| 45 |
+ if err != nil {
|
|
| 46 |
+ return nil, err |
|
| 47 |
+ } |
|
| 48 |
+ |
|
| 49 |
+ secretRef := &swarmtypes.SecretReference{
|
|
| 50 |
+ SecretName: n, |
|
| 51 |
+ Mode: swarmtypes.SecretReferenceFile, |
|
| 52 |
+ Target: t, |
|
| 53 |
+ } |
|
| 54 |
+ |
|
| 55 |
+ lookupSecretNames = append(lookupSecretNames, n) |
|
| 56 |
+ needSecrets[n] = secretRef |
|
| 57 |
+ } |
|
| 58 |
+ |
|
| 59 |
+ args := filters.NewArgs() |
|
| 60 |
+ for _, s := range lookupSecretNames {
|
|
| 61 |
+ args.Add("names", s)
|
|
| 62 |
+ } |
|
| 63 |
+ |
|
| 64 |
+ secrets, err := client.SecretList(ctx, types.SecretListOptions{
|
|
| 65 |
+ Filter: args, |
|
| 66 |
+ }) |
|
| 67 |
+ if err != nil {
|
|
| 68 |
+ return nil, err |
|
| 69 |
+ } |
|
| 70 |
+ |
|
| 71 |
+ foundSecrets := make(map[string]*swarmtypes.Secret) |
|
| 72 |
+ for _, secret := range secrets {
|
|
| 73 |
+ foundSecrets[secret.Spec.Annotations.Name] = &secret |
|
| 74 |
+ } |
|
| 75 |
+ |
|
| 76 |
+ addedSecrets := []*swarmtypes.SecretReference{}
|
|
| 77 |
+ |
|
| 78 |
+ for secretName, secretRef := range needSecrets {
|
|
| 79 |
+ s, ok := foundSecrets[secretName] |
|
| 80 |
+ if !ok {
|
|
| 81 |
+ return nil, fmt.Errorf("secret not found: %s", secretName)
|
|
| 82 |
+ } |
|
| 83 |
+ |
|
| 84 |
+ // set the id for the ref to properly assign in swarm |
|
| 85 |
+ // since swarm needs the ID instead of the name |
|
| 86 |
+ secretRef.SecretID = s.ID |
|
| 87 |
+ addedSecrets = append(addedSecrets, secretRef) |
|
| 88 |
+ } |
|
| 89 |
+ |
|
| 90 |
+ return addedSecrets, nil |
|
| 91 |
+} |
| ... | ... |
@@ -217,3 +217,25 @@ func (cli *Client) NewVersionError(APIrequired, feature string) error {
|
| 217 | 217 |
} |
| 218 | 218 |
return nil |
| 219 | 219 |
} |
| 220 |
+ |
|
| 221 |
+// secretNotFoundError implements an error returned when a secret is not found. |
|
| 222 |
+type secretNotFoundError struct {
|
|
| 223 |
+ name string |
|
| 224 |
+} |
|
| 225 |
+ |
|
| 226 |
+// Error returns a string representation of a secretNotFoundError |
|
| 227 |
+func (e secretNotFoundError) Error() string {
|
|
| 228 |
+ return fmt.Sprintf("Error: No such secret: %s", e.name)
|
|
| 229 |
+} |
|
| 230 |
+ |
|
| 231 |
+// NoFound indicates that this error type is of NotFound |
|
| 232 |
+func (e secretNotFoundError) NotFound() bool {
|
|
| 233 |
+ return true |
|
| 234 |
+} |
|
| 235 |
+ |
|
| 236 |
+// IsErrSecretNotFound returns true if the error is caused |
|
| 237 |
+// when a secret is not found. |
|
| 238 |
+func IsErrSecretNotFound(err error) bool {
|
|
| 239 |
+ _, ok := err.(secretNotFoundError) |
|
| 240 |
+ return ok |
|
| 241 |
+} |
| ... | ... |
@@ -23,6 +23,7 @@ type CommonAPIClient interface {
|
| 23 | 23 |
NetworkAPIClient |
| 24 | 24 |
ServiceAPIClient |
| 25 | 25 |
SwarmAPIClient |
| 26 |
+ SecretAPIClient |
|
| 26 | 27 |
SystemAPIClient |
| 27 | 28 |
VolumeAPIClient |
| 28 | 29 |
ClientVersion() string |
| ... | ... |
@@ -141,3 +142,11 @@ type VolumeAPIClient interface {
|
| 141 | 141 |
VolumeRemove(ctx context.Context, volumeID string, force bool) error |
| 142 | 142 |
VolumesPrune(ctx context.Context, cfg types.VolumesPruneConfig) (types.VolumesPruneReport, error) |
| 143 | 143 |
} |
| 144 |
+ |
|
| 145 |
+// SecretAPIClient defines API client methods for secrets |
|
| 146 |
+type SecretAPIClient interface {
|
|
| 147 |
+ SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) |
|
| 148 |
+ SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error) |
|
| 149 |
+ SecretRemove(ctx context.Context, id string) error |
|
| 150 |
+ SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error) |
|
| 151 |
+} |
| 144 | 152 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,24 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "encoding/json" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/docker/docker/api/types" |
|
| 6 |
+ "github.com/docker/docker/api/types/swarm" |
|
| 7 |
+ "golang.org/x/net/context" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+// SecretCreate creates a new Secret. |
|
| 11 |
+func (cli *Client) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error) {
|
|
| 12 |
+ var headers map[string][]string |
|
| 13 |
+ |
|
| 14 |
+ var response types.SecretCreateResponse |
|
| 15 |
+ resp, err := cli.post(ctx, "/secrets/create", nil, secret, headers) |
|
| 16 |
+ if err != nil {
|
|
| 17 |
+ return response, err |
|
| 18 |
+ } |
|
| 19 |
+ |
|
| 20 |
+ err = json.NewDecoder(resp.body).Decode(&response) |
|
| 21 |
+ ensureReaderClosed(resp) |
|
| 22 |
+ return response, err |
|
| 23 |
+} |
| 0 | 24 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,57 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "fmt" |
|
| 6 |
+ "io/ioutil" |
|
| 7 |
+ "net/http" |
|
| 8 |
+ "strings" |
|
| 9 |
+ "testing" |
|
| 10 |
+ |
|
| 11 |
+ "github.com/docker/docker/api/types" |
|
| 12 |
+ "github.com/docker/docker/api/types/swarm" |
|
| 13 |
+ "golang.org/x/net/context" |
|
| 14 |
+) |
|
| 15 |
+ |
|
| 16 |
+func TestSecretCreateError(t *testing.T) {
|
|
| 17 |
+ client := &Client{
|
|
| 18 |
+ client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), |
|
| 19 |
+ } |
|
| 20 |
+ _, err := client.SecretCreate(context.Background(), swarm.SecretSpec{})
|
|
| 21 |
+ if err == nil || err.Error() != "Error response from daemon: Server error" {
|
|
| 22 |
+ t.Fatalf("expected a Server Error, got %v", err)
|
|
| 23 |
+ } |
|
| 24 |
+} |
|
| 25 |
+ |
|
| 26 |
+func TestSecretCreate(t *testing.T) {
|
|
| 27 |
+ expectedURL := "/secrets/create" |
|
| 28 |
+ client := &Client{
|
|
| 29 |
+ client: newMockClient(func(req *http.Request) (*http.Response, error) {
|
|
| 30 |
+ if !strings.HasPrefix(req.URL.Path, expectedURL) {
|
|
| 31 |
+ return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
|
|
| 32 |
+ } |
|
| 33 |
+ if req.Method != "POST" {
|
|
| 34 |
+ return nil, fmt.Errorf("expected POST method, got %s", req.Method)
|
|
| 35 |
+ } |
|
| 36 |
+ b, err := json.Marshal(types.SecretCreateResponse{
|
|
| 37 |
+ ID: "test_secret", |
|
| 38 |
+ }) |
|
| 39 |
+ if err != nil {
|
|
| 40 |
+ return nil, err |
|
| 41 |
+ } |
|
| 42 |
+ return &http.Response{
|
|
| 43 |
+ StatusCode: http.StatusOK, |
|
| 44 |
+ Body: ioutil.NopCloser(bytes.NewReader(b)), |
|
| 45 |
+ }, nil |
|
| 46 |
+ }), |
|
| 47 |
+ } |
|
| 48 |
+ |
|
| 49 |
+ r, err := client.SecretCreate(context.Background(), swarm.SecretSpec{})
|
|
| 50 |
+ if err != nil {
|
|
| 51 |
+ t.Fatal(err) |
|
| 52 |
+ } |
|
| 53 |
+ if r.ID != "test_secret" {
|
|
| 54 |
+ t.Fatalf("expected `test_secret`, got %s", r.ID)
|
|
| 55 |
+ } |
|
| 56 |
+} |
| 0 | 57 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,34 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "io/ioutil" |
|
| 6 |
+ "net/http" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/docker/docker/api/types/swarm" |
|
| 9 |
+ "golang.org/x/net/context" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+// SecretInspectWithRaw returns the secret information with raw data |
|
| 13 |
+func (cli *Client) SecretInspectWithRaw(ctx context.Context, id string) (swarm.Secret, []byte, error) {
|
|
| 14 |
+ resp, err := cli.get(ctx, "/secrets/"+id, nil, nil) |
|
| 15 |
+ if err != nil {
|
|
| 16 |
+ if resp.statusCode == http.StatusNotFound {
|
|
| 17 |
+ return swarm.Secret{}, nil, secretNotFoundError{id}
|
|
| 18 |
+ } |
|
| 19 |
+ return swarm.Secret{}, nil, err
|
|
| 20 |
+ } |
|
| 21 |
+ defer ensureReaderClosed(resp) |
|
| 22 |
+ |
|
| 23 |
+ body, err := ioutil.ReadAll(resp.body) |
|
| 24 |
+ if err != nil {
|
|
| 25 |
+ return swarm.Secret{}, nil, err
|
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ var secret swarm.Secret |
|
| 29 |
+ rdr := bytes.NewReader(body) |
|
| 30 |
+ err = json.NewDecoder(rdr).Decode(&secret) |
|
| 31 |
+ |
|
| 32 |
+ return secret, body, err |
|
| 33 |
+} |
| 0 | 34 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,65 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "fmt" |
|
| 6 |
+ "io/ioutil" |
|
| 7 |
+ "net/http" |
|
| 8 |
+ "strings" |
|
| 9 |
+ "testing" |
|
| 10 |
+ |
|
| 11 |
+ "github.com/docker/docker/api/types/swarm" |
|
| 12 |
+ "golang.org/x/net/context" |
|
| 13 |
+) |
|
| 14 |
+ |
|
| 15 |
+func TestSecretInspectError(t *testing.T) {
|
|
| 16 |
+ client := &Client{
|
|
| 17 |
+ client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), |
|
| 18 |
+ } |
|
| 19 |
+ |
|
| 20 |
+ _, _, err := client.SecretInspectWithRaw(context.Background(), "nothing") |
|
| 21 |
+ if err == nil || err.Error() != "Error response from daemon: Server error" {
|
|
| 22 |
+ t.Fatalf("expected a Server Error, got %v", err)
|
|
| 23 |
+ } |
|
| 24 |
+} |
|
| 25 |
+ |
|
| 26 |
+func TestSecretInspectSecretNotFound(t *testing.T) {
|
|
| 27 |
+ client := &Client{
|
|
| 28 |
+ client: newMockClient(errorMock(http.StatusNotFound, "Server error")), |
|
| 29 |
+ } |
|
| 30 |
+ |
|
| 31 |
+ _, _, err := client.SecretInspectWithRaw(context.Background(), "unknown") |
|
| 32 |
+ if err == nil || !IsErrSecretNotFound(err) {
|
|
| 33 |
+ t.Fatalf("expected an secretNotFoundError error, got %v", err)
|
|
| 34 |
+ } |
|
| 35 |
+} |
|
| 36 |
+ |
|
| 37 |
+func TestSecretInspect(t *testing.T) {
|
|
| 38 |
+ expectedURL := "/secrets/secret_id" |
|
| 39 |
+ client := &Client{
|
|
| 40 |
+ client: newMockClient(func(req *http.Request) (*http.Response, error) {
|
|
| 41 |
+ if !strings.HasPrefix(req.URL.Path, expectedURL) {
|
|
| 42 |
+ return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
|
|
| 43 |
+ } |
|
| 44 |
+ content, err := json.Marshal(swarm.Secret{
|
|
| 45 |
+ ID: "secret_id", |
|
| 46 |
+ }) |
|
| 47 |
+ if err != nil {
|
|
| 48 |
+ return nil, err |
|
| 49 |
+ } |
|
| 50 |
+ return &http.Response{
|
|
| 51 |
+ StatusCode: http.StatusOK, |
|
| 52 |
+ Body: ioutil.NopCloser(bytes.NewReader(content)), |
|
| 53 |
+ }, nil |
|
| 54 |
+ }), |
|
| 55 |
+ } |
|
| 56 |
+ |
|
| 57 |
+ secretInspect, _, err := client.SecretInspectWithRaw(context.Background(), "secret_id") |
|
| 58 |
+ if err != nil {
|
|
| 59 |
+ t.Fatal(err) |
|
| 60 |
+ } |
|
| 61 |
+ if secretInspect.ID != "secret_id" {
|
|
| 62 |
+ t.Fatalf("expected `secret_id`, got %s", secretInspect.ID)
|
|
| 63 |
+ } |
|
| 64 |
+} |
| 0 | 65 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,35 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "encoding/json" |
|
| 4 |
+ "net/url" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/docker/docker/api/types" |
|
| 7 |
+ "github.com/docker/docker/api/types/filters" |
|
| 8 |
+ "github.com/docker/docker/api/types/swarm" |
|
| 9 |
+ "golang.org/x/net/context" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+// SecretList returns the list of secrets. |
|
| 13 |
+func (cli *Client) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) {
|
|
| 14 |
+ query := url.Values{}
|
|
| 15 |
+ |
|
| 16 |
+ if options.Filter.Len() > 0 {
|
|
| 17 |
+ filterJSON, err := filters.ToParam(options.Filter) |
|
| 18 |
+ if err != nil {
|
|
| 19 |
+ return nil, err |
|
| 20 |
+ } |
|
| 21 |
+ |
|
| 22 |
+ query.Set("filters", filterJSON)
|
|
| 23 |
+ } |
|
| 24 |
+ |
|
| 25 |
+ resp, err := cli.get(ctx, "/secrets", query, nil) |
|
| 26 |
+ if err != nil {
|
|
| 27 |
+ return nil, err |
|
| 28 |
+ } |
|
| 29 |
+ |
|
| 30 |
+ var secrets []swarm.Secret |
|
| 31 |
+ err = json.NewDecoder(resp.body).Decode(&secrets) |
|
| 32 |
+ ensureReaderClosed(resp) |
|
| 33 |
+ return secrets, err |
|
| 34 |
+} |
| 0 | 35 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,94 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "fmt" |
|
| 6 |
+ "io/ioutil" |
|
| 7 |
+ "net/http" |
|
| 8 |
+ "strings" |
|
| 9 |
+ "testing" |
|
| 10 |
+ |
|
| 11 |
+ "github.com/docker/docker/api/types" |
|
| 12 |
+ "github.com/docker/docker/api/types/filters" |
|
| 13 |
+ "github.com/docker/docker/api/types/swarm" |
|
| 14 |
+ "golang.org/x/net/context" |
|
| 15 |
+) |
|
| 16 |
+ |
|
| 17 |
+func TestSecretListError(t *testing.T) {
|
|
| 18 |
+ client := &Client{
|
|
| 19 |
+ client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), |
|
| 20 |
+ } |
|
| 21 |
+ |
|
| 22 |
+ _, err := client.SecretList(context.Background(), types.SecretListOptions{})
|
|
| 23 |
+ if err == nil || err.Error() != "Error response from daemon: Server error" {
|
|
| 24 |
+ t.Fatalf("expected a Server Error, got %v", err)
|
|
| 25 |
+ } |
|
| 26 |
+} |
|
| 27 |
+ |
|
| 28 |
+func TestSecretList(t *testing.T) {
|
|
| 29 |
+ expectedURL := "/secrets" |
|
| 30 |
+ |
|
| 31 |
+ filters := filters.NewArgs() |
|
| 32 |
+ filters.Add("label", "label1")
|
|
| 33 |
+ filters.Add("label", "label2")
|
|
| 34 |
+ |
|
| 35 |
+ listCases := []struct {
|
|
| 36 |
+ options types.SecretListOptions |
|
| 37 |
+ expectedQueryParams map[string]string |
|
| 38 |
+ }{
|
|
| 39 |
+ {
|
|
| 40 |
+ options: types.SecretListOptions{},
|
|
| 41 |
+ expectedQueryParams: map[string]string{
|
|
| 42 |
+ "filters": "", |
|
| 43 |
+ }, |
|
| 44 |
+ }, |
|
| 45 |
+ {
|
|
| 46 |
+ options: types.SecretListOptions{
|
|
| 47 |
+ Filter: filters, |
|
| 48 |
+ }, |
|
| 49 |
+ expectedQueryParams: map[string]string{
|
|
| 50 |
+ "filters": `{"label":{"label1":true,"label2":true}}`,
|
|
| 51 |
+ }, |
|
| 52 |
+ }, |
|
| 53 |
+ } |
|
| 54 |
+ for _, listCase := range listCases {
|
|
| 55 |
+ client := &Client{
|
|
| 56 |
+ client: newMockClient(func(req *http.Request) (*http.Response, error) {
|
|
| 57 |
+ if !strings.HasPrefix(req.URL.Path, expectedURL) {
|
|
| 58 |
+ return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
|
|
| 59 |
+ } |
|
| 60 |
+ query := req.URL.Query() |
|
| 61 |
+ for key, expected := range listCase.expectedQueryParams {
|
|
| 62 |
+ actual := query.Get(key) |
|
| 63 |
+ if actual != expected {
|
|
| 64 |
+ return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
|
|
| 65 |
+ } |
|
| 66 |
+ } |
|
| 67 |
+ content, err := json.Marshal([]swarm.Secret{
|
|
| 68 |
+ {
|
|
| 69 |
+ ID: "secret_id1", |
|
| 70 |
+ }, |
|
| 71 |
+ {
|
|
| 72 |
+ ID: "secret_id2", |
|
| 73 |
+ }, |
|
| 74 |
+ }) |
|
| 75 |
+ if err != nil {
|
|
| 76 |
+ return nil, err |
|
| 77 |
+ } |
|
| 78 |
+ return &http.Response{
|
|
| 79 |
+ StatusCode: http.StatusOK, |
|
| 80 |
+ Body: ioutil.NopCloser(bytes.NewReader(content)), |
|
| 81 |
+ }, nil |
|
| 82 |
+ }), |
|
| 83 |
+ } |
|
| 84 |
+ |
|
| 85 |
+ secrets, err := client.SecretList(context.Background(), listCase.options) |
|
| 86 |
+ if err != nil {
|
|
| 87 |
+ t.Fatal(err) |
|
| 88 |
+ } |
|
| 89 |
+ if len(secrets) != 2 {
|
|
| 90 |
+ t.Fatalf("expected 2 secrets, got %v", secrets)
|
|
| 91 |
+ } |
|
| 92 |
+ } |
|
| 93 |
+} |
| 0 | 94 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,10 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import "golang.org/x/net/context" |
|
| 3 |
+ |
|
| 4 |
+// SecretRemove removes a Secret. |
|
| 5 |
+func (cli *Client) SecretRemove(ctx context.Context, id string) error {
|
|
| 6 |
+ resp, err := cli.delete(ctx, "/secrets/"+id, nil, nil) |
|
| 7 |
+ ensureReaderClosed(resp) |
|
| 8 |
+ return err |
|
| 9 |
+} |
| 0 | 10 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,47 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "io/ioutil" |
|
| 6 |
+ "net/http" |
|
| 7 |
+ "strings" |
|
| 8 |
+ "testing" |
|
| 9 |
+ |
|
| 10 |
+ "golang.org/x/net/context" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+func TestSecretRemoveError(t *testing.T) {
|
|
| 14 |
+ client := &Client{
|
|
| 15 |
+ client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), |
|
| 16 |
+ } |
|
| 17 |
+ |
|
| 18 |
+ err := client.SecretRemove(context.Background(), "secret_id") |
|
| 19 |
+ if err == nil || err.Error() != "Error response from daemon: Server error" {
|
|
| 20 |
+ t.Fatalf("expected a Server Error, got %v", err)
|
|
| 21 |
+ } |
|
| 22 |
+} |
|
| 23 |
+ |
|
| 24 |
+func TestSecretRemove(t *testing.T) {
|
|
| 25 |
+ expectedURL := "/secrets/secret_id" |
|
| 26 |
+ |
|
| 27 |
+ client := &Client{
|
|
| 28 |
+ client: newMockClient(func(req *http.Request) (*http.Response, error) {
|
|
| 29 |
+ if !strings.HasPrefix(req.URL.Path, expectedURL) {
|
|
| 30 |
+ return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
|
|
| 31 |
+ } |
|
| 32 |
+ if req.Method != "DELETE" {
|
|
| 33 |
+ return nil, fmt.Errorf("expected DELETE method, got %s", req.Method)
|
|
| 34 |
+ } |
|
| 35 |
+ return &http.Response{
|
|
| 36 |
+ StatusCode: http.StatusOK, |
|
| 37 |
+ Body: ioutil.NopCloser(bytes.NewReader([]byte("body"))),
|
|
| 38 |
+ }, nil |
|
| 39 |
+ }), |
|
| 40 |
+ } |
|
| 41 |
+ |
|
| 42 |
+ err := client.SecretRemove(context.Background(), "secret_id") |
|
| 43 |
+ if err != nil {
|
|
| 44 |
+ t.Fatal(err) |
|
| 45 |
+ } |
|
| 46 |
+} |
| ... | ... |
@@ -89,8 +89,9 @@ type CommonContainer struct {
|
| 89 | 89 |
HasBeenStartedBefore bool |
| 90 | 90 |
HasBeenManuallyStopped bool // used for unless-stopped restart policy |
| 91 | 91 |
MountPoints map[string]*volume.MountPoint |
| 92 |
- HostConfig *containertypes.HostConfig `json:"-"` // do not serialize the host config in the json, otherwise we'll make the container unportable |
|
| 93 |
- ExecCommands *exec.Store `json:"-"` |
|
| 92 |
+ HostConfig *containertypes.HostConfig `json:"-"` // do not serialize the host config in the json, otherwise we'll make the container unportable |
|
| 93 |
+ ExecCommands *exec.Store `json:"-"` |
|
| 94 |
+ Secrets []*containertypes.ContainerSecret `json:"-"` // do not serialize |
|
| 94 | 95 |
// logDriver for closing |
| 95 | 96 |
LogDriver logger.Logger `json:"-"` |
| 96 | 97 |
LogCopier *logger.Copier `json:"-"` |
| ... | ... |
@@ -23,7 +23,10 @@ import ( |
| 23 | 23 |
) |
| 24 | 24 |
|
| 25 | 25 |
// DefaultSHMSize is the default size (64MB) of the SHM which will be mounted in the container |
| 26 |
-const DefaultSHMSize int64 = 67108864 |
|
| 26 |
+const ( |
|
| 27 |
+ DefaultSHMSize int64 = 67108864 |
|
| 28 |
+ containerSecretMountPath = "/run/secrets" |
|
| 29 |
+) |
|
| 27 | 30 |
|
| 28 | 31 |
// Container holds the fields specific to unixen implementations. |
| 29 | 32 |
// See CommonContainer for standard fields common to all containers. |
| ... | ... |
@@ -175,6 +178,10 @@ func (container *Container) NetworkMounts() []Mount {
|
| 175 | 175 |
return mounts |
| 176 | 176 |
} |
| 177 | 177 |
|
| 178 |
+func (container *Container) SecretMountPath() string {
|
|
| 179 |
+ return filepath.Join(container.Root, "secrets") |
|
| 180 |
+} |
|
| 181 |
+ |
|
| 178 | 182 |
// CopyImagePathContent copies files in destination to the volume. |
| 179 | 183 |
func (container *Container) CopyImagePathContent(v volume.Volume, destination string) error {
|
| 180 | 184 |
rootfs, err := symlink.FollowSymlinkInScope(filepath.Join(container.BaseFS, destination), container.BaseFS) |
| ... | ... |
@@ -260,6 +267,26 @@ func (container *Container) IpcMounts() []Mount {
|
| 260 | 260 |
return mounts |
| 261 | 261 |
} |
| 262 | 262 |
|
| 263 |
+// SecretMounts returns the list of Secret mounts |
|
| 264 |
+func (container *Container) SecretMounts() []Mount {
|
|
| 265 |
+ var mounts []Mount |
|
| 266 |
+ |
|
| 267 |
+ if len(container.Secrets) > 0 {
|
|
| 268 |
+ mounts = append(mounts, Mount{
|
|
| 269 |
+ Source: container.SecretMountPath(), |
|
| 270 |
+ Destination: containerSecretMountPath, |
|
| 271 |
+ Writable: false, |
|
| 272 |
+ }) |
|
| 273 |
+ } |
|
| 274 |
+ |
|
| 275 |
+ return mounts |
|
| 276 |
+} |
|
| 277 |
+ |
|
| 278 |
+// UnmountSecrets unmounts the local tmpfs for secrets |
|
| 279 |
+func (container *Container) UnmountSecrets() error {
|
|
| 280 |
+ return detachMounted(container.SecretMountPath()) |
|
| 281 |
+} |
|
| 282 |
+ |
|
| 263 | 283 |
// UpdateContainer updates configuration of a container. |
| 264 | 284 |
func (container *Container) UpdateContainer(hostConfig *containertypes.HostConfig) error {
|
| 265 | 285 |
container.Lock() |
| ... | ... |
@@ -23,6 +23,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec {
|
| 23 | 23 |
User: c.User, |
| 24 | 24 |
Groups: c.Groups, |
| 25 | 25 |
TTY: c.TTY, |
| 26 |
+ Secrets: secretReferencesFromGRPC(c.Secrets), |
|
| 26 | 27 |
} |
| 27 | 28 |
|
| 28 | 29 |
if c.DNSConfig != nil {
|
| ... | ... |
@@ -75,6 +76,47 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec {
|
| 75 | 75 |
return containerSpec |
| 76 | 76 |
} |
| 77 | 77 |
|
| 78 |
+func secretReferencesToGRPC(sr []*types.SecretReference) []*swarmapi.SecretReference {
|
|
| 79 |
+ refs := []*swarmapi.SecretReference{}
|
|
| 80 |
+ for _, s := range sr {
|
|
| 81 |
+ var mode swarmapi.SecretReference_Mode |
|
| 82 |
+ switch s.Mode {
|
|
| 83 |
+ case types.SecretReferenceSystem: |
|
| 84 |
+ mode = swarmapi.SecretReference_SYSTEM |
|
| 85 |
+ default: |
|
| 86 |
+ mode = swarmapi.SecretReference_FILE |
|
| 87 |
+ } |
|
| 88 |
+ refs = append(refs, &swarmapi.SecretReference{
|
|
| 89 |
+ SecretID: s.SecretID, |
|
| 90 |
+ SecretName: s.SecretName, |
|
| 91 |
+ Target: s.Target, |
|
| 92 |
+ Mode: mode, |
|
| 93 |
+ }) |
|
| 94 |
+ } |
|
| 95 |
+ |
|
| 96 |
+ return refs |
|
| 97 |
+} |
|
| 98 |
+func secretReferencesFromGRPC(sr []*swarmapi.SecretReference) []*types.SecretReference {
|
|
| 99 |
+ refs := []*types.SecretReference{}
|
|
| 100 |
+ for _, s := range sr {
|
|
| 101 |
+ var mode types.SecretReferenceMode |
|
| 102 |
+ switch s.Mode {
|
|
| 103 |
+ case swarmapi.SecretReference_SYSTEM: |
|
| 104 |
+ mode = types.SecretReferenceSystem |
|
| 105 |
+ default: |
|
| 106 |
+ mode = types.SecretReferenceFile |
|
| 107 |
+ } |
|
| 108 |
+ refs = append(refs, &types.SecretReference{
|
|
| 109 |
+ SecretID: s.SecretID, |
|
| 110 |
+ SecretName: s.SecretName, |
|
| 111 |
+ Target: s.Target, |
|
| 112 |
+ Mode: mode, |
|
| 113 |
+ }) |
|
| 114 |
+ } |
|
| 115 |
+ |
|
| 116 |
+ return refs |
|
| 117 |
+} |
|
| 118 |
+ |
|
| 78 | 119 |
func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
|
| 79 | 120 |
containerSpec := &swarmapi.ContainerSpec{
|
| 80 | 121 |
Image: c.Image, |
| ... | ... |
@@ -87,6 +129,7 @@ func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
|
| 87 | 87 |
User: c.User, |
| 88 | 88 |
Groups: c.Groups, |
| 89 | 89 |
TTY: c.TTY, |
| 90 |
+ Secrets: secretReferencesToGRPC(c.Secrets), |
|
| 90 | 91 |
} |
| 91 | 92 |
|
| 92 | 93 |
if c.DNSConfig != nil {
|
| 93 | 94 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,46 @@ |
| 0 |
+package convert |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "github.com/Sirupsen/logrus" |
|
| 4 |
+ swarmtypes "github.com/docker/docker/api/types/swarm" |
|
| 5 |
+ swarmapi "github.com/docker/swarmkit/api" |
|
| 6 |
+ "github.com/docker/swarmkit/protobuf/ptypes" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// SecretFromGRPC converts a grpc Secret to a Secret. |
|
| 10 |
+func SecretFromGRPC(s *swarmapi.Secret) swarmtypes.Secret {
|
|
| 11 |
+ logrus.Debugf("%+v", s)
|
|
| 12 |
+ secret := swarmtypes.Secret{
|
|
| 13 |
+ ID: s.ID, |
|
| 14 |
+ Digest: s.Digest, |
|
| 15 |
+ SecretSize: s.SecretSize, |
|
| 16 |
+ } |
|
| 17 |
+ |
|
| 18 |
+ // Meta |
|
| 19 |
+ secret.Version.Index = s.Meta.Version.Index |
|
| 20 |
+ secret.CreatedAt, _ = ptypes.Timestamp(s.Meta.CreatedAt) |
|
| 21 |
+ secret.UpdatedAt, _ = ptypes.Timestamp(s.Meta.UpdatedAt) |
|
| 22 |
+ |
|
| 23 |
+ secret.Spec = &swarmtypes.SecretSpec{
|
|
| 24 |
+ Annotations: swarmtypes.Annotations{
|
|
| 25 |
+ Name: s.Spec.Annotations.Name, |
|
| 26 |
+ Labels: s.Spec.Annotations.Labels, |
|
| 27 |
+ }, |
|
| 28 |
+ Data: s.Spec.Data, |
|
| 29 |
+ } |
|
| 30 |
+ |
|
| 31 |
+ return secret |
|
| 32 |
+} |
|
| 33 |
+ |
|
| 34 |
+// SecretSpecToGRPC converts Secret to a grpc Secret. |
|
| 35 |
+func SecretSpecToGRPC(s swarmtypes.SecretSpec) (swarmapi.SecretSpec, error) {
|
|
| 36 |
+ spec := swarmapi.SecretSpec{
|
|
| 37 |
+ Annotations: swarmapi.Annotations{
|
|
| 38 |
+ Name: s.Name, |
|
| 39 |
+ Labels: s.Labels, |
|
| 40 |
+ }, |
|
| 41 |
+ Data: s.Data, |
|
| 42 |
+ } |
|
| 43 |
+ |
|
| 44 |
+ return spec, nil |
|
| 45 |
+} |
| ... | ... |
@@ -34,6 +34,7 @@ type Backend interface {
|
| 34 | 34 |
ContainerWaitWithContext(ctx context.Context, name string) error |
| 35 | 35 |
ContainerRm(name string, config *types.ContainerRmConfig) error |
| 36 | 36 |
ContainerKill(name string, sig uint64) error |
| 37 |
+ SetContainerSecrets(name string, secrets []*container.ContainerSecret) error |
|
| 37 | 38 |
SystemInfo() (*types.Info, error) |
| 38 | 39 |
VolumeCreate(name, driverName string, opts, labels map[string]string) (*types.Volume, error) |
| 39 | 40 |
Containers(config *types.ContainerListOptions) ([]*types.Container, error) |
| ... | ... |
@@ -17,6 +17,7 @@ import ( |
| 17 | 17 |
"github.com/docker/docker/api/types/versions" |
| 18 | 18 |
executorpkg "github.com/docker/docker/daemon/cluster/executor" |
| 19 | 19 |
"github.com/docker/libnetwork" |
| 20 |
+ "github.com/docker/swarmkit/agent/exec" |
|
| 20 | 21 |
"github.com/docker/swarmkit/api" |
| 21 | 22 |
"github.com/docker/swarmkit/log" |
| 22 | 23 |
"golang.org/x/net/context" |
| ... | ... |
@@ -29,9 +30,10 @@ import ( |
| 29 | 29 |
type containerAdapter struct {
|
| 30 | 30 |
backend executorpkg.Backend |
| 31 | 31 |
container *containerConfig |
| 32 |
+ secrets exec.SecretProvider |
|
| 32 | 33 |
} |
| 33 | 34 |
|
| 34 |
-func newContainerAdapter(b executorpkg.Backend, task *api.Task) (*containerAdapter, error) {
|
|
| 35 |
+func newContainerAdapter(b executorpkg.Backend, task *api.Task, secrets exec.SecretProvider) (*containerAdapter, error) {
|
|
| 35 | 36 |
ctnr, err := newContainerConfig(task) |
| 36 | 37 |
if err != nil {
|
| 37 | 38 |
return nil, err |
| ... | ... |
@@ -40,6 +42,7 @@ func newContainerAdapter(b executorpkg.Backend, task *api.Task) (*containerAdapt |
| 40 | 40 |
return &containerAdapter{
|
| 41 | 41 |
container: ctnr, |
| 42 | 42 |
backend: b, |
| 43 |
+ secrets: secrets, |
|
| 43 | 44 |
}, nil |
| 44 | 45 |
} |
| 45 | 46 |
|
| ... | ... |
@@ -215,6 +218,35 @@ func (c *containerAdapter) create(ctx context.Context) error {
|
| 215 | 215 |
} |
| 216 | 216 |
} |
| 217 | 217 |
|
| 218 |
+ secrets := []*containertypes.ContainerSecret{}
|
|
| 219 |
+ for _, s := range c.container.task.Spec.GetContainer().Secrets {
|
|
| 220 |
+ sec := c.secrets.Get(s.SecretID) |
|
| 221 |
+ if sec == nil {
|
|
| 222 |
+ logrus.Warnf("unable to get secret %s from provider", s.SecretID)
|
|
| 223 |
+ continue |
|
| 224 |
+ } |
|
| 225 |
+ |
|
| 226 |
+ name := sec.Spec.Annotations.Name |
|
| 227 |
+ target := s.Target |
|
| 228 |
+ if target == "" {
|
|
| 229 |
+ target = name |
|
| 230 |
+ } |
|
| 231 |
+ secrets = append(secrets, &containertypes.ContainerSecret{
|
|
| 232 |
+ Name: name, |
|
| 233 |
+ Target: target, |
|
| 234 |
+ Data: sec.Spec.Data, |
|
| 235 |
+ // TODO (ehazlett): enable configurable uid, gid, mode |
|
| 236 |
+ Uid: 0, |
|
| 237 |
+ Gid: 0, |
|
| 238 |
+ Mode: 0444, |
|
| 239 |
+ }) |
|
| 240 |
+ } |
|
| 241 |
+ |
|
| 242 |
+ // configure secrets |
|
| 243 |
+ if err := c.backend.SetContainerSecrets(cr.ID, secrets); err != nil {
|
|
| 244 |
+ return err |
|
| 245 |
+ } |
|
| 246 |
+ |
|
| 218 | 247 |
if err := c.backend.UpdateContainerServiceConfig(cr.ID, c.container.serviceConfig()); err != nil {
|
| 219 | 248 |
return err |
| 220 | 249 |
} |
| ... | ... |
@@ -4,6 +4,7 @@ import ( |
| 4 | 4 |
executorpkg "github.com/docker/docker/daemon/cluster/executor" |
| 5 | 5 |
"github.com/docker/swarmkit/api" |
| 6 | 6 |
"golang.org/x/net/context" |
| 7 |
+ "src/github.com/docker/swarmkit/agent/exec" |
|
| 7 | 8 |
) |
| 8 | 9 |
|
| 9 | 10 |
// networkAttacherController implements agent.Controller against docker's API. |
| ... | ... |
@@ -19,8 +20,8 @@ type networkAttacherController struct {
|
| 19 | 19 |
closed chan struct{}
|
| 20 | 20 |
} |
| 21 | 21 |
|
| 22 |
-func newNetworkAttacherController(b executorpkg.Backend, task *api.Task) (*networkAttacherController, error) {
|
|
| 23 |
- adapter, err := newContainerAdapter(b, task) |
|
| 22 |
+func newNetworkAttacherController(b executorpkg.Backend, task *api.Task, secrets exec.SecretProvider) (*networkAttacherController, error) {
|
|
| 23 |
+ adapter, err := newContainerAdapter(b, task, secrets) |
|
| 24 | 24 |
if err != nil {
|
| 25 | 25 |
return nil, err |
| 26 | 26 |
} |
| ... | ... |
@@ -33,8 +33,8 @@ type controller struct {
|
| 33 | 33 |
var _ exec.Controller = &controller{}
|
| 34 | 34 |
|
| 35 | 35 |
// NewController returns a docker exec runner for the provided task. |
| 36 |
-func newController(b executorpkg.Backend, task *api.Task) (*controller, error) {
|
|
| 37 |
- adapter, err := newContainerAdapter(b, task) |
|
| 36 |
+func newController(b executorpkg.Backend, task *api.Task, secrets exec.SecretProvider) (*controller, error) {
|
|
| 37 |
+ adapter, err := newContainerAdapter(b, task, secrets) |
|
| 38 | 38 |
if err != nil {
|
| 39 | 39 |
return nil, err |
| 40 | 40 |
} |
| ... | ... |
@@ -18,6 +18,10 @@ type executor struct {
|
| 18 | 18 |
backend executorpkg.Backend |
| 19 | 19 |
} |
| 20 | 20 |
|
| 21 |
+type secretProvider interface {
|
|
| 22 |
+ Get(secretID string) *api.Secret |
|
| 23 |
+} |
|
| 24 |
+ |
|
| 21 | 25 |
// NewExecutor returns an executor from the docker client. |
| 22 | 26 |
func NewExecutor(b executorpkg.Backend) exec.Executor {
|
| 23 | 27 |
return &executor{
|
| ... | ... |
@@ -120,12 +124,12 @@ func (e *executor) Configure(ctx context.Context, node *api.Node) error {
|
| 120 | 120 |
} |
| 121 | 121 |
|
| 122 | 122 |
// Controller returns a docker container runner. |
| 123 |
-func (e *executor) Controller(t *api.Task) (exec.Controller, error) {
|
|
| 123 |
+func (e *executor) Controller(t *api.Task, secrets exec.SecretProvider) (exec.Controller, error) {
|
|
| 124 | 124 |
if t.Spec.GetAttachment() != nil {
|
| 125 |
- return newNetworkAttacherController(e.backend, t) |
|
| 125 |
+ return newNetworkAttacherController(e.backend, t, secrets) |
|
| 126 | 126 |
} |
| 127 | 127 |
|
| 128 |
- ctlr, err := newController(e.backend, t) |
|
| 128 |
+ ctlr, err := newController(e.backend, t, secrets) |
|
| 129 | 129 |
if err != nil {
|
| 130 | 130 |
return nil, err |
| 131 | 131 |
} |
| ... | ... |
@@ -96,3 +96,21 @@ func newListTasksFilters(filter filters.Args, transformFunc func(filters.Args) e |
| 96 | 96 |
|
| 97 | 97 |
return f, nil |
| 98 | 98 |
} |
| 99 |
+ |
|
| 100 |
+func newListSecretsFilters(filter filters.Args) (*swarmapi.ListSecretsRequest_Filters, error) {
|
|
| 101 |
+ accepted := map[string]bool{
|
|
| 102 |
+ "names": true, |
|
| 103 |
+ "name": true, |
|
| 104 |
+ "id": true, |
|
| 105 |
+ "label": true, |
|
| 106 |
+ } |
|
| 107 |
+ if err := filter.Validate(accepted); err != nil {
|
|
| 108 |
+ return nil, err |
|
| 109 |
+ } |
|
| 110 |
+ return &swarmapi.ListSecretsRequest_Filters{
|
|
| 111 |
+ Names: filter.Get("names"),
|
|
| 112 |
+ NamePrefixes: filter.Get("name"),
|
|
| 113 |
+ IDPrefixes: filter.Get("id"),
|
|
| 114 |
+ Labels: runconfigopts.ConvertKVStringsToMap(filter.Get("label")),
|
|
| 115 |
+ }, nil |
|
| 116 |
+} |
| 99 | 117 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,131 @@ |
| 0 |
+package cluster |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ apitypes "github.com/docker/docker/api/types" |
|
| 4 |
+ types "github.com/docker/docker/api/types/swarm" |
|
| 5 |
+ "github.com/docker/docker/daemon/cluster/convert" |
|
| 6 |
+ swarmapi "github.com/docker/swarmkit/api" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// GetSecret returns a secret from a managed swarm cluster |
|
| 10 |
+func (c *Cluster) GetSecret(id string) (types.Secret, error) {
|
|
| 11 |
+ ctx, cancel := c.getRequestContext() |
|
| 12 |
+ defer cancel() |
|
| 13 |
+ |
|
| 14 |
+ r, err := c.node.client.GetSecret(ctx, &swarmapi.GetSecretRequest{SecretID: id})
|
|
| 15 |
+ if err != nil {
|
|
| 16 |
+ return types.Secret{}, err
|
|
| 17 |
+ } |
|
| 18 |
+ |
|
| 19 |
+ return convert.SecretFromGRPC(r.Secret), nil |
|
| 20 |
+} |
|
| 21 |
+ |
|
| 22 |
+// GetSecrets returns all secrets of a managed swarm cluster. |
|
| 23 |
+func (c *Cluster) GetSecrets(options apitypes.SecretListOptions) ([]types.Secret, error) {
|
|
| 24 |
+ c.RLock() |
|
| 25 |
+ defer c.RUnlock() |
|
| 26 |
+ |
|
| 27 |
+ if !c.isActiveManager() {
|
|
| 28 |
+ return nil, c.errNoManager() |
|
| 29 |
+ } |
|
| 30 |
+ |
|
| 31 |
+ filters, err := newListSecretsFilters(options.Filter) |
|
| 32 |
+ if err != nil {
|
|
| 33 |
+ return nil, err |
|
| 34 |
+ } |
|
| 35 |
+ ctx, cancel := c.getRequestContext() |
|
| 36 |
+ defer cancel() |
|
| 37 |
+ |
|
| 38 |
+ r, err := c.node.client.ListSecrets(ctx, |
|
| 39 |
+ &swarmapi.ListSecretsRequest{Filters: filters})
|
|
| 40 |
+ if err != nil {
|
|
| 41 |
+ return nil, err |
|
| 42 |
+ } |
|
| 43 |
+ |
|
| 44 |
+ secrets := []types.Secret{}
|
|
| 45 |
+ |
|
| 46 |
+ for _, secret := range r.Secrets {
|
|
| 47 |
+ secrets = append(secrets, convert.SecretFromGRPC(secret)) |
|
| 48 |
+ } |
|
| 49 |
+ |
|
| 50 |
+ return secrets, nil |
|
| 51 |
+} |
|
| 52 |
+ |
|
| 53 |
+// CreateSecret creates a new secret in a managed swarm cluster. |
|
| 54 |
+func (c *Cluster) CreateSecret(s types.SecretSpec) (string, error) {
|
|
| 55 |
+ c.RLock() |
|
| 56 |
+ defer c.RUnlock() |
|
| 57 |
+ |
|
| 58 |
+ if !c.isActiveManager() {
|
|
| 59 |
+ return "", c.errNoManager() |
|
| 60 |
+ } |
|
| 61 |
+ |
|
| 62 |
+ ctx, cancel := c.getRequestContext() |
|
| 63 |
+ defer cancel() |
|
| 64 |
+ |
|
| 65 |
+ secretSpec, err := convert.SecretSpecToGRPC(s) |
|
| 66 |
+ if err != nil {
|
|
| 67 |
+ return "", err |
|
| 68 |
+ } |
|
| 69 |
+ |
|
| 70 |
+ r, err := c.node.client.CreateSecret(ctx, |
|
| 71 |
+ &swarmapi.CreateSecretRequest{Spec: &secretSpec})
|
|
| 72 |
+ if err != nil {
|
|
| 73 |
+ return "", err |
|
| 74 |
+ } |
|
| 75 |
+ |
|
| 76 |
+ return r.Secret.ID, nil |
|
| 77 |
+} |
|
| 78 |
+ |
|
| 79 |
+// RemoveSecret removes a secret from a managed swarm cluster. |
|
| 80 |
+func (c *Cluster) RemoveSecret(id string) error {
|
|
| 81 |
+ c.RLock() |
|
| 82 |
+ defer c.RUnlock() |
|
| 83 |
+ |
|
| 84 |
+ if !c.isActiveManager() {
|
|
| 85 |
+ return c.errNoManager() |
|
| 86 |
+ } |
|
| 87 |
+ |
|
| 88 |
+ ctx, cancel := c.getRequestContext() |
|
| 89 |
+ defer cancel() |
|
| 90 |
+ |
|
| 91 |
+ req := &swarmapi.RemoveSecretRequest{
|
|
| 92 |
+ SecretID: id, |
|
| 93 |
+ } |
|
| 94 |
+ |
|
| 95 |
+ if _, err := c.node.client.RemoveSecret(ctx, req); err != nil {
|
|
| 96 |
+ return err |
|
| 97 |
+ } |
|
| 98 |
+ return nil |
|
| 99 |
+} |
|
| 100 |
+ |
|
| 101 |
+// UpdateSecret updates a secret in a managed swarm cluster. |
|
| 102 |
+func (c *Cluster) UpdateSecret(id string, version uint64, spec types.SecretSpec) error {
|
|
| 103 |
+ c.RLock() |
|
| 104 |
+ defer c.RUnlock() |
|
| 105 |
+ |
|
| 106 |
+ if !c.isActiveManager() {
|
|
| 107 |
+ return c.errNoManager() |
|
| 108 |
+ } |
|
| 109 |
+ |
|
| 110 |
+ ctx, cancel := c.getRequestContext() |
|
| 111 |
+ defer cancel() |
|
| 112 |
+ |
|
| 113 |
+ secretSpec, err := convert.SecretSpecToGRPC(spec) |
|
| 114 |
+ if err != nil {
|
|
| 115 |
+ return err |
|
| 116 |
+ } |
|
| 117 |
+ |
|
| 118 |
+ if _, err := c.client.UpdateSecret(ctx, |
|
| 119 |
+ &swarmapi.UpdateSecretRequest{
|
|
| 120 |
+ SecretID: id, |
|
| 121 |
+ SecretVersion: &swarmapi.Version{
|
|
| 122 |
+ Index: version, |
|
| 123 |
+ }, |
|
| 124 |
+ Spec: &secretSpec, |
|
| 125 |
+ }); err != nil {
|
|
| 126 |
+ return err |
|
| 127 |
+ } |
|
| 128 |
+ |
|
| 129 |
+ return nil |
|
| 130 |
+} |
| ... | ... |
@@ -4,6 +4,7 @@ package daemon |
| 4 | 4 |
|
| 5 | 5 |
import ( |
| 6 | 6 |
"fmt" |
| 7 |
+ "io/ioutil" |
|
| 7 | 8 |
"os" |
| 8 | 9 |
"path/filepath" |
| 9 | 10 |
"strconv" |
| ... | ... |
@@ -18,6 +19,7 @@ import ( |
| 18 | 18 |
"github.com/docker/docker/pkg/idtools" |
| 19 | 19 |
"github.com/docker/docker/pkg/stringid" |
| 20 | 20 |
"github.com/docker/docker/runconfig" |
| 21 |
+ "github.com/docker/engine-api/types/mount" |
|
| 21 | 22 |
"github.com/docker/libnetwork" |
| 22 | 23 |
"github.com/opencontainers/runc/libcontainer/configs" |
| 23 | 24 |
"github.com/opencontainers/runc/libcontainer/devices" |
| ... | ... |
@@ -139,6 +141,43 @@ func (daemon *Daemon) setupIpcDirs(c *container.Container) error {
|
| 139 | 139 |
|
| 140 | 140 |
return nil |
| 141 | 141 |
} |
| 142 |
+ |
|
| 143 |
+func (daemon *Daemon) setupSecretDir(c *container.Container) error {
|
|
| 144 |
+ localMountPath := c.SecretMountPath() |
|
| 145 |
+ logrus.Debugf("secrets: setting up secret dir: %s", localMountPath)
|
|
| 146 |
+ |
|
| 147 |
+ // create tmpfs |
|
| 148 |
+ if err := os.MkdirAll(localMountPath, 0700); err != nil {
|
|
| 149 |
+ return fmt.Errorf("error creating secret local mount path: %s", err)
|
|
| 150 |
+ } |
|
| 151 |
+ if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "nodev"); err != nil {
|
|
| 152 |
+ return fmt.Errorf("unable to setup secret mount: %s", err)
|
|
| 153 |
+ } |
|
| 154 |
+ |
|
| 155 |
+ for _, s := range c.Secrets {
|
|
| 156 |
+ fPath := filepath.Join(localMountPath, s.Target) |
|
| 157 |
+ if err := os.MkdirAll(filepath.Dir(fPath), 0700); err != nil {
|
|
| 158 |
+ return fmt.Errorf("error creating secret mount path: %s", err)
|
|
| 159 |
+ } |
|
| 160 |
+ |
|
| 161 |
+ logrus.Debugf("injecting secret: name=%s path=%s", s.Name, fPath)
|
|
| 162 |
+ if err := ioutil.WriteFile(fPath, s.Data, s.Mode); err != nil {
|
|
| 163 |
+ return fmt.Errorf("error injecting secret: %s", err)
|
|
| 164 |
+ } |
|
| 165 |
+ |
|
| 166 |
+ if err := os.Chown(fPath, s.Uid, s.Gid); err != nil {
|
|
| 167 |
+ return fmt.Errorf("error setting ownership for secret: %s", err)
|
|
| 168 |
+ } |
|
| 169 |
+ } |
|
| 170 |
+ |
|
| 171 |
+ // remount secrets ro |
|
| 172 |
+ if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "remount,ro"); err != nil {
|
|
| 173 |
+ return fmt.Errorf("unable to remount secret dir as readonly: %s", err)
|
|
| 174 |
+ } |
|
| 175 |
+ |
|
| 176 |
+ return nil |
|
| 177 |
+} |
|
| 178 |
+ |
|
| 142 | 179 |
func killProcessDirectly(container *container.Container) error {
|
| 143 | 180 |
if _, err := container.WaitStop(10 * time.Second); err != nil {
|
| 144 | 181 |
// Ensure that we don't kill ourselves |
| ... | ... |
@@ -702,16 +702,23 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
|
| 702 | 702 |
return nil, err |
| 703 | 703 |
} |
| 704 | 704 |
|
| 705 |
+ if err := daemon.setupSecretDir(c); err != nil {
|
|
| 706 |
+ return nil, err |
|
| 707 |
+ } |
|
| 708 |
+ |
|
| 705 | 709 |
ms, err := daemon.setupMounts(c) |
| 706 | 710 |
if err != nil {
|
| 707 | 711 |
return nil, err |
| 708 | 712 |
} |
| 709 | 713 |
ms = append(ms, c.IpcMounts()...) |
| 714 |
+ |
|
| 710 | 715 |
tmpfsMounts, err := c.TmpfsMounts() |
| 711 | 716 |
if err != nil {
|
| 712 | 717 |
return nil, err |
| 713 | 718 |
} |
| 714 | 719 |
ms = append(ms, tmpfsMounts...) |
| 720 |
+ |
|
| 721 |
+ ms = append(ms, c.SecretMounts()...) |
|
| 715 | 722 |
sort.Sort(mounts(ms)) |
| 716 | 723 |
if err := setMounts(daemon, &s, c, ms); err != nil {
|
| 717 | 724 |
return nil, fmt.Errorf("linux mounts: %v", err)
|
| 718 | 725 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,22 @@ |
| 0 |
+package daemon |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "github.com/Sirupsen/logrus" |
|
| 4 |
+ containertypes "github.com/docker/docker/api/types/container" |
|
| 5 |
+) |
|
| 6 |
+ |
|
| 7 |
+func (daemon *Daemon) SetContainerSecrets(name string, secrets []*containertypes.ContainerSecret) error {
|
|
| 8 |
+ if !secretsSupported() {
|
|
| 9 |
+ logrus.Warn("secrets are not supported on this platform")
|
|
| 10 |
+ return nil |
|
| 11 |
+ } |
|
| 12 |
+ |
|
| 13 |
+ c, err := daemon.GetContainer(name) |
|
| 14 |
+ if err != nil {
|
|
| 15 |
+ return err |
|
| 16 |
+ } |
|
| 17 |
+ |
|
| 18 |
+ c.Secrets = secrets |
|
| 19 |
+ |
|
| 20 |
+ return nil |
|
| 21 |
+} |
| ... | ... |
@@ -212,6 +212,10 @@ func (daemon *Daemon) Cleanup(container *container.Container) {
|
| 212 | 212 |
} |
| 213 | 213 |
} |
| 214 | 214 |
|
| 215 |
+ if err := container.UnmountSecrets(); err != nil {
|
|
| 216 |
+ logrus.Warnf("%s cleanup: failed to unmount secrets: %s", container.ID, err)
|
|
| 217 |
+ } |
|
| 218 |
+ |
|
| 215 | 219 |
for _, eConfig := range container.ExecCommands.Commands() {
|
| 216 | 220 |
daemon.unregisterExecCommand(container, eConfig) |
| 217 | 221 |
} |