Skip to content

Commit fe3a283

Browse files
authored
Add GCSArtifactService (#110)
* Add GCSArtifactService. * Fix dependecies diff * Add io.Close err validation * Standardize function and method naming for gcsService. Rename functions and methods within the gcsService to improve clarity and align with Go conventions. * Add artifactservice/gcs package, add request detailed validation Moved the gcs artifact service implementation to a separate gcs package, and the test suite to artifactservice_test. * Make mockable_gcs interfaces unexported The mockable_gcs interface is only relevant for testing and its use can be limited to the gcs package. * Add ValidatorTestCases interface to reduce duplicated code on test execution * Move mockable_storage to test file * Improve gcs service error handling, add support for parts with text. Add parallel execution on delete all versions. * Add err validation to setAttrSelection * Change test suite to include NewPartFromText example * Add Client.Options to gcs artifact service constructor for configuring the client * Add err context for request validation in artifact service
1 parent f1d8000 commit fe3a283

11 files changed

Lines changed: 1641 additions & 348 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package artifactservice
16+
17+
import (
18+
"testing"
19+
20+
"github.com/google/go-cmp/cmp"
21+
)
22+
23+
func TestArtifactKey(t *testing.T) {
24+
key := artifactKey{
25+
AppName: "testapp",
26+
UserID: "testuser",
27+
SessionID: "testsession",
28+
FileName: "testfile",
29+
Version: 123,
30+
}
31+
var key2 artifactKey
32+
err := key2.Decode(key.Encode())
33+
if err != nil {
34+
t.Fatalf("error decoding key:%s", err)
35+
}
36+
if diff := cmp.Diff(key, key2); diff != "" {
37+
t.Errorf("key mismatch (-want +got):\n%s", diff)
38+
}
39+
}

artifactservice/gcs/gcs_client.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package gcs
16+
17+
import (
18+
"context"
19+
"io"
20+
21+
"cloud.google.com/go/storage"
22+
)
23+
24+
// ------------------------ Defining interfaces to enable mocking --------------------------------
25+
// gcsClient is an interface that a gcs client must satisfy.
26+
type gcsClient interface {
27+
bucket(name string) gcsBucket
28+
}
29+
30+
// gcsBucket is an interface that a gcs bucket handle must satisfy.
31+
type gcsBucket interface {
32+
object(name string) gcsObject
33+
objects(ctx context.Context, q *storage.Query) gcsObjectIterator
34+
}
35+
36+
// gcsObject is an interface that a gcs object handle must satisfy.
37+
type gcsObject interface {
38+
newWriter(ctx context.Context) gcsWriter
39+
newReader(ctx context.Context) (io.ReadCloser, error)
40+
delete(ctx context.Context) error
41+
attrs(ctx context.Context) (*storage.ObjectAttrs, error)
42+
}
43+
44+
// gcsObjectIterator
45+
type gcsObjectIterator interface {
46+
next() (*storage.ObjectAttrs, error)
47+
}
48+
49+
// gcsObjectWriter
50+
type gcsWriter interface {
51+
io.Writer // Provides Write(p []byte) (n int, err error)
52+
io.Closer // Provides Close() error
53+
SetContentType(string)
54+
}
55+
56+
// ---------------------- Wrapper Implementations for Real gcs Types --------------------------------
57+
// gcsClientWrapper wraps a storage.Client to satisfy the gcsClient interface.
58+
type gcsClientWrapper struct {
59+
client *storage.Client
60+
}
61+
62+
// Bucket returns a gcsBucketWrapper that satisfies the gcsBucket interface.
63+
func (w *gcsClientWrapper) bucket(name string) gcsBucket {
64+
return &gcsBucketWrapper{
65+
bucket: w.client.Bucket(name),
66+
}
67+
}
68+
69+
// gcsBucketWrapper wraps a storage.BucketHandle to satisfy the gcsBucket interface.
70+
type gcsBucketWrapper struct {
71+
bucket *storage.BucketHandle
72+
}
73+
74+
// Object returns a gcsObjectWrapper that satisfies the gcsObject interface.
75+
func (w *gcsBucketWrapper) object(name string) gcsObject {
76+
objectHandle := w.bucket.Object(name)
77+
return &gcsObjectWrapper{object: objectHandle}
78+
}
79+
80+
// Objects implements the gcsBucket interface for gcsBucketWrapper.
81+
// It directly calls the underlying storage.BucketHandle's Objects method.
82+
// The gcsBucketWrapper returns an implementation of the gcsObjectIterator interface.
83+
func (w *gcsBucketWrapper) objects(ctx context.Context, q *storage.Query) gcsObjectIterator {
84+
// This is the real gcs iterator.
85+
realIterator := w.bucket.Objects(ctx, q)
86+
// We return a wrapper around the real iterator.
87+
return &gcsObjectIteratorWrapper{iter: realIterator}
88+
}
89+
90+
// gcsObjectWrapper wraps a storage.ObjectHandle to satisfy the gcsObject interface.
91+
type gcsObjectWrapper struct {
92+
object *storage.ObjectHandle
93+
}
94+
95+
// NewWriter implements the gcsObject interface for gcsObjectWrapper.
96+
func (w *gcsObjectWrapper) newWriter(ctx context.Context) gcsWriter {
97+
return &gcsWriterWrapper{w: w.object.NewWriter(ctx)}
98+
}
99+
100+
// NewReader implements the gcsObject interface for gcsObjectWrapper.
101+
func (w *gcsObjectWrapper) newReader(ctx context.Context) (io.ReadCloser, error) {
102+
return w.object.NewReader(ctx)
103+
}
104+
105+
// Delete implements the gcsObject interface for gcsObjectWrapper.
106+
func (w *gcsObjectWrapper) delete(ctx context.Context) error {
107+
return w.object.Delete(ctx)
108+
}
109+
110+
// Attrs implements the gcsObject interface for gcsObjectWrapper.
111+
func (w *gcsObjectWrapper) attrs(ctx context.Context) (*storage.ObjectAttrs, error) {
112+
return w.object.Attrs(ctx)
113+
}
114+
115+
// Create the wrapper for the real iterator.
116+
type gcsObjectIteratorWrapper struct {
117+
iter *storage.ObjectIterator
118+
}
119+
120+
func (w *gcsObjectIteratorWrapper) next() (*storage.ObjectAttrs, error) {
121+
return w.iter.Next()
122+
}
123+
124+
// gcsWriterWrapper wraps the real gcs writer to satisfy our ObjectWriter interface.
125+
type gcsWriterWrapper struct {
126+
w *storage.Writer
127+
}
128+
129+
func (g *gcsWriterWrapper) Write(p []byte) (n int, err error) {
130+
return g.w.Write(p)
131+
}
132+
133+
func (g *gcsWriterWrapper) Close() error {
134+
return g.w.Close()
135+
}
136+
137+
func (g *gcsWriterWrapper) SetContentType(cType string) {
138+
g.w.ContentType = cType
139+
}
140+
141+
var _ gcsClient = (*gcsClientWrapper)(nil)
142+
var _ gcsBucket = (*gcsBucketWrapper)(nil)
143+
var _ gcsObject = (*gcsObjectWrapper)(nil)
144+
var _ gcsObjectIterator = (*gcsObjectIteratorWrapper)(nil)
145+
var _ gcsWriter = (*gcsWriterWrapper)(nil)

artifactservice/gcs/gcs_test.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package gcs
16+
17+
import (
18+
"bytes"
19+
"context"
20+
"io"
21+
"io/fs"
22+
"strings"
23+
"sync"
24+
"testing"
25+
"time"
26+
27+
"cloud.google.com/go/storage"
28+
as "google.golang.org/adk/artifactservice"
29+
"google.golang.org/adk/artifactservice/internal/tests"
30+
"google.golang.org/api/iterator"
31+
)
32+
33+
// newGCSArtifactServiceForTesting creates a gcsService for the specified bucket using a mocked inmemory client
34+
func newGCSArtifactServiceForTesting(bucketName string) (as.Service, error) {
35+
client := newFakeClient()
36+
s := &gcsService{
37+
bucketName: bucketName,
38+
storageClient: client,
39+
bucket: client.bucket(bucketName),
40+
}
41+
return s, nil
42+
}
43+
44+
func TestGCSArtifactService(t *testing.T) {
45+
factory := func(t *testing.T) (as.Service, error) {
46+
return newGCSArtifactServiceForTesting("new")
47+
}
48+
tests.TestArtifactService(t, "GCS", factory)
49+
}
50+
51+
// ---------------------------------- Mock Implementations -----------------------------------
52+
// fakeClient implements the gcsClient interface for testing.
53+
type fakeClient struct {
54+
inMemoryBucket gcsBucket
55+
}
56+
57+
func newFakeClient() gcsClient {
58+
return &fakeClient{
59+
inMemoryBucket: &fakeBucket{
60+
objectsMap: make(map[string]*fakeObject),
61+
},
62+
}
63+
}
64+
65+
// Bucket returns the singleton in-memory bucket.
66+
func (c *fakeClient) bucket(name string) gcsBucket {
67+
return c.inMemoryBucket
68+
}
69+
70+
// fakeBucket implements the gcsBucket interface for testing.
71+
type fakeBucket struct {
72+
mu sync.Mutex
73+
objectsMap map[string]*fakeObject
74+
}
75+
76+
// Object returns a fake object from the in-memory store.
77+
func (f *fakeBucket) object(name string) gcsObject {
78+
f.mu.Lock()
79+
defer f.mu.Unlock()
80+
if _, ok := f.objectsMap[name]; !ok {
81+
f.objectsMap[name] = &fakeObject{name: name}
82+
}
83+
return f.objectsMap[name]
84+
}
85+
86+
// Objects simulates iterating over objects with a prefix.
87+
func (f *fakeBucket) objects(ctx context.Context, q *storage.Query) gcsObjectIterator {
88+
f.mu.Lock()
89+
defer f.mu.Unlock()
90+
91+
var matchingObjects []*fakeObject
92+
for name, obj := range f.objectsMap {
93+
if q != nil && q.Prefix != "" && !strings.HasPrefix(name, q.Prefix) {
94+
continue
95+
}
96+
if !obj.deleted {
97+
matchingObjects = append(matchingObjects, obj)
98+
}
99+
}
100+
101+
// This is the key change. We return a custom type that has a `Next` method
102+
// that manages its own state and returns the correct values.
103+
return &fakeObjectIterator{
104+
objects: matchingObjects,
105+
index: 0,
106+
}
107+
}
108+
109+
// fakeObject implements the gcsObject interface for testing.
110+
type fakeObject struct {
111+
mu sync.Mutex
112+
name string
113+
data []byte
114+
deleted bool
115+
contentType string
116+
}
117+
118+
// NewWriter returns a fake writer that stores data in memory.
119+
func (f *fakeObject) newWriter(ctx context.Context) gcsWriter {
120+
f.mu.Lock()
121+
defer f.mu.Unlock()
122+
f.deleted = false // A write operation "undeletes" the object
123+
f.data = nil // Clear existing data
124+
return &fakeWriter{obj: f, buffer: &bytes.Buffer{}}
125+
}
126+
127+
// Attrs returns fake attributes for the object.
128+
func (f *fakeObject) attrs(ctx context.Context) (*storage.ObjectAttrs, error) {
129+
f.mu.Lock()
130+
defer f.mu.Unlock()
131+
if f.deleted || f.data == nil {
132+
return nil, storage.ErrObjectNotExist
133+
}
134+
return &storage.ObjectAttrs{Name: f.name, Created: time.Now(), ContentType: f.contentType}, nil
135+
}
136+
137+
// Delete marks the object as deleted in memory.
138+
func (f *fakeObject) delete(ctx context.Context) error {
139+
f.mu.Lock()
140+
defer f.mu.Unlock()
141+
f.deleted = true
142+
return nil
143+
}
144+
145+
// NewReader returns a reader for the in-memory data.
146+
func (f *fakeObject) newReader(ctx context.Context) (io.ReadCloser, error) {
147+
f.mu.Lock()
148+
defer f.mu.Unlock()
149+
if f.deleted || f.data == nil {
150+
return nil, fs.ErrNotExist
151+
}
152+
return io.NopCloser(bytes.NewReader(f.data)), nil
153+
}
154+
155+
// fakeWriter is a helper type to simulate an *storage.Writer
156+
type fakeWriter struct {
157+
obj *fakeObject
158+
buffer *bytes.Buffer
159+
contentType string
160+
}
161+
162+
func (w *fakeWriter) Write(p []byte) (n int, err error) {
163+
return w.buffer.Write(p)
164+
}
165+
166+
func (w *fakeWriter) Close() error {
167+
w.obj.mu.Lock()
168+
defer w.obj.mu.Unlock()
169+
w.obj.data = w.buffer.Bytes()
170+
w.obj.contentType = w.contentType
171+
return nil
172+
}
173+
174+
// SetContentType implements the final piece of the interface.
175+
func (w *fakeWriter) SetContentType(cType string) {
176+
w.contentType = cType
177+
}
178+
179+
// fakeObjectIterator is a fake iterator that returns attributes from a slice.
180+
// This type is the key to solving the 'unknown field' error.
181+
type fakeObjectIterator struct {
182+
objects []*fakeObject
183+
index int
184+
}
185+
186+
// Next implements the iterator pattern.
187+
// It returns the next object in the slice or an iterator.Done error.
188+
func (i *fakeObjectIterator) next() (*storage.ObjectAttrs, error) {
189+
if i.index >= len(i.objects) {
190+
return nil, iterator.Done
191+
}
192+
obj := i.objects[i.index]
193+
i.index++
194+
return &storage.ObjectAttrs{Name: obj.name, ContentType: obj.contentType}, nil
195+
}
196+
197+
var _ gcsClient = (*fakeClient)(nil)
198+
var _ gcsBucket = (*fakeBucket)(nil)
199+
var _ gcsObject = (*fakeObject)(nil)
200+
var _ gcsObjectIterator = (*fakeObjectIterator)(nil)
201+
var _ gcsWriter = (*fakeWriter)(nil)

0 commit comments

Comments
 (0)