package subscribe import ( "context" "encoding/json" "fmt" "sync" "github.com/gorilla/websocket" "github.com/rancher/steve/pkg/schemaserver/types" ) type WatchSession struct { sync.Mutex apiOp *types.APIRequest watchers map[string]func() wg sync.WaitGroup ctx context.Context cancel func() } func (s *WatchSession) stop(id string, resp chan<- types.APIEvent) { s.Lock() defer s.Unlock() if cancel, ok := s.watchers[id]; ok { cancel() resp <- types.APIEvent{ Name: "resource.stop", ResourceType: id, } } delete(s.watchers, id) } func (s *WatchSession) add(resourceType, revision string, resp chan<- types.APIEvent) { s.Lock() defer s.Unlock() ctx, cancel := context.WithCancel(s.ctx) s.watchers[resourceType] = cancel s.wg.Add(1) go func() { defer s.wg.Done() defer s.stop(resourceType, resp) if err := s.stream(ctx, resourceType, revision, resp); err != nil { sendErr(resp, err, resourceType) } }() } func (s *WatchSession) stream(ctx context.Context, resourceType, revision string, result chan<- types.APIEvent) error { schema := s.apiOp.Schemas.LookupSchema(resourceType) if schema == nil { return fmt.Errorf("failed to find schema %s", resourceType) } else if schema.Store == nil { return fmt.Errorf("schema %s does not support watching", resourceType) } if err := s.apiOp.AccessControl.CanWatch(s.apiOp, schema); err != nil { return err } c, err := schema.Store.Watch(s.apiOp.WithContext(ctx), schema, types.WatchRequest{Revision: revision}) if err != nil { return err } result <- types.APIEvent{ Name: "resource.start", ResourceType: resourceType, } for event := range c { result <- event } return nil } func NewWatchSession(apiOp *types.APIRequest) *WatchSession { ws := &WatchSession{ apiOp: apiOp, watchers: map[string]func(){}, } ws.ctx, ws.cancel = context.WithCancel(apiOp.Request.Context()) return ws } func (s *WatchSession) Watch(conn *websocket.Conn) <-chan types.APIEvent { result := make(chan types.APIEvent, 100) go func() { defer close(result) if err := s.watch(conn, result); err != nil { sendErr(result, err, "") } }() return result } func (s *WatchSession) Close() { s.cancel() s.wg.Wait() } func (s *WatchSession) watch(conn *websocket.Conn, resp chan types.APIEvent) error { defer s.wg.Wait() defer s.cancel() for { _, r, err := conn.NextReader() if err != nil { return err } var sub Subscribe if err := json.NewDecoder(r).Decode(&sub); err != nil { sendErr(resp, err, "") continue } if sub.Stop { s.stop(sub.ResourceType, resp) } else if _, ok := s.watchers[sub.ResourceType]; !ok { s.add(sub.ResourceType, sub.ResourceVersion, resp) } } } func sendErr(resp chan<- types.APIEvent, err error, resourceType string) { resp <- types.APIEvent{ ResourceType: resourceType, Error: err, } }