SupportBundles: Move support bundles to services (#60986)

* move support bundles to services

* update CODEOWNERS
This commit is contained in:
Jo
2023-01-04 16:33:18 +00:00
committed by GitHub
parent 298576c6a1
commit c74d86ca27
13 changed files with 12 additions and 12 deletions

View File

@@ -0,0 +1,121 @@
package supportbundlesimpl
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/supportbundles"
"github.com/grafana/grafana/pkg/web"
)
const rootUrl = "/api/support-bundles"
func (s *Service) registerAPIEndpoints(routeRegister routing.RouteRegister) {
authorize := ac.Middleware(s.accessControl)
routeRegister.Group(rootUrl, func(subrouter routing.RouteRegister) {
subrouter.Get("/", authorize(middleware.ReqGrafanaAdmin,
ac.EvalPermission(ActionRead)), routing.Wrap(s.handleList))
subrouter.Post("/", authorize(middleware.ReqGrafanaAdmin,
ac.EvalPermission(ActionCreate)), routing.Wrap(s.handleCreate))
subrouter.Get("/:uid", authorize(middleware.ReqGrafanaAdmin,
ac.EvalPermission(ActionRead)), s.handleDownload)
subrouter.Delete("/:uid", authorize(middleware.ReqGrafanaAdmin,
ac.EvalPermission(ActionDelete)), s.handleRemove)
subrouter.Get("/collectors", authorize(middleware.ReqGrafanaAdmin,
ac.EvalPermission(ActionCreate)), routing.Wrap(s.handleGetCollectors))
})
}
func (s *Service) handleList(ctx *models.ReqContext) response.Response {
bundles, err := s.list(ctx.Req.Context())
if err != nil {
return response.Error(http.StatusInternalServerError, "failed to list bundles", err)
}
data, err := json.Marshal(bundles)
if err != nil {
return response.Error(http.StatusInternalServerError, "failed to encode bundle", err)
}
return response.JSON(http.StatusOK, data)
}
func (s *Service) handleCreate(ctx *models.ReqContext) response.Response {
type command struct {
Collectors []string `json:"collectors"`
}
var c command
if err := web.Bind(ctx.Req, &c); err != nil {
return response.Error(http.StatusBadRequest, "failed to parse request", err)
}
bundle, err := s.create(context.Background(), c.Collectors, ctx.SignedInUser)
if err != nil {
return response.Error(http.StatusInternalServerError, "failed to create support bundle", err)
}
data, err := json.Marshal(bundle)
if err != nil {
return response.Error(http.StatusInternalServerError, "failed to encode bundle", err)
}
return response.JSON(http.StatusCreated, data)
}
func (s *Service) handleDownload(ctx *models.ReqContext) {
uid := web.Params(ctx.Req)[":uid"]
bundle, err := s.get(ctx.Req.Context(), uid)
if err != nil {
ctx.Redirect("/admin/support-bundles")
return
}
if bundle.State != supportbundles.StateComplete {
ctx.Redirect("/admin/support-bundles")
return
}
if bundle.FilePath == "" {
ctx.Redirect("/admin/support-bundles")
return
}
if _, err := os.Stat(bundle.FilePath); err != nil {
ctx.Redirect("/admin/support-bundles")
return
}
ctx.Resp.Header().Set("Content-Type", "application/tar+gzip")
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%d.tar.gz", bundle.CreatedAt))
http.ServeFile(ctx.Resp, ctx.Req, bundle.FilePath)
}
func (s *Service) handleRemove(ctx *models.ReqContext) response.Response {
uid := web.Params(ctx.Req)[":uid"]
err := s.remove(ctx.Req.Context(), uid)
if err != nil {
return response.Error(http.StatusInternalServerError, "failed to remove bundle", err)
}
return response.Respond(http.StatusOK, "successfully removed the support bundle")
}
func (s *Service) handleGetCollectors(ctx *models.ReqContext) response.Response {
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

@@ -0,0 +1,203 @@
package supportbundlesimpl
import (
"context"
"encoding/json"
"fmt"
"runtime"
"time"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/pluginsettings"
"github.com/grafana/grafana/pkg/services/supportbundles"
"github.com/grafana/grafana/pkg/setting"
)
func basicCollector(cfg *setting.Cfg) supportbundles.Collector {
return supportbundles.Collector{
UID: "basic",
DisplayName: "Basic information",
Description: "Basic information about the Grafana instance",
IncludedByDefault: true,
Default: true,
Fn: func(ctx context.Context) (*supportbundles.SupportItem, error) {
type basicInfo struct {
Version string `json:"version"` // Version is the version of Grafana instance.
Commit string `json:"commit"` // Commit is the commit hash of the Grafana instance.
CollectionDate time.Time `json:"collection_date"` // CollectionDate is the date when the support bundle was created.
DefaultTimezone string `json:"default_timezone"` // DefaultTimezone is the default timezone of the Grafana instance.
Alloc uint64 `json:"alloc"` // Alloc is bytes of allocated heap objects.
TotalAlloc uint64 `json:"total_alloc"` // TotalAlloc is cumulative bytes allocated for heap objects.
Sys uint64 `json:"sys"` // Sys is the total bytes of memory obtained from the OS.
Mallocs uint64 `json:"mallocs"` // Mallocs is the cumulative count of heap objects allocated.
Frees uint64 `json:"frees"` // Frees is the cumulative count of heap objects freed.
NumGC uint32 `json:"num_gc"` // NumGC is the number of completed GC cycles.
PauseTotalNs uint64 `json:"pause_total_ns"` // PauseTotalNs is the cumulative nanoseconds in GC
NumCPU int `json:"num_cpu"` // NumCPU is the number of logical CPUs usable by the current process.
NumGoRoutines int `json:"num_go_routines"` // NumGoRoutines is the number of goroutines that currently exist.
GoVersion string `json:"go_version"` // GoVersion is the version of Go used to build the binary.
GoOS string `json:"go_os"` // GoOS is the operating system target used to build the binary.
GoArch string `json:"go_arch"` // GoArch is the architecture target used to build the binary.
GoCompiler string `json:"go_compiler"` // GoCompiler is the compiler used to build the binary.
}
memstats := runtime.MemStats{}
runtime.ReadMemStats(&memstats)
collectionDate := time.Now()
tz, offset := collectionDate.Zone()
loc, _ := time.LoadLocation("UTC")
now := collectionDate.In(loc)
data, err := json.Marshal(basicInfo{
Version: cfg.BuildVersion,
Commit: cfg.BuildCommit,
CollectionDate: now,
DefaultTimezone: fmt.Sprintf("%s (UTC%+d)", tz, offset/60/60),
Alloc: memstats.Alloc,
TotalAlloc: memstats.TotalAlloc,
Sys: memstats.Sys,
Mallocs: memstats.Mallocs,
Frees: memstats.Frees,
NumGC: memstats.NumGC,
PauseTotalNs: memstats.PauseTotalNs,
NumCPU: runtime.NumCPU(),
NumGoRoutines: runtime.NumGoroutine(),
GoVersion: runtime.Version(),
GoOS: runtime.GOOS,
GoArch: runtime.GOARCH,
GoCompiler: runtime.Compiler,
})
if err != nil {
return nil, err
}
return &supportbundles.SupportItem{
Filename: "basic.json",
FileBytes: data,
}, nil
},
}
}
func settingsCollector(settings setting.Provider) supportbundles.Collector {
return supportbundles.Collector{
UID: "settings",
DisplayName: "Settings",
Description: "Settings for grafana instance",
IncludedByDefault: false,
Default: true,
Fn: func(ctx context.Context) (*supportbundles.SupportItem, error) {
current := settings.Current()
data, err := json.Marshal(current)
if err != nil {
return nil, err
}
return &supportbundles.SupportItem{
Filename: "settings.json",
FileBytes: data,
}, nil
},
}
}
func usageStatesCollector(stats usagestats.Service) supportbundles.Collector {
return supportbundles.Collector{
UID: "usage-stats",
DisplayName: "Usage statistics",
Description: "Usage statistic for grafana instance",
IncludedByDefault: false,
Default: true,
Fn: func(ctx context.Context) (*supportbundles.SupportItem, error) {
report, err := stats.GetUsageReport(context.Background())
if err != nil {
return nil, err
}
data, err := json.Marshal(report)
if err != nil {
return nil, err
}
return &supportbundles.SupportItem{
Filename: "usage-stats.json",
FileBytes: data,
}, nil
},
}
}
func pluginInfoCollector(pluginStore plugins.Store, pluginSettings pluginsettings.Service) supportbundles.Collector {
return supportbundles.Collector{
UID: "plugins",
DisplayName: "Plugin information",
Description: "Plugin information for grafana instance",
IncludedByDefault: false,
Default: true,
Fn: func(ctx context.Context) (*supportbundles.SupportItem, error) {
type pluginInfo struct {
data plugins.JSONData
Class plugins.Class
// App fields
IncludedInAppID string
DefaultNavURL string
Pinned bool
// Signature fields
Signature plugins.SignatureStatus
// SystemJS fields
Module string
BaseURL string
PluginVersion string
Enabled bool
Updated time.Time
}
plugins := pluginStore.Plugins(context.Background())
var pluginInfoList []pluginInfo
for _, plugin := range plugins {
// skip builtin plugins
if plugin.BuiltIn {
continue
}
pInfo := pluginInfo{
data: plugin.JSONData,
Class: plugin.Class,
IncludedInAppID: plugin.IncludedInAppID,
DefaultNavURL: plugin.DefaultNavURL,
Pinned: plugin.Pinned,
Signature: plugin.Signature,
Module: plugin.Module,
BaseURL: plugin.BaseURL,
}
// TODO need to loop through all the orgs
// TODO ignore the error for now, not all plugins have settings
settings, err := pluginSettings.GetPluginSettingByPluginID(context.Background(), &pluginsettings.GetByPluginIDArgs{PluginID: plugin.ID, OrgID: 1})
if err == nil {
pInfo.PluginVersion = settings.PluginVersion
pInfo.Enabled = settings.Enabled
pInfo.Updated = settings.Updated
}
pluginInfoList = append(pluginInfoList, pInfo)
}
data, err := json.Marshal(pluginInfoList)
if err != nil {
return nil, err
}
return &supportbundles.SupportItem{
Filename: "plugins.json",
FileBytes: data,
}, nil
},
}
}

View File

@@ -0,0 +1,75 @@
package supportbundlesimpl
import (
"bytes"
"context"
"fmt"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/supportbundles"
)
func dbCollector(sql db.DB) supportbundles.Collector {
collectorFn := func(ctx context.Context) (*supportbundles.SupportItem, error) {
dbType := string(sql.GetDBType())
// buffer writer
bWriter := bytes.NewBuffer(nil)
bWriter.WriteString("# Database information\n\n")
bWriter.WriteString("dbType: " + dbType + " \n")
logItems := make([]migrator.MigrationLog, 0)
version := []string{}
err := sql.WithDbSession(ctx, func(sess *db.Session) error {
rawSQL := ""
if dbType == migrator.MySQL {
rawSQL = "SELECT @@VERSION"
} else if dbType == migrator.Postgres {
rawSQL = "SELECT version()"
} else if dbType == migrator.SQLite {
rawSQL = "SELECT sqlite_version()"
} else {
return fmt.Errorf("unsupported dbType: %s", dbType)
}
return sess.Table("migration_log").SQL(rawSQL).Find(&version)
})
if err != nil {
return nil, err
}
for _, v := range version {
bWriter.WriteString("version: " + v + " \n")
}
errD := sql.WithDbSession(ctx, func(sess *db.Session) error {
return sess.Table("migration_log").Find(&logItems)
})
if errD != nil {
return nil, err
}
bWriter.WriteString("\n## Migration Log\n\n")
for _, logItem := range logItems {
bWriter.WriteString(fmt.Sprintf("**migrationId**: %s \nsuccess: %t \nerror: %s \ntimestamp: %s\n\n",
logItem.MigrationID, logItem.Success, logItem.Error, logItem.Timestamp.UTC()))
}
return &supportbundles.SupportItem{
Filename: "db.md",
FileBytes: bWriter.Bytes(),
}, nil
}
return supportbundles.Collector{
UID: "db",
Description: "Database information and migration log",
DisplayName: "Database and Migration information",
IncludedByDefault: false,
Default: true,
Fn: collectorFn,
}
}

View File

@@ -0,0 +1,49 @@
package supportbundlesimpl
import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/org"
)
const (
ActionRead = "support.bundles:read"
ActionCreate = "support.bundles:create"
ActionDelete = "support.bundles:delete"
)
var (
bundleReaderRole = accesscontrol.RoleDTO{
Name: "fixed:support.bundles:reader",
DisplayName: "Support bundle reader",
Description: "List and download support bundles",
Group: "Support bundles",
Permissions: []accesscontrol.Permission{
{Action: ActionRead},
},
}
bundleWriterRole = accesscontrol.RoleDTO{
Name: "fixed:support.bundles:writer",
DisplayName: "Support bundle writer",
Description: "Create, delete, list and download support bundles",
Group: "Support bundles",
Permissions: []accesscontrol.Permission{
{Action: ActionRead},
{Action: ActionCreate},
{Action: ActionDelete},
},
}
)
func declareFixedRoles(ac accesscontrol.Service) error {
bundleReader := accesscontrol.RoleRegistration{
Role: bundleReaderRole,
Grants: []string{string(org.RoleAdmin)},
}
bundleWriter := accesscontrol.RoleRegistration{
Role: bundleWriterRole,
Grants: []string{string(org.RoleAdmin)},
}
return ac.DeclareFixedRoles(bundleWriter, bundleReader)
}

