SupportBundles: Do not allow multiple collectors with the same UID (#60581)

* unexport service methods

* fix typos

* make constants

* do not allow double register of support bundle collectors

* fix get collectors response
This commit is contained in:
Jo 2022-12-21 09:15:01 +00:00 committed by GitHub
parent 5ef545d290
commit d3031202b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 112 additions and 41 deletions

View File

@ -36,7 +36,7 @@ func (s *Service) registerAPIEndpoints(routeRegister routing.RouteRegister) {
} }
func (s *Service) handleList(ctx *models.ReqContext) response.Response { func (s *Service) handleList(ctx *models.ReqContext) response.Response {
bundles, err := s.List(ctx.Req.Context()) bundles, err := s.list(ctx.Req.Context())
if err != nil { if err != nil {
return response.Error(http.StatusInternalServerError, "failed to list bundles", err) return response.Error(http.StatusInternalServerError, "failed to list bundles", err)
} }
@ -59,7 +59,7 @@ func (s *Service) handleCreate(ctx *models.ReqContext) response.Response {
return response.Error(http.StatusBadRequest, "failed to parse request", err) return response.Error(http.StatusBadRequest, "failed to parse request", err)
} }
bundle, err := s.Create(context.Background(), c.Collectors, ctx.SignedInUser) bundle, err := s.create(context.Background(), c.Collectors, ctx.SignedInUser)
if err != nil { if err != nil {
return response.Error(http.StatusInternalServerError, "failed to create support bundle", err) return response.Error(http.StatusInternalServerError, "failed to create support bundle", err)
} }
@ -74,7 +74,7 @@ func (s *Service) handleCreate(ctx *models.ReqContext) response.Response {
func (s *Service) handleDownload(ctx *models.ReqContext) { func (s *Service) handleDownload(ctx *models.ReqContext) {
uid := web.Params(ctx.Req)[":uid"] uid := web.Params(ctx.Req)[":uid"]
bundle, err := s.Get(ctx.Req.Context(), uid) bundle, err := s.get(ctx.Req.Context(), uid)
if err != nil { if err != nil {
ctx.Redirect("/admin/support-bundles") ctx.Redirect("/admin/support-bundles")
return return
@ -102,7 +102,7 @@ func (s *Service) handleDownload(ctx *models.ReqContext) {
func (s *Service) handleRemove(ctx *models.ReqContext) response.Response { func (s *Service) handleRemove(ctx *models.ReqContext) response.Response {
uid := web.Params(ctx.Req)[":uid"] uid := web.Params(ctx.Req)[":uid"]
err := s.Remove(ctx.Req.Context(), uid) err := s.remove(ctx.Req.Context(), uid)
if err != nil { if err != nil {
return response.Error(http.StatusInternalServerError, "failed to remove bundle", err) return response.Error(http.StatusInternalServerError, "failed to remove bundle", err)
} }
@ -111,5 +111,11 @@ func (s *Service) handleRemove(ctx *models.ReqContext) response.Response {
} }
func (s *Service) handleGetCollectors(ctx *models.ReqContext) response.Response { func (s *Service) handleGetCollectors(ctx *models.ReqContext) response.Response {
return response.JSON(http.StatusOK, s.collectors) collectors := make([]supportbundles.Collector, 0, len(s.collectors))
for _, c := range s.collectors {
collectors = append(collectors, c)
}
return response.JSON(http.StatusOK, collectors)
} }

View File

@ -35,7 +35,7 @@ var (
} }
) )
func DeclareFixedRoles(ac accesscontrol.Service) error { func declareFixedRoles(ac accesscontrol.Service) error {
bundleReader := accesscontrol.RoleRegistration{ bundleReader := accesscontrol.RoleRegistration{
Role: bundleReaderRole, Role: bundleReaderRole,
Grants: []string{string(org.RoleAdmin)}, Grants: []string{string(org.RoleAdmin)},

View File

@ -21,9 +21,14 @@ import (
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
const (
cleanUpInterval = 24 * time.Hour
bundleCreationTimeout = 20 * time.Minute
)
type Service struct { type Service struct {
cfg *setting.Cfg cfg *setting.Cfg
store *store store bundleStore
pluginStore plugins.Store pluginStore plugins.Store
pluginSettings pluginsettings.Service pluginSettings pluginsettings.Service
accessControl ac.AccessControl accessControl ac.AccessControl
@ -31,7 +36,7 @@ type Service struct {
log log.Logger log log.Logger
collectors []supportbundles.Collector collectors map[string]supportbundles.Collector
} }
func ProvideService(cfg *setting.Cfg, func ProvideService(cfg *setting.Cfg,
@ -54,6 +59,7 @@ func ProvideService(cfg *setting.Cfg,
accessControl: accessControl, accessControl: accessControl,
features: features, features: features,
log: log.New("supportbundle.service"), log: log.New("supportbundle.service"),
collectors: make(map[string]supportbundles.Collector),
} }
if !features.IsEnabled(featuremgmt.FlagSupportBundles) { if !features.IsEnabled(featuremgmt.FlagSupportBundles) {
@ -61,7 +67,7 @@ func ProvideService(cfg *setting.Cfg,
} }
if !accessControl.IsDisabled() { if !accessControl.IsDisabled() {
if err := DeclareFixedRoles(accesscontrolService); err != nil { if err := declareFixedRoles(accesscontrolService); err != nil {
return nil, err return nil, err
} }
} }
@ -79,14 +85,39 @@ func ProvideService(cfg *setting.Cfg,
return s, nil return s, nil
} }
func (s *Service) Create(ctx context.Context, collectors []string, usr *user.SignedInUser) (*supportbundles.Bundle, error) { func (s *Service) RegisterSupportItemCollector(collector supportbundles.Collector) {
if _, ok := s.collectors[collector.UID]; ok {
s.log.Warn("Support bundle collector with the same UID already registered", "uid", collector.UID)
}
s.collectors[collector.UID] = collector
}
func (s *Service) Run(ctx context.Context) error {
if !s.features.IsEnabled(featuremgmt.FlagSupportBundles) {
return nil
}
ticker := time.NewTicker(cleanUpInterval)
defer ticker.Stop()
s.cleanup(ctx)
select {
case <-ticker.C:
s.cleanup(ctx)
case <-ctx.Done():
break
}
return ctx.Err()
}
func (s *Service) create(ctx context.Context, collectors []string, usr *user.SignedInUser) (*supportbundles.Bundle, error) {
bundle, err := s.store.Create(ctx, usr) bundle, err := s.store.Create(ctx, usr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
go func(uid string, collectors []string) { go func(uid string, collectors []string) {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Minute) ctx, cancel := context.WithTimeout(context.Background(), bundleCreationTimeout)
defer cancel() defer cancel()
s.startBundleWork(ctx, collectors, uid) s.startBundleWork(ctx, collectors, uid)
}(bundle.UID, collectors) }(bundle.UID, collectors)
@ -94,24 +125,24 @@ func (s *Service) Create(ctx context.Context, collectors []string, usr *user.Sig
return bundle, nil return bundle, nil
} }
func (s *Service) Get(ctx context.Context, uid string) (*supportbundles.Bundle, error) { func (s *Service) get(ctx context.Context, uid string) (*supportbundles.Bundle, error) {
return s.store.Get(ctx, uid) return s.store.Get(ctx, uid)
} }
func (s *Service) List(ctx context.Context) ([]supportbundles.Bundle, error) { func (s *Service) list(ctx context.Context) ([]supportbundles.Bundle, error) {
return s.store.List() return s.store.List()
} }
func (s *Service) Remove(ctx context.Context, uid string) error { func (s *Service) remove(ctx context.Context, uid string) error {
// Remove the data // Remove the data
bundle, err := s.store.Get(ctx, uid) bundle, err := s.store.Get(ctx, uid)
if err != nil { if err != nil {
return fmt.Errorf("could not retrieve support bundle with uid %s: %w", uid, err) return fmt.Errorf("could not retrieve support bundle with UID %s: %w", uid, err)
} }
// TODO handle cases when bundles aren't complete yet // TODO handle cases when bundles aren't complete yet
if bundle.State == supportbundles.StatePending { if bundle.State == supportbundles.StatePending {
return fmt.Errorf("could not remove a support bundle with uid %s as it is still beign cteated", uid) return fmt.Errorf("could not remove a support bundle with uid %s as it is still being created", uid)
} }
if bundle.FilePath != "" { if bundle.FilePath != "" {
@ -124,30 +155,8 @@ func (s *Service) Remove(ctx context.Context, uid string) error {
return s.store.Remove(ctx, uid) return s.store.Remove(ctx, uid)
} }
func (s *Service) RegisterSupportItemCollector(collector supportbundles.Collector) {
// FIXME: add check for duplicate UIDs
s.collectors = append(s.collectors, collector)
}
func (s *Service) Run(ctx context.Context) error {
if !s.features.IsEnabled(featuremgmt.FlagSupportBundles) {
return nil
}
ticker := time.NewTicker(24 * time.Hour)
defer ticker.Stop()
s.cleanup(ctx)
select {
case <-ticker.C:
s.cleanup(ctx)
case <-ctx.Done():
break
}
return ctx.Err()
}
func (s *Service) cleanup(ctx context.Context) { func (s *Service) cleanup(ctx context.Context) {
bundles, err := s.List(ctx) bundles, err := s.list(ctx)
if err != nil { if err != nil {
s.log.Error("failed to list bundles to clean up", "error", err) s.log.Error("failed to list bundles to clean up", "error", err)
} }
@ -155,7 +164,7 @@ func (s *Service) cleanup(ctx context.Context) {
if err == nil { if err == nil {
for _, b := range bundles { for _, b := range bundles {
if time.Now().Unix() >= b.ExpiresAt { if time.Now().Unix() >= b.ExpiresAt {
if err := s.Remove(ctx, b.UID); err != nil { if err := s.remove(ctx, b.UID); err != nil {
s.log.Error("failed to cleanup bundle", "error", err) s.log.Error("failed to cleanup bundle", "error", err)
} }
} }

View File

@ -0,0 +1,44 @@
package supportbundlesimpl
import (
"context"
"testing"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/supportbundles"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
func TestService_RegisterSupportItemCollector(t *testing.T) {
s := &Service{
cfg: &setting.Cfg{},
store: nil,
pluginStore: nil,
pluginSettings: nil,
accessControl: nil,
features: nil,
log: log.NewNopLogger(),
collectors: map[string]supportbundles.Collector{},
}
collector := supportbundles.Collector{
UID: "test",
DisplayName: "test",
Description: "test",
IncludedByDefault: true,
Default: true,
Fn: func(context.Context) (*supportbundles.SupportItem, error) {
return nil, nil
},
}
t.Run("should register collector", func(t *testing.T) {
s.RegisterSupportItemCollector(collector)
require.Len(t, s.collectors, 1)
})
t.Run("should not register collector with same UID", func(t *testing.T) {
s.RegisterSupportItemCollector(collector)
require.Len(t, s.collectors, 1)
})
}

View File

@ -14,6 +14,10 @@ import (
"github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user"
) )
const (
defaultBundleExpiration = 72 * time.Hour // 72h
)
func newStore(kv kvstore.KVStore) *store { func newStore(kv kvstore.KVStore) *store {
return &store{kv: kvstore.WithNamespace(kv, 0, "supportbundle")} return &store{kv: kvstore.WithNamespace(kv, 0, "supportbundle")}
} }
@ -22,6 +26,14 @@ type store struct {
kv *kvstore.NamespacedKVStore kv *kvstore.NamespacedKVStore
} }
type bundleStore interface {
Create(ctx context.Context, usr *user.SignedInUser) (*supportbundles.Bundle, error)
Get(ctx context.Context, uid string) (*supportbundles.Bundle, error)
List() ([]supportbundles.Bundle, error)
Remove(ctx context.Context, uid string) error
Update(ctx context.Context, uid string, state supportbundles.State, filePath string) error
}
func (s *store) Create(ctx context.Context, usr *user.SignedInUser) (*supportbundles.Bundle, error) { func (s *store) Create(ctx context.Context, usr *user.SignedInUser) (*supportbundles.Bundle, error) {
uid, err := uuid.NewRandom() uid, err := uuid.NewRandom()
if err != nil { if err != nil {
@ -33,7 +45,7 @@ func (s *store) Create(ctx context.Context, usr *user.SignedInUser) (*supportbun
State: supportbundles.StatePending, State: supportbundles.StatePending,
Creator: usr.Login, Creator: usr.Login,
CreatedAt: time.Now().Unix(), CreatedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(), ExpiresAt: time.Now().Add(defaultBundleExpiration).Unix(),
} }
if err := s.set(ctx, &bundle); err != nil { if err := s.set(ctx, &bundle); err != nil {