Skip to content

Commit 116d50e

Browse files
PBM-1484 GCP SDK (#1096)
* add gcs * add type * update error handling * add google storage sdk * use iterator * update mod file * add chunk size * add retryer * add credentials check * add multiplier --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent a9691ec commit 116d50e

File tree

718 files changed

+250883
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

718 files changed

+250883
-0
lines changed

go.mod

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/percona/percona-backup-mongodb
33
go 1.22
44

55
require (
6+
cloud.google.com/go/storage v1.38.0
67
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.10.0
78
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.1
89
github.com/aws/aws-sdk-go-v2 v1.33.0
@@ -16,6 +17,7 @@ require (
1617
github.com/fsnotify/fsnotify v1.7.0
1718
github.com/golang/snappy v0.0.4
1819
github.com/google/uuid v1.6.0
20+
github.com/googleapis/gax-go/v2 v2.12.3
1921
github.com/klauspost/compress v1.17.11
2022
github.com/klauspost/pgzip v1.2.6
2123
github.com/mongodb/mongo-tools v0.0.0-20240723193119-837c2bc263f4
@@ -28,10 +30,15 @@ require (
2830
go.mongodb.org/mongo-driver v1.17.1
2931
golang.org/x/mod v0.19.0
3032
golang.org/x/sync v0.11.0
33+
google.golang.org/api v0.171.0
3134
gopkg.in/yaml.v2 v2.4.0
3235
)
3336

3437
require (
38+
cloud.google.com/go v0.112.1 // indirect
39+
cloud.google.com/go/compute v1.25.1 // indirect
40+
cloud.google.com/go/compute/metadata v0.2.3 // indirect
41+
cloud.google.com/go/iam v1.1.6 // indirect
3542
dario.cat/mergo v1.0.0 // indirect
3643
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect
3744
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
@@ -62,6 +69,10 @@ require (
6269
github.com/go-logr/stdr v1.2.2 // indirect
6370
github.com/go-ole/go-ole v1.2.6 // indirect
6471
github.com/gogo/protobuf v1.3.2 // indirect
72+
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
73+
github.com/golang/protobuf v1.5.4 // indirect
74+
github.com/google/s2a-go v0.1.7 // indirect
75+
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
6576
github.com/hashicorp/hcl v1.0.0 // indirect
6677
github.com/inconshreveable/mousetrap v1.1.0 // indirect
6778
github.com/jessevdk/go-flags v1.5.0 // indirect
@@ -98,6 +109,8 @@ require (
98109
github.com/xdg-go/stringprep v1.0.4 // indirect
99110
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
100111
github.com/yusufpapurcu/wmi v1.2.3 // indirect
112+
go.opencensus.io v0.24.0 // indirect
113+
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
101114
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
102115
go.opentelemetry.io/otel v1.24.0 // indirect
103116
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect
@@ -109,9 +122,17 @@ require (
109122
golang.org/x/crypto v0.33.0 // indirect
110123
golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 // indirect
111124
golang.org/x/net v0.35.0 // indirect
125+
golang.org/x/oauth2 v0.18.0 // indirect
112126
golang.org/x/sys v0.30.0 // indirect
113127
golang.org/x/term v0.29.0 // indirect
114128
golang.org/x/text v0.22.0 // indirect
129+
golang.org/x/time v0.5.0 // indirect
130+
google.golang.org/appengine v1.6.8 // indirect
131+
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
132+
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect
133+
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
134+
google.golang.org/grpc v1.64.1 // indirect
135+
google.golang.org/protobuf v1.33.0 // indirect
115136
gopkg.in/ini.v1 v1.67.0 // indirect
116137
gopkg.in/yaml.v3 v3.0.1 // indirect
117138
)

go.sum

Lines changed: 108 additions & 0 deletions
Large diffs are not rendered by default.

pbm/config/config.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/percona/percona-backup-mongodb/pbm/storage"
2626
"github.com/percona/percona-backup-mongodb/pbm/storage/azure"
2727
"github.com/percona/percona-backup-mongodb/pbm/storage/fs"
28+
"github.com/percona/percona-backup-mongodb/pbm/storage/gcs"
2829
"github.com/percona/percona-backup-mongodb/pbm/storage/s3"
2930
"github.com/percona/percona-backup-mongodb/pbm/topo"
3031
)
@@ -145,6 +146,11 @@ func (c *Config) String() string {
145146
c.Storage.Azure.Credentials.Key = "***"
146147
}
147148
}
149+
if c.Storage.GCS != nil {
150+
if c.Storage.GCS.Credentials.PrivateKey != "" {
151+
c.Storage.GCS.Credentials.PrivateKey = "***"
152+
}
153+
}
148154

149155
b, err := yaml.Marshal(c)
150156
if err != nil {
@@ -209,6 +215,7 @@ func (cfg *PITRConf) Clone() *PITRConf {
209215
type StorageConf struct {
210216
Type storage.Type `bson:"type" json:"type" yaml:"type"`
211217
S3 *s3.Config `bson:"s3,omitempty" json:"s3,omitempty" yaml:"s3,omitempty"`
218+
GCS *gcs.Config `bson:"gcs,omitempty" json:"gcs,omitempty" yaml:"gcs,omitempty"`
212219
Azure *azure.Config `bson:"azure,omitempty" json:"azure,omitempty" yaml:"azure,omitempty"`
213220
Filesystem *fs.Config `bson:"filesystem,omitempty" json:"filesystem,omitempty" yaml:"filesystem,omitempty"`
214221
}
@@ -229,6 +236,8 @@ func (s *StorageConf) Clone() *StorageConf {
229236
rv.S3 = s.S3.Clone()
230237
case storage.Azure:
231238
rv.Azure = s.Azure.Clone()
239+
case storage.GCS:
240+
rv.GCS = s.GCS.Clone()
232241
case storage.Blackhole: // no config
233242
}
234243

@@ -260,6 +269,8 @@ func (s *StorageConf) Cast() error {
260269
return s.Filesystem.Cast()
261270
case storage.S3:
262271
return s.S3.Cast()
272+
case storage.GCS:
273+
return nil
263274
case storage.Azure: // noop
264275
return nil
265276
case storage.Blackhole: // noop

pbm/storage/gcs/gcs.go

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
package gcs
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"path"
9+
"strings"
10+
"time"
11+
12+
gcs "cloud.google.com/go/storage"
13+
"github.com/googleapis/gax-go/v2"
14+
"google.golang.org/api/iterator"
15+
"google.golang.org/api/option"
16+
17+
"github.com/percona/percona-backup-mongodb/pbm/errors"
18+
"github.com/percona/percona-backup-mongodb/pbm/log"
19+
"github.com/percona/percona-backup-mongodb/pbm/storage"
20+
)
21+
22+
type Config struct {
23+
Bucket string `bson:"bucket" json:"bucket" yaml:"bucket"`
24+
Prefix string `bson:"prefix" json:"prefix" yaml:"prefix"`
25+
Credentials Credentials `bson:"credentials" json:"credentials" yaml:"credentials"`
26+
27+
// The maximum number of bytes that the Writer will attempt to send in a single request.
28+
// https://pkg.go.dev/cloud.google.com/go/storage#Writer
29+
ChunkSize *int `bson:"chunkSize,omitempty" json:"chunkSize,omitempty" yaml:"chunkSize,omitempty"`
30+
31+
Retryer *Retryer `bson:"retryer,omitempty" json:"retryer,omitempty" yaml:"retryer,omitempty"`
32+
}
33+
34+
type Credentials struct {
35+
ProjectID string `bson:"projectId" json:"projectId,omitempty" yaml:"projectId,omitempty"`
36+
PrivateKey string `bson:"privateKey" json:"privateKey,omitempty" yaml:"privateKey,omitempty"`
37+
}
38+
39+
type Retryer struct {
40+
// BackoffInitial is the initial value of the retry period.
41+
// https://pkg.go.dev/github.com/googleapis/gax-go/[email protected]#Backoff.Initial
42+
BackoffInitial time.Duration `bson:"backoffInitial" json:"backoffInitial" yaml:"backoffInitial"`
43+
44+
// BackoffMax is the maximum value of the retry period.
45+
// https://pkg.go.dev/github.com/googleapis/gax-go/[email protected]#Backoff.Max
46+
BackoffMax time.Duration `bson:"backoffMax" json:"backoffMax" yaml:"backoffMax"`
47+
48+
// BackoffMultiplier is the factor by which the retry period increases.
49+
// https://pkg.go.dev/github.com/googleapis/gax-go/[email protected]#Backoff.Multiplier
50+
BackoffMultiplier float64 `bson:"backoffMultiplier" json:"backoffMultiplier" yaml:"backoffMultiplier"`
51+
}
52+
53+
type ServiceAccountCredentials struct {
54+
Type string `json:"type"`
55+
ProjectID string `json:"project_id"`
56+
PrivateKey string `json:"private_key"`
57+
ClientEmail string `json:"client_email"`
58+
AuthURI string `json:"auth_uri"`
59+
TokenURI string `json:"token_uri"`
60+
UniverseDomain string `json:"universe_domain"`
61+
AuthProviderCertURL string `json:"auth_provider_x509_cert_url"`
62+
ClientCertURL string `json:"client_x509_cert_url"`
63+
}
64+
65+
type GCS struct {
66+
opts *Config
67+
bucketHandle *gcs.BucketHandle
68+
log log.LogEvent
69+
}
70+
71+
func (cfg *Config) Clone() *Config {
72+
if cfg == nil {
73+
return nil
74+
}
75+
76+
rv := *cfg
77+
return &rv
78+
}
79+
80+
func New(opts *Config, node string, l log.LogEvent) (*GCS, error) {
81+
g := &GCS{
82+
opts: opts,
83+
log: l,
84+
}
85+
86+
cli, err := g.gcsClient()
87+
if err != nil {
88+
return nil, errors.Wrap(err, "GCS client")
89+
}
90+
91+
bucketHandle := cli.Bucket(opts.Bucket)
92+
93+
if opts.Retryer != nil {
94+
bucketHandle = bucketHandle.Retryer(
95+
gcs.WithBackoff(gax.Backoff{
96+
Initial: opts.Retryer.BackoffInitial,
97+
Max: opts.Retryer.BackoffMax,
98+
Multiplier: opts.Retryer.BackoffMultiplier,
99+
}),
100+
101+
gcs.WithPolicy(gcs.RetryAlways),
102+
)
103+
}
104+
105+
g.bucketHandle = bucketHandle
106+
107+
return g, nil
108+
}
109+
110+
func (*GCS) Type() storage.Type {
111+
return storage.GCS
112+
}
113+
114+
func (g *GCS) Save(name string, data io.Reader, size int64) error {
115+
ctx := context.Background()
116+
117+
w := g.bucketHandle.Object(path.Join(g.opts.Prefix, name)).NewWriter(ctx)
118+
119+
if g.opts.ChunkSize != nil {
120+
w.ChunkSize = *g.opts.ChunkSize
121+
}
122+
123+
if _, err := io.Copy(w, data); err != nil {
124+
return errors.Wrap(err, "save data")
125+
}
126+
127+
if err := w.Close(); err != nil {
128+
return errors.Wrap(err, "writer close")
129+
}
130+
131+
return nil
132+
}
133+
134+
func (g *GCS) SourceReader(name string) (io.ReadCloser, error) {
135+
ctx := context.Background()
136+
137+
reader, err := g.bucketHandle.Object(path.Join(g.opts.Prefix, name)).NewReader(ctx)
138+
if err != nil {
139+
return nil, errors.Wrap(err, "object not found")
140+
}
141+
142+
return reader, nil
143+
}
144+
145+
func (g *GCS) FileStat(name string) (storage.FileInfo, error) {
146+
ctx := context.Background()
147+
148+
attrs, err := g.bucketHandle.Object(path.Join(g.opts.Prefix, name)).Attrs(ctx)
149+
if err != nil {
150+
if errors.Is(err, gcs.ErrObjectNotExist) {
151+
return storage.FileInfo{}, storage.ErrNotExist
152+
}
153+
154+
return storage.FileInfo{}, errors.Wrap(err, "get properties")
155+
}
156+
157+
inf := storage.FileInfo{
158+
Name: attrs.Name,
159+
Size: attrs.Size,
160+
}
161+
162+
if inf.Size == 0 {
163+
return inf, storage.ErrEmpty
164+
}
165+
166+
return inf, nil
167+
}
168+
169+
func (g *GCS) List(prefix, suffix string) ([]storage.FileInfo, error) {
170+
ctx := context.Background()
171+
172+
prfx := path.Join(g.opts.Prefix, prefix)
173+
174+
if prfx != "" && !strings.HasSuffix(prfx, "/") {
175+
prfx += "/"
176+
}
177+
178+
query := &gcs.Query{
179+
Prefix: prfx,
180+
}
181+
182+
var files []storage.FileInfo
183+
it := g.bucketHandle.Objects(ctx, query)
184+
for {
185+
attrs, err := it.Next()
186+
187+
if errors.Is(err, iterator.Done) {
188+
break
189+
}
190+
191+
if err != nil {
192+
return nil, errors.Wrap(err, "list objects")
193+
}
194+
195+
name := attrs.Name
196+
name = strings.TrimPrefix(name, prfx)
197+
if len(name) == 0 {
198+
continue
199+
}
200+
if name[0] == '/' {
201+
name = name[1:]
202+
}
203+
204+
if suffix != "" && !strings.HasSuffix(name, suffix) {
205+
continue
206+
}
207+
208+
files = append(files, storage.FileInfo{
209+
Name: name,
210+
Size: attrs.Size,
211+
})
212+
}
213+
214+
return files, nil
215+
}
216+
217+
func (g *GCS) Delete(name string) error {
218+
ctx := context.Background()
219+
220+
err := g.bucketHandle.Object(path.Join(g.opts.Prefix, name)).Delete(ctx)
221+
if err != nil {
222+
if errors.Is(err, gcs.ErrObjectNotExist) {
223+
return storage.ErrNotExist
224+
}
225+
return errors.Wrap(err, "delete object")
226+
}
227+
228+
return nil
229+
}
230+
231+
func (g *GCS) Copy(src, dst string) error {
232+
ctx := context.Background()
233+
234+
srcObj := g.bucketHandle.Object(path.Join(g.opts.Prefix, src))
235+
dstObj := g.bucketHandle.Object(path.Join(g.opts.Prefix, dst))
236+
237+
_, err := dstObj.CopierFrom(srcObj).Run(ctx)
238+
return err
239+
}
240+
241+
func (g *GCS) gcsClient() (*gcs.Client, error) {
242+
ctx := context.Background()
243+
244+
if g.opts.Credentials.ProjectID == "" || g.opts.Credentials.PrivateKey == "" {
245+
return nil, errors.New("projectID and privateKey are required for GCS credentials")
246+
}
247+
248+
creds, err := json.Marshal(ServiceAccountCredentials{
249+
Type: "service_account",
250+
ProjectID: g.opts.Credentials.ProjectID,
251+
PrivateKey: g.opts.Credentials.PrivateKey,
252+
ClientEmail: fmt.Sprintf("service@%s.iam.gserviceaccount.com", g.opts.Credentials.ProjectID),
253+
AuthURI: "https://accounts.google.com/o/oauth2/auth",
254+
TokenURI: "https://oauth2.googleapis.com/token",
255+
UniverseDomain: "googleapis.com",
256+
AuthProviderCertURL: "https://www.googleapis.com/oauth2/v1/certs",
257+
ClientCertURL: fmt.Sprintf(
258+
"https://www.googleapis.com/robot/v1/metadata/x509/%s.iam.gserviceaccount.com",
259+
g.opts.Credentials.ProjectID,
260+
),
261+
})
262+
if err != nil {
263+
return nil, errors.Wrap(err, "marshal GCS credentials")
264+
}
265+
266+
return gcs.NewClient(ctx, option.WithCredentialsJSON(creds))
267+
}

0 commit comments

Comments
 (0)