View File

@@ -0,0 +1,173 @@
package supportbundlesimpl
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/plugins"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/pluginsettings"
"github.com/grafana/grafana/pkg/services/supportbundles"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
const (
cleanUpInterval = 24 * time.Hour
bundleCreationTimeout = 20 * time.Minute
)
type Service struct {
cfg *setting.Cfg
store bundleStore
pluginStore plugins.Store
pluginSettings pluginsettings.Service
accessControl ac.AccessControl
features *featuremgmt.FeatureManager
log log.Logger
collectors map[string]supportbundles.Collector
}
func ProvideService(cfg *setting.Cfg,
sql db.DB,
kvStore kvstore.KVStore,
accessControl ac.AccessControl,
accesscontrolService ac.Service,
routeRegister routing.RouteRegister,
userService user.Service,
settings setting.Provider,
pluginStore plugins.Store,
pluginSettings pluginsettings.Service,
features *featuremgmt.FeatureManager,
usageStats usagestats.Service) (*Service, error) {
s := &Service{
cfg: cfg,
store: newStore(kvStore),
pluginStore: pluginStore,
pluginSettings: pluginSettings,
accessControl: accessControl,
features: features,
log: log.New("supportbundle.service"),
collectors: make(map[string]supportbundles.Collector),
}
if !features.IsEnabled(featuremgmt.FlagSupportBundles) {
return s, nil
}
if !accessControl.IsDisabled() {
if err := declareFixedRoles(accesscontrolService); err != nil {
return nil, err
}
}
s.registerAPIEndpoints(routeRegister)
// TODO: move to relevant services
s.RegisterSupportItemCollector(basicCollector(cfg))
s.RegisterSupportItemCollector(settingsCollector(settings))
s.RegisterSupportItemCollector(usageStatesCollector(usageStats))
s.RegisterSupportItemCollector(userCollector(userService))
s.RegisterSupportItemCollector(dbCollector(sql))
s.RegisterSupportItemCollector(pluginInfoCollector(pluginStore, pluginSettings))
return s, nil
}
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)
if err != nil {
return nil, err
}
go func(uid string, collectors []string) {
ctx, cancel := context.WithTimeout(context.Background(), bundleCreationTimeout)
defer cancel()
s.startBundleWork(ctx, collectors, uid)
}(bundle.UID, collectors)
return bundle, nil
}
func (s *Service) get(ctx context.Context, uid string) (*supportbundles.Bundle, error) {
return s.store.Get(ctx, uid)
}
func (s *Service) list(ctx context.Context) ([]supportbundles.Bundle, error) {
return s.store.List()
}
func (s *Service) remove(ctx context.Context, uid string) error {
// Remove the data
bundle, err := s.store.Get(ctx, uid)
if err != nil {
return fmt.Errorf("could not retrieve support bundle with UID %s: %w", uid, err)
}
// TODO handle cases when bundles aren't complete yet
if bundle.State == supportbundles.StatePending {
return fmt.Errorf("could not remove a support bundle with uid %s as it is still being created", uid)
}
if bundle.FilePath != "" {
if err := os.RemoveAll(filepath.Dir(bundle.FilePath)); err != nil {
return fmt.Errorf("could not remove directory for support bundle %s: %w", uid, err)
}
}
// Remove the KV store entry
return s.store.Remove(ctx, uid)
}
func (s *Service) cleanup(ctx context.Context) {
bundles, err := s.list(ctx)
if err != nil {
s.log.Error("failed to list bundles to clean up", "error", err)
}
if err == nil {
for _, b := range bundles {
if time.Now().Unix() >= b.ExpiresAt {
if err := s.remove(ctx, b.UID); err != nil {
s.log.Error("failed to cleanup bundle", "error", err)
}
}
}
}
}

