secrets: secret management for swarm
Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>
wip: use tmpfs for swarm secrets
Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>
wip: inject secrets from swarm secret store
Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>
secrets: use secret names in cli for service create
Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>
switch to use mounts instead of volumes
Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>
vendor: use ehazlett swarmkit
Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>
secrets: finish secret update
Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>
Upstream-commit: 72ff77999c
Component: cli
This commit is contained in:
@ -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/cli/secret_create.go
Normal file
24
components/cli/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/create", nil, secret, headers)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
err = json.NewDecoder(resp.body).Decode(&response)
|
||||
ensureReaderClosed(resp)
|
||||
return response, err
|
||||
}
|
||||
57
components/cli/secret_create_test.go
Normal file
57
components/cli/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/create"
|
||||
client := &Client{
|
||||
client: newMockClient(func(req *http.Request) (*http.Response, error) {
|
||||
if !strings.HasPrefix(req.URL.Path, expectedURL) {
|
||||
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
|
||||
}
|
||||
if req.Method != "POST" {
|
||||
return nil, fmt.Errorf("expected POST method, got %s", req.Method)
|
||||
}
|
||||
b, err := json.Marshal(types.SecretCreateResponse{
|
||||
ID: "test_secret",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(b)),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
r, err := client.SecretCreate(context.Background(), swarm.SecretSpec{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.ID != "test_secret" {
|
||||
t.Fatalf("expected `test_secret`, got %s", r.ID)
|
||||
}
|
||||
}
|
||||
34
components/cli/secret_inspect.go
Normal file
34
components/cli/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/cli/secret_inspect_test.go
Normal file
65
components/cli/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/cli/secret_list.go
Normal file
35
components/cli/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.Filter.Len() > 0 {
|
||||
filterJSON, err := filters.ToParam(options.Filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query.Set("filters", filterJSON)
|
||||
}
|
||||
|
||||
resp, err := cli.get(ctx, "/secrets", query, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var secrets []swarm.Secret
|
||||
err = json.NewDecoder(resp.body).Decode(&secrets)
|
||||
ensureReaderClosed(resp)
|
||||
return secrets, err
|
||||
}
|
||||
94
components/cli/secret_list_test.go
Normal file
94
components/cli/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{
|
||||
Filter: filters,
|
||||
},
|
||||
expectedQueryParams: map[string]string{
|
||||
"filters": `{"label":{"label1":true,"label2":true}}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, listCase := range listCases {
|
||||
client := &Client{
|
||||
client: newMockClient(func(req *http.Request) (*http.Response, error) {
|
||||
if !strings.HasPrefix(req.URL.Path, expectedURL) {
|
||||
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
|
||||
}
|
||||
query := req.URL.Query()
|
||||
for key, expected := range listCase.expectedQueryParams {
|
||||
actual := query.Get(key)
|
||||
if actual != expected {
|
||||
return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
|
||||
}
|
||||
}
|
||||
content, err := json.Marshal([]swarm.Secret{
|
||||
{
|
||||
ID: "secret_id1",
|
||||
},
|
||||
{
|
||||
ID: "secret_id2",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(content)),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
secrets, err := client.SecretList(context.Background(), listCase.options)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(secrets) != 2 {
|
||||
t.Fatalf("expected 2 secrets, got %v", secrets)
|
||||
}
|
||||
}
|
||||
}
|
||||
10
components/cli/secret_remove.go
Normal file
10
components/cli/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/cli/secret_remove_test.go
Normal file
47
components/cli/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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user