grafana/pkg/components/imguploader/gcs/gcsuploader.go
Jo 062d255124
Handle ioutil deprecations (#53526)
* replace ioutil.ReadFile -> os.ReadFile

* replace ioutil.ReadAll -> io.ReadAll

* replace ioutil.TempFile -> os.CreateTemp

* replace ioutil.NopCloser -> io.NopCloser

* replace ioutil.WriteFile -> os.WriteFile

* replace ioutil.TempDir -> os.MkdirTemp

* replace ioutil.Discard -> io.Discard
2022-08-10 15:37:51 +02:00

230 lines
5.8 KiB
Go

// Package gcs provides an image uploader for GCS.
package gcs
import (
"context"
"fmt"
"io"
"os"
"path"
"path/filepath"
"time"
"cloud.google.com/go/storage"
"github.com/grafana/grafana/pkg/ifaces/gcsifaces"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/util"
"golang.org/x/oauth2/google"
"golang.org/x/oauth2/jwt"
"google.golang.org/api/option"
)
// NewUploader returns a new Uploader.
func NewUploader(keyFile, bucket, path string, enableSignedURLs bool, signedURLExpiration time.Duration) (*Uploader, error) {
if signedURLExpiration <= 0 {
return nil, fmt.Errorf("invalid signed URL expiration: %q", signedURLExpiration)
}
uploader := &Uploader{
KeyFile: keyFile,
Bucket: bucket,
path: path,
log: log.New("gcsuploader"),
enableSignedURLs: enableSignedURLs,
signedURLExpiration: signedURLExpiration,
}
uploader.log.Debug("Created uploader", "key", keyFile, "bucket", bucket, "path", path, "enableSignedUrls",
enableSignedURLs, "signedUrlExpiration", signedURLExpiration.String())
return uploader, nil
}
// newClient returns a new GCS client.
// Stubbable by tests.
var newClient = func(ctx context.Context, opts ...option.ClientOption) (gcsifaces.StorageClient, error) {
client, err := storage.NewClient(ctx, opts...)
return clientWrapper{client}, err
}
// Uploader supports uploading images to GCS.
type Uploader struct {
KeyFile string
Bucket string
path string
log log.Logger
enableSignedURLs bool
signedURLExpiration time.Duration
}
// Upload uploads an image to GCS.
func (u *Uploader) Upload(ctx context.Context, imageDiskPath string) (string, error) {
fileName, err := util.GetRandomString(20)
if err != nil {
return "", err
}
ext := filepath.Ext(imageDiskPath)
if ext == "" {
ext = ".png"
}
fileName += ext
key := path.Join(u.path, fileName)
var keyData []byte
if u.KeyFile != "" {
u.log.Debug("Opening key file ", u.KeyFile)
keyData, err = os.ReadFile(u.KeyFile)
if err != nil {
return "", err
}
}
const scope = storage.ScopeReadWrite
var client gcsifaces.StorageClient
if u.KeyFile != "" {
u.log.Debug("Creating Google credentials from JSON")
creds, err := google.CredentialsFromJSON(ctx, keyData, scope)
if err != nil {
return "", err
}
u.log.Debug("Creating GCS client")
client, err = newClient(ctx, option.WithCredentials(creds))
if err != nil {
return "", err
}
} else {
u.log.Debug("Creating GCS client with default application credentials")
client, err = newClient(ctx, option.WithScopes(scope))
if err != nil {
return "", err
}
}
if err := u.uploadFile(ctx, client, imageDiskPath, key); err != nil {
return "", err
}
if !u.enableSignedURLs {
return fmt.Sprintf("https://storage.googleapis.com/%s/%s", u.Bucket, key), nil
}
u.log.Debug("Signing GCS URL")
var jwtData []byte
if u.KeyFile != "" {
jwtData = keyData
} else {
creds, err := client.FindDefaultCredentials(ctx, scope)
if err != nil {
return "", fmt.Errorf("failed to find default Google credentials: %s", err)
}
jwtData = creds.JSON
}
conf, err := client.JWTConfigFromJSON(jwtData)
if err != nil {
return "", err
}
opts := &storage.SignedURLOptions{
Scheme: storage.SigningSchemeV4,
Method: "GET",
GoogleAccessID: conf.Email,
PrivateKey: conf.PrivateKey,
Expires: time.Now().Add(u.signedURLExpiration),
}
signedURL, err := client.SignedURL(u.Bucket, key, opts)
if err != nil {
return "", err
}
return signedURL, nil
}
func (u *Uploader) uploadFile(
ctx context.Context,
client gcsifaces.StorageClient,
imageDiskPath,
key string,
) error {
u.log.Debug("Opening image file", "path", imageDiskPath)
// We can ignore the gosec G304 warning on this one because `imageDiskPath` comes
// from alert notifiers and is only used to upload images generated by alerting.
// nolint:gosec
fileReader, err := os.Open(imageDiskPath)
if err != nil {
return err
}
defer func() {
if err := fileReader.Close(); err != nil {
u.log.Warn("Failed to close file", "err", err, "path", imageDiskPath)
}
}()
// Set public access if not generating a signed URL
pubAcc := !u.enableSignedURLs
u.log.Debug("Uploading to GCS bucket using SDK", "bucket", u.Bucket, "key", key, "public", pubAcc)
uri := fmt.Sprintf("gs://%s/%s", u.Bucket, key)
wc := client.Bucket(u.Bucket).Object(key).NewWriter(ctx)
if pubAcc {
wc.SetACL("publicRead")
}
if _, err := io.Copy(wc, fileReader); err != nil {
_ = wc.Close()
return fmt.Errorf("failed to upload to %s: %s", uri, err)
}
if err := wc.Close(); err != nil {
return fmt.Errorf("failed to upload to %s: %s", uri, err)
}
return nil
}
type clientWrapper struct {
client *storage.Client
}
func (c clientWrapper) Bucket(key string) gcsifaces.StorageBucket {
return bucketWrapper{c.client.Bucket(key)}
}
func (c clientWrapper) FindDefaultCredentials(ctx context.Context, scope string) (*google.Credentials, error) {
return google.FindDefaultCredentials(ctx, scope)
}
func (c clientWrapper) JWTConfigFromJSON(keyJSON []byte) (*jwt.Config, error) {
return google.JWTConfigFromJSON(keyJSON)
}
func (c clientWrapper) SignedURL(bucket, name string, opts *storage.SignedURLOptions) (string, error) {
return storage.SignedURL(bucket, name, opts)
}
type bucketWrapper struct {
bucket *storage.BucketHandle
}
func (b bucketWrapper) Object(key string) gcsifaces.StorageObject {
return objectWrapper{b.bucket.Object(key)}
}
type objectWrapper struct {
object *storage.ObjectHandle
}
func (o objectWrapper) NewWriter(ctx context.Context) gcsifaces.StorageWriter {
return writerWrapper{o.object.NewWriter(ctx)}
}
type writerWrapper struct {
*storage.Writer
}
func (w writerWrapper) SetACL(acl string) {
w.ObjectAttrs.PredefinedACL = acl
}