Merge pull request #27794 from ehazlett/secrets
Secret Management Upstream-commit: 5e143b5f8d82537635a57f3d9eda8126caf68a6f Component: engine
This commit is contained in:
@ -23,4 +23,9 @@ type Backend interface {
|
||||
RemoveNode(string, bool) error
|
||||
GetTasks(basictypes.TaskListOptions) ([]types.Task, error)
|
||||
GetTask(string) (types.Task, error)
|
||||
GetSecrets(opts basictypes.SecretListOptions) ([]types.Secret, error)
|
||||
CreateSecret(s types.SecretSpec) (string, error)
|
||||
RemoveSecret(id string) error
|
||||
GetSecret(id string) (types.Secret, error)
|
||||
UpdateSecret(id string, version uint64, spec types.SecretSpec) error
|
||||
}
|
||||
|
||||
@ -40,5 +40,10 @@ func (sr *swarmRouter) initRoutes() {
|
||||
router.NewPostRoute("/nodes/{id:.*}/update", sr.updateNode),
|
||||
router.NewGetRoute("/tasks", sr.getTasks),
|
||||
router.NewGetRoute("/tasks/{id:.*}", sr.getTask),
|
||||
router.NewGetRoute("/secrets", sr.getSecrets),
|
||||
router.NewPostRoute("/secrets", sr.createSecret),
|
||||
router.NewDeleteRoute("/secrets/{id:.*}", sr.removeSecret),
|
||||
router.NewGetRoute("/secrets/{id:.*}", sr.getSecret),
|
||||
router.NewPostRoute("/secrets/{id:.*}/update", sr.updateSecret),
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/docker/api/errors"
|
||||
"github.com/docker/docker/api/server/httputils"
|
||||
basictypes "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
@ -261,3 +262,73 @@ func (sr *swarmRouter) getTask(ctx context.Context, w http.ResponseWriter, r *ht
|
||||
|
||||
return httputils.WriteJSON(w, http.StatusOK, task)
|
||||
}
|
||||
|
||||
func (sr *swarmRouter) getSecrets(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
if err := httputils.ParseForm(r); err != nil {
|
||||
return err
|
||||
}
|
||||
filters, err := filters.FromParam(r.Form.Get("filters"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
secrets, err := sr.backend.GetSecrets(basictypes.SecretListOptions{Filters: filters})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return httputils.WriteJSON(w, http.StatusOK, secrets)
|
||||
}
|
||||
|
||||
func (sr *swarmRouter) createSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
var secret types.SecretSpec
|
||||
if err := json.NewDecoder(r.Body).Decode(&secret); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id, err := sr.backend.CreateSecret(secret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return httputils.WriteJSON(w, http.StatusCreated, &basictypes.SecretCreateResponse{
|
||||
ID: id,
|
||||
})
|
||||
}
|
||||
|
||||
func (sr *swarmRouter) removeSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
if err := sr.backend.RemoveSecret(vars["id"]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sr *swarmRouter) getSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
secret, err := sr.backend.GetSecret(vars["id"])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return httputils.WriteJSON(w, http.StatusOK, secret)
|
||||
}
|
||||
|
||||
func (sr *swarmRouter) updateSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
var secret types.SecretSpec
|
||||
if err := json.NewDecoder(r.Body).Decode(&secret); err != nil {
|
||||
return errors.NewBadRequestError(err)
|
||||
}
|
||||
|
||||
rawVersion := r.URL.Query().Get("version")
|
||||
version, err := strconv.ParseUint(rawVersion, 10, 64)
|
||||
if err != nil {
|
||||
return errors.NewBadRequestError(fmt.Errorf("invalid secret version"))
|
||||
}
|
||||
|
||||
id := vars["id"]
|
||||
if err := sr.backend.UpdateSecret(id, version, secret); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
@ -339,3 +340,12 @@ type PluginInstallOptions struct {
|
||||
AcceptPermissionsFunc func(PluginPrivileges) (bool, error)
|
||||
Args []string
|
||||
}
|
||||
|
||||
// SecretRequestOption is a type for requesting secrets
|
||||
type SecretRequestOption struct {
|
||||
Source string
|
||||
Target string
|
||||
UID string
|
||||
GID string
|
||||
Mode os.FileMode
|
||||
}
|
||||
|
||||
14
components/engine/api/types/container/secret.go
Normal file
14
components/engine/api/types/container/secret.go
Normal file
@ -0,0 +1,14 @@
|
||||
package container
|
||||
|
||||
import "os"
|
||||
|
||||
// ContainerSecret represents a secret in a container. This gets realized
|
||||
// in the container tmpfs
|
||||
type ContainerSecret struct {
|
||||
Name string
|
||||
Target string
|
||||
Data []byte
|
||||
UID string
|
||||
GID string
|
||||
Mode os.FileMode
|
||||
}
|
||||
@ -37,4 +37,5 @@ type ContainerSpec struct {
|
||||
StopGracePeriod *time.Duration `json:",omitempty"`
|
||||
Healthcheck *container.HealthConfig `json:",omitempty"`
|
||||
DNSConfig *DNSConfig `json:",omitempty"`
|
||||
Secrets []*SecretReference `json:",omitempty"`
|
||||
}
|
||||
|
||||
33
components/engine/api/types/swarm/secret.go
Normal file
33
components/engine/api/types/swarm/secret.go
Normal file
@ -0,0 +1,33 @@
|
||||
package swarm
|
||||
|
||||
import "os"
|
||||
|
||||
// Secret represents a secret.
|
||||
type Secret struct {
|
||||
ID string
|
||||
Meta
|
||||
Spec SecretSpec
|
||||
Digest string
|
||||
SecretSize int64
|
||||
}
|
||||
|
||||
// SecretSpec represents a secret specification from a secret in swarm
|
||||
type SecretSpec struct {
|
||||
Annotations
|
||||
Data []byte `json:",omitempty"`
|
||||
}
|
||||
|
||||
// SecretReferenceFileTarget is a file target in a secret reference
|
||||
type SecretReferenceFileTarget struct {
|
||||
Name string
|
||||
UID string
|
||||
GID string
|
||||
Mode os.FileMode
|
||||
}
|
||||
|
||||
// SecretReference is a reference to a secret in swarm
|
||||
type SecretReference struct {
|
||||
SecretID string
|
||||
SecretName string
|
||||
Target *SecretReferenceFileTarget
|
||||
}
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
@ -128,7 +129,7 @@ type ContainerProcessList struct {
|
||||
Titles []string
|
||||
}
|
||||
|
||||
// Info contains response of Remote API:
|
||||
// Ping contains response of Remote API:
|
||||
// GET "/_ping"
|
||||
type Ping struct {
|
||||
APIVersion string
|
||||
@ -509,3 +510,15 @@ type ImagesPruneReport struct {
|
||||
type NetworksPruneReport struct {
|
||||
NetworksDeleted []string
|
||||
}
|
||||
|
||||
// SecretCreateResponse contains the information returned to a client
|
||||
// on the creation of a new secret.
|
||||
type SecretCreateResponse struct {
|
||||
// ID is the id of the created secret.
|
||||
ID string
|
||||
}
|
||||
|
||||
// SecretListOptions holds parameters to list secrets
|
||||
type SecretListOptions struct {
|
||||
Filters filters.Args
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"github.com/docker/docker/cli/command/node"
|
||||
"github.com/docker/docker/cli/command/plugin"
|
||||
"github.com/docker/docker/cli/command/registry"
|
||||
"github.com/docker/docker/cli/command/secret"
|
||||
"github.com/docker/docker/cli/command/service"
|
||||
"github.com/docker/docker/cli/command/stack"
|
||||
"github.com/docker/docker/cli/command/swarm"
|
||||
@ -25,6 +26,7 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
|
||||
node.NewNodeCommand(dockerCli),
|
||||
service.NewServiceCommand(dockerCli),
|
||||
swarm.NewSwarmCommand(dockerCli),
|
||||
secret.NewSecretCommand(dockerCli),
|
||||
container.NewContainerCommand(dockerCli),
|
||||
image.NewImageCommand(dockerCli),
|
||||
system.NewSystemCommand(dockerCli),
|
||||
|
||||
29
components/engine/cli/command/secret/cmd.go
Normal file
29
components/engine/cli/command/secret/cmd.go
Normal file
@ -0,0 +1,29 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
)
|
||||
|
||||
// NewSecretCommand returns a cobra command for `secret` subcommands
|
||||
func NewSecretCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "secret",
|
||||
Short: "Manage Docker secrets",
|
||||
Args: cli.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
|
||||
},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newSecretListCommand(dockerCli),
|
||||
newSecretCreateCommand(dockerCli),
|
||||
newSecretInspectCommand(dockerCli),
|
||||
newSecretRemoveCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
66
components/engine/cli/command/secret/create.go
Normal file
66
components/engine/cli/command/secret/create.go
Normal file
@ -0,0 +1,66 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/opts"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type createOptions struct {
|
||||
name string
|
||||
labels opts.ListOpts
|
||||
}
|
||||
|
||||
func newSecretCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
createOpts := createOptions{
|
||||
labels: opts.NewListOpts(runconfigopts.ValidateEnv),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create [name]",
|
||||
Short: "Create a secret using stdin as content",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
createOpts.name = args[0]
|
||||
return runSecretCreate(dockerCli, createOpts)
|
||||
},
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.VarP(&createOpts.labels, "label", "l", "Secret labels")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runSecretCreate(dockerCli *command.DockerCli, options createOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
secretData, err := ioutil.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error reading content from STDIN: %v", err)
|
||||
}
|
||||
|
||||
spec := swarm.SecretSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: options.name,
|
||||
Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()),
|
||||
},
|
||||
Data: secretData,
|
||||
}
|
||||
|
||||
r, err := client.SecretCreate(ctx, spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(dockerCli.Out(), r.ID)
|
||||
return nil
|
||||
}
|
||||
56
components/engine/cli/command/secret/inspect.go
Normal file
56
components/engine/cli/command/secret/inspect.go
Normal file
@ -0,0 +1,56 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/cli/command/inspect"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type inspectOptions struct {
|
||||
name string
|
||||
format string
|
||||
}
|
||||
|
||||
func newSecretInspectCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
opts := inspectOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect [name]",
|
||||
Short: "Inspect a secret",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.name = args[0]
|
||||
return runSecretInspect(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runSecretInspect(dockerCli *command.DockerCli, opts inspectOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
// attempt to lookup secret by name
|
||||
secrets, err := getSecretsByName(client, ctx, []string{opts.name})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id := opts.name
|
||||
for _, s := range secrets {
|
||||
if s.Spec.Annotations.Name == opts.name {
|
||||
id = s.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
getRef := func(name string) (interface{}, []byte, error) {
|
||||
return client.SecretInspectWithRaw(ctx, id)
|
||||
}
|
||||
|
||||
return inspect.Inspect(dockerCli.Out(), []string{id}, opts.format, getRef)
|
||||
}
|
||||
68
components/engine/cli/command/secret/ls.go
Normal file
68
components/engine/cli/command/secret/ls.go
Normal file
@ -0,0 +1,68 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type listOptions struct {
|
||||
quiet bool
|
||||
}
|
||||
|
||||
func newSecretListCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
opts := listOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "ls",
|
||||
Short: "List secrets",
|
||||
Args: cli.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runSecretList(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runSecretList(dockerCli *command.DockerCli, opts listOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
secrets, err := client.SecretList(ctx, types.SecretListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
|
||||
if opts.quiet {
|
||||
for _, s := range secrets {
|
||||
fmt.Fprintf(w, "%s\n", s.ID)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(w, "ID\tNAME\tCREATED\tUPDATED\tSIZE")
|
||||
fmt.Fprintf(w, "\n")
|
||||
|
||||
for _, s := range secrets {
|
||||
created := units.HumanDuration(time.Now().UTC().Sub(s.Meta.CreatedAt)) + " ago"
|
||||
updated := units.HumanDuration(time.Now().UTC().Sub(s.Meta.UpdatedAt)) + " ago"
|
||||
size := units.HumanSizeWithPrecision(float64(s.SecretSize), 3)
|
||||
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", s.ID, s.Spec.Annotations.Name, created, updated, size)
|
||||
}
|
||||
}
|
||||
|
||||
w.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
66
components/engine/cli/command/secret/remove.go
Normal file
66
components/engine/cli/command/secret/remove.go
Normal file
@ -0,0 +1,66 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type removeOptions struct {
|
||||
ids []string
|
||||
}
|
||||
|
||||
func newSecretRemoveCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "rm [id]",
|
||||
Short: "Remove a secret",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts := removeOptions{
|
||||
ids: args,
|
||||
}
|
||||
return runSecretRemove(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runSecretRemove(dockerCli *command.DockerCli, opts removeOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
// attempt to lookup secret by name
|
||||
secrets, err := getSecretsByName(client, ctx, opts.ids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ids := opts.ids
|
||||
|
||||
names := make(map[string]int)
|
||||
for _, id := range ids {
|
||||
names[id] = 1
|
||||
}
|
||||
|
||||
if len(secrets) > 0 {
|
||||
ids = []string{}
|
||||
|
||||
for _, s := range secrets {
|
||||
if _, ok := names[s.Spec.Annotations.Name]; ok {
|
||||
ids = append(ids, s.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
if err := client.SecretRemove(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(dockerCli.Out(), id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
21
components/engine/cli/command/secret/utils.go
Normal file
21
components/engine/cli/command/secret/utils.go
Normal file
@ -0,0 +1,21 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
func getSecretsByName(client client.APIClient, ctx context.Context, names []string) ([]swarm.Secret, error) {
|
||||
args := filters.NewArgs()
|
||||
for _, n := range names {
|
||||
args.Add("names", n)
|
||||
}
|
||||
|
||||
return client.SecretList(ctx, types.SecretListOptions{
|
||||
Filters: args,
|
||||
})
|
||||
}
|
||||
@ -39,6 +39,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
flags.Var(&opts.mounts, flagMount, "Attach a filesystem mount to the service")
|
||||
flags.Var(&opts.constraints, flagConstraint, "Placement constraints")
|
||||
flags.Var(&opts.networks, flagNetwork, "Network attachments")
|
||||
flags.Var(&opts.secrets, flagSecret, "Specify secrets to expose to the service")
|
||||
flags.VarP(&opts.endpoint.ports, flagPublish, "p", "Publish a port as a node port")
|
||||
flags.Var(&opts.groups, flagGroup, "Set one or more supplementary user groups for the container")
|
||||
flags.Var(&opts.dns, flagDNS, "Set custom DNS servers")
|
||||
@ -58,6 +59,13 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// parse and validate secrets
|
||||
secrets, err := parseSecrets(apiClient, opts.secrets.Value())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
service.TaskTemplate.ContainerSpec.Secrets = secrets
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// only send auth if flag was set
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -139,6 +142,98 @@ func (f *floatValue) Value() float32 {
|
||||
return float32(*f)
|
||||
}
|
||||
|
||||
// SecretRequestSpec is a type for requesting secrets
|
||||
type SecretRequestSpec struct {
|
||||
source string
|
||||
target string
|
||||
uid string
|
||||
gid string
|
||||
mode os.FileMode
|
||||
}
|
||||
|
||||
// SecretOpt is a Value type for parsing secrets
|
||||
type SecretOpt struct {
|
||||
values []*SecretRequestSpec
|
||||
}
|
||||
|
||||
// Set a new secret value
|
||||
func (o *SecretOpt) Set(value string) error {
|
||||
csvReader := csv.NewReader(strings.NewReader(value))
|
||||
fields, err := csvReader.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
spec := &SecretRequestSpec{
|
||||
source: "",
|
||||
target: "",
|
||||
uid: "0",
|
||||
gid: "0",
|
||||
mode: 0444,
|
||||
}
|
||||
|
||||
for _, field := range fields {
|
||||
parts := strings.SplitN(field, "=", 2)
|
||||
key := strings.ToLower(parts[0])
|
||||
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
|
||||
}
|
||||
|
||||
value := parts[1]
|
||||
switch key {
|
||||
case "source":
|
||||
spec.source = value
|
||||
case "target":
|
||||
tDir, _ := filepath.Split(value)
|
||||
if tDir != "" {
|
||||
return fmt.Errorf("target must not have a path")
|
||||
}
|
||||
spec.target = value
|
||||
case "uid":
|
||||
spec.uid = value
|
||||
case "gid":
|
||||
spec.gid = value
|
||||
case "mode":
|
||||
m, err := strconv.ParseUint(value, 0, 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid mode specified: %v", err)
|
||||
}
|
||||
|
||||
spec.mode = os.FileMode(m)
|
||||
default:
|
||||
return fmt.Errorf("invalid field in secret request: %s", key)
|
||||
}
|
||||
}
|
||||
|
||||
if spec.source == "" {
|
||||
return fmt.Errorf("source is required")
|
||||
}
|
||||
|
||||
o.values = append(o.values, spec)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Type returns the type of this option
|
||||
func (o *SecretOpt) Type() string {
|
||||
return "secret"
|
||||
}
|
||||
|
||||
// String returns a string repr of this option
|
||||
func (o *SecretOpt) String() string {
|
||||
secrets := []string{}
|
||||
for _, secret := range o.values {
|
||||
repr := fmt.Sprintf("%s -> %s", secret.source, secret.target)
|
||||
secrets = append(secrets, repr)
|
||||
}
|
||||
return strings.Join(secrets, ", ")
|
||||
}
|
||||
|
||||
// Value returns the secret requests
|
||||
func (o *SecretOpt) Value() []*SecretRequestSpec {
|
||||
return o.values
|
||||
}
|
||||
|
||||
type updateOptions struct {
|
||||
parallelism uint64
|
||||
delay time.Duration
|
||||
@ -337,6 +432,7 @@ type serviceOptions struct {
|
||||
logDriver logDriverOptions
|
||||
|
||||
healthcheck healthCheckOptions
|
||||
secrets opts.SecretOpt
|
||||
}
|
||||
|
||||
func newServiceOptions() *serviceOptions {
|
||||
@ -403,6 +499,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
|
||||
Options: opts.dnsOptions.GetAll(),
|
||||
},
|
||||
StopGracePeriod: opts.stopGrace.Value(),
|
||||
Secrets: nil,
|
||||
},
|
||||
Networks: convertNetworks(opts.networks.GetAll()),
|
||||
Resources: opts.resources.ToResourceRequirements(),
|
||||
@ -553,4 +650,7 @@ const (
|
||||
flagHealthRetries = "health-retries"
|
||||
flagHealthTimeout = "health-timeout"
|
||||
flagNoHealthcheck = "no-healthcheck"
|
||||
flagSecret = "secret"
|
||||
flagSecretAdd = "secret-add"
|
||||
flagSecretRemove = "secret-rm"
|
||||
)
|
||||
|
||||
68
components/engine/cli/command/service/parse.go
Normal file
68
components/engine/cli/command/service/parse.go
Normal file
@ -0,0 +1,68 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
swarmtypes "github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// parseSecrets retrieves the secrets from the requested names and converts
|
||||
// them to secret references to use with the spec
|
||||
func parseSecrets(client client.APIClient, requestedSecrets []*types.SecretRequestOption) ([]*swarmtypes.SecretReference, error) {
|
||||
secretRefs := make(map[string]*swarmtypes.SecretReference)
|
||||
ctx := context.Background()
|
||||
|
||||
for _, secret := range requestedSecrets {
|
||||
secretRef := &swarmtypes.SecretReference{
|
||||
SecretName: secret.Source,
|
||||
Target: &swarmtypes.SecretReferenceFileTarget{
|
||||
Name: secret.Target,
|
||||
UID: secret.UID,
|
||||
GID: secret.GID,
|
||||
Mode: secret.Mode,
|
||||
},
|
||||
}
|
||||
|
||||
if _, exists := secretRefs[secret.Target]; exists {
|
||||
return nil, fmt.Errorf("duplicate secret target for %s not allowed", secret.Source)
|
||||
}
|
||||
secretRefs[secret.Target] = secretRef
|
||||
}
|
||||
|
||||
args := filters.NewArgs()
|
||||
for _, s := range secretRefs {
|
||||
args.Add("names", s.SecretName)
|
||||
}
|
||||
|
||||
secrets, err := client.SecretList(ctx, types.SecretListOptions{
|
||||
Filters: args,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
foundSecrets := make(map[string]string)
|
||||
for _, secret := range secrets {
|
||||
foundSecrets[secret.Spec.Annotations.Name] = secret.ID
|
||||
}
|
||||
|
||||
addedSecrets := []*swarmtypes.SecretReference{}
|
||||
|
||||
for _, ref := range secretRefs {
|
||||
id, ok := foundSecrets[ref.SecretName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("secret not found: %s", ref.SecretName)
|
||||
}
|
||||
|
||||
// set the id for the ref to properly assign in swarm
|
||||
// since swarm needs the ID instead of the name
|
||||
ref.SecretID = id
|
||||
addedSecrets = append(addedSecrets, ref)
|
||||
}
|
||||
|
||||
return addedSecrets, nil
|
||||
}
|
||||
@ -14,6 +14,7 @@ import (
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/opts"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
"github.com/docker/go-connections/nat"
|
||||
@ -54,6 +55,8 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
flags.Var(&opts.labels, flagLabelAdd, "Add or update a service label")
|
||||
flags.Var(&opts.containerLabels, flagContainerLabelAdd, "Add or update a container label")
|
||||
flags.Var(&opts.env, flagEnvAdd, "Add or update an environment variable")
|
||||
flags.Var(newListOptsVar(), flagSecretRemove, "Remove a secret")
|
||||
flags.Var(&opts.secrets, flagSecretAdd, "Add or update a secret on a service")
|
||||
flags.Var(&opts.mounts, flagMountAdd, "Add or update a mount on a service")
|
||||
flags.Var(&opts.constraints, flagConstraintAdd, "Add or update a placement constraint")
|
||||
flags.Var(&opts.endpoint.ports, flagPublishAdd, "Add or update a published port")
|
||||
@ -97,6 +100,13 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str
|
||||
return err
|
||||
}
|
||||
|
||||
updatedSecrets, err := getUpdatedSecrets(apiClient, flags, spec.TaskTemplate.ContainerSpec.Secrets)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
spec.TaskTemplate.ContainerSpec.Secrets = updatedSecrets
|
||||
|
||||
// only send auth if flag was set
|
||||
sendAuth, err := flags.GetBool(flagRegistryAuth)
|
||||
if err != nil {
|
||||
@ -401,6 +411,27 @@ func updateEnvironment(flags *pflag.FlagSet, field *[]string) {
|
||||
*field = removeItems(*field, toRemove, envKey)
|
||||
}
|
||||
|
||||
func getUpdatedSecrets(apiClient client.APIClient, flags *pflag.FlagSet, secrets []*swarm.SecretReference) ([]*swarm.SecretReference, error) {
|
||||
if flags.Changed(flagSecretAdd) {
|
||||
values := flags.Lookup(flagSecretAdd).Value.(*opts.SecretOpt).Value()
|
||||
|
||||
addSecrets, err := parseSecrets(apiClient, values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
secrets = append(secrets, addSecrets...)
|
||||
}
|
||||
toRemove := buildToRemoveSet(flags, flagSecretRemove)
|
||||
newSecrets := []*swarm.SecretReference{}
|
||||
for _, secret := range secrets {
|
||||
if _, exists := toRemove[secret.SecretName]; !exists {
|
||||
newSecrets = append(newSecrets, secret)
|
||||
}
|
||||
}
|
||||
|
||||
return newSecrets, nil
|
||||
}
|
||||
|
||||
func envKey(value string) string {
|
||||
kv := strings.SplitN(value, "=", 2)
|
||||
return kv[0]
|
||||
|
||||
@ -217,3 +217,25 @@ func (cli *Client) NewVersionError(APIrequired, feature string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// secretNotFoundError implements an error returned when a secret is not found.
|
||||
type secretNotFoundError struct {
|
||||
name string
|
||||
}
|
||||
|
||||
// Error returns a string representation of a secretNotFoundError
|
||||
func (e secretNotFoundError) Error() string {
|
||||
return fmt.Sprintf("Error: no such secret: %s", e.name)
|
||||
}
|
||||
|
||||
// NoFound indicates that this error type is of NotFound
|
||||
func (e secretNotFoundError) NotFound() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsErrSecretNotFound returns true if the error is caused
|
||||
// when a secret is not found.
|
||||
func IsErrSecretNotFound(err error) bool {
|
||||
_, ok := err.(secretNotFoundError)
|
||||
return ok
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ type CommonAPIClient interface {
|
||||
NetworkAPIClient
|
||||
ServiceAPIClient
|
||||
SwarmAPIClient
|
||||
SecretAPIClient
|
||||
SystemAPIClient
|
||||
VolumeAPIClient
|
||||
ClientVersion() string
|
||||
@ -141,3 +142,11 @@ type VolumeAPIClient interface {
|
||||
VolumeRemove(ctx context.Context, volumeID string, force bool) error
|
||||
VolumesPrune(ctx context.Context, cfg types.VolumesPruneConfig) (types.VolumesPruneReport, error)
|
||||
}
|
||||
|
||||
// SecretAPIClient defines API client methods for secrets
|
||||
type SecretAPIClient interface {
|
||||
SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error)
|
||||
SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error)
|
||||
SecretRemove(ctx context.Context, id string) error
|
||||
SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error)
|
||||
}
|
||||
|
||||
24
components/engine/client/secret_create.go
Normal file
24
components/engine/client/secret_create.go
Normal file
@ -0,0 +1,24 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// SecretCreate creates a new Secret.
|
||||
func (cli *Client) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error) {
|
||||
var headers map[string][]string
|
||||
|
||||
var response types.SecretCreateResponse
|
||||
resp, err := cli.post(ctx, "/secrets", nil, secret, headers)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
err = json.NewDecoder(resp.body).Decode(&response)
|
||||
ensureReaderClosed(resp)
|
||||
return response, err
|
||||
}
|
||||
57
components/engine/client/secret_create_test.go
Normal file
57
components/engine/client/secret_create_test.go
Normal file
@ -0,0 +1,57 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestSecretCreateError(t *testing.T) {
|
||||
client := &Client{
|
||||
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
|
||||
}
|
||||
_, err := client.SecretCreate(context.Background(), swarm.SecretSpec{})
|
||||
if err == nil || err.Error() != "Error response from daemon: Server error" {
|
||||
t.Fatalf("expected a Server Error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretCreate(t *testing.T) {
|
||||
expectedURL := "/secrets"
|
||||
client := &Client{
|
||||
client: newMockClient(func(req *http.Request) (*http.Response, error) {
|
||||
if !strings.HasPrefix(req.URL.Path, expectedURL) {
|
||||
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
|
||||
}
|
||||
if req.Method != "POST" {
|
||||
return nil, fmt.Errorf("expected POST method, got %s", req.Method)
|
||||
}
|
||||
b, err := json.Marshal(types.SecretCreateResponse{
|
||||
ID: "test_secret",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(b)),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
r, err := client.SecretCreate(context.Background(), swarm.SecretSpec{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.ID != "test_secret" {
|
||||
t.Fatalf("expected `test_secret`, got %s", r.ID)
|
||||
}
|
||||
}
|
||||
34
components/engine/client/secret_inspect.go
Normal file
34
components/engine/client/secret_inspect.go
Normal file
@ -0,0 +1,34 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// SecretInspectWithRaw returns the secret information with raw data
|
||||
func (cli *Client) SecretInspectWithRaw(ctx context.Context, id string) (swarm.Secret, []byte, error) {
|
||||
resp, err := cli.get(ctx, "/secrets/"+id, nil, nil)
|
||||
if err != nil {
|
||||
if resp.statusCode == http.StatusNotFound {
|
||||
return swarm.Secret{}, nil, secretNotFoundError{id}
|
||||
}
|
||||
return swarm.Secret{}, nil, err
|
||||
}
|
||||
defer ensureReaderClosed(resp)
|
||||
|
||||
body, err := ioutil.ReadAll(resp.body)
|
||||
if err != nil {
|
||||
return swarm.Secret{}, nil, err
|
||||
}
|
||||
|
||||
var secret swarm.Secret
|
||||
rdr := bytes.NewReader(body)
|
||||
err = json.NewDecoder(rdr).Decode(&secret)
|
||||
|
||||
return secret, body, err
|
||||
}
|
||||
65
components/engine/client/secret_inspect_test.go
Normal file
65
components/engine/client/secret_inspect_test.go
Normal file
@ -0,0 +1,65 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestSecretInspectError(t *testing.T) {
|
||||
client := &Client{
|
||||
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
|
||||
}
|
||||
|
||||
_, _, err := client.SecretInspectWithRaw(context.Background(), "nothing")
|
||||
if err == nil || err.Error() != "Error response from daemon: Server error" {
|
||||
t.Fatalf("expected a Server Error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretInspectSecretNotFound(t *testing.T) {
|
||||
client := &Client{
|
||||
client: newMockClient(errorMock(http.StatusNotFound, "Server error")),
|
||||
}
|
||||
|
||||
_, _, err := client.SecretInspectWithRaw(context.Background(), "unknown")
|
||||
if err == nil || !IsErrSecretNotFound(err) {
|
||||
t.Fatalf("expected an secretNotFoundError error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretInspect(t *testing.T) {
|
||||
expectedURL := "/secrets/secret_id"
|
||||
client := &Client{
|
||||
client: newMockClient(func(req *http.Request) (*http.Response, error) {
|
||||
if !strings.HasPrefix(req.URL.Path, expectedURL) {
|
||||
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
|
||||
}
|
||||
content, err := json.Marshal(swarm.Secret{
|
||||
ID: "secret_id",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(content)),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
secretInspect, _, err := client.SecretInspectWithRaw(context.Background(), "secret_id")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if secretInspect.ID != "secret_id" {
|
||||
t.Fatalf("expected `secret_id`, got %s", secretInspect.ID)
|
||||
}
|
||||
}
|
||||
35
components/engine/client/secret_list.go
Normal file
35
components/engine/client/secret_list.go
Normal file
@ -0,0 +1,35 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// SecretList returns the list of secrets.
|
||||
func (cli *Client) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) {
|
||||
query := url.Values{}
|
||||
|
||||
if options.Filters.Len() > 0 {
|
||||
filterJSON, err := filters.ToParam(options.Filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query.Set("filters", filterJSON)
|
||||
}
|
||||
|
||||
resp, err := cli.get(ctx, "/secrets", query, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var secrets []swarm.Secret
|
||||
err = json.NewDecoder(resp.body).Decode(&secrets)
|
||||
ensureReaderClosed(resp)
|
||||
return secrets, err
|
||||
}
|
||||
94
components/engine/client/secret_list_test.go
Normal file
94
components/engine/client/secret_list_test.go
Normal file
@ -0,0 +1,94 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestSecretListError(t *testing.T) {
|
||||
client := &Client{
|
||||
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
|
||||
}
|
||||
|
||||
_, err := client.SecretList(context.Background(), types.SecretListOptions{})
|
||||
if err == nil || err.Error() != "Error response from daemon: Server error" {
|
||||
t.Fatalf("expected a Server Error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretList(t *testing.T) {
|
||||
expectedURL := "/secrets"
|
||||
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("label", "label1")
|
||||
filters.Add("label", "label2")
|
||||
|
||||
listCases := []struct {
|
||||
options types.SecretListOptions
|
||||
expectedQueryParams map[string]string
|
||||
}{
|
||||
{
|
||||
options: types.SecretListOptions{},
|
||||
expectedQueryParams: map[string]string{
|
||||
"filters": "",
|
||||
},
|
||||
},
|
||||
{
|
||||
options: types.SecretListOptions{
|
||||
Filters: filters,
|
||||
},
|
||||
expectedQueryParams: map[string]string{
|
||||
"filters": `{"label":{"label1":true,"label2":true}}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, listCase := range listCases {
|
||||
client := &Client{
|
||||
client: newMockClient(func(req *http.Request) (*http.Response, error) {
|
||||
if !strings.HasPrefix(req.URL.Path, expectedURL) {
|
||||
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
|
||||
}
|
||||
query := req.URL.Query()
|
||||
for key, expected := range listCase.expectedQueryParams {
|
||||
actual := query.Get(key)
|
||||
if actual != expected {
|
||||
return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
|
||||
}
|
||||
}
|
||||
content, err := json.Marshal([]swarm.Secret{
|
||||
{
|
||||
ID: "secret_id1",
|
||||
},
|
||||
{
|
||||
ID: "secret_id2",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(content)),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
secrets, err := client.SecretList(context.Background(), listCase.options)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(secrets) != 2 {
|
||||
t.Fatalf("expected 2 secrets, got %v", secrets)
|
||||
}
|
||||
}
|
||||
}
|
||||
10
components/engine/client/secret_remove.go
Normal file
10
components/engine/client/secret_remove.go
Normal file
@ -0,0 +1,10 @@
|
||||
package client
|
||||
|
||||
import "golang.org/x/net/context"
|
||||
|
||||
// SecretRemove removes a Secret.
|
||||
func (cli *Client) SecretRemove(ctx context.Context, id string) error {
|
||||
resp, err := cli.delete(ctx, "/secrets/"+id, nil, nil)
|
||||
ensureReaderClosed(resp)
|
||||
return err
|
||||
}
|
||||
47
components/engine/client/secret_remove_test.go
Normal file
47
components/engine/client/secret_remove_test.go
Normal file
@ -0,0 +1,47 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestSecretRemoveError(t *testing.T) {
|
||||
client := &Client{
|
||||
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
|
||||
}
|
||||
|
||||
err := client.SecretRemove(context.Background(), "secret_id")
|
||||
if err == nil || err.Error() != "Error response from daemon: Server error" {
|
||||
t.Fatalf("expected a Server Error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretRemove(t *testing.T) {
|
||||
expectedURL := "/secrets/secret_id"
|
||||
|
||||
client := &Client{
|
||||
client: newMockClient(func(req *http.Request) (*http.Response, error) {
|
||||
if !strings.HasPrefix(req.URL.Path, expectedURL) {
|
||||
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
|
||||
}
|
||||
if req.Method != "DELETE" {
|
||||
return nil, fmt.Errorf("expected DELETE method, got %s", req.Method)
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte("body"))),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
err := client.SecretRemove(context.Background(), "secret_id")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@ -89,8 +89,9 @@ type CommonContainer struct {
|
||||
HasBeenStartedBefore bool
|
||||
HasBeenManuallyStopped bool // used for unless-stopped restart policy
|
||||
MountPoints map[string]*volume.MountPoint
|
||||
HostConfig *containertypes.HostConfig `json:"-"` // do not serialize the host config in the json, otherwise we'll make the container unportable
|
||||
ExecCommands *exec.Store `json:"-"`
|
||||
HostConfig *containertypes.HostConfig `json:"-"` // do not serialize the host config in the json, otherwise we'll make the container unportable
|
||||
ExecCommands *exec.Store `json:"-"`
|
||||
Secrets []*containertypes.ContainerSecret `json:"-"` // do not serialize
|
||||
// logDriver for closing
|
||||
LogDriver logger.Logger `json:"-"`
|
||||
LogCopier *logger.Copier `json:"-"`
|
||||
|
||||
@ -11,3 +11,13 @@ func detachMounted(path string) error {
|
||||
// Therefore there are separate definitions for this.
|
||||
return unix.Unmount(path, 0)
|
||||
}
|
||||
|
||||
// SecretMount returns the mount for the secret path
|
||||
func (container *Container) SecretMount() *Mount {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmountSecrets unmounts the fs for secrets
|
||||
func (container *Container) UnmountSecrets() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -22,8 +22,11 @@ import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// DefaultSHMSize is the default size (64MB) of the SHM which will be mounted in the container
|
||||
const DefaultSHMSize int64 = 67108864
|
||||
const (
|
||||
// DefaultSHMSize is the default size (64MB) of the SHM which will be mounted in the container
|
||||
DefaultSHMSize int64 = 67108864
|
||||
containerSecretMountPath = "/run/secrets"
|
||||
)
|
||||
|
||||
// Container holds the fields specific to unixen implementations.
|
||||
// See CommonContainer for standard fields common to all containers.
|
||||
@ -175,6 +178,11 @@ func (container *Container) NetworkMounts() []Mount {
|
||||
return mounts
|
||||
}
|
||||
|
||||
// SecretMountPath returns the path of the secret mount for the container
|
||||
func (container *Container) SecretMountPath() string {
|
||||
return filepath.Join(container.Root, "secrets")
|
||||
}
|
||||
|
||||
// CopyImagePathContent copies files in destination to the volume.
|
||||
func (container *Container) CopyImagePathContent(v volume.Volume, destination string) error {
|
||||
rootfs, err := symlink.FollowSymlinkInScope(filepath.Join(container.BaseFS, destination), container.BaseFS)
|
||||
@ -260,6 +268,31 @@ func (container *Container) IpcMounts() []Mount {
|
||||
return mounts
|
||||
}
|
||||
|
||||
// SecretMount returns the mount for the secret path
|
||||
func (container *Container) SecretMount() *Mount {
|
||||
if len(container.Secrets) > 0 {
|
||||
return &Mount{
|
||||
Source: container.SecretMountPath(),
|
||||
Destination: containerSecretMountPath,
|
||||
Writable: false,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmountSecrets unmounts the local tmpfs for secrets
|
||||
func (container *Container) UnmountSecrets() error {
|
||||
if _, err := os.Stat(container.SecretMountPath()); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return detachMounted(container.SecretMountPath())
|
||||
}
|
||||
|
||||
// UpdateContainer updates configuration of a container.
|
||||
func (container *Container) UpdateContainer(hostConfig *containertypes.HostConfig) error {
|
||||
container.Lock()
|
||||
|
||||
@ -44,6 +44,16 @@ func (container *Container) IpcMounts() []Mount {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SecretMount returns the mount for the secret path
|
||||
func (container *Container) SecretMount() *Mount {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmountSecrets unmounts the fs for secrets
|
||||
func (container *Container) UnmountSecrets() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmountVolumes explicitly unmounts volumes from the container.
|
||||
func (container *Container) UnmountVolumes(forceSyscall bool, volumeEventLog func(name, action string, attributes map[string]string)) error {
|
||||
var (
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
container "github.com/docker/docker/api/types/container"
|
||||
mounttypes "github.com/docker/docker/api/types/mount"
|
||||
types "github.com/docker/docker/api/types/swarm"
|
||||
@ -23,6 +24,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec {
|
||||
User: c.User,
|
||||
Groups: c.Groups,
|
||||
TTY: c.TTY,
|
||||
Secrets: secretReferencesFromGRPC(c.Secrets),
|
||||
}
|
||||
|
||||
if c.DNSConfig != nil {
|
||||
@ -75,6 +77,49 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec {
|
||||
return containerSpec
|
||||
}
|
||||
|
||||
func secretReferencesToGRPC(sr []*types.SecretReference) []*swarmapi.SecretReference {
|
||||
refs := make([]*swarmapi.SecretReference, 0, len(sr))
|
||||
for _, s := range sr {
|
||||
refs = append(refs, &swarmapi.SecretReference{
|
||||
SecretID: s.SecretID,
|
||||
SecretName: s.SecretName,
|
||||
Target: &swarmapi.SecretReference_File{
|
||||
File: &swarmapi.SecretReference_FileTarget{
|
||||
Name: s.Target.Name,
|
||||
UID: s.Target.UID,
|
||||
GID: s.Target.GID,
|
||||
Mode: s.Target.Mode,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return refs
|
||||
}
|
||||
func secretReferencesFromGRPC(sr []*swarmapi.SecretReference) []*types.SecretReference {
|
||||
refs := make([]*types.SecretReference, 0, len(sr))
|
||||
for _, s := range sr {
|
||||
target := s.GetFile()
|
||||
if target == nil {
|
||||
// not a file target
|
||||
logrus.Warnf("secret target not a file: secret=%s", s.SecretID)
|
||||
continue
|
||||
}
|
||||
refs = append(refs, &types.SecretReference{
|
||||
SecretID: s.SecretID,
|
||||
SecretName: s.SecretName,
|
||||
Target: &types.SecretReferenceFileTarget{
|
||||
Name: target.Name,
|
||||
UID: target.UID,
|
||||
GID: target.GID,
|
||||
Mode: target.Mode,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return refs
|
||||
}
|
||||
|
||||
func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
|
||||
containerSpec := &swarmapi.ContainerSpec{
|
||||
Image: c.Image,
|
||||
@ -87,6 +132,7 @@ func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
|
||||
User: c.User,
|
||||
Groups: c.Groups,
|
||||
TTY: c.TTY,
|
||||
Secrets: secretReferencesToGRPC(c.Secrets),
|
||||
}
|
||||
|
||||
if c.DNSConfig != nil {
|
||||
|
||||
41
components/engine/daemon/cluster/convert/secret.go
Normal file
41
components/engine/daemon/cluster/convert/secret.go
Normal file
@ -0,0 +1,41 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
swarmtypes "github.com/docker/docker/api/types/swarm"
|
||||
swarmapi "github.com/docker/swarmkit/api"
|
||||
"github.com/docker/swarmkit/protobuf/ptypes"
|
||||
)
|
||||
|
||||
// SecretFromGRPC converts a grpc Secret to a Secret.
|
||||
func SecretFromGRPC(s *swarmapi.Secret) swarmtypes.Secret {
|
||||
secret := swarmtypes.Secret{
|
||||
ID: s.ID,
|
||||
Digest: s.Digest,
|
||||
SecretSize: s.SecretSize,
|
||||
Spec: swarmtypes.SecretSpec{
|
||||
Annotations: swarmtypes.Annotations{
|
||||
Name: s.Spec.Annotations.Name,
|
||||
Labels: s.Spec.Annotations.Labels,
|
||||
},
|
||||
Data: s.Spec.Data,
|
||||
},
|
||||
}
|
||||
|
||||
secret.Version.Index = s.Meta.Version.Index
|
||||
// Meta
|
||||
secret.CreatedAt, _ = ptypes.Timestamp(s.Meta.CreatedAt)
|
||||
secret.UpdatedAt, _ = ptypes.Timestamp(s.Meta.UpdatedAt)
|
||||
|
||||
return secret
|
||||
}
|
||||
|
||||
// SecretSpecToGRPC converts Secret to a grpc Secret.
|
||||
func SecretSpecToGRPC(s swarmtypes.SecretSpec) swarmapi.SecretSpec {
|
||||
return swarmapi.SecretSpec{
|
||||
Annotations: swarmapi.Annotations{
|
||||
Name: s.Name,
|
||||
Labels: s.Labels,
|
||||
},
|
||||
Data: s.Data,
|
||||
}
|
||||
}
|
||||
@ -34,6 +34,7 @@ type Backend interface {
|
||||
ContainerWaitWithContext(ctx context.Context, name string) error
|
||||
ContainerRm(name string, config *types.ContainerRmConfig) error
|
||||
ContainerKill(name string, sig uint64) error
|
||||
SetContainerSecrets(name string, secrets []*container.ContainerSecret) error
|
||||
SystemInfo() (*types.Info, error)
|
||||
VolumeCreate(name, driverName string, opts, labels map[string]string) (*types.Volume, error)
|
||||
Containers(config *types.ContainerListOptions) ([]*types.Container, error)
|
||||
|
||||
@ -17,6 +17,7 @@ import (
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
executorpkg "github.com/docker/docker/daemon/cluster/executor"
|
||||
"github.com/docker/libnetwork"
|
||||
"github.com/docker/swarmkit/agent/exec"
|
||||
"github.com/docker/swarmkit/api"
|
||||
"github.com/docker/swarmkit/log"
|
||||
"golang.org/x/net/context"
|
||||
@ -29,9 +30,10 @@ import (
|
||||
type containerAdapter struct {
|
||||
backend executorpkg.Backend
|
||||
container *containerConfig
|
||||
secrets exec.SecretGetter
|
||||
}
|
||||
|
||||
func newContainerAdapter(b executorpkg.Backend, task *api.Task) (*containerAdapter, error) {
|
||||
func newContainerAdapter(b executorpkg.Backend, task *api.Task, secrets exec.SecretGetter) (*containerAdapter, error) {
|
||||
ctnr, err := newContainerConfig(task)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -40,6 +42,7 @@ func newContainerAdapter(b executorpkg.Backend, task *api.Task) (*containerAdapt
|
||||
return &containerAdapter{
|
||||
container: ctnr,
|
||||
backend: b,
|
||||
secrets: secrets,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -215,6 +218,40 @@ func (c *containerAdapter) create(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
container := c.container.task.Spec.GetContainer()
|
||||
if container == nil {
|
||||
return fmt.Errorf("unable to get container from task spec")
|
||||
}
|
||||
secrets := make([]*containertypes.ContainerSecret, 0, len(container.Secrets))
|
||||
for _, s := range container.Secrets {
|
||||
sec := c.secrets.Get(s.SecretID)
|
||||
if sec == nil {
|
||||
logrus.Warnf("unable to get secret %s from provider", s.SecretID)
|
||||
continue
|
||||
}
|
||||
|
||||
name := sec.Spec.Annotations.Name
|
||||
target := s.GetFile()
|
||||
if target == nil {
|
||||
logrus.Warnf("secret target was not a file: secret=%s", s.SecretID)
|
||||
continue
|
||||
}
|
||||
|
||||
secrets = append(secrets, &containertypes.ContainerSecret{
|
||||
Name: name,
|
||||
Target: target.Name,
|
||||
Data: sec.Spec.Data,
|
||||
UID: target.UID,
|
||||
GID: target.GID,
|
||||
Mode: target.Mode,
|
||||
})
|
||||
}
|
||||
|
||||
// configure secrets
|
||||
if err := c.backend.SetContainerSecrets(cr.ID, secrets); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.backend.UpdateContainerServiceConfig(cr.ID, c.container.serviceConfig()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package container
|
||||
|
||||
import (
|
||||
executorpkg "github.com/docker/docker/daemon/cluster/executor"
|
||||
"github.com/docker/swarmkit/agent/exec"
|
||||
"github.com/docker/swarmkit/api"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
@ -19,8 +20,8 @@ type networkAttacherController struct {
|
||||
closed chan struct{}
|
||||
}
|
||||
|
||||
func newNetworkAttacherController(b executorpkg.Backend, task *api.Task) (*networkAttacherController, error) {
|
||||
adapter, err := newContainerAdapter(b, task)
|
||||
func newNetworkAttacherController(b executorpkg.Backend, task *api.Task, secrets exec.SecretGetter) (*networkAttacherController, error) {
|
||||
adapter, err := newContainerAdapter(b, task, secrets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -33,8 +33,8 @@ type controller struct {
|
||||
var _ exec.Controller = &controller{}
|
||||
|
||||
// NewController returns a docker exec runner for the provided task.
|
||||
func newController(b executorpkg.Backend, task *api.Task) (*controller, error) {
|
||||
adapter, err := newContainerAdapter(b, task)
|
||||
func newController(b executorpkg.Backend, task *api.Task, secrets exec.SecretGetter) (*controller, error) {
|
||||
adapter, err := newContainerAdapter(b, task, secrets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -10,18 +10,21 @@ import (
|
||||
clustertypes "github.com/docker/docker/daemon/cluster/provider"
|
||||
networktypes "github.com/docker/libnetwork/types"
|
||||
"github.com/docker/swarmkit/agent/exec"
|
||||
"github.com/docker/swarmkit/agent/secrets"
|
||||
"github.com/docker/swarmkit/api"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type executor struct {
|
||||
backend executorpkg.Backend
|
||||
secrets exec.SecretsManager
|
||||
}
|
||||
|
||||
// NewExecutor returns an executor from the docker client.
|
||||
func NewExecutor(b executorpkg.Backend) exec.Executor {
|
||||
return &executor{
|
||||
backend: b,
|
||||
secrets: secrets.NewManager(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,10 +125,10 @@ func (e *executor) Configure(ctx context.Context, node *api.Node) error {
|
||||
// Controller returns a docker container runner.
|
||||
func (e *executor) Controller(t *api.Task) (exec.Controller, error) {
|
||||
if t.Spec.GetAttachment() != nil {
|
||||
return newNetworkAttacherController(e.backend, t)
|
||||
return newNetworkAttacherController(e.backend, t, e.secrets)
|
||||
}
|
||||
|
||||
ctlr, err := newController(e.backend, t)
|
||||
ctlr, err := newController(e.backend, t, e.secrets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -150,6 +153,10 @@ func (e *executor) SetNetworkBootstrapKeys(keys []*api.EncryptionKey) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *executor) Secrets() exec.SecretsManager {
|
||||
return e.secrets
|
||||
}
|
||||
|
||||
type sortedPlugins []api.PluginDescription
|
||||
|
||||
func (sp sortedPlugins) Len() int { return len(sp) }
|
||||
|
||||
@ -54,7 +54,7 @@ func TestHealthStates(t *testing.T) {
|
||||
EventsService: e,
|
||||
}
|
||||
|
||||
controller, err := newController(daemon, task)
|
||||
controller, err := newController(daemon, task, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("create controller fail %v", err)
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ func newTestControllerWithMount(m api.Mount) (*controller, error) {
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func TestControllerValidateMountBind(t *testing.T) {
|
||||
|
||||
@ -96,3 +96,21 @@ func newListTasksFilters(filter filters.Args, transformFunc func(filters.Args) e
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func newListSecretsFilters(filter filters.Args) (*swarmapi.ListSecretsRequest_Filters, error) {
|
||||
accepted := map[string]bool{
|
||||
"names": true,
|
||||
"name": true,
|
||||
"id": true,
|
||||
"label": true,
|
||||
}
|
||||
if err := filter.Validate(accepted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &swarmapi.ListSecretsRequest_Filters{
|
||||
Names: filter.Get("names"),
|
||||
NamePrefixes: filter.Get("name"),
|
||||
IDPrefixes: filter.Get("id"),
|
||||
Labels: runconfigopts.ConvertKVStringsToMap(filter.Get("label")),
|
||||
}, nil
|
||||
}
|
||||
|
||||
126
components/engine/daemon/cluster/secrets.go
Normal file
126
components/engine/daemon/cluster/secrets.go
Normal file
@ -0,0 +1,126 @@
|
||||
package cluster
|
||||
|
||||
import (
|
||||
apitypes "github.com/docker/docker/api/types"
|
||||
types "github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/daemon/cluster/convert"
|
||||
swarmapi "github.com/docker/swarmkit/api"
|
||||
)
|
||||
|
||||
// GetSecret returns a secret from a managed swarm cluster
|
||||
func (c *Cluster) GetSecret(id string) (types.Secret, error) {
|
||||
ctx, cancel := c.getRequestContext()
|
||||
defer cancel()
|
||||
|
||||
r, err := c.node.client.GetSecret(ctx, &swarmapi.GetSecretRequest{SecretID: id})
|
||||
if err != nil {
|
||||
return types.Secret{}, err
|
||||
}
|
||||
|
||||
return convert.SecretFromGRPC(r.Secret), nil
|
||||
}
|
||||
|
||||
// GetSecrets returns all secrets of a managed swarm cluster.
|
||||
func (c *Cluster) GetSecrets(options apitypes.SecretListOptions) ([]types.Secret, error) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
|
||||
if !c.isActiveManager() {
|
||||
return nil, c.errNoManager()
|
||||
}
|
||||
|
||||
filters, err := newListSecretsFilters(options.Filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx, cancel := c.getRequestContext()
|
||||
defer cancel()
|
||||
|
||||
r, err := c.node.client.ListSecrets(ctx,
|
||||
&swarmapi.ListSecretsRequest{Filters: filters})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
secrets := []types.Secret{}
|
||||
|
||||
for _, secret := range r.Secrets {
|
||||
secrets = append(secrets, convert.SecretFromGRPC(secret))
|
||||
}
|
||||
|
||||
return secrets, nil
|
||||
}
|
||||
|
||||
// CreateSecret creates a new secret in a managed swarm cluster.
|
||||
func (c *Cluster) CreateSecret(s types.SecretSpec) (string, error) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
|
||||
if !c.isActiveManager() {
|
||||
return "", c.errNoManager()
|
||||
}
|
||||
|
||||
ctx, cancel := c.getRequestContext()
|
||||
defer cancel()
|
||||
|
||||
secretSpec := convert.SecretSpecToGRPC(s)
|
||||
|
||||
r, err := c.node.client.CreateSecret(ctx,
|
||||
&swarmapi.CreateSecretRequest{Spec: &secretSpec})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return r.Secret.ID, nil
|
||||
}
|
||||
|
||||
// RemoveSecret removes a secret from a managed swarm cluster.
|
||||
func (c *Cluster) RemoveSecret(id string) error {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
|
||||
if !c.isActiveManager() {
|
||||
return c.errNoManager()
|
||||
}
|
||||
|
||||
ctx, cancel := c.getRequestContext()
|
||||
defer cancel()
|
||||
|
||||
req := &swarmapi.RemoveSecretRequest{
|
||||
SecretID: id,
|
||||
}
|
||||
|
||||
if _, err := c.node.client.RemoveSecret(ctx, req); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateSecret updates a secret in a managed swarm cluster.
|
||||
// Note: this is not exposed to the CLI but is available from the API only
|
||||
func (c *Cluster) UpdateSecret(id string, version uint64, spec types.SecretSpec) error {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
|
||||
if !c.isActiveManager() {
|
||||
return c.errNoManager()
|
||||
}
|
||||
|
||||
ctx, cancel := c.getRequestContext()
|
||||
defer cancel()
|
||||
|
||||
secretSpec := convert.SecretSpecToGRPC(spec)
|
||||
|
||||
if _, err := c.client.UpdateSecret(ctx,
|
||||
&swarmapi.UpdateSecretRequest{
|
||||
SecretID: id,
|
||||
SecretVersion: &swarmapi.Version{
|
||||
Index: version,
|
||||
},
|
||||
Spec: &secretSpec,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -4,6 +4,7 @@ package daemon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@ -12,10 +13,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/cloudflare/cfssl/log"
|
||||
containertypes "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/container"
|
||||
"github.com/docker/docker/daemon/links"
|
||||
"github.com/docker/docker/pkg/idtools"
|
||||
"github.com/docker/docker/pkg/mount"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/runconfig"
|
||||
"github.com/docker/libnetwork"
|
||||
@ -23,6 +26,7 @@ import (
|
||||
"github.com/opencontainers/runc/libcontainer/devices"
|
||||
"github.com/opencontainers/runc/libcontainer/label"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func u32Ptr(i int64) *uint32 { u := uint32(i); return &u }
|
||||
@ -139,6 +143,79 @@ func (daemon *Daemon) setupIpcDirs(c *container.Container) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) {
|
||||
if len(c.Secrets) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
localMountPath := c.SecretMountPath()
|
||||
logrus.Debugf("secrets: setting up secret dir: %s", localMountPath)
|
||||
|
||||
defer func() {
|
||||
if setupErr != nil {
|
||||
// cleanup
|
||||
_ = detachMounted(localMountPath)
|
||||
|
||||
if err := os.RemoveAll(localMountPath); err != nil {
|
||||
log.Errorf("error cleaning up secret mount: %s", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// retrieve possible remapped range start for root UID, GID
|
||||
rootUID, rootGID := daemon.GetRemappedUIDGID()
|
||||
// create tmpfs
|
||||
if err := idtools.MkdirAllAs(localMountPath, 0700, rootUID, rootGID); err != nil {
|
||||
return errors.Wrap(err, "error creating secret local mount path")
|
||||
}
|
||||
tmpfsOwnership := fmt.Sprintf("uid=%d,gid=%d", rootUID, rootGID)
|
||||
if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "nodev,nosuid,noexec,"+tmpfsOwnership); err != nil {
|
||||
return errors.Wrap(err, "unable to setup secret mount")
|
||||
}
|
||||
|
||||
for _, s := range c.Secrets {
|
||||
targetPath := filepath.Clean(s.Target)
|
||||
// ensure that the target is a filename only; no paths allowed
|
||||
if targetPath != filepath.Base(targetPath) {
|
||||
return fmt.Errorf("error creating secret: secret must not be a path")
|
||||
}
|
||||
|
||||
fPath := filepath.Join(localMountPath, targetPath)
|
||||
if err := idtools.MkdirAllAs(filepath.Dir(fPath), 0700, rootUID, rootGID); err != nil {
|
||||
return errors.Wrap(err, "error creating secret mount path")
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"name": s.Name,
|
||||
"path": fPath,
|
||||
}).Debug("injecting secret")
|
||||
if err := ioutil.WriteFile(fPath, s.Data, s.Mode); err != nil {
|
||||
return errors.Wrap(err, "error injecting secret")
|
||||
}
|
||||
|
||||
uid, err := strconv.Atoi(s.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gid, err := strconv.Atoi(s.GID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Chown(fPath, rootUID+uid, rootGID+gid); err != nil {
|
||||
return errors.Wrap(err, "error setting ownership for secret")
|
||||
}
|
||||
}
|
||||
|
||||
// remount secrets ro
|
||||
if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "remount,ro,"+tmpfsOwnership); err != nil {
|
||||
return errors.Wrap(err, "unable to remount secret dir as readonly")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func killProcessDirectly(container *container.Container) error {
|
||||
if _, err := container.WaitStop(10 * time.Second); err != nil {
|
||||
// Ensure that we don't kill ourselves
|
||||
|
||||
@ -854,6 +854,7 @@ func (daemon *Daemon) Unmount(container *container.Container) error {
|
||||
logrus.Errorf("Error unmounting container %s: %s", container.ID, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -702,16 +702,27 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := daemon.setupSecretDir(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ms, err := daemon.setupMounts(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ms = append(ms, c.IpcMounts()...)
|
||||
|
||||
tmpfsMounts, err := c.TmpfsMounts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ms = append(ms, tmpfsMounts...)
|
||||
|
||||
if m := c.SecretMount(); m != nil {
|
||||
ms = append(ms, *m)
|
||||
}
|
||||
|
||||
sort.Sort(mounts(ms))
|
||||
if err := setMounts(daemon, &s, c, ms); err != nil {
|
||||
return nil, fmt.Errorf("linux mounts: %v", err)
|
||||
|
||||
23
components/engine/daemon/secrets.go
Normal file
23
components/engine/daemon/secrets.go
Normal file
@ -0,0 +1,23 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"github.com/Sirupsen/logrus"
|
||||
containertypes "github.com/docker/docker/api/types/container"
|
||||
)
|
||||
|
||||
// SetContainerSecrets sets the container secrets needed
|
||||
func (daemon *Daemon) SetContainerSecrets(name string, secrets []*containertypes.ContainerSecret) error {
|
||||
if !secretsSupported() && len(secrets) > 0 {
|
||||
logrus.Warn("secrets are not supported on this platform")
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := daemon.GetContainer(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Secrets = secrets
|
||||
|
||||
return nil
|
||||
}
|
||||
7
components/engine/daemon/secrets_linux.go
Normal file
7
components/engine/daemon/secrets_linux.go
Normal file
@ -0,0 +1,7 @@
|
||||
// +build linux
|
||||
|
||||
package daemon
|
||||
|
||||
func secretsSupported() bool {
|
||||
return true
|
||||
}
|
||||
7
components/engine/daemon/secrets_unsupported.go
Normal file
7
components/engine/daemon/secrets_unsupported.go
Normal file
@ -0,0 +1,7 @@
|
||||
// +build !linux
|
||||
|
||||
package daemon
|
||||
|
||||
func secretsSupported() bool {
|
||||
return false
|
||||
}
|
||||
@ -212,6 +212,10 @@ func (daemon *Daemon) Cleanup(container *container.Container) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := container.UnmountSecrets(); err != nil {
|
||||
logrus.Warnf("%s cleanup: failed to unmount secrets: %s", container.ID, err)
|
||||
}
|
||||
|
||||
for _, eConfig := range container.ExecCommands.Commands() {
|
||||
daemon.unregisterExecCommand(container, eConfig)
|
||||
}
|
||||
|
||||
@ -240,7 +240,7 @@ List containers
|
||||
- `volume`=(`<volume name>` or `<mount point destination>`)
|
||||
- `network`=(`<network id>` or `<network name>`)
|
||||
- `health`=(`starting`|`healthy`|`unhealthy`|`none`)
|
||||
|
||||
|
||||
**Status codes**:
|
||||
|
||||
- **200** – no error
|
||||
@ -5842,6 +5842,134 @@ Get details on a task
|
||||
- **404** – unknown task
|
||||
- **500** – server error
|
||||
|
||||
## 3.11 Secrets
|
||||
|
||||
**Note**: Secret operations require the engine to be part of a swarm.
|
||||
|
||||
### List secrets
|
||||
|
||||
`GET /secrets`
|
||||
|
||||
List secrets
|
||||
|
||||
**Example request**:
|
||||
|
||||
GET /secrets HTTP/1.1
|
||||
|
||||
**Example response**:
|
||||
|
||||
[
|
||||
{
|
||||
"ID": "ktnbjxoalbkvbvedmg1urrz8h",
|
||||
"Version": {
|
||||
"Index": 11
|
||||
},
|
||||
"CreatedAt": "2016-11-05T01:20:17.327670065Z",
|
||||
"UpdatedAt": "2016-11-05T01:20:17.327670065Z",
|
||||
"Spec": {
|
||||
"Name": "app-dev.crt"
|
||||
},
|
||||
"Digest": "sha256:11d7c6f38253b73e608153c9f662a191ae605e1a3d9b756b0b3426388f91d3fa",
|
||||
"SecretSize": 31
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
**Query parameters**:
|
||||
|
||||
- **filters** - a JSON encoded value of the filters (a `map[string][]string`) to process on the secrets list. Available filters:
|
||||
- `names=<secret name>`
|
||||
|
||||
**Status codes**:
|
||||
|
||||
- **200** – no error
|
||||
|
||||
### Create a secret
|
||||
|
||||
`POST /secrets/create`
|
||||
|
||||
Create a secret
|
||||
|
||||
**Example request**:
|
||||
|
||||
POST /secrets/create HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"Name": "app-key.crt",
|
||||
"Labels": {
|
||||
"foo": "bar"
|
||||
},
|
||||
"Data": "VEhJUyBJUyBOT1QgQSBSRUFMIENFUlRJRklDQVRFCg=="
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"ID": "ktnbjxoalbkvbvedmg1urrz8h"
|
||||
}
|
||||
|
||||
**Status codes**:
|
||||
|
||||
- **201** – no error
|
||||
- **406** – server error or node is not part of a swarm
|
||||
- **409** – name conflicts with an existing object
|
||||
|
||||
**JSON Parameters**:
|
||||
|
||||
- **Name** – User-defined name for the secret.
|
||||
- **Labels** – A map of labels to associate with the secret (e.g., `{"key":"value", "key2":"value2"}`).
|
||||
- **Data** – Base64-url-safe-encoded secret data
|
||||
|
||||
### Inspect a secret
|
||||
|
||||
`GET /secrets/(secret id)`
|
||||
|
||||
Get details on a secret
|
||||
|
||||
**Example request**:
|
||||
|
||||
GET /secrets/ktnbjxoalbkvbvedmg1urrz8h HTTP/1.1
|
||||
|
||||
**Example response**:
|
||||
|
||||
{
|
||||
"ID": "ktnbjxoalbkvbvedmg1urrz8h",
|
||||
"Version": {
|
||||
"Index": 11
|
||||
},
|
||||
"CreatedAt": "2016-11-05T01:20:17.327670065Z",
|
||||
"UpdatedAt": "2016-11-05T01:20:17.327670065Z",
|
||||
"Spec": {
|
||||
"Name": "app-dev.crt"
|
||||
},
|
||||
"Digest": "sha256:11d7c6f38253b73e608153c9f662a191ae605e1a3d9b756b0b3426388f91d3fa",
|
||||
"SecretSize": 31
|
||||
}
|
||||
|
||||
**Status codes**:
|
||||
|
||||
- **200** – no error
|
||||
- **404** – unknown secret
|
||||
- **500** – server error
|
||||
|
||||
### Remove a secret
|
||||
|
||||
`DELETE /secrets/(id)`
|
||||
|
||||
Remove the secret `id` from the secret store
|
||||
|
||||
**Example request**:
|
||||
|
||||
DELETE /secrets/ktnbjxoalbkvbvedmg1urrz8h HTTP/1.1
|
||||
|
||||
**Example response**:
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
|
||||
# 4. Going further
|
||||
|
||||
## 4.1 Inside `docker run`
|
||||
|
||||
@ -0,0 +1,80 @@
|
||||
---
|
||||
title: "secret create"
|
||||
description: "The secret create command description and usage"
|
||||
keywords: ["secret, create"]
|
||||
---
|
||||
|
||||
<!-- This file is maintained within the docker/docker Github
|
||||
repository at https://github.com/docker/docker/. Make all
|
||||
pull requests against that repo. If you see this file in
|
||||
another repository, consider it read-only there, as it will
|
||||
periodically be overwritten by the definitive file. Pull
|
||||
requests which include edits to this file in other repositories
|
||||
will be rejected.
|
||||
-->
|
||||
|
||||
# secret create
|
||||
|
||||
```Markdown
|
||||
Usage: docker secret create [NAME]
|
||||
|
||||
Create a secret using stdin as content
|
||||
Options:
|
||||
--help Print usage
|
||||
-l, --label list Secret labels (default [])
|
||||
```
|
||||
|
||||
Creates a secret using standard input for the secret content. You must run this
|
||||
command on a manager node.
|
||||
|
||||
## Examples
|
||||
|
||||
### Create a secret
|
||||
|
||||
```bash
|
||||
$ cat secret.json | docker secret create secret.json
|
||||
mhv17xfe3gh6xc4rij5orpfds
|
||||
|
||||
$ docker secret ls
|
||||
ID NAME CREATED UPDATED SIZE
|
||||
mhv17xfe3gh6xc4rij5orpfds secret.json 2016-10-27 23:25:43.909181089 +0000 UTC 2016-10-27 23:25:43.909181089 +0000 UTC 1679
|
||||
```
|
||||
|
||||
### Create a secret with labels
|
||||
|
||||
```bash
|
||||
$ cat secret.json | docker secret create secret.json --label env=dev --label rev=20161102
|
||||
jtn7g6aukl5ky7nr9gvwafoxh
|
||||
|
||||
$ docker secret inspect secret.json
|
||||
[
|
||||
{
|
||||
"ID": "jtn7g6aukl5ky7nr9gvwafoxh",
|
||||
"Version": {
|
||||
"Index": 541
|
||||
},
|
||||
"CreatedAt": "2016-11-03T20:54:12.924766548Z",
|
||||
"UpdatedAt": "2016-11-03T20:54:12.924766548Z",
|
||||
"Spec": {
|
||||
"Name": "secret.json",
|
||||
"Labels": {
|
||||
"env": "dev",
|
||||
"rev": "20161102"
|
||||
},
|
||||
"Data": null
|
||||
},
|
||||
"Digest": "sha256:4212a44b14e94154359569333d3fc6a80f6b9959dfdaff26412f4b2796b1f387",
|
||||
"SecretSize": 1679
|
||||
}
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Related information
|
||||
|
||||
* [secret inspect](secret_inspect.md)
|
||||
* [secret ls](secret_ls.md)
|
||||
* [secret rm](secret_rm.md)
|
||||
|
||||
<style>table tr > td:first-child { white-space: nowrap;}</style>
|
||||
@ -0,0 +1,88 @@
|
||||
---
|
||||
title: "secret inspect"
|
||||
description: "The secret inspect command description and usage"
|
||||
keywords: ["secret, inspect"]
|
||||
---
|
||||
|
||||
<!-- This file is maintained within the docker/docker Github
|
||||
repository at https://github.com/docker/docker/. Make all
|
||||
pull requests against that repo. If you see this file in
|
||||
another repository, consider it read-only there, as it will
|
||||
periodically be overwritten by the definitive file. Pull
|
||||
requests which include edits to this file in other repositories
|
||||
will be rejected.
|
||||
-->
|
||||
|
||||
# secret inspect
|
||||
|
||||
```Markdown
|
||||
Usage: docker secret inspect [OPTIONS] SECRET [SECRET...]
|
||||
|
||||
Display detailed information on one or more secrets
|
||||
|
||||
Options:
|
||||
-f, --format string Format the output using the given Go template
|
||||
--help Print usage
|
||||
```
|
||||
|
||||
|
||||
Inspects the specified secret. This command has to be run targeting a manager
|
||||
node.
|
||||
|
||||
By default, this renders all results in a JSON array. If a format is specified,
|
||||
the given template will be executed for each result.
|
||||
|
||||
Go's [text/template](http://golang.org/pkg/text/template/) package
|
||||
describes all the details of the format.
|
||||
|
||||
## Examples
|
||||
|
||||
### Inspecting a secret by name or ID
|
||||
|
||||
You can inspect a secret, either by its *name*, or *ID*
|
||||
|
||||
For example, given the following secret:
|
||||
|
||||
```bash
|
||||
$ docker secret ls
|
||||
ID NAME CREATED UPDATED SIZE
|
||||
mhv17xfe3gh6xc4rij5orpfds secret.json 2016-10-27 23:25:43.909181089 +0000 UTC 2016-10-27 23:25:43.909181089 +0000 UTC 1679
|
||||
```
|
||||
|
||||
```bash
|
||||
$ docker secret inspect secret.json
|
||||
[
|
||||
{
|
||||
"ID": "mhv17xfe3gh6xc4rij5orpfds",
|
||||
"Version": {
|
||||
"Index": 1198
|
||||
},
|
||||
"CreatedAt": "2016-10-27T23:25:43.909181089Z",
|
||||
"UpdatedAt": "2016-10-27T23:25:43.909181089Z",
|
||||
"Spec": {
|
||||
"Name": "secret.json",
|
||||
"Data": null
|
||||
},
|
||||
"Digest": "sha256:8281c6d924520986e3c6af23ed8926710a611c90339db582c2a9ac480ba622b7",
|
||||
"SecretSize": 1679
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Formatting secret output
|
||||
|
||||
You can use the --format option to obtain specific information about a
|
||||
secret. The following example command outputs the digest of the
|
||||
secret.
|
||||
|
||||
```bash{% raw %}
|
||||
$ docker secret inspect --format='{{.Digest}}' mhv17xfe3gh6xc4rij5orpfds
|
||||
sha256:8281c6d924520986e3c6af23ed8926710a611c90339db582c2a9ac480ba622b7
|
||||
{% endraw %}```
|
||||
|
||||
|
||||
## Related information
|
||||
|
||||
* [secret create](secret_create.md)
|
||||
* [secret ls](secret_ls.md)
|
||||
* [secret rm](secret_rm.md)
|
||||
43
components/engine/docs/reference/commandline/secret_ls.md
Normal file
43
components/engine/docs/reference/commandline/secret_ls.md
Normal file
@ -0,0 +1,43 @@
|
||||
---
|
||||
title: "secret ls"
|
||||
description: "The secret ls command description and usage"
|
||||
keywords: ["secret, ls"]
|
||||
---
|
||||
|
||||
<!-- This file is maintained within the docker/docker Github
|
||||
repository at https://github.com/docker/docker/. Make all
|
||||
pull requests against that repo. If you see this file in
|
||||
another repository, consider it read-only there, as it will
|
||||
periodically be overwritten by the definitive file. Pull
|
||||
requests which include edits to this file in other repositories
|
||||
will be rejected.
|
||||
-->
|
||||
|
||||
# secret ls
|
||||
|
||||
```Markdown
|
||||
Usage: docker secret ls [OPTIONS]
|
||||
|
||||
List secrets
|
||||
|
||||
Aliases:
|
||||
ls, list
|
||||
|
||||
Options:
|
||||
-q, --quiet Only display IDs
|
||||
```
|
||||
|
||||
Run this command from a manager to list the secrets in the Swarm.
|
||||
|
||||
On a manager node:
|
||||
|
||||
```bash
|
||||
$ docker secret ls
|
||||
ID NAME CREATED UPDATED SIZE
|
||||
mhv17xfe3gh6xc4rij5orpfds secret.json 2016-10-27 23:25:43.909181089 +0000 UTC 2016-10-27 23:25:43.909181089 +0000 UTC 1679
|
||||
```
|
||||
## Related information
|
||||
|
||||
* [secret create](secret_create.md)
|
||||
* [secret inspect](secret_inspect.md)
|
||||
* [secret rm](secret_rm.md)
|
||||
48
components/engine/docs/reference/commandline/secret_rm.md
Normal file
48
components/engine/docs/reference/commandline/secret_rm.md
Normal file
@ -0,0 +1,48 @@
|
||||
---
|
||||
title: "secret rm"
|
||||
description: "The secret rm command description and usage"
|
||||
keywords: ["secret, rm"]
|
||||
---
|
||||
|
||||
<!-- This file is maintained within the docker/docker Github
|
||||
repository at https://github.com/docker/docker/. Make all
|
||||
pull requests against that repo. If you see this file in
|
||||
another repository, consider it read-only there, as it will
|
||||
periodically be overwritten by the definitive file. Pull
|
||||
requests which include edits to this file in other repositories
|
||||
will be rejected.
|
||||
-->
|
||||
|
||||
# secret rm
|
||||
|
||||
```Markdown
|
||||
Usage: docker secret rm SECRET [SECRET...]
|
||||
|
||||
Remove one or more secrets
|
||||
|
||||
Aliases:
|
||||
rm, remove
|
||||
|
||||
Options:
|
||||
--help Print usage
|
||||
```
|
||||
|
||||
Removes the specified secrets from the swarm. This command has to be run
|
||||
targeting a manager node.
|
||||
|
||||
This example removes a secret:
|
||||
|
||||
```bash
|
||||
$ docker secret rm secret.json
|
||||
sapth4csdo5b6wz2p5uimh5xg
|
||||
```
|
||||
|
||||
> **Warning**: Unlike `docker rm`, this command does not ask for confirmation
|
||||
> before removing a secret.
|
||||
|
||||
|
||||
## Related information
|
||||
|
||||
* [secret create](secret_create.md)
|
||||
* [secret inspect](secret_inspect.md)
|
||||
* [secret ls](secret_ls.md)
|
||||
@ -54,6 +54,7 @@ Options:
|
||||
--restart-delay duration Delay between restart attempts (default none)
|
||||
--restart-max-attempts uint Maximum number of restarts before giving up (default none)
|
||||
--restart-window duration Window used to evaluate the restart policy (default none)
|
||||
--secret value Specify secrets to expose to the service (default [])
|
||||
--stop-grace-period duration Time to wait before force killing a container (default none)
|
||||
-t, --tty Allocate a pseudo-TTY
|
||||
--update-delay duration Delay between updates (ns|us|ms|s|m|h) (default 0s)
|
||||
@ -119,6 +120,33 @@ ID NAME MODE REPLICAS IMAGE
|
||||
4cdgfyky7ozw redis replicated 5/5 redis:3.0.7
|
||||
```
|
||||
|
||||
### Create a service with secrets
|
||||
Use the `--secret` flag to give a container access to a
|
||||
[secret](secret_create.md).
|
||||
|
||||
Create a service specifying a secret:
|
||||
|
||||
```bash
|
||||
$ docker service create --name redis --secret secret.json redis:3.0.6
|
||||
4cdgfyky7ozwh3htjfw0d12qv
|
||||
```
|
||||
|
||||
Create a service specifying the secret, target, user/group ID and mode:
|
||||
|
||||
```bash
|
||||
$ docker service create --name redis \
|
||||
--secret source=ssh-key,target=ssh \
|
||||
--secret source=app-key,target=app,uid=1000,gid=1001,mode=0400 \
|
||||
redis:3.0.6
|
||||
4cdgfyky7ozwh3htjfw0d12qv
|
||||
```
|
||||
|
||||
Secrets are located in `/run/secrets` in the container. If no target is
|
||||
specified, the name of the secret will be used as the in memory file in the
|
||||
container. If a target is specified, that will be the filename. In the
|
||||
example above, two files will be created: `/run/secrets/ssh` and
|
||||
`/run/secrets/app` for each of the secret targets specified.
|
||||
|
||||
### Create a service with a rolling update policy
|
||||
|
||||
```bash
|
||||
|
||||
@ -63,6 +63,8 @@ Options:
|
||||
--restart-max-attempts uint Maximum number of restarts before giving up (default none)
|
||||
--restart-window duration Window used to evaluate the restart policy (default none)
|
||||
--rollback Rollback to previous specification
|
||||
--secret-add list Add a secret (default [])
|
||||
--secret-rm list Remove a secret (default [])
|
||||
--stop-grace-period duration Time to wait before force killing a container (default none)
|
||||
-t, --tty Allocate a pseudo-TTY
|
||||
--update-delay duration Delay between updates (ns|us|ms|s|m|h) (default 0s)
|
||||
@ -146,6 +148,20 @@ $ docker service update --mount-rm /somewhere myservice
|
||||
myservice
|
||||
```
|
||||
|
||||
### Adding and removing secrets
|
||||
|
||||
Use the `--secret-add` or `--secret-rm` options add or remove a service's
|
||||
secrets.
|
||||
|
||||
The following example adds a secret named `ssh-2` and removes `ssh-1`:
|
||||
|
||||
```bash
|
||||
$ docker service update \
|
||||
--secret-add source=ssh-2,target=ssh-2 \
|
||||
--secret-rm ssh-1 \
|
||||
myservice
|
||||
```
|
||||
|
||||
## Related information
|
||||
|
||||
* [service create](service_create.md)
|
||||
|
||||
@ -284,6 +284,42 @@ func (d *SwarmDaemon) listServices(c *check.C) []swarm.Service {
|
||||
return services
|
||||
}
|
||||
|
||||
func (d *SwarmDaemon) createSecret(c *check.C, secretSpec swarm.SecretSpec) string {
|
||||
status, out, err := d.SockRequest("POST", "/secrets", secretSpec)
|
||||
|
||||
c.Assert(err, checker.IsNil, check.Commentf(string(out)))
|
||||
c.Assert(status, checker.Equals, http.StatusCreated, check.Commentf("output: %q", string(out)))
|
||||
|
||||
var scr types.SecretCreateResponse
|
||||
c.Assert(json.Unmarshal(out, &scr), checker.IsNil)
|
||||
return scr.ID
|
||||
}
|
||||
|
||||
func (d *SwarmDaemon) listSecrets(c *check.C) []swarm.Secret {
|
||||
status, out, err := d.SockRequest("GET", "/secrets", nil)
|
||||
c.Assert(err, checker.IsNil, check.Commentf(string(out)))
|
||||
c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out)))
|
||||
|
||||
secrets := []swarm.Secret{}
|
||||
c.Assert(json.Unmarshal(out, &secrets), checker.IsNil)
|
||||
return secrets
|
||||
}
|
||||
|
||||
func (d *SwarmDaemon) getSecret(c *check.C, id string) *swarm.Secret {
|
||||
var secret swarm.Secret
|
||||
status, out, err := d.SockRequest("GET", "/secrets/"+id, nil)
|
||||
c.Assert(err, checker.IsNil, check.Commentf(string(out)))
|
||||
c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out)))
|
||||
c.Assert(json.Unmarshal(out, &secret), checker.IsNil)
|
||||
return &secret
|
||||
}
|
||||
|
||||
func (d *SwarmDaemon) deleteSecret(c *check.C, id string) {
|
||||
status, out, err := d.SockRequest("DELETE", "/secrets/"+id, nil)
|
||||
c.Assert(err, checker.IsNil, check.Commentf(string(out)))
|
||||
c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out)))
|
||||
}
|
||||
|
||||
func (d *SwarmDaemon) getSwarm(c *check.C) swarm.Swarm {
|
||||
var sw swarm.Swarm
|
||||
status, out, err := d.SockRequest("GET", "/swarm", nil)
|
||||
|
||||
@ -1263,3 +1263,50 @@ func (s *DockerSwarmSuite) TestAPISwarmServicesUpdateWithName(c *check.C) {
|
||||
c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out)))
|
||||
waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, instances)
|
||||
}
|
||||
|
||||
func (s *DockerSwarmSuite) TestAPISwarmSecretsEmptyList(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
secrets := d.listSecrets(c)
|
||||
c.Assert(secrets, checker.NotNil)
|
||||
c.Assert(len(secrets), checker.Equals, 0, check.Commentf("secrets: %#v", secrets))
|
||||
}
|
||||
|
||||
func (s *DockerSwarmSuite) TestAPISwarmSecretsCreate(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
testName := "test_secret"
|
||||
id := d.createSecret(c, swarm.SecretSpec{
|
||||
swarm.Annotations{
|
||||
Name: testName,
|
||||
},
|
||||
[]byte("TESTINGDATA"),
|
||||
})
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id))
|
||||
|
||||
secrets := d.listSecrets(c)
|
||||
c.Assert(len(secrets), checker.Equals, 1, check.Commentf("secrets: %#v", secrets))
|
||||
name := secrets[0].Spec.Annotations.Name
|
||||
c.Assert(name, checker.Equals, testName, check.Commentf("secret: %s", name))
|
||||
}
|
||||
|
||||
func (s *DockerSwarmSuite) TestAPISwarmSecretsDelete(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
testName := "test_secret"
|
||||
id := d.createSecret(c, swarm.SecretSpec{
|
||||
swarm.Annotations{
|
||||
Name: testName,
|
||||
},
|
||||
[]byte("TESTINGDATA"),
|
||||
})
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id))
|
||||
|
||||
secret := d.getSecret(c, id)
|
||||
c.Assert(secret.ID, checker.Equals, id, check.Commentf("secret: %v", secret))
|
||||
|
||||
d.deleteSecret(c, secret.ID)
|
||||
status, out, err := d.SockRequest("GET", "/secrets/"+id, nil)
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(status, checker.Equals, http.StatusNotFound, check.Commentf("secret delete: %s", string(out)))
|
||||
}
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/pkg/integration/checker"
|
||||
"github.com/go-check/check"
|
||||
)
|
||||
|
||||
func (s *DockerSwarmSuite) TestSecretCreate(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
testName := "test_secret"
|
||||
id := d.createSecret(c, swarm.SecretSpec{
|
||||
swarm.Annotations{
|
||||
Name: testName,
|
||||
},
|
||||
[]byte("TESTINGDATA"),
|
||||
})
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id))
|
||||
|
||||
secret := d.getSecret(c, id)
|
||||
c.Assert(secret.Spec.Name, checker.Equals, testName)
|
||||
}
|
||||
|
||||
func (s *DockerSwarmSuite) TestSecretCreateWithLabels(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
testName := "test_secret"
|
||||
id := d.createSecret(c, swarm.SecretSpec{
|
||||
swarm.Annotations{
|
||||
Name: testName,
|
||||
Labels: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
},
|
||||
[]byte("TESTINGDATA"),
|
||||
})
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id))
|
||||
|
||||
secret := d.getSecret(c, id)
|
||||
c.Assert(secret.Spec.Name, checker.Equals, testName)
|
||||
c.Assert(len(secret.Spec.Labels), checker.Equals, 2)
|
||||
c.Assert(secret.Spec.Labels["key1"], checker.Equals, "value1")
|
||||
c.Assert(secret.Spec.Labels["key2"], checker.Equals, "value2")
|
||||
}
|
||||
@ -4,6 +4,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
@ -43,3 +44,62 @@ func (s *DockerSwarmSuite) TestServiceCreateMountVolume(c *check.C) {
|
||||
c.Assert(mounts[0].Destination, checker.Equals, "/foo")
|
||||
c.Assert(mounts[0].RW, checker.Equals, true)
|
||||
}
|
||||
|
||||
func (s *DockerSwarmSuite) TestServiceCreateWithSecretSimple(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
serviceName := "test-service-secret"
|
||||
testName := "test_secret"
|
||||
id := d.createSecret(c, swarm.SecretSpec{
|
||||
swarm.Annotations{
|
||||
Name: testName,
|
||||
},
|
||||
[]byte("TESTINGDATA"),
|
||||
})
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id))
|
||||
|
||||
out, err := d.Cmd("service", "create", "--name", serviceName, "--secret", testName, "busybox", "top")
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
|
||||
out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Secrets }}", serviceName)
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
var refs []swarm.SecretReference
|
||||
c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil)
|
||||
c.Assert(refs, checker.HasLen, 1)
|
||||
|
||||
c.Assert(refs[0].SecretName, checker.Equals, testName)
|
||||
c.Assert(refs[0].Target, checker.Not(checker.IsNil))
|
||||
c.Assert(refs[0].Target.Name, checker.Equals, testName)
|
||||
c.Assert(refs[0].Target.UID, checker.Equals, "0")
|
||||
c.Assert(refs[0].Target.GID, checker.Equals, "0")
|
||||
}
|
||||
|
||||
func (s *DockerSwarmSuite) TestServiceCreateWithSecretSourceTarget(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
serviceName := "test-service-secret"
|
||||
testName := "test_secret"
|
||||
id := d.createSecret(c, swarm.SecretSpec{
|
||||
swarm.Annotations{
|
||||
Name: testName,
|
||||
},
|
||||
[]byte("TESTINGDATA"),
|
||||
})
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id))
|
||||
testTarget := "testing"
|
||||
|
||||
out, err := d.Cmd("service", "create", "--name", serviceName, "--secret", fmt.Sprintf("source=%s,target=%s", testName, testTarget), "busybox", "top")
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
|
||||
out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Secrets }}", serviceName)
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
var refs []swarm.SecretReference
|
||||
c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil)
|
||||
c.Assert(refs, checker.HasLen, 1)
|
||||
|
||||
c.Assert(refs[0].SecretName, checker.Equals, testName)
|
||||
c.Assert(refs[0].Target, checker.Not(checker.IsNil))
|
||||
c.Assert(refs[0].Target.Name, checker.Equals, testTarget)
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/pkg/integration/checker"
|
||||
@ -84,3 +85,45 @@ func (s *DockerSwarmSuite) TestServiceUpdateLabel(c *check.C) {
|
||||
c.Assert(service.Spec.Labels, checker.HasLen, 1)
|
||||
c.Assert(service.Spec.Labels["foo"], checker.Equals, "bar")
|
||||
}
|
||||
|
||||
func (s *DockerSwarmSuite) TestServiceUpdateSecrets(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
testName := "test_secret"
|
||||
id := d.createSecret(c, swarm.SecretSpec{
|
||||
swarm.Annotations{
|
||||
Name: testName,
|
||||
},
|
||||
[]byte("TESTINGDATA"),
|
||||
})
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id))
|
||||
testTarget := "testing"
|
||||
serviceName := "test"
|
||||
|
||||
out, err := d.Cmd("service", "create", "--name", serviceName, "busybox", "top")
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
|
||||
// add secret
|
||||
out, err = d.Cmd("service", "update", "test", "--secret-add", fmt.Sprintf("source=%s,target=%s", testName, testTarget))
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
|
||||
out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Secrets }}", serviceName)
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
var refs []swarm.SecretReference
|
||||
c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil)
|
||||
c.Assert(refs, checker.HasLen, 1)
|
||||
|
||||
c.Assert(refs[0].SecretName, checker.Equals, testName)
|
||||
c.Assert(refs[0].Target, checker.Not(checker.IsNil))
|
||||
c.Assert(refs[0].Target.Name, checker.Equals, testTarget)
|
||||
|
||||
// remove
|
||||
out, err = d.Cmd("service", "update", "test", "--secret-rm", testName)
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
|
||||
out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Secrets }}", serviceName)
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil)
|
||||
c.Assert(refs, checker.HasLen, 0)
|
||||
}
|
||||
|
||||
107
components/engine/opts/secret.go
Normal file
107
components/engine/opts/secret.go
Normal file
@ -0,0 +1,107 @@
|
||||
package opts
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
)
|
||||
|
||||
// SecretOpt is a Value type for parsing secrets
|
||||
type SecretOpt struct {
|
||||
values []*types.SecretRequestOption
|
||||
}
|
||||
|
||||
// Set a new secret value
|
||||
func (o *SecretOpt) Set(value string) error {
|
||||
csvReader := csv.NewReader(strings.NewReader(value))
|
||||
fields, err := csvReader.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
options := &types.SecretRequestOption{
|
||||
Source: "",
|
||||
Target: "",
|
||||
UID: "0",
|
||||
GID: "0",
|
||||
Mode: 0444,
|
||||
}
|
||||
|
||||
// support a simple syntax of --secret foo
|
||||
if len(fields) == 1 {
|
||||
options.Source = fields[0]
|
||||
options.Target = fields[0]
|
||||
o.values = append(o.values, options)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, field := range fields {
|
||||
parts := strings.SplitN(field, "=", 2)
|
||||
key := strings.ToLower(parts[0])
|
||||
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
|
||||
}
|
||||
|
||||
value := parts[1]
|
||||
switch key {
|
||||
case "source":
|
||||
options.Source = value
|
||||
case "target":
|
||||
tDir, _ := filepath.Split(value)
|
||||
if tDir != "" {
|
||||
return fmt.Errorf("target must not be a path")
|
||||
}
|
||||
options.Target = value
|
||||
case "uid":
|
||||
options.UID = value
|
||||
case "gid":
|
||||
options.GID = value
|
||||
case "mode":
|
||||
m, err := strconv.ParseUint(value, 0, 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid mode specified: %v", err)
|
||||
}
|
||||
|
||||
options.Mode = os.FileMode(m)
|
||||
default:
|
||||
if len(fields) == 1 && value == "" {
|
||||
|
||||
} else {
|
||||
return fmt.Errorf("invalid field in secret request: %s", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if options.Source == "" {
|
||||
return fmt.Errorf("source is required")
|
||||
}
|
||||
|
||||
o.values = append(o.values, options)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Type returns the type of this option
|
||||
func (o *SecretOpt) Type() string {
|
||||
return "secret"
|
||||
}
|
||||
|
||||
// String returns a string repr of this option
|
||||
func (o *SecretOpt) String() string {
|
||||
secrets := []string{}
|
||||
for _, secret := range o.values {
|
||||
repr := fmt.Sprintf("%s -> %s", secret.Source, secret.Target)
|
||||
secrets = append(secrets, repr)
|
||||
}
|
||||
return strings.Join(secrets, ", ")
|
||||
}
|
||||
|
||||
// Value returns the secret requests
|
||||
func (o *SecretOpt) Value() []*types.SecretRequestOption {
|
||||
return o.values
|
||||
}
|
||||
67
components/engine/opts/secret_test.go
Normal file
67
components/engine/opts/secret_test.go
Normal file
@ -0,0 +1,67 @@
|
||||
package opts
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/pkg/testutil/assert"
|
||||
)
|
||||
|
||||
func TestSecretOptionsSimple(t *testing.T) {
|
||||
var opt SecretOpt
|
||||
|
||||
testCase := "app-secret"
|
||||
assert.NilError(t, opt.Set(testCase))
|
||||
|
||||
reqs := opt.Value()
|
||||
assert.Equal(t, len(reqs), 1)
|
||||
req := reqs[0]
|
||||
assert.Equal(t, req.Source, "app-secret")
|
||||
assert.Equal(t, req.Target, "app-secret")
|
||||
assert.Equal(t, req.UID, "0")
|
||||
assert.Equal(t, req.GID, "0")
|
||||
}
|
||||
|
||||
func TestSecretOptionsSourceTarget(t *testing.T) {
|
||||
var opt SecretOpt
|
||||
|
||||
testCase := "source=foo,target=testing"
|
||||
assert.NilError(t, opt.Set(testCase))
|
||||
|
||||
reqs := opt.Value()
|
||||
assert.Equal(t, len(reqs), 1)
|
||||
req := reqs[0]
|
||||
assert.Equal(t, req.Source, "foo")
|
||||
assert.Equal(t, req.Target, "testing")
|
||||
}
|
||||
|
||||
func TestSecretOptionsCustomUidGid(t *testing.T) {
|
||||
var opt SecretOpt
|
||||
|
||||
testCase := "source=foo,target=testing,uid=1000,gid=1001"
|
||||
assert.NilError(t, opt.Set(testCase))
|
||||
|
||||
reqs := opt.Value()
|
||||
assert.Equal(t, len(reqs), 1)
|
||||
req := reqs[0]
|
||||
assert.Equal(t, req.Source, "foo")
|
||||
assert.Equal(t, req.Target, "testing")
|
||||
assert.Equal(t, req.UID, "1000")
|
||||
assert.Equal(t, req.GID, "1001")
|
||||
}
|
||||
|
||||
func TestSecretOptionsCustomMode(t *testing.T) {
|
||||
var opt SecretOpt
|
||||
|
||||
testCase := "source=foo,target=testing,uid=1000,gid=1001,mode=0444"
|
||||
assert.NilError(t, opt.Set(testCase))
|
||||
|
||||
reqs := opt.Value()
|
||||
assert.Equal(t, len(reqs), 1)
|
||||
req := reqs[0]
|
||||
assert.Equal(t, req.Source, "foo")
|
||||
assert.Equal(t, req.Target, "testing")
|
||||
assert.Equal(t, req.UID, "1000")
|
||||
assert.Equal(t, req.GID, "1001")
|
||||
assert.Equal(t, req.Mode, os.FileMode(0444))
|
||||
}
|
||||
87
components/engine/vendor/github.com/docker/swarmkit/agent/secrets/secrets.go
generated
vendored
Normal file
87
components/engine/vendor/github.com/docker/swarmkit/agent/secrets/secrets.go
generated
vendored
Normal file
@ -0,0 +1,87 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/docker/swarmkit/agent/exec"
|
||||
"github.com/docker/swarmkit/api"
|
||||
)
|
||||
|
||||
// secrets is a map that keeps all the currenty available secrets to the agent
|
||||
// mapped by secret ID
|
||||
type secrets struct {
|
||||
mu sync.RWMutex
|
||||
m map[string]*api.Secret
|
||||
}
|
||||
|
||||
// NewManager returns a place to store secrets.
|
||||
func NewManager() exec.SecretsManager {
|
||||
return &secrets{
|
||||
m: make(map[string]*api.Secret),
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns a secret by ID. If the secret doesn't exist, returns nil.
|
||||
func (s *secrets) Get(secretID string) *api.Secret {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if s, ok := s.m[secretID]; ok {
|
||||
return s
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// add adds one or more secrets to the secret map
|
||||
func (s *secrets) Add(secrets ...api.Secret) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for _, secret := range secrets {
|
||||
s.m[secret.ID] = secret.Copy()
|
||||
}
|
||||
}
|
||||
|
||||
// remove removes one or more secrets by ID from the secret map. Succeeds
|
||||
// whether or not the given IDs are in the map.
|
||||
func (s *secrets) Remove(secrets []string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for _, secret := range secrets {
|
||||
delete(s.m, secret)
|
||||
}
|
||||
}
|
||||
|
||||
// reset removes all the secrets
|
||||
func (s *secrets) Reset() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.m = make(map[string]*api.Secret)
|
||||
}
|
||||
|
||||
// taskRestrictedSecretsProvider restricts the ids to the task.
|
||||
type taskRestrictedSecretsProvider struct {
|
||||
secrets exec.SecretGetter
|
||||
secretIDs map[string]struct{} // allow list of secret ids
|
||||
}
|
||||
|
||||
func (sp *taskRestrictedSecretsProvider) Get(secretID string) *api.Secret {
|
||||
if _, ok := sp.secretIDs[secretID]; !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return sp.secrets.Get(secretID)
|
||||
}
|
||||
|
||||
// Restrict provides a getter that only allows access to the secrets
|
||||
// referenced by the task.
|
||||
func Restrict(secrets exec.SecretGetter, t *api.Task) exec.SecretGetter {
|
||||
sids := map[string]struct{}{}
|
||||
|
||||
container := t.Spec.GetContainer()
|
||||
if container != nil {
|
||||
for _, ref := range container.Secrets {
|
||||
sids[ref.SecretID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return &taskRestrictedSecretsProvider{secrets: secrets, secretIDs: sids}
|
||||
}
|
||||
Reference in New Issue
Block a user