View File

@@ -0,0 +1,153 @@
package supportbundlesimpl
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"fmt"
"io"
"os"
"path/filepath"
"github.com/grafana/grafana/pkg/services/supportbundles"
)
type bundleResult struct {
path string
err error
}
func (s *Service) startBundleWork(ctx context.Context, collectors []string, uid string) {
result := make(chan bundleResult)
go func() {
sbFilePath, err := s.bundle(ctx, collectors, uid)
if err != nil {
result <- bundleResult{err: err}
}
result <- bundleResult{
path: sbFilePath,
}
close(result)
}()
select {
case <-ctx.Done():
s.log.Warn("Context cancelled while collecting support bundle")
if err := s.store.Update(ctx, uid, supportbundles.StateTimeout, ""); err != nil {
s.log.Error("failed to update bundle after timeout")
}
return
case r := <-result:
if r.err != nil {
if err := s.store.Update(ctx, uid, supportbundles.StateError, ""); err != nil {
s.log.Error("failed to update bundle after error")
}
return
}
if err := s.store.Update(ctx, uid, supportbundles.StateComplete, r.path); err != nil {
s.log.Error("failed to update bundle after completion")
}
return
}
}
func (s *Service) bundle(ctx context.Context, collectors []string, uid string) (string, error) {
lookup := make(map[string]bool, len(collectors))
for _, c := range collectors {
lookup[c] = true
}
sbDir, err := os.MkdirTemp("", "")
if err != nil {
return "", err
}
for _, collector := range s.collectors {
if !lookup[collector.UID] && !collector.IncludedByDefault {
continue
}
item, err := collector.Fn(ctx)
if err != nil {
s.log.Warn("Failed to collect support bundle item", "error", err)
}
// write item to file
if item != nil {
if err := os.WriteFile(filepath.Join(sbDir, item.Filename), item.FileBytes, 0600); err != nil {
s.log.Warn("Failed to collect support bundle item", "error", err)
}
}
}
// create tar.gz file
var buf bytes.Buffer
errCompress := compress(sbDir, &buf)
if errCompress != nil {
return "", errCompress
}
finalFilePath := filepath.Join(sbDir, fmt.Sprintf("%s.tar.gz", uid))
// Ignore gosec G304 as this function is only used internally.
//nolint:gosec
fileToWrite, err := os.OpenFile(finalFilePath, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return "", err
}
if _, err := io.Copy(fileToWrite, &buf); err != nil {
return "", err
}
return finalFilePath, nil
}
func compress(src string, buf io.Writer) error {
// tar > gzip > buf
zr := gzip.NewWriter(buf)
tw := tar.NewWriter(zr)
// walk through every file in the folder
err := filepath.Walk(src, func(file string, fi os.FileInfo, err error) error {
// if not a dir, write file content
if !fi.IsDir() {
// generate tar header
header, err := tar.FileInfoHeader(fi, file)
if err != nil {
return err
}
header.Name = filepath.ToSlash("/bundle/" + header.Name)
// write header
if err := tw.WriteHeader(header); err != nil {
return err
}
// Ignore gosec G304 as this function is only used internally.
//nolint:gosec
data, err := os.Open(file)
if err != nil {
return err
}
if _, err := io.Copy(tw, data); err != nil {
return err
}
}
return nil
})
if err != nil {
return err
}
// produce tar
if err := tw.Close(); err != nil {
return err
}
// produce gzip
if err := zr.Close(); err != nil {
return err
}
//
return nil
}

