package features import ( "context" "fmt" "strings" "sync" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/dynamic" provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" "github.com/grafana/authlib/types" "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data/utils/jsoniter" "github.com/grafana/grafana-plugin-sdk-go/live" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/live/model" ) // WatchRunner will start a watch task and broadcast results type WatchRunner struct { publisher model.ChannelPublisher configProvider apiserver.RestConfigProvider watchingMu sync.Mutex watching map[string]*watcher } func NewWatchRunner(publisher model.ChannelPublisher, configProvider apiserver.RestConfigProvider) *WatchRunner { return &WatchRunner{ publisher: publisher, configProvider: configProvider, watching: make(map[string]*watcher), } } func (b *WatchRunner) GetHandlerForPath(_ string) (model.ChannelHandler, error) { return b, nil // all dashboards share the same handler } // Valid paths look like: {version}/{resource}[={name}]/{user.uid} // * v0alpha1/dashboards/u12345 // * v0alpha1/dashboards=ABCD/u12345 func (b *WatchRunner) OnSubscribe(_ context.Context, u identity.Requester, e model.SubscribeEvent) (model.SubscribeReply, backend.SubscribeStreamStatus, error) { // To make sure we do not share resources across users, in clude the UID in the path userID := u.GetIdentifier() if userID == "" { return model.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, fmt.Errorf("missing user identity") } if !strings.HasSuffix(e.Path, userID) { return model.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, fmt.Errorf("path must end with user uid (%s)", userID) } // While testing with provisioning repositories, we will limit this to admin only if !u.HasRole(identity.RoleAdmin) { return model.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, fmt.Errorf("only admin users for now") } b.watchingMu.Lock() defer b.watchingMu.Unlock() current, ok := b.watching[e.Channel] if ok && !current.done { return model.SubscribeReply{ JoinLeave: false, Presence: false, Recover: false, }, backend.SubscribeStreamStatusOK, nil } // Try to start a watcher for this request gvr, name, err := parseWatchRequest(e.Channel, userID) if err != nil { return model.SubscribeReply{}, backend.SubscribeStreamStatusNotFound, err } // Test this with only provisiong support -- then we can evaluate a broader rollout if gvr.Group != provisioning.GROUP { return model.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, fmt.Errorf("watching provisioned resources is OK allowed (for now)") } // doesn't matter what GetRestConfig sees for context, matters for watch below cfg, err := b.configProvider.GetRestConfig(context.Background()) if err != nil { return model.SubscribeReply{}, backend.SubscribeStreamStatusNotFound, err } // add user to both requester and authInfo context keys, older implementations are still using requester ctx := identity.WithRequester(types.WithAuthInfo(context.Background(), u), u) uclient, err := dynamic.NewForConfig(cfg) if err != nil { return model.SubscribeReply{}, backend.SubscribeStreamStatusNotFound, err } client := uclient.Resource(gvr).Namespace(u.GetNamespace()) opts := v1.ListOptions{} if len(name) > 1 { opts.FieldSelector = "metadata.name=" + name } watch, err := client.Watch(ctx, opts) if err != nil { return model.SubscribeReply{}, backend.SubscribeStreamStatusNotFound, err } current = &watcher{ orgId: u.GetOrgID(), channel: e.Channel, publisher: b.publisher, watch: watch, } b.watching[e.Channel] = current go current.run(ctx) return model.SubscribeReply{ JoinLeave: false, // need unsubscribe envents Presence: false, Recover: false, }, backend.SubscribeStreamStatusOK, nil } func parseWatchRequest(channel string, user string) (gvr schema.GroupVersionResource, name string, err error) { addr, err := live.ParseChannel(channel) if err != nil { return gvr, "", err } parts := strings.Split(addr.Path, "/") if len(parts) != 3 { return gvr, "", fmt.Errorf("expecting path: {version}/{resource}={name}/{user}") } if parts[2] != user { return gvr, "", fmt.Errorf("expecting user suffix: %s", user) } resource := strings.Split(parts[1], "=") gvr = schema.GroupVersionResource{ Group: addr.Namespace, Version: parts[0], Resource: resource[0], } if len(resource) > 1 { name = resource[1] } return gvr, name, nil } // OnPublish is called when a client wants to broadcast on the websocket func (b *WatchRunner) OnPublish(_ context.Context, u identity.Requester, e model.PublishEvent) (model.PublishReply, backend.PublishStreamStatus, error) { return model.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("watch does not support publish") } type watcher struct { orgId int64 channel string publisher model.ChannelPublisher done bool watch watch.Interface } func (b *watcher) run(ctx context.Context) { logger := logging.FromContext(ctx).With("channel", b.channel) ch := b.watch.ResultChan() for { select { // This is sent when there are no longer any subscriptions case <-ctx.Done(): logger.Info("context done", "channel", b.channel) b.watch.Stop() b.done = true return // Each watch event case event, ok := <-ch: if !ok { logger.Info("watch stream broken", "channel", b.channel) b.watch.Stop() b.done = true // will force reconnect from the frontend return } cfg := jsoniter.ConfigCompatibleWithStandardLibrary stream := cfg.BorrowStream(nil) defer cfg.ReturnStream(stream) // regular json.Marshal() uses upper case stream.WriteObjectStart() stream.WriteObjectField("type") stream.WriteString(string(event.Type)) stream.WriteMore() stream.WriteObjectField("object") stream.WriteVal(event.Object) stream.WriteObjectEnd() buf := stream.Buffer() data := make([]byte, len(buf)) copy(data, buf) err := b.publisher(b.orgId, b.channel, data) if err != nil { logger.Error("publish error", "channel", b.channel, "err", err) b.watch.Stop() b.done = true // will force reconnect from the frontend continue } } } }