mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
SupportBundles: Move support bundles to services (#60986)
* move support bundles to services * update CODEOWNERS
This commit is contained in:
121
pkg/services/supportbundles/supportbundlesimpl/api.go
Normal file
121
pkg/services/supportbundles/supportbundlesimpl/api.go
Normal 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)
|
||||
}
|
||||
203
pkg/services/supportbundles/supportbundlesimpl/collectors.go
Normal file
203
pkg/services/supportbundles/supportbundlesimpl/collectors.go
Normal 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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
49
pkg/services/supportbundles/supportbundlesimpl/models.go
Normal file
49
pkg/services/supportbundles/supportbundlesimpl/models.go
Normal 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)
|
||||
}
|
||||
173
pkg/services/supportbundles/supportbundlesimpl/service.go
Normal file
173
pkg/services/supportbundles/supportbundlesimpl/service.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
153
pkg/services/supportbundles/supportbundlesimpl/service_bundle.go
Normal file
153
pkg/services/supportbundles/supportbundlesimpl/service_bundle.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
120
pkg/services/supportbundles/supportbundlesimpl/store.go
Normal file
120
pkg/services/supportbundles/supportbundlesimpl/store.go
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user