github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/nomad/stream/event_broker.go (about) 1 package stream 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "sync" 8 "sync/atomic" 9 "time" 10 11 "github.com/armon/go-metrics" 12 "github.com/hashicorp/go-memdb" 13 lru "github.com/hashicorp/golang-lru" 14 "github.com/hashicorp/nomad/acl" 15 "github.com/hashicorp/nomad/nomad/structs" 16 17 "github.com/hashicorp/go-hclog" 18 ) 19 20 const ( 21 ACLCheckNodeRead = "node-read" 22 ACLCheckManagement = "management" 23 aclCacheSize = 32 24 ) 25 26 type EventBrokerCfg struct { 27 EventBufferSize int64 28 Logger hclog.Logger 29 } 30 31 type EventBroker struct { 32 // mu protects subscriptions 33 mu sync.Mutex 34 subscriptions *subscriptions 35 36 // eventBuf stores a configurable amount of events in memory 37 eventBuf *eventBuffer 38 39 // publishCh is used to send messages from an active txn to a goroutine which 40 // publishes events, so that publishing can happen asynchronously from 41 // the Commit call in the FSM hot path. 42 publishCh chan *structs.Events 43 44 aclDelegate ACLDelegate 45 aclCache *lru.TwoQueueCache 46 47 aclCh chan structs.Event 48 49 logger hclog.Logger 50 } 51 52 // NewEventBroker returns an EventBroker for publishing change events. 53 // A goroutine is run in the background to publish events to an event buffer. 54 // Cancelling the context will shutdown the goroutine to free resources, and stop 55 // all publishing. 56 func NewEventBroker(ctx context.Context, aclDelegate ACLDelegate, cfg EventBrokerCfg) (*EventBroker, error) { 57 if cfg.Logger == nil { 58 cfg.Logger = hclog.NewNullLogger() 59 } 60 61 // Set the event buffer size to a minimum 62 if cfg.EventBufferSize == 0 { 63 cfg.EventBufferSize = 100 64 } 65 66 aclCache, err := lru.New2Q(aclCacheSize) 67 if err != nil { 68 return nil, err 69 } 70 71 buffer := newEventBuffer(cfg.EventBufferSize) 72 e := &EventBroker{ 73 logger: cfg.Logger.Named("event_broker"), 74 eventBuf: buffer, 75 publishCh: make(chan *structs.Events, 64), 76 aclCh: make(chan structs.Event, 10), 77 aclDelegate: aclDelegate, 78 aclCache: aclCache, 79 subscriptions: &subscriptions{ 80 byToken: make(map[string]map[*SubscribeRequest]*Subscription), 81 }, 82 } 83 84 go e.handleUpdates(ctx) 85 go e.handleACLUpdates(ctx) 86 87 return e, nil 88 } 89 90 // Len returns the current length of the event buffer. 91 func (e *EventBroker) Len() int { 92 return e.eventBuf.Len() 93 } 94 95 // Publish events to all subscribers of the event Topic. 96 func (e *EventBroker) Publish(events *structs.Events) { 97 if len(events.Events) == 0 { 98 return 99 } 100 101 // Notify the broker to check running subscriptions against potentially 102 // updated ACL Token or Policy 103 for _, event := range events.Events { 104 if event.Topic == structs.TopicACLToken || event.Topic == structs.TopicACLPolicy { 105 e.aclCh <- event 106 } 107 } 108 109 e.publishCh <- events 110 } 111 112 // SubscribeWithACLCheck validates the SubscribeRequest's token and requested 113 // topics to ensure that the tokens privileges are sufficient. It will also 114 // return the token expiry time, if any. It is the callers responsibility to 115 // check this before publishing events to the caller. 116 func (e *EventBroker) SubscribeWithACLCheck(req *SubscribeRequest) (*Subscription, *time.Time, error) { 117 aclObj, expiryTime, err := aclObjFromSnapshotForTokenSecretID(e.aclDelegate.TokenProvider(), e.aclCache, req.Token) 118 if err != nil { 119 return nil, nil, structs.ErrPermissionDenied 120 } 121 122 if allowed := aclAllowsSubscription(aclObj, req); !allowed { 123 return nil, nil, structs.ErrPermissionDenied 124 } 125 126 sub, err := e.Subscribe(req) 127 if err != nil { 128 return nil, nil, err 129 } 130 return sub, expiryTime, nil 131 } 132 133 // Subscribe returns a new Subscription for a given request. A Subscription 134 // will receive an initial empty currentItem value which points to the first item 135 // in the buffer. This allows the new subscription to call Next() without first checking 136 // for the current Item. 137 // 138 // A Subscription will start at the requested index, or as close as possible to 139 // the requested index if it is no longer in the buffer. If StartExactlyAtIndex is 140 // set and the index is no longer in the buffer or not yet in the buffer an error 141 // will be returned. 142 // 143 // When a caller is finished with the subscription it must call Subscription.Unsubscribe 144 // to free ACL tracking resources. 145 func (e *EventBroker) Subscribe(req *SubscribeRequest) (*Subscription, error) { 146 e.mu.Lock() 147 defer e.mu.Unlock() 148 149 var head *bufferItem 150 var offset int 151 if req.Index != 0 { 152 head, offset = e.eventBuf.StartAtClosest(req.Index) 153 } else { 154 head = e.eventBuf.Head() 155 } 156 if offset > 0 && req.StartExactlyAtIndex { 157 return nil, fmt.Errorf("requested index not in buffer") 158 } else if offset > 0 { 159 metrics.SetGauge([]string{"nomad", "event_broker", "subscription", "request_offset"}, float32(offset)) 160 e.logger.Debug("requested index no longer in buffer", "requsted", int(req.Index), "closest", int(head.Events.Index)) 161 } 162 163 // Empty head so that calling Next on sub 164 start := newBufferItem(&structs.Events{Index: req.Index}) 165 start.link.next.Store(head) 166 close(start.link.nextCh) 167 168 sub := newSubscription(req, start, e.subscriptions.unsubscribeFn(req)) 169 170 e.subscriptions.add(req, sub) 171 return sub, nil 172 } 173 174 // CloseAll closes all subscriptions 175 func (e *EventBroker) CloseAll() { 176 e.subscriptions.closeAll() 177 } 178 179 func (e *EventBroker) handleUpdates(ctx context.Context) { 180 for { 181 select { 182 case <-ctx.Done(): 183 e.subscriptions.closeAll() 184 return 185 case update := <-e.publishCh: 186 e.eventBuf.Append(update) 187 } 188 } 189 } 190 191 func (e *EventBroker) handleACLUpdates(ctx context.Context) { 192 for { 193 select { 194 case <-ctx.Done(): 195 return 196 case update := <-e.aclCh: 197 switch payload := update.Payload.(type) { 198 case *structs.ACLTokenEvent: 199 tokenSecretID := payload.SecretID() 200 201 // Token was deleted 202 if update.Type == structs.TypeACLTokenDeleted { 203 e.subscriptions.closeSubscriptionsForTokens([]string{tokenSecretID}) 204 continue 205 } 206 207 // If broker cannot fetch state there is nothing more to do 208 if e.aclDelegate == nil { 209 continue 210 } 211 212 aclObj, expiryTime, err := aclObjFromSnapshotForTokenSecretID(e.aclDelegate.TokenProvider(), e.aclCache, tokenSecretID) 213 if err != nil || aclObj == nil { 214 e.logger.Error("failed resolving ACL for secretID, closing subscriptions", "error", err) 215 e.subscriptions.closeSubscriptionsForTokens([]string{tokenSecretID}) 216 continue 217 } 218 219 if expiryTime != nil && expiryTime.Before(time.Now().UTC()) { 220 e.logger.Info("ACL token is expired, closing subscriptions") 221 e.subscriptions.closeSubscriptionsForTokens([]string{tokenSecretID}) 222 continue 223 } 224 225 e.subscriptions.closeSubscriptionFunc(tokenSecretID, func(sub *Subscription) bool { 226 return !aclAllowsSubscription(aclObj, sub.req) 227 }) 228 229 case *structs.ACLPolicyEvent, *structs.ACLRoleStreamEvent: 230 // Re-evaluate each subscription permission since a policy or 231 // role change may alter the permissions of the token being 232 // used for the subscription. 233 e.checkSubscriptionsAgainstACLChange() 234 } 235 } 236 } 237 } 238 239 // checkSubscriptionsAgainstACLChange iterates over the brokers subscriptions 240 // and evaluates whether the token used for the subscription is still valid. A 241 // token may become invalid is the assigned policies or roles have been updated 242 // which removed the required permission. If the token is no long valid, the 243 // subscription is closed. 244 func (e *EventBroker) checkSubscriptionsAgainstACLChange() { 245 e.mu.Lock() 246 defer e.mu.Unlock() 247 248 // If broker cannot fetch state there is nothing more to do 249 if e.aclDelegate == nil { 250 return 251 } 252 253 aclSnapshot := e.aclDelegate.TokenProvider() 254 for tokenSecretID := range e.subscriptions.byToken { 255 // if tokenSecretID is empty ACLs were disabled at time of subscribing 256 if tokenSecretID == "" { 257 continue 258 } 259 260 aclObj, expiryTime, err := aclObjFromSnapshotForTokenSecretID(aclSnapshot, e.aclCache, tokenSecretID) 261 if err != nil || aclObj == nil { 262 e.logger.Debug("failed resolving ACL for secretID, closing subscriptions", "error", err) 263 e.subscriptions.closeSubscriptionsForTokens([]string{tokenSecretID}) 264 continue 265 } 266 267 if expiryTime != nil && expiryTime.Before(time.Now().UTC()) { 268 e.logger.Info("ACL token is expired, closing subscriptions") 269 e.subscriptions.closeSubscriptionsForTokens([]string{tokenSecretID}) 270 continue 271 } 272 273 e.subscriptions.closeSubscriptionFunc(tokenSecretID, func(sub *Subscription) bool { 274 return !aclAllowsSubscription(aclObj, sub.req) 275 }) 276 } 277 } 278 279 func aclObjFromSnapshotForTokenSecretID( 280 aclSnapshot ACLTokenProvider, aclCache *lru.TwoQueueCache, tokenSecretID string) ( 281 *acl.ACL, *time.Time, error) { 282 283 aclToken, err := aclSnapshot.ACLTokenBySecretID(nil, tokenSecretID) 284 if err != nil { 285 return nil, nil, err 286 } 287 288 if aclToken == nil { 289 return nil, nil, structs.ErrTokenNotFound 290 } 291 if aclToken.IsExpired(time.Now().UTC()) { 292 return nil, nil, structs.ErrTokenExpired 293 } 294 295 // Check if this is a management token 296 if aclToken.Type == structs.ACLManagementToken { 297 return acl.ManagementACL, aclToken.ExpirationTime, nil 298 } 299 300 aclPolicies := make([]*structs.ACLPolicy, 0, len(aclToken.Policies)+len(aclToken.Roles)) 301 302 for _, policyName := range aclToken.Policies { 303 policy, err := aclSnapshot.ACLPolicyByName(nil, policyName) 304 if err != nil || policy == nil { 305 return nil, nil, errors.New("error finding acl policy") 306 } 307 aclPolicies = append(aclPolicies, policy) 308 } 309 310 // Iterate all the token role links, so we can unpack these and identify 311 // the ACL policies. 312 for _, roleLink := range aclToken.Roles { 313 314 role, err := aclSnapshot.GetACLRoleByID(nil, roleLink.ID) 315 if err != nil { 316 return nil, nil, err 317 } 318 if role == nil { 319 continue 320 } 321 322 for _, policyLink := range role.Policies { 323 policy, err := aclSnapshot.ACLPolicyByName(nil, policyLink.Name) 324 if err != nil || policy == nil { 325 return nil, nil, errors.New("error finding acl policy") 326 } 327 aclPolicies = append(aclPolicies, policy) 328 } 329 } 330 331 aclObj, err := structs.CompileACLObject(aclCache, aclPolicies) 332 if err != nil { 333 return nil, nil, err 334 } 335 return aclObj, aclToken.ExpirationTime, nil 336 } 337 338 type ACLTokenProvider interface { 339 ACLTokenBySecretID(ws memdb.WatchSet, secretID string) (*structs.ACLToken, error) 340 ACLPolicyByName(ws memdb.WatchSet, policyName string) (*structs.ACLPolicy, error) 341 GetACLRoleByID(ws memdb.WatchSet, roleID string) (*structs.ACLRole, error) 342 } 343 344 type ACLDelegate interface { 345 TokenProvider() ACLTokenProvider 346 } 347 348 func aclAllowsSubscription(aclObj *acl.ACL, subReq *SubscribeRequest) bool { 349 for topic := range subReq.Topics { 350 switch topic { 351 case structs.TopicDeployment, 352 structs.TopicEvaluation, 353 structs.TopicAllocation, 354 structs.TopicJob, 355 structs.TopicService: 356 if ok := aclObj.AllowNsOp(subReq.Namespace, acl.NamespaceCapabilityReadJob); !ok { 357 return false 358 } 359 case structs.TopicNode: 360 if ok := aclObj.AllowNodeRead(); !ok { 361 return false 362 } 363 default: 364 if ok := aclObj.IsManagement(); !ok { 365 return false 366 } 367 } 368 } 369 370 return true 371 } 372 373 func (s *Subscription) forceClose() { 374 if atomic.CompareAndSwapUint32(&s.state, subscriptionStateOpen, subscriptionStateClosed) { 375 close(s.forceClosed) 376 } 377 } 378 379 type subscriptions struct { 380 // mu for byToken. If both subscription.mu and EventBroker.mu need 381 // to be held, EventBroker mutex MUST always be acquired first. 382 mu sync.RWMutex 383 384 // byToken is an mapping of active Subscriptions indexed by a token and 385 // a pointer to the request. 386 // When the token is modified all subscriptions under that token will be 387 // reloaded. 388 // A subscription may be unsubscribed by using the pointer to the request. 389 byToken map[string]map[*SubscribeRequest]*Subscription 390 } 391 392 func (s *subscriptions) add(req *SubscribeRequest, sub *Subscription) { 393 s.mu.Lock() 394 defer s.mu.Unlock() 395 396 subsByToken, ok := s.byToken[req.Token] 397 if !ok { 398 subsByToken = make(map[*SubscribeRequest]*Subscription) 399 s.byToken[req.Token] = subsByToken 400 } 401 subsByToken[req] = sub 402 } 403 404 func (s *subscriptions) closeSubscriptionsForTokens(tokenSecretIDs []string) { 405 s.mu.RLock() 406 defer s.mu.RUnlock() 407 408 for _, secretID := range tokenSecretIDs { 409 if subs, ok := s.byToken[secretID]; ok { 410 for _, sub := range subs { 411 sub.forceClose() 412 } 413 } 414 } 415 } 416 417 func (s *subscriptions) closeSubscriptionFunc(tokenSecretID string, fn func(*Subscription) bool) { 418 s.mu.RLock() 419 defer s.mu.RUnlock() 420 421 for _, sub := range s.byToken[tokenSecretID] { 422 if fn(sub) { 423 sub.forceClose() 424 } 425 } 426 } 427 428 // unsubscribeFn returns a function that the subscription will call to remove 429 // itself from the subsByToken. 430 // This function is returned as a closure so that the caller doesn't need to keep 431 // track of the SubscriptionRequest, and can not accidentally call unsubscribeFn with the 432 // wrong pointer. 433 func (s *subscriptions) unsubscribeFn(req *SubscribeRequest) func() { 434 return func() { 435 s.mu.Lock() 436 defer s.mu.Unlock() 437 438 subsByToken, ok := s.byToken[req.Token] 439 if !ok { 440 return 441 } 442 443 sub := subsByToken[req] 444 if sub == nil { 445 return 446 } 447 448 // close the subscription 449 sub.forceClose() 450 451 delete(subsByToken, req) 452 if len(subsByToken) == 0 { 453 delete(s.byToken, req.Token) 454 } 455 } 456 } 457 458 func (s *subscriptions) closeAll() { 459 s.mu.Lock() 460 defer s.mu.Unlock() 461 462 for _, byRequest := range s.byToken { 463 for _, sub := range byRequest { 464 sub.forceClose() 465 } 466 } 467 }