Merge pull request #32336 from aaronlehmann/configs
Configuration files for services Upstream-commit: 69c35dad8e7ec21de32d42b9dd606d3416ae1566 Component: engine
This commit is contained in:
@@ -16,21 +16,32 @@ type Backend interface {
|
||||
Update(uint64, types.Spec, types.UpdateFlags) error
|
||||
GetUnlockKey() (string, error)
|
||||
UnlockSwarm(req types.UnlockRequest) error
|
||||
|
||||
GetServices(basictypes.ServiceListOptions) ([]types.Service, error)
|
||||
GetService(idOrName string, insertDefaults bool) (types.Service, error)
|
||||
CreateService(types.ServiceSpec, string) (*basictypes.ServiceCreateResponse, error)
|
||||
UpdateService(string, uint64, types.ServiceSpec, basictypes.ServiceUpdateOptions) (*basictypes.ServiceUpdateResponse, error)
|
||||
RemoveService(string) error
|
||||
|
||||
ServiceLogs(context.Context, *backend.LogSelector, *basictypes.ContainerLogsOptions) (<-chan *backend.LogMessage, error)
|
||||
|
||||
GetNodes(basictypes.NodeListOptions) ([]types.Node, error)
|
||||
GetNode(string) (types.Node, error)
|
||||
UpdateNode(string, uint64, types.NodeSpec) error
|
||||
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(idOrName string) error
|
||||
GetSecret(id string) (types.Secret, error)
|
||||
UpdateSecret(idOrName string, version uint64, spec types.SecretSpec) error
|
||||
|
||||
GetConfigs(opts basictypes.ConfigListOptions) ([]types.Config, error)
|
||||
CreateConfig(s types.ConfigSpec) (string, error)
|
||||
RemoveConfig(id string) error
|
||||
GetConfig(id string) (types.Config, error)
|
||||
UpdateConfig(idOrName string, version uint64, spec types.ConfigSpec) error
|
||||
}
|
||||
|
||||
@@ -31,23 +31,33 @@ func (sr *swarmRouter) initRoutes() {
|
||||
router.NewGetRoute("/swarm/unlockkey", sr.getUnlockKey),
|
||||
router.NewPostRoute("/swarm/update", sr.updateCluster),
|
||||
router.NewPostRoute("/swarm/unlock", sr.unlockCluster),
|
||||
|
||||
router.NewGetRoute("/services", sr.getServices),
|
||||
router.NewGetRoute("/services/{id}", sr.getService),
|
||||
router.NewPostRoute("/services/create", sr.createService),
|
||||
router.NewPostRoute("/services/{id}/update", sr.updateService),
|
||||
router.NewDeleteRoute("/services/{id}", sr.removeService),
|
||||
router.NewGetRoute("/services/{id}/logs", sr.getServiceLogs, router.WithCancel),
|
||||
|
||||
router.NewGetRoute("/nodes", sr.getNodes),
|
||||
router.NewGetRoute("/nodes/{id}", sr.getNode),
|
||||
router.NewDeleteRoute("/nodes/{id}", sr.removeNode),
|
||||
router.NewPostRoute("/nodes/{id}/update", sr.updateNode),
|
||||
|
||||
router.NewGetRoute("/tasks", sr.getTasks),
|
||||
router.NewGetRoute("/tasks/{id}", sr.getTask),
|
||||
router.NewGetRoute("/tasks/{id}/logs", sr.getTaskLogs, router.WithCancel),
|
||||
|
||||
router.NewGetRoute("/secrets", sr.getSecrets),
|
||||
router.NewPostRoute("/secrets/create", sr.createSecret),
|
||||
router.NewDeleteRoute("/secrets/{id}", sr.removeSecret),
|
||||
router.NewGetRoute("/secrets/{id}", sr.getSecret),
|
||||
router.NewPostRoute("/secrets/{id}/update", sr.updateSecret),
|
||||
|
||||
router.NewGetRoute("/configs", sr.getConfigs),
|
||||
router.NewPostRoute("/configs/create", sr.createConfig),
|
||||
router.NewDeleteRoute("/configs/{id}", sr.removeConfig),
|
||||
router.NewGetRoute("/configs/{id}", sr.getConfig),
|
||||
router.NewPostRoute("/configs/{id}/update", sr.updateConfig),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,3 +408,74 @@ func (sr *swarmRouter) updateSecret(ctx context.Context, w http.ResponseWriter,
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sr *swarmRouter) getConfigs(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
|
||||
}
|
||||
|
||||
configs, err := sr.backend.GetConfigs(basictypes.ConfigListOptions{Filters: filters})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return httputils.WriteJSON(w, http.StatusOK, configs)
|
||||
}
|
||||
|
||||
func (sr *swarmRouter) createConfig(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
var config types.ConfigSpec
|
||||
if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id, err := sr.backend.CreateConfig(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return httputils.WriteJSON(w, http.StatusCreated, &basictypes.ConfigCreateResponse{
|
||||
ID: id,
|
||||
})
|
||||
}
|
||||
|
||||
func (sr *swarmRouter) removeConfig(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
if err := sr.backend.RemoveConfig(vars["id"]); err != nil {
|
||||
return err
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sr *swarmRouter) getConfig(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
config, err := sr.backend.GetConfig(vars["id"])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return httputils.WriteJSON(w, http.StatusOK, config)
|
||||
}
|
||||
|
||||
func (sr *swarmRouter) updateConfig(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
var config types.ConfigSpec
|
||||
if err := json.NewDecoder(r.Body).Decode(&config); 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 config version"))
|
||||
}
|
||||
|
||||
id := vars["id"]
|
||||
if err := sr.backend.UpdateConfig(id, version, config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package swarm
|
||||
|
||||
import "os"
|
||||
|
||||
// Config represents a config.
|
||||
type Config struct {
|
||||
ID string
|
||||
Meta
|
||||
Spec ConfigSpec
|
||||
}
|
||||
|
||||
// ConfigSpec represents a config specification from a config in swarm
|
||||
type ConfigSpec struct {
|
||||
Annotations
|
||||
Data []byte `json:",omitempty"`
|
||||
}
|
||||
|
||||
// ConfigReferenceFileTarget is a file target in a config reference
|
||||
type ConfigReferenceFileTarget struct {
|
||||
Name string
|
||||
UID string
|
||||
GID string
|
||||
Mode os.FileMode
|
||||
}
|
||||
|
||||
// ConfigReference is a reference to a config in swarm
|
||||
type ConfigReference struct {
|
||||
File *ConfigReferenceFileTarget
|
||||
ConfigID string
|
||||
ConfigName string
|
||||
}
|
||||
@@ -68,4 +68,5 @@ type ContainerSpec struct {
|
||||
Hosts []string `json:",omitempty"`
|
||||
DNSConfig *DNSConfig `json:",omitempty"`
|
||||
Secrets []*SecretReference `json:",omitempty"`
|
||||
Configs []*ConfigReference `json:",omitempty"`
|
||||
}
|
||||
|
||||
@@ -522,6 +522,18 @@ type SecretListOptions struct {
|
||||
Filters filters.Args
|
||||
}
|
||||
|
||||
// ConfigCreateResponse contains the information returned to a client
|
||||
// on the creation of a new config.
|
||||
type ConfigCreateResponse struct {
|
||||
// ID is the id of the created config.
|
||||
ID string
|
||||
}
|
||||
|
||||
// ConfigListOptions holds parameters to list configs
|
||||
type ConfigListOptions struct {
|
||||
Filters filters.Args
|
||||
}
|
||||
|
||||
// PushResult contains the tag, manifest digest, and manifest size from the
|
||||
// push. It's used to signal this information to the trust code in the client
|
||||
// so it can sign the manifest if necessary.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// ConfigCreate creates a new Config.
|
||||
func (cli *Client) ConfigCreate(ctx context.Context, config swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||
var response types.ConfigCreateResponse
|
||||
resp, err := cli.post(ctx, "/configs/create", nil, config, nil)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
err = json.NewDecoder(resp.body).Decode(&response)
|
||||
ensureReaderClosed(resp)
|
||||
return response, err
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestConfigCreateError(t *testing.T) {
|
||||
client := &Client{
|
||||
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
|
||||
}
|
||||
_, err := client.ConfigCreate(context.Background(), swarm.ConfigSpec{})
|
||||
if err == nil || err.Error() != "Error response from daemon: Server error" {
|
||||
t.Fatalf("expected a Server Error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigCreate(t *testing.T) {
|
||||
expectedURL := "/configs/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.ConfigCreateResponse{
|
||||
ID: "test_config",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusCreated,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(b)),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
r, err := client.ConfigCreate(context.Background(), swarm.ConfigSpec{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.ID != "test_config" {
|
||||
t.Fatalf("expected `test_config`, got %s", r.ID)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// ConfigInspectWithRaw returns the config information with raw data
|
||||
func (cli *Client) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error) {
|
||||
resp, err := cli.get(ctx, "/configs/"+id, nil, nil)
|
||||
if err != nil {
|
||||
if resp.statusCode == http.StatusNotFound {
|
||||
return swarm.Config{}, nil, configNotFoundError{id}
|
||||
}
|
||||
return swarm.Config{}, nil, err
|
||||
}
|
||||
defer ensureReaderClosed(resp)
|
||||
|
||||
body, err := ioutil.ReadAll(resp.body)
|
||||
if err != nil {
|
||||
return swarm.Config{}, nil, err
|
||||
}
|
||||
|
||||
var config swarm.Config
|
||||
rdr := bytes.NewReader(body)
|
||||
err = json.NewDecoder(rdr).Decode(&config)
|
||||
|
||||
return config, body, err
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestConfigInspectError(t *testing.T) {
|
||||
client := &Client{
|
||||
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
|
||||
}
|
||||
|
||||
_, _, err := client.ConfigInspectWithRaw(context.Background(), "nothing")
|
||||
if err == nil || err.Error() != "Error response from daemon: Server error" {
|
||||
t.Fatalf("expected a Server Error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigInspectConfigNotFound(t *testing.T) {
|
||||
client := &Client{
|
||||
client: newMockClient(errorMock(http.StatusNotFound, "Server error")),
|
||||
}
|
||||
|
||||
_, _, err := client.ConfigInspectWithRaw(context.Background(), "unknown")
|
||||
if err == nil || !IsErrConfigNotFound(err) {
|
||||
t.Fatalf("expected a configNotFoundError error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigInspect(t *testing.T) {
|
||||
expectedURL := "/configs/config_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.Config{
|
||||
ID: "config_id",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(content)),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
configInspect, _, err := client.ConfigInspectWithRaw(context.Background(), "config_id")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if configInspect.ID != "config_id" {
|
||||
t.Fatalf("expected `config_id`, got %s", configInspect.ID)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// ConfigList returns the list of configs.
|
||||
func (cli *Client) ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, 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, "/configs", query, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var configs []swarm.Config
|
||||
err = json.NewDecoder(resp.body).Decode(&configs)
|
||||
ensureReaderClosed(resp)
|
||||
return configs, err
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestConfigListError(t *testing.T) {
|
||||
client := &Client{
|
||||
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
|
||||
}
|
||||
|
||||
_, err := client.ConfigList(context.Background(), types.ConfigListOptions{})
|
||||
if err == nil || err.Error() != "Error response from daemon: Server error" {
|
||||
t.Fatalf("expected a Server Error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigList(t *testing.T) {
|
||||
expectedURL := "/configs"
|
||||
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("label", "label1")
|
||||
filters.Add("label", "label2")
|
||||
|
||||
listCases := []struct {
|
||||
options types.ConfigListOptions
|
||||
expectedQueryParams map[string]string
|
||||
}{
|
||||
{
|
||||
options: types.ConfigListOptions{},
|
||||
expectedQueryParams: map[string]string{
|
||||
"filters": "",
|
||||
},
|
||||
},
|
||||
{
|
||||
options: types.ConfigListOptions{
|
||||
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.Config{
|
||||
{
|
||||
ID: "config_id1",
|
||||
},
|
||||
{
|
||||
ID: "config_id2",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(content)),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
configs, err := client.ConfigList(context.Background(), listCase.options)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(configs) != 2 {
|
||||
t.Fatalf("expected 2 configs, got %v", configs)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package client
|
||||
|
||||
import "golang.org/x/net/context"
|
||||
|
||||
// ConfigRemove removes a Config.
|
||||
func (cli *Client) ConfigRemove(ctx context.Context, id string) error {
|
||||
resp, err := cli.delete(ctx, "/configs/"+id, nil, nil)
|
||||
ensureReaderClosed(resp)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestConfigRemoveError(t *testing.T) {
|
||||
client := &Client{
|
||||
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
|
||||
}
|
||||
|
||||
err := client.ConfigRemove(context.Background(), "config_id")
|
||||
if err == nil || err.Error() != "Error response from daemon: Server error" {
|
||||
t.Fatalf("expected a Server Error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigRemove(t *testing.T) {
|
||||
expectedURL := "/configs/config_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.ConfigRemove(context.Background(), "config_id")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// ConfigUpdate attempts to updates a Config
|
||||
func (cli *Client) ConfigUpdate(ctx context.Context, id string, version swarm.Version, config swarm.ConfigSpec) error {
|
||||
query := url.Values{}
|
||||
query.Set("version", strconv.FormatUint(version.Index, 10))
|
||||
resp, err := cli.post(ctx, "/configs/"+id+"/update", query, config, nil)
|
||||
ensureReaderClosed(resp)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
)
|
||||
|
||||
func TestConfigUpdateError(t *testing.T) {
|
||||
client := &Client{
|
||||
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
|
||||
}
|
||||
|
||||
err := client.ConfigUpdate(context.Background(), "config_id", swarm.Version{}, swarm.ConfigSpec{})
|
||||
if err == nil || err.Error() != "Error response from daemon: Server error" {
|
||||
t.Fatalf("expected a Server Error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigUpdate(t *testing.T) {
|
||||
expectedURL := "/configs/config_id/update"
|
||||
|
||||
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)
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte("body"))),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
err := client.ConfigUpdate(context.Background(), "config_id", swarm.Version{}, swarm.ConfigSpec{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -256,6 +256,28 @@ func IsErrSecretNotFound(err error) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
// configNotFoundError implements an error returned when a config is not found.
|
||||
type configNotFoundError struct {
|
||||
name string
|
||||
}
|
||||
|
||||
// Error returns a string representation of a configNotFoundError
|
||||
func (e configNotFoundError) Error() string {
|
||||
return fmt.Sprintf("Error: no such config: %s", e.name)
|
||||
}
|
||||
|
||||
// NotFound indicates that this error type is of NotFound
|
||||
func (e configNotFoundError) NotFound() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsErrConfigNotFound returns true if the error is caused
|
||||
// when a config is not found.
|
||||
func IsErrConfigNotFound(err error) bool {
|
||||
_, ok := err.(configNotFoundError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// pluginNotFoundError implements an error returned when a plugin is not in the docker host.
|
||||
type pluginNotFoundError struct {
|
||||
name string
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
// CommonAPIClient is the common methods between stable and experimental versions of APIClient.
|
||||
type CommonAPIClient interface {
|
||||
ConfigAPIClient
|
||||
ContainerAPIClient
|
||||
ImageAPIClient
|
||||
NodeAPIClient
|
||||
@@ -171,3 +172,12 @@ type SecretAPIClient interface {
|
||||
SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error)
|
||||
SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error
|
||||
}
|
||||
|
||||
// ConfigAPIClient defines API client methods for configs
|
||||
type ConfigAPIClient interface {
|
||||
ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error)
|
||||
ConfigCreate(ctx context.Context, config swarm.ConfigSpec) (types.ConfigCreateResponse, error)
|
||||
ConfigRemove(ctx context.Context, id string) error
|
||||
ConfigInspectWithRaw(ctx context.Context, name string) (swarm.Config, []byte, error)
|
||||
ConfigUpdate(ctx context.Context, id string, version swarm.Version, config swarm.ConfigSpec) error
|
||||
}
|
||||
|
||||
@@ -87,8 +87,9 @@ type CommonContainer struct {
|
||||
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:"-"`
|
||||
SecretStore agentexec.SecretGetter `json:"-"`
|
||||
DependencyStore agentexec.DependencyGetter `json:"-"`
|
||||
SecretReferences []*swarmtypes.SecretReference
|
||||
ConfigReferences []*swarmtypes.ConfigReference
|
||||
// logDriver for closing
|
||||
LogDriver logger.Logger `json:"-"`
|
||||
LogCopier *logger.Copier `json:"-"`
|
||||
@@ -966,3 +967,14 @@ func getSecretTargetPath(r *swarmtypes.SecretReference) string {
|
||||
|
||||
return filepath.Join(containerSecretMountPath, r.File.Name)
|
||||
}
|
||||
|
||||
// ConfigsDirPath returns the path to the directory where configs are stored on
|
||||
// disk.
|
||||
func (container *Container) ConfigsDirPath() string {
|
||||
return filepath.Join(container.Root, "configs")
|
||||
}
|
||||
|
||||
// ConfigFilePath returns the path to the on-disk location of a config.
|
||||
func (container *Container) ConfigFilePath(configRef swarmtypes.ConfigReference) string {
|
||||
return filepath.Join(container.ConfigsDirPath(), configRef.ConfigID)
|
||||
}
|
||||
|
||||
@@ -277,6 +277,23 @@ func (container *Container) UnmountSecrets() error {
|
||||
return detachMounted(container.SecretMountPath())
|
||||
}
|
||||
|
||||
// ConfigMounts returns the mounts for configs.
|
||||
func (container *Container) ConfigMounts() []Mount {
|
||||
var mounts []Mount
|
||||
for _, configRef := range container.ConfigReferences {
|
||||
if configRef.File == nil {
|
||||
continue
|
||||
}
|
||||
mounts = append(mounts, Mount{
|
||||
Source: container.ConfigFilePath(*configRef),
|
||||
Destination: configRef.File.Name,
|
||||
Writable: false,
|
||||
})
|
||||
}
|
||||
|
||||
return mounts
|
||||
}
|
||||
|
||||
// UpdateContainer updates configuration of a container.
|
||||
func (container *Container) UpdateContainer(hostConfig *containertypes.HostConfig) error {
|
||||
container.Lock()
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
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"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// GetConfig returns a config from a managed swarm cluster
|
||||
func (c *Cluster) GetConfig(input string) (types.Config, error) {
|
||||
var config *swarmapi.Config
|
||||
|
||||
if err := c.lockedManagerAction(func(ctx context.Context, state nodeState) error {
|
||||
s, err := getConfig(ctx, state.controlClient, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config = s
|
||||
return nil
|
||||
}); err != nil {
|
||||
return types.Config{}, err
|
||||
}
|
||||
return convert.ConfigFromGRPC(config), nil
|
||||
}
|
||||
|
||||
// GetConfigs returns all configs of a managed swarm cluster.
|
||||
func (c *Cluster) GetConfigs(options apitypes.ConfigListOptions) ([]types.Config, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
state := c.currentNodeState()
|
||||
if !state.IsActiveManager() {
|
||||
return nil, c.errNoManager(state)
|
||||
}
|
||||
|
||||
filters, err := newListConfigsFilters(options.Filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx, cancel := c.getRequestContext()
|
||||
defer cancel()
|
||||
|
||||
r, err := state.controlClient.ListConfigs(ctx,
|
||||
&swarmapi.ListConfigsRequest{Filters: filters})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configs := []types.Config{}
|
||||
|
||||
for _, config := range r.Configs {
|
||||
configs = append(configs, convert.ConfigFromGRPC(config))
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// CreateConfig creates a new config in a managed swarm cluster.
|
||||
func (c *Cluster) CreateConfig(s types.ConfigSpec) (string, error) {
|
||||
var resp *swarmapi.CreateConfigResponse
|
||||
if err := c.lockedManagerAction(func(ctx context.Context, state nodeState) error {
|
||||
configSpec := convert.ConfigSpecToGRPC(s)
|
||||
|
||||
r, err := state.controlClient.CreateConfig(ctx,
|
||||
&swarmapi.CreateConfigRequest{Spec: &configSpec})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp = r
|
||||
return nil
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.Config.ID, nil
|
||||
}
|
||||
|
||||
// RemoveConfig removes a config from a managed swarm cluster.
|
||||
func (c *Cluster) RemoveConfig(input string) error {
|
||||
return c.lockedManagerAction(func(ctx context.Context, state nodeState) error {
|
||||
config, err := getConfig(ctx, state.controlClient, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &swarmapi.RemoveConfigRequest{
|
||||
ConfigID: config.ID,
|
||||
}
|
||||
|
||||
_, err = state.controlClient.RemoveConfig(ctx, req)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateConfig updates a config in a managed swarm cluster.
|
||||
// Note: this is not exposed to the CLI but is available from the API only
|
||||
func (c *Cluster) UpdateConfig(input string, version uint64, spec types.ConfigSpec) error {
|
||||
return c.lockedManagerAction(func(ctx context.Context, state nodeState) error {
|
||||
config, err := getConfig(ctx, state.controlClient, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configSpec := convert.ConfigSpecToGRPC(spec)
|
||||
|
||||
_, err = state.controlClient.UpdateConfig(ctx,
|
||||
&swarmapi.UpdateConfigRequest{
|
||||
ConfigID: config.ID,
|
||||
ConfigVersion: &swarmapi.Version{
|
||||
Index: version,
|
||||
},
|
||||
Spec: &configSpec,
|
||||
})
|
||||
return err
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
swarmtypes "github.com/docker/docker/api/types/swarm"
|
||||
swarmapi "github.com/docker/swarmkit/api"
|
||||
gogotypes "github.com/gogo/protobuf/types"
|
||||
)
|
||||
|
||||
// ConfigFromGRPC converts a grpc Config to a Config.
|
||||
func ConfigFromGRPC(s *swarmapi.Config) swarmtypes.Config {
|
||||
config := swarmtypes.Config{
|
||||
ID: s.ID,
|
||||
Spec: swarmtypes.ConfigSpec{
|
||||
Annotations: annotationsFromGRPC(s.Spec.Annotations),
|
||||
Data: s.Spec.Data,
|
||||
},
|
||||
}
|
||||
|
||||
config.Version.Index = s.Meta.Version.Index
|
||||
// Meta
|
||||
config.CreatedAt, _ = gogotypes.TimestampFromProto(s.Meta.CreatedAt)
|
||||
config.UpdatedAt, _ = gogotypes.TimestampFromProto(s.Meta.UpdatedAt)
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// ConfigSpecToGRPC converts Config to a grpc Config.
|
||||
func ConfigSpecToGRPC(s swarmtypes.ConfigSpec) swarmapi.ConfigSpec {
|
||||
return swarmapi.ConfigSpec{
|
||||
Annotations: swarmapi.Annotations{
|
||||
Name: s.Name,
|
||||
Labels: s.Labels,
|
||||
},
|
||||
Data: s.Data,
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigReferencesFromGRPC converts a slice of grpc ConfigReference to ConfigReference
|
||||
func ConfigReferencesFromGRPC(s []*swarmapi.ConfigReference) []*swarmtypes.ConfigReference {
|
||||
refs := []*swarmtypes.ConfigReference{}
|
||||
|
||||
for _, r := range s {
|
||||
ref := &swarmtypes.ConfigReference{
|
||||
ConfigID: r.ConfigID,
|
||||
ConfigName: r.ConfigName,
|
||||
}
|
||||
|
||||
if t, ok := r.Target.(*swarmapi.ConfigReference_File); ok {
|
||||
ref.File = &swarmtypes.ConfigReferenceFileTarget{
|
||||
Name: t.File.Name,
|
||||
UID: t.File.UID,
|
||||
GID: t.File.GID,
|
||||
Mode: t.File.Mode,
|
||||
}
|
||||
}
|
||||
|
||||
refs = append(refs, ref)
|
||||
}
|
||||
|
||||
return refs
|
||||
}
|
||||
@@ -30,6 +30,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec {
|
||||
ReadOnly: c.ReadOnly,
|
||||
Hosts: c.Hosts,
|
||||
Secrets: secretReferencesFromGRPC(c.Secrets),
|
||||
Configs: configReferencesFromGRPC(c.Configs),
|
||||
}
|
||||
|
||||
if c.DNSConfig != nil {
|
||||
@@ -137,6 +138,7 @@ func secretReferencesToGRPC(sr []*types.SecretReference) []*swarmapi.SecretRefer
|
||||
|
||||
return refs
|
||||
}
|
||||
|
||||
func secretReferencesFromGRPC(sr []*swarmapi.SecretReference) []*types.SecretReference {
|
||||
refs := make([]*types.SecretReference, 0, len(sr))
|
||||
for _, s := range sr {
|
||||
@@ -161,6 +163,54 @@ func secretReferencesFromGRPC(sr []*swarmapi.SecretReference) []*types.SecretRef
|
||||
return refs
|
||||
}
|
||||
|
||||
func configReferencesToGRPC(sr []*types.ConfigReference) []*swarmapi.ConfigReference {
|
||||
refs := make([]*swarmapi.ConfigReference, 0, len(sr))
|
||||
for _, s := range sr {
|
||||
ref := &swarmapi.ConfigReference{
|
||||
ConfigID: s.ConfigID,
|
||||
ConfigName: s.ConfigName,
|
||||
}
|
||||
if s.File != nil {
|
||||
ref.Target = &swarmapi.ConfigReference_File{
|
||||
File: &swarmapi.FileTarget{
|
||||
Name: s.File.Name,
|
||||
UID: s.File.UID,
|
||||
GID: s.File.GID,
|
||||
Mode: s.File.Mode,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
refs = append(refs, ref)
|
||||
}
|
||||
|
||||
return refs
|
||||
}
|
||||
|
||||
func configReferencesFromGRPC(sr []*swarmapi.ConfigReference) []*types.ConfigReference {
|
||||
refs := make([]*types.ConfigReference, 0, len(sr))
|
||||
for _, s := range sr {
|
||||
target := s.GetFile()
|
||||
if target == nil {
|
||||
// not a file target
|
||||
logrus.Warnf("config target not a file: config=%s", s.ConfigID)
|
||||
continue
|
||||
}
|
||||
refs = append(refs, &types.ConfigReference{
|
||||
File: &types.ConfigReferenceFileTarget{
|
||||
Name: target.Name,
|
||||
UID: target.UID,
|
||||
GID: target.GID,
|
||||
Mode: target.Mode,
|
||||
},
|
||||
ConfigID: s.ConfigID,
|
||||
ConfigName: s.ConfigName,
|
||||
})
|
||||
}
|
||||
|
||||
return refs
|
||||
}
|
||||
|
||||
func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
|
||||
containerSpec := &swarmapi.ContainerSpec{
|
||||
Image: c.Image,
|
||||
@@ -178,6 +228,7 @@ func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
|
||||
ReadOnly: c.ReadOnly,
|
||||
Hosts: c.Hosts,
|
||||
Secrets: secretReferencesToGRPC(c.Secrets),
|
||||
Configs: configReferencesToGRPC(c.Configs),
|
||||
}
|
||||
|
||||
if c.DNSConfig != nil {
|
||||
|
||||
@@ -42,8 +42,9 @@ type Backend interface {
|
||||
ContainerWaitWithContext(ctx context.Context, name string) error
|
||||
ContainerRm(name string, config *types.ContainerRmConfig) error
|
||||
ContainerKill(name string, sig uint64) error
|
||||
SetContainerSecretStore(name string, store exec.SecretGetter) error
|
||||
SetContainerDependencyStore(name string, store exec.DependencyGetter) error
|
||||
SetContainerSecretReferences(name string, refs []*swarmtypes.SecretReference) error
|
||||
SetContainerConfigReferences(name string, refs []*swarmtypes.ConfigReference) error
|
||||
SystemInfo() (*types.Info, error)
|
||||
VolumeCreate(name, driverName string, opts, labels map[string]string) (*types.Volume, error)
|
||||
Containers(config *types.ContainerListOptions) ([]*types.Container, error)
|
||||
|
||||
@@ -33,21 +33,21 @@ import (
|
||||
// are mostly naked calls to the client API, seeded with information from
|
||||
// containerConfig.
|
||||
type containerAdapter struct {
|
||||
backend executorpkg.Backend
|
||||
container *containerConfig
|
||||
secrets exec.SecretGetter
|
||||
backend executorpkg.Backend
|
||||
container *containerConfig
|
||||
dependencies exec.DependencyGetter
|
||||
}
|
||||
|
||||
func newContainerAdapter(b executorpkg.Backend, task *api.Task, secrets exec.SecretGetter) (*containerAdapter, error) {
|
||||
func newContainerAdapter(b executorpkg.Backend, task *api.Task, dependencies exec.DependencyGetter) (*containerAdapter, error) {
|
||||
ctnr, err := newContainerConfig(task)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &containerAdapter{
|
||||
container: ctnr,
|
||||
backend: b,
|
||||
secrets: secrets,
|
||||
container: ctnr,
|
||||
backend: b,
|
||||
dependencies: dependencies,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -243,13 +243,18 @@ func (c *containerAdapter) create(ctx context.Context) error {
|
||||
return errors.New("unable to get container from task spec")
|
||||
}
|
||||
|
||||
// configure secrets
|
||||
if err := c.backend.SetContainerSecretStore(cr.ID, c.secrets); err != nil {
|
||||
if err := c.backend.SetContainerDependencyStore(cr.ID, c.dependencies); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
refs := convert.SecretReferencesFromGRPC(container.Secrets)
|
||||
if err := c.backend.SetContainerSecretReferences(cr.ID, refs); err != nil {
|
||||
// configure secrets
|
||||
secretRefs := convert.SecretReferencesFromGRPC(container.Secrets)
|
||||
if err := c.backend.SetContainerSecretReferences(cr.ID, secretRefs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configRefs := convert.ConfigReferencesFromGRPC(container.Configs)
|
||||
if err := c.backend.SetContainerConfigReferences(cr.ID, configRefs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ type networkAttacherController struct {
|
||||
closed chan struct{}
|
||||
}
|
||||
|
||||
func newNetworkAttacherController(b executorpkg.Backend, task *api.Task, secrets exec.SecretGetter) (*networkAttacherController, error) {
|
||||
adapter, err := newContainerAdapter(b, task, secrets)
|
||||
func newNetworkAttacherController(b executorpkg.Backend, task *api.Task, dependencies exec.DependencyGetter) (*networkAttacherController, error) {
|
||||
adapter, err := newContainerAdapter(b, task, dependencies)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -39,8 +39,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, secrets exec.SecretGetter) (*controller, error) {
|
||||
adapter, err := newContainerAdapter(b, task, secrets)
|
||||
func newController(b executorpkg.Backend, task *api.Task, dependencies exec.DependencyGetter) (*controller, error) {
|
||||
adapter, err := newContainerAdapter(b, task, dependencies)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -14,23 +14,23 @@ import (
|
||||
executorpkg "github.com/docker/docker/daemon/cluster/executor"
|
||||
clustertypes "github.com/docker/docker/daemon/cluster/provider"
|
||||
networktypes "github.com/docker/libnetwork/types"
|
||||
"github.com/docker/swarmkit/agent"
|
||||
"github.com/docker/swarmkit/agent/exec"
|
||||
"github.com/docker/swarmkit/agent/secrets"
|
||||
"github.com/docker/swarmkit/api"
|
||||
"github.com/docker/swarmkit/api/naming"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type executor struct {
|
||||
backend executorpkg.Backend
|
||||
secrets exec.SecretsManager
|
||||
backend executorpkg.Backend
|
||||
dependencies exec.DependencyManager
|
||||
}
|
||||
|
||||
// NewExecutor returns an executor from the docker client.
|
||||
func NewExecutor(b executorpkg.Backend) exec.Executor {
|
||||
return &executor{
|
||||
backend: b,
|
||||
secrets: secrets.NewManager(),
|
||||
backend: b,
|
||||
dependencies: agent.NewDependencyManager(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,8 +162,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) {
|
||||
dependencyGetter := agent.Restrict(e.dependencies, t)
|
||||
|
||||
if t.Spec.GetAttachment() != nil {
|
||||
return newNetworkAttacherController(e.backend, t, e.secrets)
|
||||
return newNetworkAttacherController(e.backend, t, dependencyGetter)
|
||||
}
|
||||
|
||||
var ctlr exec.Controller
|
||||
@@ -188,7 +190,7 @@ func (e *executor) Controller(t *api.Task) (exec.Controller, error) {
|
||||
return ctlr, fmt.Errorf("unsupported runtime type: %q", r.Generic.Kind)
|
||||
}
|
||||
case *api.TaskSpec_Container:
|
||||
c, err := newController(e.backend, t, secrets.Restrict(e.secrets, t))
|
||||
c, err := newController(e.backend, t, dependencyGetter)
|
||||
if err != nil {
|
||||
return ctlr, err
|
||||
}
|
||||
@@ -218,7 +220,11 @@ func (e *executor) SetNetworkBootstrapKeys(keys []*api.EncryptionKey) error {
|
||||
}
|
||||
|
||||
func (e *executor) Secrets() exec.SecretsManager {
|
||||
return e.secrets
|
||||
return e.dependencies.Secrets()
|
||||
}
|
||||
|
||||
func (e *executor) Configs() exec.ConfigsManager {
|
||||
return e.dependencies.Configs()
|
||||
}
|
||||
|
||||
type sortedPlugins []api.PluginDescription
|
||||
|
||||
@@ -103,3 +103,19 @@ func newListSecretsFilters(filter filters.Args) (*swarmapi.ListSecretsRequest_Fi
|
||||
Labels: runconfigopts.ConvertKVStringsToMap(filter.Get("label")),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newListConfigsFilters(filter filters.Args) (*swarmapi.ListConfigsRequest_Filters, error) {
|
||||
accepted := map[string]bool{
|
||||
"name": true,
|
||||
"id": true,
|
||||
"label": true,
|
||||
}
|
||||
if err := filter.Validate(accepted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &swarmapi.ListConfigsRequest_Filters{
|
||||
NamePrefixes: filter.Get("name"),
|
||||
IDPrefixes: filter.Get("id"),
|
||||
Labels: runconfigopts.ConvertKVStringsToMap(filter.Get("label")),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -55,3 +55,48 @@ func TestNewListSecretsFilters(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewListConfigsFilters(t *testing.T) {
|
||||
validNameFilter := filters.NewArgs()
|
||||
validNameFilter.Add("name", "test_name")
|
||||
|
||||
validIDFilter := filters.NewArgs()
|
||||
validIDFilter.Add("id", "7c9009d6720f6de3b492f5")
|
||||
|
||||
validLabelFilter := filters.NewArgs()
|
||||
validLabelFilter.Add("label", "type=test")
|
||||
validLabelFilter.Add("label", "storage=ssd")
|
||||
validLabelFilter.Add("label", "memory")
|
||||
|
||||
validAllFilter := filters.NewArgs()
|
||||
validAllFilter.Add("name", "nodeName")
|
||||
validAllFilter.Add("id", "7c9009d6720f6de3b492f5")
|
||||
validAllFilter.Add("label", "type=test")
|
||||
validAllFilter.Add("label", "memory")
|
||||
|
||||
validFilters := []filters.Args{
|
||||
validNameFilter,
|
||||
validIDFilter,
|
||||
validLabelFilter,
|
||||
validAllFilter,
|
||||
}
|
||||
|
||||
invalidTypeFilter := filters.NewArgs()
|
||||
invalidTypeFilter.Add("nonexist", "aaaa")
|
||||
|
||||
invalidFilters := []filters.Args{
|
||||
invalidTypeFilter,
|
||||
}
|
||||
|
||||
for _, filter := range validFilters {
|
||||
if _, err := newListConfigsFilters(filter); err != nil {
|
||||
t.Fatalf("Should get no error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, filter := range invalidFilters {
|
||||
if _, err := newListConfigsFilters(filter); err == nil {
|
||||
t.Fatalf("Should get an error for filter %s, while got nil", filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +174,42 @@ func getSecret(ctx context.Context, c swarmapi.ControlClient, input string) (*sw
|
||||
return rl.Secrets[0], nil
|
||||
}
|
||||
|
||||
func getConfig(ctx context.Context, c swarmapi.ControlClient, input string) (*swarmapi.Config, error) {
|
||||
// attempt to lookup config by full ID
|
||||
if rg, err := c.GetConfig(ctx, &swarmapi.GetConfigRequest{ConfigID: input}); err == nil {
|
||||
return rg.Config, nil
|
||||
}
|
||||
|
||||
// If any error (including NotFound), ListConfigs to match via full name.
|
||||
rl, err := c.ListConfigs(ctx, &swarmapi.ListConfigsRequest{
|
||||
Filters: &swarmapi.ListConfigsRequest_Filters{
|
||||
Names: []string{input},
|
||||
},
|
||||
})
|
||||
if err != nil || len(rl.Configs) == 0 {
|
||||
// If any error or 0 result, ListConfigs to match via ID prefix.
|
||||
rl, err = c.ListConfigs(ctx, &swarmapi.ListConfigsRequest{
|
||||
Filters: &swarmapi.ListConfigsRequest_Filters{
|
||||
IDPrefixes: []string{input},
|
||||
},
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(rl.Configs) == 0 {
|
||||
err := fmt.Errorf("config %s not found", input)
|
||||
return nil, errors.NewRequestNotFoundError(err)
|
||||
}
|
||||
|
||||
if l := len(rl.Configs); l > 1 {
|
||||
return nil, fmt.Errorf("config %s is ambiguous (%d matches found)", input, l)
|
||||
}
|
||||
|
||||
return rl.Configs[0], nil
|
||||
}
|
||||
|
||||
func getNetwork(ctx context.Context, c swarmapi.ControlClient, input string) (*swarmapi.Network, error) {
|
||||
// GetNetwork to match via full ID.
|
||||
if rg, err := c.GetNetwork(ctx, &swarmapi.GetNetworkRequest{NetworkID: input}); err == nil {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"github.com/Sirupsen/logrus"
|
||||
swarmtypes "github.com/docker/docker/api/types/swarm"
|
||||
)
|
||||
|
||||
// SetContainerConfigReferences sets the container config references needed
|
||||
func (daemon *Daemon) SetContainerConfigReferences(name string, refs []*swarmtypes.ConfigReference) error {
|
||||
if !configsSupported() && len(refs) > 0 {
|
||||
logrus.Warn("configs are not supported on this platform")
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := daemon.GetContainer(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.ConfigReferences = refs
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// +build linux
|
||||
|
||||
package daemon
|
||||
|
||||
func configsSupported() bool {
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// +build !linux
|
||||
|
||||
package daemon
|
||||
|
||||
func configsSupported() bool {
|
||||
return false
|
||||
}
|
||||
@@ -145,6 +145,13 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) {
|
||||
localMountPath := c.SecretMountPath()
|
||||
logrus.Debugf("secrets: setting up secret dir: %s", localMountPath)
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if setupErr != nil {
|
||||
// cleanup
|
||||
@@ -156,25 +163,20 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) {
|
||||
}
|
||||
}()
|
||||
|
||||
// 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.SecretReferences {
|
||||
if c.SecretStore == nil {
|
||||
return fmt.Errorf("secret store is not initialized")
|
||||
}
|
||||
if c.DependencyStore == nil {
|
||||
return fmt.Errorf("secret store is not initialized")
|
||||
}
|
||||
|
||||
for _, s := range c.SecretReferences {
|
||||
// TODO (ehazlett): use type switch when more are supported
|
||||
if s.File == nil {
|
||||
return fmt.Errorf("secret target type is not a file target")
|
||||
logrus.Error("secret target type is not a file target")
|
||||
continue
|
||||
}
|
||||
|
||||
// secrets are created in the SecretMountPath on the host, at a
|
||||
@@ -188,7 +190,7 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) {
|
||||
"name": s.File.Name,
|
||||
"path": fPath,
|
||||
}).Debug("injecting secret")
|
||||
secret := c.SecretStore.Get(s.SecretID)
|
||||
secret := c.DependencyStore.Secrets().Get(s.SecretID)
|
||||
if secret == nil {
|
||||
return fmt.Errorf("unable to get secret from secret store")
|
||||
}
|
||||
@@ -220,6 +222,74 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (daemon *Daemon) setupConfigDir(c *container.Container) (setupErr error) {
|
||||
if len(c.ConfigReferences) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
localPath := c.ConfigsDirPath()
|
||||
logrus.Debugf("configs: setting up config dir: %s", localPath)
|
||||
|
||||
// retrieve possible remapped range start for root UID, GID
|
||||
rootUID, rootGID := daemon.GetRemappedUIDGID()
|
||||
// create tmpfs
|
||||
if err := idtools.MkdirAllAs(localPath, 0700, rootUID, rootGID); err != nil {
|
||||
return errors.Wrap(err, "error creating config dir")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if setupErr != nil {
|
||||
if err := os.RemoveAll(localPath); err != nil {
|
||||
logrus.Errorf("error cleaning up config dir: %s", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if c.DependencyStore == nil {
|
||||
return fmt.Errorf("config store is not initialized")
|
||||
}
|
||||
|
||||
for _, configRef := range c.ConfigReferences {
|
||||
// TODO (ehazlett): use type switch when more are supported
|
||||
if configRef.File == nil {
|
||||
logrus.Error("config target type is not a file target")
|
||||
continue
|
||||
}
|
||||
|
||||
fPath := c.ConfigFilePath(*configRef)
|
||||
|
||||
log := logrus.WithFields(logrus.Fields{"name": configRef.File.Name, "path": fPath})
|
||||
|
||||
if err := idtools.MkdirAllAs(filepath.Dir(fPath), 0700, rootUID, rootGID); err != nil {
|
||||
return errors.Wrap(err, "error creating config path")
|
||||
}
|
||||
|
||||
log.Debug("injecting config")
|
||||
config := c.DependencyStore.Configs().Get(configRef.ConfigID)
|
||||
if config == nil {
|
||||
return fmt.Errorf("unable to get config from config store")
|
||||
}
|
||||
if err := ioutil.WriteFile(fPath, config.Spec.Data, configRef.File.Mode); err != nil {
|
||||
return errors.Wrap(err, "error injecting config")
|
||||
}
|
||||
|
||||
uid, err := strconv.Atoi(configRef.File.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gid, err := strconv.Atoi(configRef.File.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 config")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func killProcessDirectly(container *container.Container) error {
|
||||
if _, err := container.WaitStop(10 * time.Second); err != nil {
|
||||
// Ensure that we don't kill ourselves
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"github.com/docker/swarmkit/agent/exec"
|
||||
)
|
||||
|
||||
// SetContainerDependencyStore sets the dependency store backend for the container
|
||||
func (daemon *Daemon) SetContainerDependencyStore(name string, store exec.DependencyGetter) error {
|
||||
c, err := daemon.GetContainer(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.DependencyStore = store
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -737,6 +737,10 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := daemon.setupConfigDir(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ms, err := daemon.setupMounts(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -754,6 +758,8 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
|
||||
ms = append(ms, m...)
|
||||
}
|
||||
|
||||
ms = append(ms, c.ConfigMounts()...)
|
||||
|
||||
sort.Sort(mounts(ms))
|
||||
if err := setMounts(daemon, &s, c, ms); err != nil {
|
||||
return nil, fmt.Errorf("linux mounts: %v", err)
|
||||
|
||||
@@ -3,21 +3,8 @@ package daemon
|
||||
import (
|
||||
"github.com/Sirupsen/logrus"
|
||||
swarmtypes "github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/swarmkit/agent/exec"
|
||||
)
|
||||
|
||||
// SetContainerSecretStore sets the secret store backend for the container
|
||||
func (daemon *Daemon) SetContainerSecretStore(name string, store exec.SecretGetter) error {
|
||||
c, err := daemon.GetContainer(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.SecretStore = store
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetContainerSecretReferences sets the container secret references needed
|
||||
func (daemon *Daemon) SetContainerSecretReferences(name string, refs []*swarmtypes.SecretReference) error {
|
||||
if !secretsSupported() && len(refs) > 0 {
|
||||
|
||||
@@ -167,12 +167,20 @@ property is not set, the client falls back to the default table
|
||||
format. For a list of supported formatting directives, see
|
||||
[**Formatting** section in the `docker secret ls` documentation](secret_ls.md)
|
||||
|
||||
|
||||
The property `nodesFormat` specifies the default format for `docker node ls` output.
|
||||
When the `--format` flag is not provided with the `docker node ls` command,
|
||||
Docker's client uses the value of `nodesFormat`. If the value of `nodesFormat` is not set,
|
||||
the client uses the default table format. For a list of supported formatting
|
||||
directives, see the [**Formatting** section in the `docker node ls` documentation](node_ls.md)
|
||||
|
||||
The property `configFormat` specifies the default format for `docker
|
||||
config ls` output. When the `--format` flag is not provided with the
|
||||
`docker config ls` command, Docker's client uses this property. If this
|
||||
property is not set, the client falls back to the default table
|
||||
format. For a list of supported formatting directives, see
|
||||
[**Formatting** section in the `docker config ls` documentation](config_ls.md)
|
||||
|
||||
The property `credsStore` specifies an external binary to serve as the default
|
||||
credential store. When this property is set, `docker login` will attempt to
|
||||
store credentials in the binary specified by `docker-credential-<value>` which
|
||||
@@ -218,6 +226,7 @@ Following is a sample `config.json` file:
|
||||
"statsFormat": "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}",
|
||||
"servicesFormat": "table {{.ID}}\t{{.Name}}\t{{.Mode}}",
|
||||
"secretFormat": "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}",
|
||||
"configFormat": "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}",
|
||||
"serviceInspectFormat": "pretty",
|
||||
"nodesFormat": "table {{.ID}}\t{{.Hostname}}\t{{.Availability}}",
|
||||
"detachKeys": "ctrl-e,e",
|
||||
|
||||
@@ -11,5 +11,5 @@ VNDR_COMMIT=c56e082291115e369f77601f9c071dd0b87c7120
|
||||
BINDATA_COMMIT=a0ff2567cfb70903282db057e799fd826784d41d
|
||||
|
||||
# CLI
|
||||
DOCKERCLI_REPO=https://github.com/docker/cli
|
||||
DOCKERCLI_COMMIT=0f6f1eafe31c4beceba31490017878b80b609331
|
||||
DOCKERCLI_REPO=https://github.com/aaronlehmann/cli
|
||||
DOCKERCLI_COMMIT=0e2b9d30814eaecd6518bfb112bb4adef0265d4b
|
||||
|
||||
@@ -118,6 +118,9 @@ type NodeConstructor func(*swarm.Node)
|
||||
// SecretConstructor defines a swarm secret constructor
|
||||
type SecretConstructor func(*swarm.Secret)
|
||||
|
||||
// ConfigConstructor defines a swarm config constructor
|
||||
type ConfigConstructor func(*swarm.Config)
|
||||
|
||||
// SpecConstructor defines a swarm spec constructor
|
||||
type SpecConstructor func(*swarm.Spec)
|
||||
|
||||
@@ -409,6 +412,59 @@ func (d *Swarm) UpdateSecret(c *check.C, id string, f ...SecretConstructor) {
|
||||
c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out)))
|
||||
}
|
||||
|
||||
// CreateConfig creates a config given the specified spec
|
||||
func (d *Swarm) CreateConfig(c *check.C, configSpec swarm.ConfigSpec) string {
|
||||
status, out, err := d.SockRequest("POST", "/configs/create", configSpec)
|
||||
|
||||
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.ConfigCreateResponse
|
||||
c.Assert(json.Unmarshal(out, &scr), checker.IsNil)
|
||||
return scr.ID
|
||||
}
|
||||
|
||||
// ListConfigs returns the list of the current swarm configs
|
||||
func (d *Swarm) ListConfigs(c *check.C) []swarm.Config {
|
||||
status, out, err := d.SockRequest("GET", "/configs", nil)
|
||||
c.Assert(err, checker.IsNil, check.Commentf(string(out)))
|
||||
c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out)))
|
||||
|
||||
configs := []swarm.Config{}
|
||||
c.Assert(json.Unmarshal(out, &configs), checker.IsNil)
|
||||
return configs
|
||||
}
|
||||
|
||||
// GetConfig returns a swarm config identified by the specified id
|
||||
func (d *Swarm) GetConfig(c *check.C, id string) *swarm.Config {
|
||||
var config swarm.Config
|
||||
status, out, err := d.SockRequest("GET", "/configs/"+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, &config), checker.IsNil)
|
||||
return &config
|
||||
}
|
||||
|
||||
// DeleteConfig removes the swarm config identified by the specified id
|
||||
func (d *Swarm) DeleteConfig(c *check.C, id string) {
|
||||
status, out, err := d.SockRequest("DELETE", "/configs/"+id, nil)
|
||||
c.Assert(err, checker.IsNil, check.Commentf(string(out)))
|
||||
c.Assert(status, checker.Equals, http.StatusNoContent, check.Commentf("output: %q", string(out)))
|
||||
}
|
||||
|
||||
// UpdateConfig updates the swarm config identified by the specified id
|
||||
// Currently, only label update is supported.
|
||||
func (d *Swarm) UpdateConfig(c *check.C, id string, f ...ConfigConstructor) {
|
||||
config := d.GetConfig(c, id)
|
||||
for _, fn := range f {
|
||||
fn(config)
|
||||
}
|
||||
url := fmt.Sprintf("/configs/%s/update?version=%d", config.ID, config.Version.Index)
|
||||
status, out, err := d.SockRequest("POST", url, config.Spec)
|
||||
c.Assert(err, checker.IsNil, check.Commentf(string(out)))
|
||||
c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out)))
|
||||
}
|
||||
|
||||
// GetSwarm returns the current swarm object
|
||||
func (d *Swarm) GetSwarm(c *check.C) swarm.Swarm {
|
||||
var sw swarm.Swarm
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/integration-cli/checker"
|
||||
"github.com/go-check/check"
|
||||
)
|
||||
|
||||
func (s *DockerSwarmSuite) TestAPISwarmConfigsEmptyList(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
configs := d.ListConfigs(c)
|
||||
c.Assert(configs, checker.NotNil)
|
||||
c.Assert(len(configs), checker.Equals, 0, check.Commentf("configs: %#v", configs))
|
||||
}
|
||||
|
||||
func (s *DockerSwarmSuite) TestAPISwarmConfigsCreate(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
testName := "test_config"
|
||||
id := d.CreateConfig(c, swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: testName,
|
||||
},
|
||||
Data: []byte("TESTINGDATA"),
|
||||
})
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
|
||||
|
||||
configs := d.ListConfigs(c)
|
||||
c.Assert(len(configs), checker.Equals, 1, check.Commentf("configs: %#v", configs))
|
||||
name := configs[0].Spec.Annotations.Name
|
||||
c.Assert(name, checker.Equals, testName, check.Commentf("configs: %s", name))
|
||||
}
|
||||
|
||||
func (s *DockerSwarmSuite) TestAPISwarmConfigsDelete(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
testName := "test_config"
|
||||
id := d.CreateConfig(c, swarm.ConfigSpec{Annotations: swarm.Annotations{
|
||||
Name: testName,
|
||||
},
|
||||
Data: []byte("TESTINGDATA"),
|
||||
})
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
|
||||
|
||||
config := d.GetConfig(c, id)
|
||||
c.Assert(config.ID, checker.Equals, id, check.Commentf("config: %v", config))
|
||||
|
||||
d.DeleteConfig(c, config.ID)
|
||||
status, out, err := d.SockRequest("GET", "/configs/"+id, nil)
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(status, checker.Equals, http.StatusNotFound, check.Commentf("config delete: %s", string(out)))
|
||||
}
|
||||
|
||||
func (s *DockerSwarmSuite) TestAPISwarmConfigsUpdate(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
testName := "test_config"
|
||||
id := d.CreateConfig(c, swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: testName,
|
||||
Labels: map[string]string{
|
||||
"test": "test1",
|
||||
},
|
||||
},
|
||||
Data: []byte("TESTINGDATA"),
|
||||
})
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
|
||||
|
||||
config := d.GetConfig(c, id)
|
||||
c.Assert(config.ID, checker.Equals, id, check.Commentf("config: %v", config))
|
||||
|
||||
// test UpdateConfig with full ID
|
||||
d.UpdateConfig(c, id, func(s *swarm.Config) {
|
||||
s.Spec.Labels = map[string]string{
|
||||
"test": "test1",
|
||||
}
|
||||
})
|
||||
|
||||
config = d.GetConfig(c, id)
|
||||
c.Assert(config.Spec.Labels["test"], checker.Equals, "test1", check.Commentf("config: %v", config))
|
||||
|
||||
// test UpdateConfig with full name
|
||||
d.UpdateConfig(c, config.Spec.Name, func(s *swarm.Config) {
|
||||
s.Spec.Labels = map[string]string{
|
||||
"test": "test2",
|
||||
}
|
||||
})
|
||||
|
||||
config = d.GetConfig(c, id)
|
||||
c.Assert(config.Spec.Labels["test"], checker.Equals, "test2", check.Commentf("config: %v", config))
|
||||
|
||||
// test UpdateConfig with prefix ID
|
||||
d.UpdateConfig(c, id[:1], func(s *swarm.Config) {
|
||||
s.Spec.Labels = map[string]string{
|
||||
"test": "test3",
|
||||
}
|
||||
})
|
||||
|
||||
config = d.GetConfig(c, id)
|
||||
c.Assert(config.Spec.Labels["test"], checker.Equals, "test3", check.Commentf("config: %v", config))
|
||||
|
||||
// test UpdateConfig in updating Data which is not supported in daemon
|
||||
// this test will produce an error in func UpdateConfig
|
||||
config = d.GetConfig(c, id)
|
||||
config.Spec.Data = []byte("TESTINGDATA2")
|
||||
|
||||
url := fmt.Sprintf("/configs/%s/update?version=%d", config.ID, config.Version.Index)
|
||||
status, out, err := d.SockRequest("POST", url, config.Spec)
|
||||
|
||||
c.Assert(err, checker.IsNil, check.Commentf(string(out)))
|
||||
c.Assert(status, checker.Equals, http.StatusInternalServerError, check.Commentf("output: %q", string(out)))
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/integration-cli/checker"
|
||||
"github.com/go-check/check"
|
||||
)
|
||||
|
||||
func (s *DockerSwarmSuite) TestConfigCreate(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
testName := "test_config"
|
||||
id := d.CreateConfig(c, swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: testName,
|
||||
},
|
||||
Data: []byte("TESTINGDATA"),
|
||||
})
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
|
||||
|
||||
config := d.GetConfig(c, id)
|
||||
c.Assert(config.Spec.Name, checker.Equals, testName)
|
||||
}
|
||||
|
||||
func (s *DockerSwarmSuite) TestConfigCreateWithLabels(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
testName := "test_config"
|
||||
id := d.CreateConfig(c, swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: testName,
|
||||
Labels: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
},
|
||||
Data: []byte("TESTINGDATA"),
|
||||
})
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
|
||||
|
||||
config := d.GetConfig(c, id)
|
||||
c.Assert(config.Spec.Name, checker.Equals, testName)
|
||||
c.Assert(len(config.Spec.Labels), checker.Equals, 2)
|
||||
c.Assert(config.Spec.Labels["key1"], checker.Equals, "value1")
|
||||
c.Assert(config.Spec.Labels["key2"], checker.Equals, "value2")
|
||||
}
|
||||
|
||||
// Test case for 28884
|
||||
func (s *DockerSwarmSuite) TestConfigCreateResolve(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
name := "test_config"
|
||||
id := d.CreateConfig(c, swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: name,
|
||||
},
|
||||
Data: []byte("foo"),
|
||||
})
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
|
||||
|
||||
fake := d.CreateConfig(c, swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: id,
|
||||
},
|
||||
Data: []byte("fake foo"),
|
||||
})
|
||||
c.Assert(fake, checker.Not(checker.Equals), "", check.Commentf("configs: %s", fake))
|
||||
|
||||
out, err := d.Cmd("config", "ls")
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(out, checker.Contains, name)
|
||||
c.Assert(out, checker.Contains, fake)
|
||||
|
||||
out, err = d.Cmd("config", "rm", id)
|
||||
c.Assert(out, checker.Contains, id)
|
||||
|
||||
// Fake one will remain
|
||||
out, err = d.Cmd("config", "ls")
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(out, checker.Not(checker.Contains), name)
|
||||
c.Assert(out, checker.Contains, fake)
|
||||
|
||||
// Remove based on name prefix of the fake one
|
||||
// (which is the same as the ID of foo one) should not work
|
||||
// as search is only done based on:
|
||||
// - Full ID
|
||||
// - Full Name
|
||||
// - Partial ID (prefix)
|
||||
out, err = d.Cmd("config", "rm", id[:5])
|
||||
c.Assert(out, checker.Not(checker.Contains), id)
|
||||
out, err = d.Cmd("config", "ls")
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(out, checker.Not(checker.Contains), name)
|
||||
c.Assert(out, checker.Contains, fake)
|
||||
|
||||
// Remove based on ID prefix of the fake one should succeed
|
||||
out, err = d.Cmd("config", "rm", fake[:5])
|
||||
c.Assert(out, checker.Contains, fake[:5])
|
||||
out, err = d.Cmd("config", "ls")
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(out, checker.Not(checker.Contains), name)
|
||||
c.Assert(out, checker.Not(checker.Contains), id)
|
||||
c.Assert(out, checker.Not(checker.Contains), fake)
|
||||
}
|
||||
|
||||
func (s *DockerSwarmSuite) TestConfigCreateWithFile(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
testFile, err := ioutil.TempFile("", "configCreateTest")
|
||||
c.Assert(err, checker.IsNil, check.Commentf("failed to create temporary file"))
|
||||
defer os.Remove(testFile.Name())
|
||||
|
||||
testData := "TESTINGDATA"
|
||||
_, err = testFile.Write([]byte(testData))
|
||||
c.Assert(err, checker.IsNil, check.Commentf("failed to write to temporary file"))
|
||||
|
||||
testName := "test_config"
|
||||
out, err := d.Cmd("config", "create", testName, testFile.Name())
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "", check.Commentf(out))
|
||||
|
||||
id := strings.TrimSpace(out)
|
||||
config := d.GetConfig(c, id)
|
||||
c.Assert(config.Spec.Name, checker.Equals, testName)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/integration-cli/checker"
|
||||
"github.com/go-check/check"
|
||||
)
|
||||
|
||||
func (s *DockerSwarmSuite) TestConfigInspect(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
testName := "test_config"
|
||||
id := d.CreateConfig(c, swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: testName,
|
||||
},
|
||||
Data: []byte("TESTINGDATA"),
|
||||
})
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
|
||||
|
||||
config := d.GetConfig(c, id)
|
||||
c.Assert(config.Spec.Name, checker.Equals, testName)
|
||||
|
||||
out, err := d.Cmd("config", "inspect", testName)
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
|
||||
var configs []swarm.Config
|
||||
c.Assert(json.Unmarshal([]byte(out), &configs), checker.IsNil)
|
||||
c.Assert(configs, checker.HasLen, 1)
|
||||
}
|
||||
|
||||
func (s *DockerSwarmSuite) TestConfigInspectMultiple(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
testNames := []string{
|
||||
"test0",
|
||||
"test1",
|
||||
}
|
||||
for _, n := range testNames {
|
||||
id := d.CreateConfig(c, swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: n,
|
||||
},
|
||||
Data: []byte("TESTINGDATA"),
|
||||
})
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
|
||||
|
||||
config := d.GetConfig(c, id)
|
||||
c.Assert(config.Spec.Name, checker.Equals, n)
|
||||
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"config",
|
||||
"inspect",
|
||||
}
|
||||
args = append(args, testNames...)
|
||||
out, err := d.Cmd(args...)
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
|
||||
var configs []swarm.Config
|
||||
c.Assert(json.Unmarshal([]byte(out), &configs), checker.IsNil)
|
||||
c.Assert(configs, checker.HasLen, 2)
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/integration-cli/checker"
|
||||
"github.com/go-check/check"
|
||||
)
|
||||
|
||||
func (s *DockerSwarmSuite) TestConfigList(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
testName0 := "test0"
|
||||
testName1 := "test1"
|
||||
|
||||
// create config test0
|
||||
id0 := d.CreateConfig(c, swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: testName0,
|
||||
Labels: map[string]string{"type": "test"},
|
||||
},
|
||||
Data: []byte("TESTINGDATA0"),
|
||||
})
|
||||
c.Assert(id0, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id0))
|
||||
|
||||
config := d.GetConfig(c, id0)
|
||||
c.Assert(config.Spec.Name, checker.Equals, testName0)
|
||||
|
||||
// create config test1
|
||||
id1 := d.CreateConfig(c, swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: testName1,
|
||||
Labels: map[string]string{"type": "production"},
|
||||
},
|
||||
Data: []byte("TESTINGDATA1"),
|
||||
})
|
||||
c.Assert(id1, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id1))
|
||||
|
||||
config = d.GetConfig(c, id1)
|
||||
c.Assert(config.Spec.Name, checker.Equals, testName1)
|
||||
|
||||
// test by command `docker config ls`
|
||||
out, err := d.Cmd("config", "ls")
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
c.Assert(strings.TrimSpace(out), checker.Contains, testName0)
|
||||
c.Assert(strings.TrimSpace(out), checker.Contains, testName1)
|
||||
|
||||
// test filter by name `docker config ls --filter name=xxx`
|
||||
args := []string{
|
||||
"config",
|
||||
"ls",
|
||||
"--filter",
|
||||
"name=test0",
|
||||
}
|
||||
out, err = d.Cmd(args...)
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
|
||||
c.Assert(strings.TrimSpace(out), checker.Contains, testName0)
|
||||
c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), testName1)
|
||||
|
||||
// test filter by id `docker config ls --filter id=xxx`
|
||||
args = []string{
|
||||
"config",
|
||||
"ls",
|
||||
"--filter",
|
||||
"id=" + id1,
|
||||
}
|
||||
out, err = d.Cmd(args...)
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
|
||||
c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), testName0)
|
||||
c.Assert(strings.TrimSpace(out), checker.Contains, testName1)
|
||||
|
||||
// test filter by label `docker config ls --filter label=xxx`
|
||||
args = []string{
|
||||
"config",
|
||||
"ls",
|
||||
"--filter",
|
||||
"label=type",
|
||||
}
|
||||
out, err = d.Cmd(args...)
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
|
||||
c.Assert(strings.TrimSpace(out), checker.Contains, testName0)
|
||||
c.Assert(strings.TrimSpace(out), checker.Contains, testName1)
|
||||
|
||||
args = []string{
|
||||
"config",
|
||||
"ls",
|
||||
"--filter",
|
||||
"label=type=test",
|
||||
}
|
||||
out, err = d.Cmd(args...)
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
|
||||
c.Assert(strings.TrimSpace(out), checker.Contains, testName0)
|
||||
c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), testName1)
|
||||
|
||||
args = []string{
|
||||
"config",
|
||||
"ls",
|
||||
"--filter",
|
||||
"label=type=production",
|
||||
}
|
||||
out, err = d.Cmd(args...)
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
|
||||
c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), testName0)
|
||||
c.Assert(strings.TrimSpace(out), checker.Contains, testName1)
|
||||
|
||||
// test invalid filter `docker config ls --filter noexisttype=xxx`
|
||||
args = []string{
|
||||
"config",
|
||||
"ls",
|
||||
"--filter",
|
||||
"noexisttype=test0",
|
||||
}
|
||||
out, err = d.Cmd(args...)
|
||||
c.Assert(err, checker.NotNil, check.Commentf(out))
|
||||
|
||||
c.Assert(strings.TrimSpace(out), checker.Contains, "Error response from daemon: Invalid filter 'noexisttype'")
|
||||
}
|
||||
@@ -211,6 +211,153 @@ func (s *DockerSwarmSuite) TestServiceCreateWithSecretReferencedTwice(c *check.C
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
}
|
||||
|
||||
func (s *DockerSwarmSuite) TestServiceCreateWithConfigSimple(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
serviceName := "test-service-config"
|
||||
testName := "test_config"
|
||||
id := d.CreateConfig(c, swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: testName,
|
||||
},
|
||||
Data: []byte("TESTINGDATA"),
|
||||
})
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
|
||||
|
||||
out, err := d.Cmd("service", "create", "--name", serviceName, "--config", testName, "busybox", "top")
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
|
||||
out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Configs }}", serviceName)
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
var refs []swarm.ConfigReference
|
||||
c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil)
|
||||
c.Assert(refs, checker.HasLen, 1)
|
||||
|
||||
c.Assert(refs[0].ConfigName, checker.Equals, testName)
|
||||
c.Assert(refs[0].File, checker.Not(checker.IsNil))
|
||||
c.Assert(refs[0].File.Name, checker.Equals, testName)
|
||||
c.Assert(refs[0].File.UID, checker.Equals, "0")
|
||||
c.Assert(refs[0].File.GID, checker.Equals, "0")
|
||||
|
||||
out, err = d.Cmd("service", "rm", serviceName)
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
d.DeleteConfig(c, testName)
|
||||
}
|
||||
|
||||
func (s *DockerSwarmSuite) TestServiceCreateWithConfigSourceTargetPaths(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
testPaths := map[string]string{
|
||||
"app": "/etc/config",
|
||||
"test_config": "test_config",
|
||||
"relative_config": "relative/config",
|
||||
}
|
||||
|
||||
var configFlags []string
|
||||
|
||||
for testName, testTarget := range testPaths {
|
||||
id := d.CreateConfig(c, swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: testName,
|
||||
},
|
||||
Data: []byte("TESTINGDATA " + testName + " " + testTarget),
|
||||
})
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
|
||||
|
||||
configFlags = append(configFlags, "--config", fmt.Sprintf("source=%s,target=%s", testName, testTarget))
|
||||
}
|
||||
|
||||
serviceName := "svc"
|
||||
serviceCmd := []string{"service", "create", "--name", serviceName}
|
||||
serviceCmd = append(serviceCmd, configFlags...)
|
||||
serviceCmd = append(serviceCmd, "busybox", "top")
|
||||
out, err := d.Cmd(serviceCmd...)
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
|
||||
out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Configs }}", serviceName)
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
var refs []swarm.ConfigReference
|
||||
c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil)
|
||||
c.Assert(refs, checker.HasLen, len(testPaths))
|
||||
|
||||
var tasks []swarm.Task
|
||||
waitAndAssert(c, defaultReconciliationTimeout, func(c *check.C) (interface{}, check.CommentInterface) {
|
||||
tasks = d.GetServiceTasks(c, serviceName)
|
||||
return len(tasks) > 0, nil
|
||||
}, checker.Equals, true)
|
||||
|
||||
task := tasks[0]
|
||||
waitAndAssert(c, defaultReconciliationTimeout, func(c *check.C) (interface{}, check.CommentInterface) {
|
||||
if task.NodeID == "" || task.Status.ContainerStatus.ContainerID == "" {
|
||||
task = d.GetTask(c, task.ID)
|
||||
}
|
||||
return task.NodeID != "" && task.Status.ContainerStatus.ContainerID != "", nil
|
||||
}, checker.Equals, true)
|
||||
|
||||
for testName, testTarget := range testPaths {
|
||||
path := testTarget
|
||||
if !filepath.IsAbs(path) {
|
||||
path = filepath.Join("/", path)
|
||||
}
|
||||
out, err := d.Cmd("exec", task.Status.ContainerStatus.ContainerID, "cat", path)
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(out, checker.Equals, "TESTINGDATA "+testName+" "+testTarget)
|
||||
}
|
||||
|
||||
out, err = d.Cmd("service", "rm", serviceName)
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
}
|
||||
|
||||
func (s *DockerSwarmSuite) TestServiceCreateWithConfigReferencedTwice(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
id := d.CreateConfig(c, swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: "myconfig",
|
||||
},
|
||||
Data: []byte("TESTINGDATA"),
|
||||
})
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
|
||||
|
||||
serviceName := "svc"
|
||||
out, err := d.Cmd("service", "create", "--name", serviceName, "--config", "source=myconfig,target=target1", "--config", "source=myconfig,target=target2", "busybox", "top")
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
|
||||
out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Configs }}", serviceName)
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
var refs []swarm.ConfigReference
|
||||
c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil)
|
||||
c.Assert(refs, checker.HasLen, 2)
|
||||
|
||||
var tasks []swarm.Task
|
||||
waitAndAssert(c, defaultReconciliationTimeout, func(c *check.C) (interface{}, check.CommentInterface) {
|
||||
tasks = d.GetServiceTasks(c, serviceName)
|
||||
return len(tasks) > 0, nil
|
||||
}, checker.Equals, true)
|
||||
|
||||
task := tasks[0]
|
||||
waitAndAssert(c, defaultReconciliationTimeout, func(c *check.C) (interface{}, check.CommentInterface) {
|
||||
if task.NodeID == "" || task.Status.ContainerStatus.ContainerID == "" {
|
||||
task = d.GetTask(c, task.ID)
|
||||
}
|
||||
return task.NodeID != "" && task.Status.ContainerStatus.ContainerID != "", nil
|
||||
}, checker.Equals, true)
|
||||
|
||||
for _, target := range []string{"target1", "target2"} {
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
path := filepath.Join("/", target)
|
||||
out, err := d.Cmd("exec", task.Status.ContainerStatus.ContainerID, "cat", path)
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(out, checker.Equals, "TESTINGDATA")
|
||||
}
|
||||
|
||||
out, err = d.Cmd("service", "rm", serviceName)
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
}
|
||||
|
||||
func (s *DockerSwarmSuite) TestServiceCreateMountTmpfs(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
out, err := d.Cmd("service", "create", "--detach=true", "--mount", "type=tmpfs,target=/foo,tmpfs-size=1MB", "busybox", "sh", "-c", "mount | grep foo; tail -f /dev/null")
|
||||
|
||||
@@ -128,3 +128,45 @@ func (s *DockerSwarmSuite) TestServiceUpdateSecrets(c *check.C) {
|
||||
c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil)
|
||||
c.Assert(refs, checker.HasLen, 0)
|
||||
}
|
||||
|
||||
func (s *DockerSwarmSuite) TestServiceUpdateConfigs(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
testName := "test_config"
|
||||
id := d.CreateConfig(c, swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: testName,
|
||||
},
|
||||
Data: []byte("TESTINGDATA"),
|
||||
})
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %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 config
|
||||
out, err = d.CmdRetryOutOfSequence("service", "update", "test", "--config-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.Configs }}", serviceName)
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
var refs []swarm.ConfigReference
|
||||
c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil)
|
||||
c.Assert(refs, checker.HasLen, 1)
|
||||
|
||||
c.Assert(refs[0].ConfigName, checker.Equals, testName)
|
||||
c.Assert(refs[0].File, checker.Not(checker.IsNil))
|
||||
c.Assert(refs[0].File.Name, checker.Equals, testTarget)
|
||||
|
||||
// remove
|
||||
out, err = d.CmdRetryOutOfSequence("service", "update", "test", "--config-rm", testName)
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
|
||||
out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.TaskTemplate.ContainerSpec.Configs }}", serviceName)
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
c.Assert(json.Unmarshal([]byte(out), &refs), checker.IsNil)
|
||||
c.Assert(refs, checker.HasLen, 0)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package opts
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
swarmtypes "github.com/docker/docker/api/types/swarm"
|
||||
)
|
||||
|
||||
// ConfigOpt is a Value type for parsing configs
|
||||
type ConfigOpt struct {
|
||||
values []*swarmtypes.ConfigReference
|
||||
}
|
||||
|
||||
// Set a new config value
|
||||
func (o *ConfigOpt) Set(value string) error {
|
||||
csvReader := csv.NewReader(strings.NewReader(value))
|
||||
fields, err := csvReader.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
options := &swarmtypes.ConfigReference{
|
||||
File: &swarmtypes.ConfigReferenceFileTarget{
|
||||
UID: "0",
|
||||
GID: "0",
|
||||
Mode: 0444,
|
||||
},
|
||||
}
|
||||
|
||||
// support a simple syntax of --config foo
|
||||
if len(fields) == 1 {
|
||||
options.File.Name = fields[0]
|
||||
options.ConfigName = 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", "src":
|
||||
options.ConfigName = value
|
||||
case "target":
|
||||
options.File.Name = value
|
||||
case "uid":
|
||||
options.File.UID = value
|
||||
case "gid":
|
||||
options.File.GID = value
|
||||
case "mode":
|
||||
m, err := strconv.ParseUint(value, 0, 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid mode specified: %v", err)
|
||||
}
|
||||
|
||||
options.File.Mode = os.FileMode(m)
|
||||
default:
|
||||
return fmt.Errorf("invalid field in config request: %s", key)
|
||||
}
|
||||
}
|
||||
|
||||
if options.ConfigName == "" {
|
||||
return fmt.Errorf("source is required")
|
||||
}
|
||||
|
||||
o.values = append(o.values, options)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Type returns the type of this option
|
||||
func (o *ConfigOpt) Type() string {
|
||||
return "config"
|
||||
}
|
||||
|
||||
// String returns a string repr of this option
|
||||
func (o *ConfigOpt) String() string {
|
||||
configs := []string{}
|
||||
for _, config := range o.values {
|
||||
repr := fmt.Sprintf("%s -> %s", config.ConfigName, config.File.Name)
|
||||
configs = append(configs, repr)
|
||||
}
|
||||
return strings.Join(configs, ", ")
|
||||
}
|
||||
|
||||
// Value returns the config requests
|
||||
func (o *ConfigOpt) Value() []*swarmtypes.ConfigReference {
|
||||
return o.values
|
||||
}
|
||||
Reference in New Issue
Block a user