github.com/ferranbt/nomad@v0.9.3-0.20190607002617-85c449b7667c/plugins/drivers/client.go (about)

     1  package drivers
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"io"
     7  	"time"
     8  
     9  	"github.com/LK4D4/joincontext"
    10  	"github.com/golang/protobuf/ptypes"
    11  	hclog "github.com/hashicorp/go-hclog"
    12  	cstructs "github.com/hashicorp/nomad/client/structs"
    13  	"github.com/hashicorp/nomad/helper/pluginutils/grpcutils"
    14  	"github.com/hashicorp/nomad/nomad/structs"
    15  	"github.com/hashicorp/nomad/plugins/base"
    16  	"github.com/hashicorp/nomad/plugins/drivers/proto"
    17  	"github.com/hashicorp/nomad/plugins/shared/hclspec"
    18  	pstructs "github.com/hashicorp/nomad/plugins/shared/structs"
    19  	sproto "github.com/hashicorp/nomad/plugins/shared/structs/proto"
    20  	"google.golang.org/grpc/status"
    21  )
    22  
    23  var _ DriverPlugin = &driverPluginClient{}
    24  
    25  type driverPluginClient struct {
    26  	*base.BasePluginClient
    27  
    28  	client proto.DriverClient
    29  	logger hclog.Logger
    30  
    31  	// doneCtx is closed when the plugin exits
    32  	doneCtx context.Context
    33  }
    34  
    35  func (d *driverPluginClient) TaskConfigSchema() (*hclspec.Spec, error) {
    36  	req := &proto.TaskConfigSchemaRequest{}
    37  
    38  	resp, err := d.client.TaskConfigSchema(d.doneCtx, req)
    39  	if err != nil {
    40  		return nil, grpcutils.HandleGrpcErr(err, d.doneCtx)
    41  	}
    42  
    43  	return resp.Spec, nil
    44  }
    45  
    46  func (d *driverPluginClient) Capabilities() (*Capabilities, error) {
    47  	req := &proto.CapabilitiesRequest{}
    48  
    49  	resp, err := d.client.Capabilities(d.doneCtx, req)
    50  	if err != nil {
    51  		return nil, grpcutils.HandleGrpcErr(err, d.doneCtx)
    52  	}
    53  
    54  	caps := &Capabilities{}
    55  	if resp.Capabilities != nil {
    56  		caps.SendSignals = resp.Capabilities.SendSignals
    57  		caps.Exec = resp.Capabilities.Exec
    58  
    59  		switch resp.Capabilities.FsIsolation {
    60  		case proto.DriverCapabilities_NONE:
    61  			caps.FSIsolation = FSIsolationNone
    62  		case proto.DriverCapabilities_CHROOT:
    63  			caps.FSIsolation = FSIsolationChroot
    64  		case proto.DriverCapabilities_IMAGE:
    65  			caps.FSIsolation = FSIsolationImage
    66  		default:
    67  			caps.FSIsolation = FSIsolationNone
    68  		}
    69  	}
    70  
    71  	return caps, nil
    72  }
    73  
    74  // Fingerprint the driver, return a chan that will be pushed to periodically and on changes to health
    75  func (d *driverPluginClient) Fingerprint(ctx context.Context) (<-chan *Fingerprint, error) {
    76  	req := &proto.FingerprintRequest{}
    77  
    78  	// Join the passed context and the shutdown context
    79  	joinedCtx, _ := joincontext.Join(ctx, d.doneCtx)
    80  
    81  	stream, err := d.client.Fingerprint(joinedCtx, req)
    82  	if err != nil {
    83  		return nil, grpcutils.HandleReqCtxGrpcErr(err, ctx, d.doneCtx)
    84  	}
    85  
    86  	ch := make(chan *Fingerprint, 1)
    87  	go d.handleFingerprint(ctx, ch, stream)
    88  
    89  	return ch, nil
    90  }
    91  
    92  func (d *driverPluginClient) handleFingerprint(reqCtx context.Context, ch chan *Fingerprint, stream proto.Driver_FingerprintClient) {
    93  	defer close(ch)
    94  	for {
    95  		pb, err := stream.Recv()
    96  		if err != nil {
    97  			if err != io.EOF {
    98  				ch <- &Fingerprint{
    99  					Err: grpcutils.HandleReqCtxGrpcErr(err, reqCtx, d.doneCtx),
   100  				}
   101  			}
   102  
   103  			// End the stream
   104  			return
   105  		}
   106  
   107  		f := &Fingerprint{
   108  			Attributes:        pstructs.ConvertProtoAttributeMap(pb.Attributes),
   109  			Health:            healthStateFromProto(pb.Health),
   110  			HealthDescription: pb.HealthDescription,
   111  		}
   112  
   113  		select {
   114  		case <-reqCtx.Done():
   115  			return
   116  		case ch <- f:
   117  		}
   118  	}
   119  }
   120  
   121  // RecoverTask does internal state recovery to be able to control the task of
   122  // the given TaskHandle
   123  func (d *driverPluginClient) RecoverTask(h *TaskHandle) error {
   124  	req := &proto.RecoverTaskRequest{Handle: taskHandleToProto(h)}
   125  
   126  	_, err := d.client.RecoverTask(d.doneCtx, req)
   127  	return grpcutils.HandleGrpcErr(err, d.doneCtx)
   128  }
   129  
   130  // StartTask starts execution of a task with the given TaskConfig. A TaskHandle
   131  // is returned to the caller that can be used to recover state of the task,
   132  // should the driver crash or exit prematurely.
   133  func (d *driverPluginClient) StartTask(c *TaskConfig) (*TaskHandle, *DriverNetwork, error) {
   134  	req := &proto.StartTaskRequest{
   135  		Task: taskConfigToProto(c),
   136  	}
   137  
   138  	resp, err := d.client.StartTask(d.doneCtx, req)
   139  	if err != nil {
   140  		st := status.Convert(err)
   141  		if len(st.Details()) > 0 {
   142  			if rec, ok := st.Details()[0].(*sproto.RecoverableError); ok {
   143  				return nil, nil, structs.NewRecoverableError(err, rec.Recoverable)
   144  			}
   145  		}
   146  		return nil, nil, grpcutils.HandleGrpcErr(err, d.doneCtx)
   147  	}
   148  
   149  	var net *DriverNetwork
   150  	if resp.NetworkOverride != nil {
   151  		net = &DriverNetwork{
   152  			PortMap:       map[string]int{},
   153  			IP:            resp.NetworkOverride.Addr,
   154  			AutoAdvertise: resp.NetworkOverride.AutoAdvertise,
   155  		}
   156  		for k, v := range resp.NetworkOverride.PortMap {
   157  			net.PortMap[k] = int(v)
   158  		}
   159  	}
   160  
   161  	return taskHandleFromProto(resp.Handle), net, nil
   162  }
   163  
   164  // WaitTask returns a channel that will have an ExitResult pushed to it once when the task
   165  // exits on its own or is killed. If WaitTask is called after the task has exited, the channel
   166  // will immedialy return the ExitResult. WaitTask can be called multiple times for
   167  // the same task without issue.
   168  func (d *driverPluginClient) WaitTask(ctx context.Context, id string) (<-chan *ExitResult, error) {
   169  	ch := make(chan *ExitResult)
   170  	go d.handleWaitTask(ctx, id, ch)
   171  	return ch, nil
   172  }
   173  
   174  func (d *driverPluginClient) handleWaitTask(ctx context.Context, id string, ch chan *ExitResult) {
   175  	defer close(ch)
   176  	var result ExitResult
   177  	req := &proto.WaitTaskRequest{
   178  		TaskId: id,
   179  	}
   180  
   181  	// Join the passed context and the shutdown context
   182  	joinedCtx, _ := joincontext.Join(ctx, d.doneCtx)
   183  
   184  	resp, err := d.client.WaitTask(joinedCtx, req)
   185  	if err != nil {
   186  		result.Err = grpcutils.HandleReqCtxGrpcErr(err, ctx, d.doneCtx)
   187  	} else {
   188  		result.ExitCode = int(resp.Result.ExitCode)
   189  		result.Signal = int(resp.Result.Signal)
   190  		result.OOMKilled = resp.Result.OomKilled
   191  		if len(resp.Err) > 0 {
   192  			result.Err = errors.New(resp.Err)
   193  		}
   194  	}
   195  	ch <- &result
   196  }
   197  
   198  // StopTask stops the task with the given taskID. A timeout and signal can be
   199  // given to control a graceful termination of the task. The driver will send the
   200  // given signal to the task and wait for the given timeout for it to exit. If the
   201  // task does not exit within the timeout it will be forcefully killed.
   202  func (d *driverPluginClient) StopTask(taskID string, timeout time.Duration, signal string) error {
   203  	req := &proto.StopTaskRequest{
   204  		TaskId:  taskID,
   205  		Timeout: ptypes.DurationProto(timeout),
   206  		Signal:  signal,
   207  	}
   208  
   209  	_, err := d.client.StopTask(d.doneCtx, req)
   210  	return grpcutils.HandleGrpcErr(err, d.doneCtx)
   211  }
   212  
   213  // DestroyTask removes the task from the driver's in memory state. The task
   214  // cannot be running unless force is set to true. If force is set to true the
   215  // driver will forcefully terminate the task before removing it.
   216  func (d *driverPluginClient) DestroyTask(taskID string, force bool) error {
   217  	req := &proto.DestroyTaskRequest{
   218  		TaskId: taskID,
   219  		Force:  force,
   220  	}
   221  
   222  	_, err := d.client.DestroyTask(d.doneCtx, req)
   223  	return grpcutils.HandleGrpcErr(err, d.doneCtx)
   224  }
   225  
   226  // InspectTask returns status information for a task
   227  func (d *driverPluginClient) InspectTask(taskID string) (*TaskStatus, error) {
   228  	req := &proto.InspectTaskRequest{TaskId: taskID}
   229  
   230  	resp, err := d.client.InspectTask(d.doneCtx, req)
   231  	if err != nil {
   232  		return nil, grpcutils.HandleGrpcErr(err, d.doneCtx)
   233  	}
   234  
   235  	status, err := taskStatusFromProto(resp.Task)
   236  	if err != nil {
   237  		return nil, err
   238  	}
   239  
   240  	if resp.Driver != nil {
   241  		status.DriverAttributes = resp.Driver.Attributes
   242  	}
   243  	if resp.NetworkOverride != nil {
   244  		status.NetworkOverride = &DriverNetwork{
   245  			PortMap:       map[string]int{},
   246  			IP:            resp.NetworkOverride.Addr,
   247  			AutoAdvertise: resp.NetworkOverride.AutoAdvertise,
   248  		}
   249  		for k, v := range resp.NetworkOverride.PortMap {
   250  			status.NetworkOverride.PortMap[k] = int(v)
   251  		}
   252  	}
   253  
   254  	return status, nil
   255  }
   256  
   257  // TaskStats returns resource usage statistics for the task
   258  func (d *driverPluginClient) TaskStats(ctx context.Context, taskID string, interval time.Duration) (<-chan *cstructs.TaskResourceUsage, error) {
   259  	req := &proto.TaskStatsRequest{
   260  		TaskId:             taskID,
   261  		CollectionInterval: ptypes.DurationProto(interval),
   262  	}
   263  	ctx, _ = joincontext.Join(ctx, d.doneCtx)
   264  	stream, err := d.client.TaskStats(ctx, req)
   265  	if err != nil {
   266  		st := status.Convert(err)
   267  		if len(st.Details()) > 0 {
   268  			if rec, ok := st.Details()[0].(*sproto.RecoverableError); ok {
   269  				return nil, structs.NewRecoverableError(err, rec.Recoverable)
   270  			}
   271  		}
   272  		return nil, err
   273  	}
   274  
   275  	ch := make(chan *cstructs.TaskResourceUsage, 1)
   276  	go d.handleStats(ctx, ch, stream)
   277  
   278  	return ch, nil
   279  }
   280  
   281  func (d *driverPluginClient) handleStats(ctx context.Context, ch chan<- *cstructs.TaskResourceUsage, stream proto.Driver_TaskStatsClient) {
   282  	defer close(ch)
   283  	for {
   284  		resp, err := stream.Recv()
   285  		if ctx.Err() != nil {
   286  			// Context canceled; exit gracefully
   287  			return
   288  		}
   289  
   290  		if err != nil {
   291  			if err != io.EOF {
   292  				d.logger.Error("error receiving stream from TaskStats driver RPC, closing stream", "error", err)
   293  			}
   294  
   295  			// End of stream
   296  			return
   297  		}
   298  
   299  		stats, err := TaskStatsFromProto(resp.Stats)
   300  		if err != nil {
   301  			d.logger.Error("failed to decode stats from RPC", "error", err, "stats", resp.Stats)
   302  			continue
   303  		}
   304  
   305  		select {
   306  		case ch <- stats:
   307  		case <-ctx.Done():
   308  		}
   309  	}
   310  }
   311  
   312  // TaskEvents returns a channel that will receive events from the driver about all
   313  // tasks such as lifecycle events, terminal errors, etc.
   314  func (d *driverPluginClient) TaskEvents(ctx context.Context) (<-chan *TaskEvent, error) {
   315  	req := &proto.TaskEventsRequest{}
   316  
   317  	// Join the passed context and the shutdown context
   318  	joinedCtx, _ := joincontext.Join(ctx, d.doneCtx)
   319  
   320  	stream, err := d.client.TaskEvents(joinedCtx, req)
   321  	if err != nil {
   322  		return nil, grpcutils.HandleReqCtxGrpcErr(err, ctx, d.doneCtx)
   323  	}
   324  
   325  	ch := make(chan *TaskEvent, 1)
   326  	go d.handleTaskEvents(ctx, ch, stream)
   327  	return ch, nil
   328  }
   329  
   330  func (d *driverPluginClient) handleTaskEvents(reqCtx context.Context, ch chan *TaskEvent, stream proto.Driver_TaskEventsClient) {
   331  	defer close(ch)
   332  	for {
   333  		ev, err := stream.Recv()
   334  		if err != nil {
   335  			if err != io.EOF {
   336  				ch <- &TaskEvent{
   337  					Err: grpcutils.HandleReqCtxGrpcErr(err, reqCtx, d.doneCtx),
   338  				}
   339  			}
   340  
   341  			// End the stream
   342  			return
   343  		}
   344  
   345  		timestamp, _ := ptypes.Timestamp(ev.Timestamp)
   346  		event := &TaskEvent{
   347  			TaskID:      ev.TaskId,
   348  			AllocID:     ev.AllocId,
   349  			TaskName:    ev.TaskName,
   350  			Annotations: ev.Annotations,
   351  			Message:     ev.Message,
   352  			Timestamp:   timestamp,
   353  		}
   354  		select {
   355  		case <-reqCtx.Done():
   356  			return
   357  		case ch <- event:
   358  		}
   359  	}
   360  }
   361  
   362  // SignalTask will send the given signal to the specified task
   363  func (d *driverPluginClient) SignalTask(taskID string, signal string) error {
   364  	req := &proto.SignalTaskRequest{
   365  		TaskId: taskID,
   366  		Signal: signal,
   367  	}
   368  	_, err := d.client.SignalTask(d.doneCtx, req)
   369  	return grpcutils.HandleGrpcErr(err, d.doneCtx)
   370  }
   371  
   372  // ExecTask will run the given command within the execution context of the task.
   373  // The driver will wait for the given timeout for the command to complete before
   374  // terminating it. The stdout and stderr of the command will be return to the caller,
   375  // along with other exit information such as exit code.
   376  func (d *driverPluginClient) ExecTask(taskID string, cmd []string, timeout time.Duration) (*ExecTaskResult, error) {
   377  	req := &proto.ExecTaskRequest{
   378  		TaskId:  taskID,
   379  		Command: cmd,
   380  		Timeout: ptypes.DurationProto(timeout),
   381  	}
   382  
   383  	resp, err := d.client.ExecTask(d.doneCtx, req)
   384  	if err != nil {
   385  		return nil, grpcutils.HandleGrpcErr(err, d.doneCtx)
   386  	}
   387  
   388  	result := &ExecTaskResult{
   389  		Stdout:     resp.Stdout,
   390  		Stderr:     resp.Stderr,
   391  		ExitResult: exitResultFromProto(resp.Result),
   392  	}
   393  
   394  	return result, nil
   395  }
   396  
   397  var _ ExecTaskStreamingRawDriver = (*driverPluginClient)(nil)
   398  
   399  func (d *driverPluginClient) ExecTaskStreamingRaw(ctx context.Context,
   400  	taskID string,
   401  	command []string,
   402  	tty bool,
   403  	execStream ExecTaskStream) error {
   404  
   405  	stream, err := d.client.ExecTaskStreaming(ctx)
   406  	if err != nil {
   407  		return grpcutils.HandleGrpcErr(err, d.doneCtx)
   408  	}
   409  
   410  	err = stream.Send(&proto.ExecTaskStreamingRequest{
   411  		Setup: &proto.ExecTaskStreamingRequest_Setup{
   412  			TaskId:  taskID,
   413  			Command: command,
   414  			Tty:     tty,
   415  		},
   416  	})
   417  	if err != nil {
   418  		return grpcutils.HandleGrpcErr(err, d.doneCtx)
   419  	}
   420  
   421  	errCh := make(chan error, 1)
   422  
   423  	go func() {
   424  		for {
   425  			m, err := execStream.Recv()
   426  			if err == io.EOF {
   427  				return
   428  			} else if err != nil {
   429  				errCh <- err
   430  				return
   431  			}
   432  
   433  			if err := stream.Send(m); err != nil {
   434  				errCh <- err
   435  				return
   436  			}
   437  
   438  		}
   439  	}()
   440  
   441  	for {
   442  		select {
   443  		case err := <-errCh:
   444  			return err
   445  		default:
   446  		}
   447  
   448  		m, err := stream.Recv()
   449  		if err == io.EOF {
   450  			// Once we get to the end of stream successfully, we can ignore errCh:
   451  			// e.g. input write failures after process terminates shouldn't cause method to fail
   452  			return nil
   453  		} else if err != nil {
   454  			return err
   455  		}
   456  
   457  		if err := execStream.Send(m); err != nil {
   458  			return err
   459  		}
   460  	}
   461  }