2026-07-20 11:13:52 -04:00
|
|
|
package diagnostics
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const ReportDownloadContentType = BundleContentType
|
|
|
|
|
|
|
|
|
|
var ErrReportNotReady = errors.New("diagnostic report is not ready")
|
|
|
|
|
|
|
|
|
|
func (s *Service) ListForAdmin(ctx context.Context, filters ListFilters) (ListResult, error) {
|
|
|
|
|
if s.reports == nil {
|
|
|
|
|
return ListResult{}, ErrReportStoreUnavailable
|
|
|
|
|
}
|
|
|
|
|
return s.reports.ListForAdmin(ctx, filters)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) GetReport(ctx context.Context, id string) (*Report, error) {
|
|
|
|
|
if s.reports == nil {
|
|
|
|
|
return nil, ErrReportStoreUnavailable
|
|
|
|
|
}
|
|
|
|
|
return s.reports.GetByID(ctx, id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) PresignReportDownload(ctx context.Context, report *Report, expiry time.Duration) (string, error) {
|
|
|
|
|
bucket, key, err := s.readyReportBlobLocation(report)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
if expiry <= 0 {
|
|
|
|
|
expiry = 15 * time.Minute
|
|
|
|
|
}
|
|
|
|
|
return s.store.PresignGetURL(ctx, bucket, key, expiry)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) EffectiveReportDownloadTTL(requested time.Duration) time.Duration {
|
|
|
|
|
if requested <= 0 {
|
|
|
|
|
return requested
|
|
|
|
|
}
|
|
|
|
|
effectiveStore, ok := s.store.(interface {
|
|
|
|
|
EffectivePresignTTL(time.Duration) time.Duration
|
|
|
|
|
})
|
|
|
|
|
if !ok {
|
|
|
|
|
return requested
|
|
|
|
|
}
|
|
|
|
|
effective := effectiveStore.EffectivePresignTTL(requested)
|
|
|
|
|
if effective <= 0 {
|
|
|
|
|
return requested
|
|
|
|
|
}
|
|
|
|
|
return effective
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) OpenReportDownload(ctx context.Context, report *Report) (io.ReadCloser, error) {
|
|
|
|
|
bucket, key, err := s.readyReportBlobLocation(report)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return s.store.GetObject(ctx, bucket, key)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) DeleteReport(ctx context.Context, id string) (*Report, error) {
|
|
|
|
|
if s.reports == nil {
|
|
|
|
|
return nil, ErrReportStoreUnavailable
|
|
|
|
|
}
|
2026-07-21 10:03:11 -04:00
|
|
|
// Delete the row before the blob: a DB failure after the object is gone
|
|
|
|
|
// would otherwise leave a visible report whose bundle can no longer be
|
|
|
|
|
// downloaded. DeleteByID returns the deleted row (and ErrNotFound when
|
|
|
|
|
// absent), so the blob location is captured from it. If the blob delete
|
|
|
|
|
// fails the row is already gone, so log the bucket/key for an operator (and
|
|
|
|
|
// the orphan reconciler) to reap instead of failing the delete.
|
2026-07-20 11:13:52 -04:00
|
|
|
deleted, err := s.reports.DeleteByID(ctx, id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-07-21 10:03:11 -04:00
|
|
|
if err := deleteReportObjects(ctx, s.store, deleted, s.logger); err != nil {
|
2026-07-21 13:36:08 -04:00
|
|
|
logDeferredBlobDeletion(ctx, s.logger, s.store, deleted, err)
|
2026-07-21 10:03:11 -04:00
|
|
|
}
|
2026-07-20 11:13:52 -04:00
|
|
|
return deleted, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 10:03:11 -04:00
|
|
|
func reportBlobBucket(report *Report, store ObjectStore) string {
|
|
|
|
|
bucket := stringValue(report.BlobBucket)
|
|
|
|
|
if bucket == "" && store != nil {
|
|
|
|
|
bucket = store.Bucket()
|
|
|
|
|
}
|
|
|
|
|
return bucket
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 11:13:52 -04:00
|
|
|
func (s *Service) readyReportBlobLocation(report *Report) (string, string, error) {
|
|
|
|
|
if report == nil {
|
|
|
|
|
return "", "", ErrNotFound
|
|
|
|
|
}
|
|
|
|
|
if report.State != StateReady {
|
|
|
|
|
return "", "", ErrReportNotReady
|
|
|
|
|
}
|
|
|
|
|
if s.store == nil || strings.TrimSpace(s.store.Bucket()) == "" {
|
|
|
|
|
return "", "", ErrStorageUnavailable
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bucket := stringValue(report.BlobBucket)
|
|
|
|
|
if bucket == "" {
|
|
|
|
|
bucket = s.store.Bucket()
|
|
|
|
|
}
|
|
|
|
|
key := stringValue(report.BlobKey)
|
|
|
|
|
if key == "" {
|
|
|
|
|
return "", "", fmt.Errorf("%w: diagnostic report has no blob key", ErrStorageUnavailable)
|
|
|
|
|
}
|
|
|
|
|
return bucket, key, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func stringValue(value *string) string {
|
|
|
|
|
if value == nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return strings.TrimSpace(*value)
|
|
|
|
|
}
|