Signed-off-by: Andrea Luzzardi <aluzzardi@gmail.com>
| ... | ... |
@@ -2,7 +2,9 @@ package swarm |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
basictypes "github.com/docker/docker/api/types" |
| 5 |
+ "github.com/docker/docker/api/types/backend" |
|
| 5 | 6 |
types "github.com/docker/docker/api/types/swarm" |
| 7 |
+ "golang.org/x/net/context" |
|
| 6 | 8 |
) |
| 7 | 9 |
|
| 8 | 10 |
// Backend abstracts an swarm commands manager. |
| ... | ... |
@@ -19,6 +21,7 @@ type Backend interface {
|
| 19 | 19 |
CreateService(types.ServiceSpec, string) (string, error) |
| 20 | 20 |
UpdateService(string, uint64, types.ServiceSpec, string, string) error |
| 21 | 21 |
RemoveService(string) error |
| 22 |
+ ServiceLogs(context.Context, string, *backend.ContainerLogsConfig, chan struct{}) error
|
|
| 22 | 23 |
GetNodes(basictypes.NodeListOptions) ([]types.Node, error) |
| 23 | 24 |
GetNode(string) (types.Node, error) |
| 24 | 25 |
UpdateNode(string, uint64, types.NodeSpec) error |
| ... | ... |
@@ -1,6 +1,9 @@ |
| 1 | 1 |
package swarm |
| 2 | 2 |
|
| 3 |
-import "github.com/docker/docker/api/server/router" |
|
| 3 |
+import ( |
|
| 4 |
+ "github.com/docker/docker/api/server/router" |
|
| 5 |
+ "github.com/docker/docker/daemon" |
|
| 6 |
+) |
|
| 4 | 7 |
|
| 5 | 8 |
// buildRouter is a router to talk with the build controller |
| 6 | 9 |
type swarmRouter struct {
|
| ... | ... |
@@ -9,11 +12,14 @@ type swarmRouter struct {
|
| 9 | 9 |
} |
| 10 | 10 |
|
| 11 | 11 |
// NewRouter initializes a new build router |
| 12 |
-func NewRouter(b Backend) router.Router {
|
|
| 12 |
+func NewRouter(d *daemon.Daemon, b Backend) router.Router {
|
|
| 13 | 13 |
r := &swarmRouter{
|
| 14 | 14 |
backend: b, |
| 15 | 15 |
} |
| 16 | 16 |
r.initRoutes() |
| 17 |
+ if d.HasExperimental() {
|
|
| 18 |
+ r.addExperimentalRoutes() |
|
| 19 |
+ } |
|
| 17 | 20 |
return r |
| 18 | 21 |
} |
| 19 | 22 |
|
| ... | ... |
@@ -22,6 +28,12 @@ func (sr *swarmRouter) Routes() []router.Route {
|
| 22 | 22 |
return sr.routes |
| 23 | 23 |
} |
| 24 | 24 |
|
| 25 |
+func (sr *swarmRouter) addExperimentalRoutes() {
|
|
| 26 |
+ sr.routes = append(sr.routes, |
|
| 27 |
+ router.Cancellable(router.NewGetRoute("/services/{id}/logs", sr.getServiceLogs)),
|
|
| 28 |
+ ) |
|
| 29 |
+} |
|
| 30 |
+ |
|
| 25 | 31 |
func (sr *swarmRouter) initRoutes() {
|
| 26 | 32 |
sr.routes = []router.Route{
|
| 27 | 33 |
router.NewPostRoute("/swarm/init", sr.initCluster),
|
| ... | ... |
@@ -32,20 +44,20 @@ func (sr *swarmRouter) initRoutes() {
|
| 32 | 32 |
router.NewPostRoute("/swarm/update", sr.updateCluster),
|
| 33 | 33 |
router.NewPostRoute("/swarm/unlock", sr.unlockCluster),
|
| 34 | 34 |
router.NewGetRoute("/services", sr.getServices),
|
| 35 |
- router.NewGetRoute("/services/{id:.*}", sr.getService),
|
|
| 35 |
+ router.NewGetRoute("/services/{id}", sr.getService),
|
|
| 36 | 36 |
router.NewPostRoute("/services/create", sr.createService),
|
| 37 |
- router.NewPostRoute("/services/{id:.*}/update", sr.updateService),
|
|
| 38 |
- router.NewDeleteRoute("/services/{id:.*}", sr.removeService),
|
|
| 37 |
+ router.NewPostRoute("/services/{id}/update", sr.updateService),
|
|
| 38 |
+ router.NewDeleteRoute("/services/{id}", sr.removeService),
|
|
| 39 | 39 |
router.NewGetRoute("/nodes", sr.getNodes),
|
| 40 |
- router.NewGetRoute("/nodes/{id:.*}", sr.getNode),
|
|
| 41 |
- router.NewDeleteRoute("/nodes/{id:.*}", sr.removeNode),
|
|
| 42 |
- router.NewPostRoute("/nodes/{id:.*}/update", sr.updateNode),
|
|
| 40 |
+ router.NewGetRoute("/nodes/{id}", sr.getNode),
|
|
| 41 |
+ router.NewDeleteRoute("/nodes/{id}", sr.removeNode),
|
|
| 42 |
+ router.NewPostRoute("/nodes/{id}/update", sr.updateNode),
|
|
| 43 | 43 |
router.NewGetRoute("/tasks", sr.getTasks),
|
| 44 |
- router.NewGetRoute("/tasks/{id:.*}", sr.getTask),
|
|
| 44 |
+ router.NewGetRoute("/tasks/{id}", sr.getTask),
|
|
| 45 | 45 |
router.NewGetRoute("/secrets", sr.getSecrets),
|
| 46 | 46 |
router.NewPostRoute("/secrets", sr.createSecret),
|
| 47 |
- router.NewDeleteRoute("/secrets/{id:.*}", sr.removeSecret),
|
|
| 48 |
- router.NewGetRoute("/secrets/{id:.*}", sr.getSecret),
|
|
| 49 |
- router.NewPostRoute("/secrets/{id:.*}/update", sr.updateSecret),
|
|
| 47 |
+ router.NewDeleteRoute("/secrets/{id}", sr.removeSecret),
|
|
| 48 |
+ router.NewGetRoute("/secrets/{id}", sr.getSecret),
|
|
| 49 |
+ router.NewPostRoute("/secrets/{id}/update", sr.updateSecret),
|
|
| 50 | 50 |
} |
| 51 | 51 |
} |
| ... | ... |
@@ -10,6 +10,7 @@ import ( |
| 10 | 10 |
"github.com/docker/docker/api/errors" |
| 11 | 11 |
"github.com/docker/docker/api/server/httputils" |
| 12 | 12 |
basictypes "github.com/docker/docker/api/types" |
| 13 |
+ "github.com/docker/docker/api/types/backend" |
|
| 13 | 14 |
"github.com/docker/docker/api/types/filters" |
| 14 | 15 |
types "github.com/docker/docker/api/types/swarm" |
| 15 | 16 |
"golang.org/x/net/context" |
| ... | ... |
@@ -208,6 +209,59 @@ func (sr *swarmRouter) removeService(ctx context.Context, w http.ResponseWriter, |
| 208 | 208 |
return nil |
| 209 | 209 |
} |
| 210 | 210 |
|
| 211 |
+func (sr *swarmRouter) getServiceLogs(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
| 212 |
+ if err := httputils.ParseForm(r); err != nil {
|
|
| 213 |
+ return err |
|
| 214 |
+ } |
|
| 215 |
+ |
|
| 216 |
+ // Args are validated before the stream starts because when it starts we're |
|
| 217 |
+ // sending HTTP 200 by writing an empty chunk of data to tell the client that |
|
| 218 |
+ // daemon is going to stream. By sending this initial HTTP 200 we can't report |
|
| 219 |
+ // any error after the stream starts (i.e. container not found, wrong parameters) |
|
| 220 |
+ // with the appropriate status code. |
|
| 221 |
+ stdout, stderr := httputils.BoolValue(r, "stdout"), httputils.BoolValue(r, "stderr") |
|
| 222 |
+ if !(stdout || stderr) {
|
|
| 223 |
+ return fmt.Errorf("Bad parameters: you must choose at least one stream")
|
|
| 224 |
+ } |
|
| 225 |
+ |
|
| 226 |
+ serviceName := vars["id"] |
|
| 227 |
+ logsConfig := &backend.ContainerLogsConfig{
|
|
| 228 |
+ ContainerLogsOptions: basictypes.ContainerLogsOptions{
|
|
| 229 |
+ Follow: httputils.BoolValue(r, "follow"), |
|
| 230 |
+ Timestamps: httputils.BoolValue(r, "timestamps"), |
|
| 231 |
+ Since: r.Form.Get("since"),
|
|
| 232 |
+ Tail: r.Form.Get("tail"),
|
|
| 233 |
+ ShowStdout: stdout, |
|
| 234 |
+ ShowStderr: stderr, |
|
| 235 |
+ Details: httputils.BoolValue(r, "details"), |
|
| 236 |
+ }, |
|
| 237 |
+ OutStream: w, |
|
| 238 |
+ } |
|
| 239 |
+ |
|
| 240 |
+ if !logsConfig.Follow {
|
|
| 241 |
+ return fmt.Errorf("Bad parameters: Only follow mode is currently supported")
|
|
| 242 |
+ } |
|
| 243 |
+ |
|
| 244 |
+ if logsConfig.Details {
|
|
| 245 |
+ return fmt.Errorf("Bad parameters: details is not currently supported")
|
|
| 246 |
+ } |
|
| 247 |
+ |
|
| 248 |
+ chStarted := make(chan struct{})
|
|
| 249 |
+ if err := sr.backend.ServiceLogs(ctx, serviceName, logsConfig, chStarted); err != nil {
|
|
| 250 |
+ select {
|
|
| 251 |
+ case <-chStarted: |
|
| 252 |
+ // The client may be expecting all of the data we're sending to |
|
| 253 |
+ // be multiplexed, so send it through OutStream, which will |
|
| 254 |
+ // have been set up to handle that if needed. |
|
| 255 |
+ fmt.Fprintf(logsConfig.OutStream, "Error grabbing service logs: %v\n", err) |
|
| 256 |
+ default: |
|
| 257 |
+ return err |
|
| 258 |
+ } |
|
| 259 |
+ } |
|
| 260 |
+ |
|
| 261 |
+ return nil |
|
| 262 |
+} |
|
| 263 |
+ |
|
| 211 | 264 |
func (sr *swarmRouter) getNodes(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
| 212 | 265 |
if err := httputils.ParseForm(r); err != nil {
|
| 213 | 266 |
return err |
| ... | ... |
@@ -111,6 +111,7 @@ type ServiceAPIClient interface {
|
| 111 | 111 |
ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) |
| 112 | 112 |
ServiceRemove(ctx context.Context, serviceID string) error |
| 113 | 113 |
ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) error |
| 114 |
+ ServiceLogs(ctx context.Context, serviceID string, options types.ContainerLogsOptions) (io.ReadCloser, error) |
|
| 114 | 115 |
TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error) |
| 115 | 116 |
TaskList(ctx context.Context, options types.TaskListOptions) ([]swarm.Task, error) |
| 116 | 117 |
} |
| 117 | 118 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,52 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "io" |
|
| 4 |
+ "net/url" |
|
| 5 |
+ "time" |
|
| 6 |
+ |
|
| 7 |
+ "golang.org/x/net/context" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/docker/docker/api/types" |
|
| 10 |
+ timetypes "github.com/docker/docker/api/types/time" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+// ServiceLogs returns the logs generated by a service in an io.ReadCloser. |
|
| 14 |
+// It's up to the caller to close the stream. |
|
| 15 |
+func (cli *Client) ServiceLogs(ctx context.Context, serviceID string, options types.ContainerLogsOptions) (io.ReadCloser, error) {
|
|
| 16 |
+ query := url.Values{}
|
|
| 17 |
+ if options.ShowStdout {
|
|
| 18 |
+ query.Set("stdout", "1")
|
|
| 19 |
+ } |
|
| 20 |
+ |
|
| 21 |
+ if options.ShowStderr {
|
|
| 22 |
+ query.Set("stderr", "1")
|
|
| 23 |
+ } |
|
| 24 |
+ |
|
| 25 |
+ if options.Since != "" {
|
|
| 26 |
+ ts, err := timetypes.GetTimestamp(options.Since, time.Now()) |
|
| 27 |
+ if err != nil {
|
|
| 28 |
+ return nil, err |
|
| 29 |
+ } |
|
| 30 |
+ query.Set("since", ts)
|
|
| 31 |
+ } |
|
| 32 |
+ |
|
| 33 |
+ if options.Timestamps {
|
|
| 34 |
+ query.Set("timestamps", "1")
|
|
| 35 |
+ } |
|
| 36 |
+ |
|
| 37 |
+ if options.Details {
|
|
| 38 |
+ query.Set("details", "1")
|
|
| 39 |
+ } |
|
| 40 |
+ |
|
| 41 |
+ if options.Follow {
|
|
| 42 |
+ query.Set("follow", "1")
|
|
| 43 |
+ } |
|
| 44 |
+ query.Set("tail", options.Tail)
|
|
| 45 |
+ |
|
| 46 |
+ resp, err := cli.get(ctx, "/services/"+serviceID+"/logs", query, nil) |
|
| 47 |
+ if err != nil {
|
|
| 48 |
+ return nil, err |
|
| 49 |
+ } |
|
| 50 |
+ return resp.body, nil |
|
| 51 |
+} |
| 0 | 52 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,133 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "io" |
|
| 6 |
+ "io/ioutil" |
|
| 7 |
+ "log" |
|
| 8 |
+ "net/http" |
|
| 9 |
+ "os" |
|
| 10 |
+ "strings" |
|
| 11 |
+ "testing" |
|
| 12 |
+ "time" |
|
| 13 |
+ |
|
| 14 |
+ "github.com/docker/docker/api/types" |
|
| 15 |
+ |
|
| 16 |
+ "golang.org/x/net/context" |
|
| 17 |
+) |
|
| 18 |
+ |
|
| 19 |
+func TestServiceLogsError(t *testing.T) {
|
|
| 20 |
+ client := &Client{
|
|
| 21 |
+ client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), |
|
| 22 |
+ } |
|
| 23 |
+ _, err := client.ServiceLogs(context.Background(), "service_id", types.ContainerLogsOptions{})
|
|
| 24 |
+ if err == nil || err.Error() != "Error response from daemon: Server error" {
|
|
| 25 |
+ t.Fatalf("expected a Server Error, got %v", err)
|
|
| 26 |
+ } |
|
| 27 |
+ _, err = client.ServiceLogs(context.Background(), "service_id", types.ContainerLogsOptions{
|
|
| 28 |
+ Since: "2006-01-02TZ", |
|
| 29 |
+ }) |
|
| 30 |
+ if err == nil || !strings.Contains(err.Error(), `parsing time "2006-01-02TZ"`) {
|
|
| 31 |
+ t.Fatalf("expected a 'parsing time' error, got %v", err)
|
|
| 32 |
+ } |
|
| 33 |
+} |
|
| 34 |
+ |
|
| 35 |
+func TestServiceLogs(t *testing.T) {
|
|
| 36 |
+ expectedURL := "/services/service_id/logs" |
|
| 37 |
+ cases := []struct {
|
|
| 38 |
+ options types.ContainerLogsOptions |
|
| 39 |
+ expectedQueryParams map[string]string |
|
| 40 |
+ }{
|
|
| 41 |
+ {
|
|
| 42 |
+ expectedQueryParams: map[string]string{
|
|
| 43 |
+ "tail": "", |
|
| 44 |
+ }, |
|
| 45 |
+ }, |
|
| 46 |
+ {
|
|
| 47 |
+ options: types.ContainerLogsOptions{
|
|
| 48 |
+ Tail: "any", |
|
| 49 |
+ }, |
|
| 50 |
+ expectedQueryParams: map[string]string{
|
|
| 51 |
+ "tail": "any", |
|
| 52 |
+ }, |
|
| 53 |
+ }, |
|
| 54 |
+ {
|
|
| 55 |
+ options: types.ContainerLogsOptions{
|
|
| 56 |
+ ShowStdout: true, |
|
| 57 |
+ ShowStderr: true, |
|
| 58 |
+ Timestamps: true, |
|
| 59 |
+ Details: true, |
|
| 60 |
+ Follow: true, |
|
| 61 |
+ }, |
|
| 62 |
+ expectedQueryParams: map[string]string{
|
|
| 63 |
+ "tail": "", |
|
| 64 |
+ "stdout": "1", |
|
| 65 |
+ "stderr": "1", |
|
| 66 |
+ "timestamps": "1", |
|
| 67 |
+ "details": "1", |
|
| 68 |
+ "follow": "1", |
|
| 69 |
+ }, |
|
| 70 |
+ }, |
|
| 71 |
+ {
|
|
| 72 |
+ options: types.ContainerLogsOptions{
|
|
| 73 |
+ // An complete invalid date, timestamp or go duration will be |
|
| 74 |
+ // passed as is |
|
| 75 |
+ Since: "invalid but valid", |
|
| 76 |
+ }, |
|
| 77 |
+ expectedQueryParams: map[string]string{
|
|
| 78 |
+ "tail": "", |
|
| 79 |
+ "since": "invalid but valid", |
|
| 80 |
+ }, |
|
| 81 |
+ }, |
|
| 82 |
+ } |
|
| 83 |
+ for _, logCase := range cases {
|
|
| 84 |
+ client := &Client{
|
|
| 85 |
+ client: newMockClient(func(r *http.Request) (*http.Response, error) {
|
|
| 86 |
+ if !strings.HasPrefix(r.URL.Path, expectedURL) {
|
|
| 87 |
+ return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
|
|
| 88 |
+ } |
|
| 89 |
+ // Check query parameters |
|
| 90 |
+ query := r.URL.Query() |
|
| 91 |
+ for key, expected := range logCase.expectedQueryParams {
|
|
| 92 |
+ actual := query.Get(key) |
|
| 93 |
+ if actual != expected {
|
|
| 94 |
+ return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
|
|
| 95 |
+ } |
|
| 96 |
+ } |
|
| 97 |
+ return &http.Response{
|
|
| 98 |
+ StatusCode: http.StatusOK, |
|
| 99 |
+ Body: ioutil.NopCloser(bytes.NewReader([]byte("response"))),
|
|
| 100 |
+ }, nil |
|
| 101 |
+ }), |
|
| 102 |
+ } |
|
| 103 |
+ body, err := client.ServiceLogs(context.Background(), "service_id", logCase.options) |
|
| 104 |
+ if err != nil {
|
|
| 105 |
+ t.Fatal(err) |
|
| 106 |
+ } |
|
| 107 |
+ defer body.Close() |
|
| 108 |
+ content, err := ioutil.ReadAll(body) |
|
| 109 |
+ if err != nil {
|
|
| 110 |
+ t.Fatal(err) |
|
| 111 |
+ } |
|
| 112 |
+ if string(content) != "response" {
|
|
| 113 |
+ t.Fatalf("expected response to contain 'response', got %s", string(content))
|
|
| 114 |
+ } |
|
| 115 |
+ } |
|
| 116 |
+} |
|
| 117 |
+ |
|
| 118 |
+func ExampleClient_ServiceLogs_withTimeout() {
|
|
| 119 |
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
| 120 |
+ defer cancel() |
|
| 121 |
+ |
|
| 122 |
+ client, _ := NewEnvClient() |
|
| 123 |
+ reader, err := client.ServiceLogs(ctx, "service_id", types.ContainerLogsOptions{})
|
|
| 124 |
+ if err != nil {
|
|
| 125 |
+ log.Fatal(err) |
|
| 126 |
+ } |
|
| 127 |
+ |
|
| 128 |
+ _, err = io.Copy(os.Stdout, reader) |
|
| 129 |
+ if err != nil && err != io.EOF {
|
|
| 130 |
+ log.Fatal(err) |
|
| 131 |
+ } |
|
| 132 |
+} |
| ... | ... |
@@ -456,7 +456,7 @@ func initRouter(s *apiserver.Server, d *daemon.Daemon, c *cluster.Cluster) {
|
| 456 | 456 |
systemrouter.NewRouter(d, c), |
| 457 | 457 |
volume.NewRouter(d), |
| 458 | 458 |
build.NewRouter(dockerfile.NewBuildManager(d)), |
| 459 |
- swarmrouter.NewRouter(c), |
|
| 459 |
+ swarmrouter.NewRouter(d, c), |
|
| 460 | 460 |
}...) |
| 461 | 461 |
|
| 462 | 462 |
if d.NetworkControllerEnabled() {
|
| ... | ... |
@@ -4,6 +4,7 @@ import ( |
| 4 | 4 |
"encoding/base64" |
| 5 | 5 |
"encoding/json" |
| 6 | 6 |
"fmt" |
| 7 |
+ "io" |
|
| 7 | 8 |
"io/ioutil" |
| 8 | 9 |
"net" |
| 9 | 10 |
"os" |
| ... | ... |
@@ -16,20 +17,24 @@ import ( |
| 16 | 16 |
"github.com/Sirupsen/logrus" |
| 17 | 17 |
apierrors "github.com/docker/docker/api/errors" |
| 18 | 18 |
apitypes "github.com/docker/docker/api/types" |
| 19 |
+ "github.com/docker/docker/api/types/backend" |
|
| 19 | 20 |
"github.com/docker/docker/api/types/filters" |
| 20 | 21 |
"github.com/docker/docker/api/types/network" |
| 21 | 22 |
types "github.com/docker/docker/api/types/swarm" |
| 22 | 23 |
"github.com/docker/docker/daemon/cluster/convert" |
| 23 | 24 |
executorpkg "github.com/docker/docker/daemon/cluster/executor" |
| 24 | 25 |
"github.com/docker/docker/daemon/cluster/executor/container" |
| 26 |
+ "github.com/docker/docker/daemon/logger" |
|
| 25 | 27 |
"github.com/docker/docker/opts" |
| 26 | 28 |
"github.com/docker/docker/pkg/ioutils" |
| 27 | 29 |
"github.com/docker/docker/pkg/signal" |
| 30 |
+ "github.com/docker/docker/pkg/stdcopy" |
|
| 28 | 31 |
"github.com/docker/docker/reference" |
| 29 | 32 |
"github.com/docker/docker/runconfig" |
| 30 | 33 |
swarmapi "github.com/docker/swarmkit/api" |
| 31 | 34 |
"github.com/docker/swarmkit/manager/encryption" |
| 32 | 35 |
swarmnode "github.com/docker/swarmkit/node" |
| 36 |
+ "github.com/docker/swarmkit/protobuf/ptypes" |
|
| 33 | 37 |
"github.com/pkg/errors" |
| 34 | 38 |
"golang.org/x/net/context" |
| 35 | 39 |
"google.golang.org/grpc" |
| ... | ... |
@@ -45,6 +50,7 @@ const defaultAddr = "0.0.0.0:2377" |
| 45 | 45 |
const ( |
| 46 | 46 |
initialReconnectDelay = 100 * time.Millisecond |
| 47 | 47 |
maxReconnectDelay = 30 * time.Second |
| 48 |
+ contextPrefix = "com.docker.swarm" |
|
| 48 | 49 |
) |
| 49 | 50 |
|
| 50 | 51 |
// ErrNoSwarm is returned on leaving a cluster that was never initialized |
| ... | ... |
@@ -120,6 +126,7 @@ type node struct {
|
| 120 | 120 |
ready bool |
| 121 | 121 |
conn *grpc.ClientConn |
| 122 | 122 |
client swarmapi.ControlClient |
| 123 |
+ logs swarmapi.LogsClient |
|
| 123 | 124 |
reconnectDelay time.Duration |
| 124 | 125 |
config nodeStartConfig |
| 125 | 126 |
} |
| ... | ... |
@@ -371,8 +378,10 @@ func (c *Cluster) startNewNode(conf nodeStartConfig) (*node, error) {
|
| 371 | 371 |
if node.conn != conn {
|
| 372 | 372 |
if conn == nil {
|
| 373 | 373 |
node.client = nil |
| 374 |
+ node.logs = nil |
|
| 374 | 375 |
} else {
|
| 375 | 376 |
node.client = swarmapi.NewControlClient(conn) |
| 377 |
+ node.logs = swarmapi.NewLogsClient(conn) |
|
| 376 | 378 |
} |
| 377 | 379 |
} |
| 378 | 380 |
node.conn = conn |
| ... | ... |
@@ -1205,6 +1214,88 @@ func (c *Cluster) RemoveService(input string) error {
|
| 1205 | 1205 |
return nil |
| 1206 | 1206 |
} |
| 1207 | 1207 |
|
| 1208 |
+// ServiceLogs collects service logs and writes them back to `config.OutStream` |
|
| 1209 |
+func (c *Cluster) ServiceLogs(ctx context.Context, input string, config *backend.ContainerLogsConfig, started chan struct{}) error {
|
|
| 1210 |
+ c.RLock() |
|
| 1211 |
+ if !c.isActiveManager() {
|
|
| 1212 |
+ c.RUnlock() |
|
| 1213 |
+ return c.errNoManager() |
|
| 1214 |
+ } |
|
| 1215 |
+ |
|
| 1216 |
+ service, err := getService(ctx, c.client, input) |
|
| 1217 |
+ if err != nil {
|
|
| 1218 |
+ c.RUnlock() |
|
| 1219 |
+ return err |
|
| 1220 |
+ } |
|
| 1221 |
+ |
|
| 1222 |
+ stream, err := c.logs.SubscribeLogs(ctx, &swarmapi.SubscribeLogsRequest{
|
|
| 1223 |
+ Selector: &swarmapi.LogSelector{
|
|
| 1224 |
+ ServiceIDs: []string{service.ID},
|
|
| 1225 |
+ }, |
|
| 1226 |
+ Options: &swarmapi.LogSubscriptionOptions{
|
|
| 1227 |
+ Follow: true, |
|
| 1228 |
+ }, |
|
| 1229 |
+ }) |
|
| 1230 |
+ if err != nil {
|
|
| 1231 |
+ c.RUnlock() |
|
| 1232 |
+ return err |
|
| 1233 |
+ } |
|
| 1234 |
+ |
|
| 1235 |
+ wf := ioutils.NewWriteFlusher(config.OutStream) |
|
| 1236 |
+ defer wf.Close() |
|
| 1237 |
+ close(started) |
|
| 1238 |
+ wf.Flush() |
|
| 1239 |
+ |
|
| 1240 |
+ outStream := stdcopy.NewStdWriter(wf, stdcopy.Stdout) |
|
| 1241 |
+ errStream := stdcopy.NewStdWriter(wf, stdcopy.Stderr) |
|
| 1242 |
+ |
|
| 1243 |
+ // Release the lock before starting the stream. |
|
| 1244 |
+ c.RUnlock() |
|
| 1245 |
+ for {
|
|
| 1246 |
+ // Check the context before doing anything. |
|
| 1247 |
+ select {
|
|
| 1248 |
+ case <-ctx.Done(): |
|
| 1249 |
+ return ctx.Err() |
|
| 1250 |
+ default: |
|
| 1251 |
+ } |
|
| 1252 |
+ |
|
| 1253 |
+ subscribeMsg, err := stream.Recv() |
|
| 1254 |
+ if err == io.EOF {
|
|
| 1255 |
+ return nil |
|
| 1256 |
+ } |
|
| 1257 |
+ if err != nil {
|
|
| 1258 |
+ return err |
|
| 1259 |
+ } |
|
| 1260 |
+ |
|
| 1261 |
+ for _, msg := range subscribeMsg.Messages {
|
|
| 1262 |
+ data := []byte{}
|
|
| 1263 |
+ |
|
| 1264 |
+ if config.Timestamps {
|
|
| 1265 |
+ ts, err := ptypes.Timestamp(msg.Timestamp) |
|
| 1266 |
+ if err != nil {
|
|
| 1267 |
+ return err |
|
| 1268 |
+ } |
|
| 1269 |
+ data = append(data, []byte(ts.Format(logger.TimeFormat)+" ")...) |
|
| 1270 |
+ } |
|
| 1271 |
+ |
|
| 1272 |
+ data = append(data, []byte(fmt.Sprintf("%s.node.id=%s,%s.service.id=%s,%s.task.id=%s ",
|
|
| 1273 |
+ contextPrefix, msg.Context.NodeID, |
|
| 1274 |
+ contextPrefix, msg.Context.ServiceID, |
|
| 1275 |
+ contextPrefix, msg.Context.TaskID, |
|
| 1276 |
+ ))...) |
|
| 1277 |
+ |
|
| 1278 |
+ data = append(data, msg.Data...) |
|
| 1279 |
+ |
|
| 1280 |
+ switch msg.Stream {
|
|
| 1281 |
+ case swarmapi.LogStreamStdout: |
|
| 1282 |
+ outStream.Write(data) |
|
| 1283 |
+ case swarmapi.LogStreamStderr: |
|
| 1284 |
+ errStream.Write(data) |
|
| 1285 |
+ } |
|
| 1286 |
+ } |
|
| 1287 |
+ } |
|
| 1288 |
+} |
|
| 1289 |
+ |
|
| 1208 | 1290 |
// GetNodes returns a list of all nodes known to a cluster. |
| 1209 | 1291 |
func (c *Cluster) GetNodes(options apitypes.NodeListOptions) ([]types.Node, error) {
|
| 1210 | 1292 |
c.RLock() |