View File

@@ -0,0 +1,44 @@
package supportbundlesimpl
import (
"context"
"testing"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/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

@@ -0,0 +1,120 @@
package supportbundlesimpl
import (
"context"
"encoding/json"
"errors"
"sort"
"strings"
"time"
"github.com/google/uuid"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/services/supportbundles"
"github.com/grafana/grafana/pkg/services/user"
)
const (
defaultBundleExpiration = 72 * time.Hour // 72h
)
func newStore(kv kvstore.KVStore) *store {
return &store{kv: kvstore.WithNamespace(kv, 0, "supportbundle")}
}
type store struct {
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) {
uid, err := uuid.NewRandom()
if err != nil {
return nil, err
}
bundle := supportbundles.Bundle{
UID: uid.String(),
State: supportbundles.StatePending,
Creator: usr.Login,
CreatedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(defaultBundleExpiration).Unix(),
}
if err := s.set(ctx, &bundle); err != nil {
return nil, err
}
return &bundle, nil
}
func (s *store) Update(ctx context.Context, uid string, state supportbundles.State, filePath string) error {
bundle, err := s.Get(ctx, uid)
if err != nil {
return err
}
bundle.State = state
bundle.FilePath = filePath
return s.set(ctx, bundle)
}
func (s *store) set(ctx context.Context, bundle *supportbundles.Bundle) error {
data, err := json.Marshal(&bundle)
if err != nil {
return err
}
return s.kv.Set(ctx, bundle.UID, string(data))
}
func (s *store) Get(ctx context.Context, uid string) (*supportbundles.Bundle, error) {
data, ok, err := s.kv.Get(ctx, uid)
if err != nil {
return nil, err
}
if !ok {
// FIXME: handle not found
return nil, errors.New("not found")
}
var b supportbundles.Bundle
if err := json.NewDecoder(strings.NewReader(data)).Decode(&b); err != nil {
return nil, err
}
return &b, nil
}
func (s *store) Remove(ctx context.Context, uid string) error {
return s.kv.Del(ctx, uid)
}
func (s *store) List() ([]supportbundles.Bundle, error) {
data, err := s.kv.GetAll(context.Background())
if err != nil {
return nil, err
}
var res []supportbundles.Bundle
for _, items := range data {
for _, s := range items {
var b supportbundles.Bundle
if err := json.NewDecoder(strings.NewReader(s)).Decode(&b); err != nil {
return nil, err
}
res = append(res, b)
}
}
sort.Slice(res, func(i, j int) bool {
return res[i].CreatedAt > res[j].CreatedAt
})
return res, nil
}

View File

@@ -0,0 +1,47 @@
package supportbundlesimpl
import (
"context"
"encoding/json"
"github.com/grafana/grafana/pkg/services/supportbundles"
"github.com/grafana/grafana/pkg/services/user"
)
func userCollector(users user.Service) supportbundles.Collector {
collectorFn := func(ctx context.Context) (*supportbundles.SupportItem, error) {
query := &user.SearchUsersQuery{
SignedInUser: &user.SignedInUser{},
OrgID: 0,
Query: "",
Page: 0,
Limit: 0,
AuthModule: "",
Filters: []user.Filter{},
IsDisabled: new(bool),
}
res, err := users.Search(ctx, query)
if err != nil {
return nil, err
}
userBytes, err := json.Marshal(res.Users)
if err != nil {
return nil, err
}
return &supportbundles.SupportItem{
Filename: "users.json",
FileBytes: userBytes,
}, nil
}
return supportbundles.Collector{
UID: "users",
Description: "User information",
DisplayName: "A list of users of the Grafana instance",
IncludedByDefault: false,
Default: true,
Fn: collectorFn,
}
}