2020-09-28 14:13:10 +00:00
|
|
|
package nomad
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2020-10-08 18:27:52 +00:00
|
|
|
"fmt"
|
2020-09-28 14:13:10 +00:00
|
|
|
"io"
|
2020-10-08 18:27:52 +00:00
|
|
|
"io/ioutil"
|
2020-09-28 14:13:10 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/hashicorp/go-msgpack/codec"
|
2020-10-08 18:27:52 +00:00
|
|
|
"github.com/hashicorp/nomad/acl"
|
2020-09-28 14:13:10 +00:00
|
|
|
"github.com/hashicorp/nomad/helper"
|
|
|
|
"github.com/hashicorp/nomad/nomad/stream"
|
|
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Event struct {
|
|
|
|
srv *Server
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *Event) register() {
|
|
|
|
e.srv.streamingRpcs.Register("Event.Stream", e.stream)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *Event) stream(conn io.ReadWriteCloser) {
|
|
|
|
defer conn.Close()
|
|
|
|
|
|
|
|
var args structs.EventStreamRequest
|
|
|
|
decoder := codec.NewDecoder(conn, structs.MsgpackHandle)
|
|
|
|
encoder := codec.NewEncoder(conn, structs.MsgpackHandle)
|
|
|
|
|
|
|
|
if err := decoder.Decode(&args); err != nil {
|
|
|
|
handleJsonResultError(err, helper.Int64ToPtr(500), encoder)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// forward to appropriate region
|
|
|
|
if args.Region != e.srv.config.Region {
|
|
|
|
err := e.forwardStreamingRPC(args.Region, "Event.Stream", args, conn)
|
|
|
|
if err != nil {
|
|
|
|
handleJsonResultError(err, helper.Int64ToPtr(500), encoder)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-10-08 18:27:52 +00:00
|
|
|
aclObj, err := e.srv.ResolveToken(args.AuthToken)
|
|
|
|
if err != nil {
|
2020-09-28 14:13:10 +00:00
|
|
|
handleJsonResultError(err, nil, encoder)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
subReq := &stream.SubscribeRequest{
|
2020-10-08 18:27:52 +00:00
|
|
|
Token: args.AuthToken,
|
2020-10-08 15:57:21 +00:00
|
|
|
Topics: args.Topics,
|
|
|
|
Index: uint64(args.Index),
|
|
|
|
Namespace: args.Namespace,
|
2020-09-28 14:13:10 +00:00
|
|
|
}
|
2020-10-08 18:27:52 +00:00
|
|
|
|
|
|
|
// Check required ACL permissions for requested Topics
|
|
|
|
if aclObj != nil {
|
|
|
|
if err := aclCheckForEvents(subReq, aclObj); err != nil {
|
|
|
|
handleJsonResultError(structs.ErrPermissionDenied, helper.Int64ToPtr(403), encoder)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the servers broker and subscribe
|
|
|
|
publisher, err := e.srv.State().EventBroker()
|
2020-09-28 14:13:10 +00:00
|
|
|
if err != nil {
|
|
|
|
handleJsonResultError(err, helper.Int64ToPtr(500), encoder)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
// start subscription to publisher
|
|
|
|
subscription, err := publisher.Subscribe(subReq)
|
|
|
|
if err != nil {
|
|
|
|
handleJsonResultError(err, helper.Int64ToPtr(500), encoder)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer subscription.Unsubscribe()
|
|
|
|
|
|
|
|
errCh := make(chan error)
|
|
|
|
|
2020-10-08 18:27:52 +00:00
|
|
|
jsonStream := stream.NewJsonStream(ctx, 30*time.Second)
|
2020-09-28 14:13:10 +00:00
|
|
|
|
|
|
|
// goroutine to detect remote side closing
|
|
|
|
go func() {
|
2020-10-08 18:27:52 +00:00
|
|
|
io.Copy(ioutil.Discard, conn)
|
|
|
|
cancel()
|
2020-09-28 14:13:10 +00:00
|
|
|
}()
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
defer cancel()
|
|
|
|
for {
|
|
|
|
events, err := subscription.Next(ctx)
|
|
|
|
if err != nil {
|
|
|
|
select {
|
|
|
|
case errCh <- err:
|
|
|
|
case <-ctx.Done():
|
|
|
|
}
|
2020-10-06 15:08:12 +00:00
|
|
|
return
|
2020-09-28 14:13:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Continue if there are no events
|
2020-10-01 18:43:28 +00:00
|
|
|
if len(events.Events) == 0 {
|
2020-09-28 14:13:10 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2020-10-01 18:43:28 +00:00
|
|
|
if err := jsonStream.Send(events); err != nil {
|
|
|
|
select {
|
|
|
|
case errCh <- err:
|
|
|
|
case <-ctx.Done():
|
2020-09-28 14:13:10 +00:00
|
|
|
}
|
2020-10-06 15:08:12 +00:00
|
|
|
return
|
2020-09-28 14:13:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
var streamErr error
|
|
|
|
OUTER:
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case streamErr = <-errCh:
|
|
|
|
break OUTER
|
|
|
|
case <-ctx.Done():
|
|
|
|
break OUTER
|
2020-10-08 18:27:52 +00:00
|
|
|
case eventJSON, ok := <-jsonStream.OutCh():
|
2020-09-28 14:13:10 +00:00
|
|
|
// check if ndjson may have been closed when an error occurred,
|
|
|
|
// check once more for an error.
|
|
|
|
if !ok {
|
|
|
|
select {
|
|
|
|
case streamErr = <-errCh:
|
|
|
|
// There was a pending error
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
break OUTER
|
|
|
|
}
|
|
|
|
|
|
|
|
var resp structs.EventStreamWrapper
|
|
|
|
resp.Event = eventJSON
|
|
|
|
|
|
|
|
if err := encoder.Encode(resp); err != nil {
|
|
|
|
streamErr = err
|
|
|
|
break OUTER
|
|
|
|
}
|
|
|
|
encoder.Reset(conn)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if streamErr != nil {
|
|
|
|
handleJsonResultError(streamErr, helper.Int64ToPtr(500), encoder)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *Event) forwardStreamingRPC(region string, method string, args interface{}, in io.ReadWriteCloser) error {
|
|
|
|
server, err := e.srv.findRegionServer(region)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return e.forwardStreamingRPCToServer(server, method, args, in)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *Event) forwardStreamingRPCToServer(server *serverParts, method string, args interface{}, in io.ReadWriteCloser) error {
|
|
|
|
srvConn, err := e.srv.streamingRpc(server, method)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer srvConn.Close()
|
|
|
|
|
|
|
|
outEncoder := codec.NewEncoder(srvConn, structs.MsgpackHandle)
|
|
|
|
if err := outEncoder.Encode(args); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
structs.Bridge(in, srvConn)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleJsonResultError is a helper for sending an error with a potential
|
|
|
|
// error code. The transmission of the error is ignored if the error has been
|
|
|
|
// generated by the closing of the underlying transport.
|
|
|
|
func handleJsonResultError(err error, code *int64, encoder *codec.Encoder) {
|
|
|
|
// Nothing to do as the conn is closed
|
|
|
|
if err == io.EOF {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
encoder.Encode(&structs.EventStreamWrapper{
|
|
|
|
Error: structs.NewRpcError(err, code),
|
|
|
|
})
|
|
|
|
}
|
2020-10-08 18:27:52 +00:00
|
|
|
|
|
|
|
func aclCheckForEvents(subReq *stream.SubscribeRequest, aclObj *acl.ACL) error {
|
|
|
|
if len(subReq.Topics) == 0 {
|
|
|
|
return fmt.Errorf("invalid topic request")
|
|
|
|
}
|
|
|
|
|
|
|
|
reqPolicies := make(map[string]struct{})
|
|
|
|
var required = struct{}{}
|
|
|
|
|
|
|
|
for topic := range subReq.Topics {
|
|
|
|
switch topic {
|
|
|
|
case structs.TopicDeployment, structs.TopicEval,
|
|
|
|
structs.TopicAlloc, structs.TopicJob:
|
|
|
|
if _, ok := reqPolicies[acl.NamespaceCapabilityReadJob]; !ok {
|
|
|
|
reqPolicies[acl.NamespaceCapabilityReadJob] = required
|
|
|
|
}
|
|
|
|
case structs.TopicNode:
|
|
|
|
reqPolicies["node-read"] = required
|
|
|
|
case structs.TopicAll:
|
|
|
|
reqPolicies["management"] = required
|
|
|
|
default:
|
|
|
|
return fmt.Errorf("unknown topic %s", topic)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for checks := range reqPolicies {
|
|
|
|
switch checks {
|
|
|
|
case acl.NamespaceCapabilityReadJob:
|
|
|
|
if ok := aclObj.AllowNsOp(subReq.Namespace, acl.NamespaceCapabilityReadJob); !ok {
|
|
|
|
return structs.ErrPermissionDenied
|
|
|
|
}
|
|
|
|
case "node-read":
|
|
|
|
if ok := aclObj.AllowNodeRead(); !ok {
|
|
|
|
return structs.ErrPermissionDenied
|
|
|
|
}
|
|
|
|
case "management":
|
|
|
|
if ok := aclObj.IsManagement(); !ok {
|
|
|
|
return structs.ErrPermissionDenied
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|