Skip to content
Snippets Groups Projects
Verified Commit b39893a0 authored by Tomasz Maczukin's avatar Tomasz Maczukin :speech_balloon:
Browse files

Support GCS credentials from file

parent 75b42ca3
No related branches found
No related tags found
No related merge requests found
This commit is part of merge request !968. Comments created here will be created in the context of that merge request.
......@@ -140,6 +140,7 @@ mocks: $(MOCKERY)
GOPATH=$(ORIGINAL_GOPATH) mockery $(MOCKERY_FLAGS) -dir=./common -all -inpkg
GOPATH=$(ORIGINAL_GOPATH) mockery $(MOCKERY_FLAGS) -dir=./shells/cache -all -inpkg
GOPATH=$(ORIGINAL_GOPATH) mockery $(MOCKERY_FLAGS) -dir=./shells/cache/s3 -all -inpkg
GOPATH=$(ORIGINAL_GOPATH) mockery $(MOCKERY_FLAGS) -dir=./shells/cache/gcs -all -inpkg
test-docker:
make test-docker-image IMAGE=centos:6 TYPE=rpm
......
......@@ -218,9 +218,14 @@ type RunnerCredentials struct {
TLSKeyFile string `toml:"tls-key-file,omitempty" json:"tls-key-file" long:"tls-key-file" env:"CI_SERVER_TLS_KEY_FILE" description:"File containing private key for TLS client auth when using HTTPS"`
}
type CacheGCSCredentials struct {
AccessID string `toml:"AccessID,omitempty" long:"access-id" env:"CACHE_GCS_GOOGLE_ACCESS_ID" description:"ID of GCP Service Account used to access the storage"`
PrivateKey string `toml:"PrivateKey,omitempty" long:"private-key" env:"CACHE_GCS_PRIVATE_KEY" description:"Private key used to sign GCS requests"`
}
type CacheGCSConfig struct {
AccessID string `toml:"AccessID" long:"access-id" env:"CACHE_GCS_GOOGLE_ACCESS_ID" description:"ID of GCP Service Account used to access the storage"`
PrivateKey string `toml:"PrivateKey" long:"private-key" env:"CACHE_GCS_PRIVATE_KEY" description:"Private key used to sign GCS requests"`
*CacheGCSCredentials
CredentialsFile string `toml:"CredentialsFile,omitempty" long:"credentials-file" env:"GOOGLE_APPLICATION_CREDENTIALS" description:"File with GCP credentials, containing AccessID and PrivateKey"`
}
type CacheConfig struct {
......
......@@ -12,6 +12,20 @@ import (
"gitlab.com/gitlab-org/gitlab-runner/common"
)
type SignedURLGenerator interface {
GetSignedURL(bucket string, name string, opts *storage.SignedURLOptions) (string, error)
}
type DefaultSignedURLGenerator struct{}
func (dsug *DefaultSignedURLGenerator) GetSignedURL(bucket string, name string, opts *storage.SignedURLOptions) (string, error) {
return storage.SignedURL(bucket, name, opts)
}
var NewSignedURLGenerator = func() SignedURLGenerator {
return &DefaultSignedURLGenerator{}
}
type Adapter struct {
build *common.Build
config *common.CacheConfig
......@@ -41,31 +55,45 @@ func (a *Adapter) GetUploadURL() *url.URL {
func (a *Adapter) getGCSURL(method string, contentType string) (URL *url.URL) {
gcs := a.config.GCS
if gcs == nil {
logrus.Warningln("Missing GCS credentials")
logrus.Errorln("Missing GCS configuration")
return
}
cr, err := NewCredentialsResolver(gcs)
if err != nil {
logrus.Errorf("error while initializing GCS credentials resolver: %v", err)
return
}
err = cr.Resolve()
if err != nil {
logrus.Errorf("error while resolving GCS credentials: %v", err)
return
}
credentials := cr.Credentials()
var privateKey []byte
if gcs.PrivateKey != "" {
privateKey = []byte(gcs.PrivateKey)
if credentials.PrivateKey != "" {
privateKey = []byte(credentials.PrivateKey)
}
rawURL, err := storage.SignedURL(a.config.BucketName, a.objectName, &storage.SignedURLOptions{
GoogleAccessID: gcs.AccessID,
generator := NewSignedURLGenerator()
rawURL, err := generator.GetSignedURL(a.config.BucketName, a.objectName, &storage.SignedURLOptions{
GoogleAccessID: credentials.AccessID,
PrivateKey: privateKey,
Method: method,
Expires: time.Now().Add(time.Duration(a.build.RunnerInfo.Timeout) * time.Second),
ContentType: contentType,
})
if err != nil {
logrus.Warningln(err)
logrus.Errorf("error while generating GCS pre-signed URL: %v", err)
return
}
URL, err = url.Parse(rawURL)
if err != nil {
logrus.Warningln(err)
logrus.Errorf("error while parsing generated URL: %v", err)
return
}
......
......@@ -2,16 +2,24 @@ package gcs
import (
"bytes"
"fmt"
"net/http"
"net/url"
"testing"
"cloud.google.com/go/storage"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"gitlab.com/gitlab-org/gitlab-runner/common"
)
var privateKey = `-----BEGIN RSA PRIVATE KEY-----
var (
accessID = "test-access-id@X.iam.gserviceaccount.com"
privateKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAzIrvApxNX3VxH5eYe4vI2kLTqOA9uFTV4clGy8uzQsGQvMjl
frTWCffayxaSvoKxPlvUYbecYpqqqaByLTE+kSDU/D44yrCiLAyWHWXYGZqfEMEG
uHBg4fJK6KcIXlJ3Hp3EGTPw92sCKKzLXyoY7mNN9iP8mnshc39wjdrqm2YgKvQU
......@@ -39,24 +47,18 @@ vvm6J1WGbnxmuhzzvGNNExeZx9dfGLmcvSAvrweiFbi2yHAc1cBLBkc5/CqfS6QW
336Qe2lgsM61/jrYYYqu7W8l6W2juCz0SPqml6rugsP8r6IMJxfziO8=
-----END RSA PRIVATE KEY-----`
bucketName = "test"
objectName = "key"
)
func defaultGCSCache() *common.CacheConfig {
return &common.CacheConfig{
Type: "gcs",
BucketName: "test",
GCS: &common.CacheGCSConfig{
AccessID: "test-access-id@X.iam.gserviceaccount.com",
PrivateKey: privateKey,
},
BucketName: bucketName,
GCS: &common.CacheGCSConfig{},
}
}
func emptyCredentialsGCSCache() *common.CacheConfig {
config := defaultGCSCache()
config.GCS = &common.CacheGCSConfig{}
return config
}
func defaultCacheBuild(cacheConfig *common.CacheConfig) *common.Build {
return &common.Build{
JobResponse: common.JobResponse{
......@@ -82,78 +84,212 @@ func runOnHijackedLogrusOutput(t *testing.T, handler func(t *testing.T, output *
handler(t, buf)
}
type adapterOperationTestCase struct {
noGCSConfig bool
accessId string
type adapterOperationInvalidConfigTestCase struct {
noGCSConfig bool
credentialsResolverInitializationError bool
credentialsResolverResolveError bool
accessID string
privateKey string
expectedError string
}
func testAdapterOperationWithMissingCredentials(t *testing.T, name string, tc adapterOperationTestCase, operation func() *url.URL) {
func onMockedCredentialsResolverForInvalidConfig(t *testing.T, tc adapterOperationInvalidConfigTestCase, handler func(t *testing.T)) {
if tc.credentialsResolverInitializationError {
oldNewCredentialsResolver := NewCredentialsResolver
defer func() {
NewCredentialsResolver = oldNewCredentialsResolver
}()
NewCredentialsResolver = func(config *common.CacheGCSConfig) (CredentialsResolver, error) {
return nil, fmt.Errorf("test error")
}
} else {
cr := &MockCredentialsResolver{}
resolveCall := cr.On("Resolve")
if tc.credentialsResolverResolveError {
resolveCall.Return(fmt.Errorf("test error"))
} else {
resolveCall.Return(nil)
}
cr.On("Credentials").Return(&common.CacheGCSCredentials{
AccessID: tc.accessID,
PrivateKey: tc.privateKey,
})
oldNewCredentialsResolver := NewCredentialsResolver
defer func() {
NewCredentialsResolver = oldNewCredentialsResolver
}()
NewCredentialsResolver = func(config *common.CacheGCSConfig) (CredentialsResolver, error) {
return cr, nil
}
}
handler(t)
}
func testAdapterOperationWithInvalidConfig(t *testing.T, name string, tc adapterOperationInvalidConfigTestCase, operation func() *url.URL) {
t.Run(name, func(t *testing.T) {
runOnHijackedLogrusOutput(t, func(t *testing.T, output *bytes.Buffer) {
u := operation()
onMockedCredentialsResolverForInvalidConfig(t, tc, func(t *testing.T) {
u := operation()
assert.Nil(t, u)
assert.Contains(t, output.String(), tc.expectedError)
assert.Nil(t, u)
assert.Contains(t, output.String(), tc.expectedError)
})
})
})
}
func TestAdapterOperation_MissingCredentials(t *testing.T) {
tests := map[string]adapterOperationTestCase{
"no-credentials": {expectedError: "storage: missing required GoogleAccessID"},
"no-access-id": {privateKey: privateKey, expectedError: "storage: missing required GoogleAccessID"},
"no-private-key": {accessId: "access-id", expectedError: "storage: exactly one of PrivateKey or SignedBytes must be set"},
"no-gcs-config": {noGCSConfig: true, expectedError: "Missing GCS credentials"},
func TestAdapterOperation_InvalidConfig(t *testing.T) {
tests := map[string]adapterOperationInvalidConfigTestCase{
"no-gcs-config": {noGCSConfig: true, expectedError: "Missing GCS configuration"},
"credentials-resolver-initialization-error": {credentialsResolverInitializationError: true, expectedError: "error while initializing GCS credentials resolver: test error"},
"credentials-resolver-resolve-error": {credentialsResolverResolveError: true, expectedError: "error while resolving GCS credentials: test error"},
"no-credentials": {expectedError: "storage: missing required GoogleAccessID"},
"no-access-id": {privateKey: privateKey, expectedError: "storage: missing required GoogleAccessID"},
"no-private-key": {accessID: "access-id", expectedError: "storage: exactly one of PrivateKey or SignedBytes must be set"},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
config := emptyCredentialsGCSCache()
config := defaultGCSCache()
if tc.noGCSConfig {
config.GCS = nil
} else {
config.GCS.AccessID = tc.accessId
config.GCS.PrivateKey = tc.privateKey
}
build := defaultCacheBuild(config)
adapter := new(Adapter)
adapter.SetBuild(build)
adapter.SetBuild(defaultCacheBuild(config))
adapter.SetConfig(config)
adapter.SetObjectName("key")
adapter.SetObjectName(objectName)
testAdapterOperationWithMissingCredentials(t, "GetDownloadURL", tc, adapter.GetDownloadURL)
testAdapterOperationWithMissingCredentials(t, "GetUploadURL", tc, adapter.GetUploadURL)
testAdapterOperationWithInvalidConfig(t, "GetDownloadURL", tc, adapter.GetDownloadURL)
testAdapterOperationWithInvalidConfig(t, "GetUploadURL", tc, adapter.GetUploadURL)
})
}
}
func testAdapterOperation(t *testing.T, name string, operation func() *url.URL) {
type adapterOperationTestCase struct {
returnedURL string
returnedError error
expectedError string
}
func onMockedCredentialsResolver(t *testing.T, handler func(t *testing.T)) {
cr := &MockCredentialsResolver{}
cr.On("Resolve").Return(nil)
cr.On("Credentials").Return(&common.CacheGCSCredentials{
AccessID: accessID,
PrivateKey: privateKey,
})
defer cr.AssertExpectations(t)
oldNewCredentialsResolver := NewCredentialsResolver
defer func() {
NewCredentialsResolver = oldNewCredentialsResolver
}()
NewCredentialsResolver = func(config *common.CacheGCSConfig) (CredentialsResolver, error) {
return cr, nil
}
handler(t)
}
func optsMatcher(expectedMethod string, expectedContentType string) interface{} {
return mock.MatchedBy(func(opts *storage.SignedURLOptions) bool {
if opts.GoogleAccessID != accessID {
return false
}
if string(opts.PrivateKey) != privateKey {
return false
}
if opts.Method != expectedMethod {
return false
}
if opts.ContentType != expectedContentType {
return false
}
return true
})
}
func onMockedSigndeURLGenerator(t *testing.T, tc adapterOperationTestCase, expectedMethod string, expectedContentType string, handler func(t *testing.T)) {
ug := &MockSignedURLGenerator{}
ug.On("GetSignedURL", bucketName, objectName, optsMatcher(expectedMethod, expectedContentType)).Return(tc.returnedURL, tc.returnedError)
defer ug.AssertExpectations(t)
oldNewSignedURLGenerator := NewSignedURLGenerator
defer func() {
NewSignedURLGenerator = oldNewSignedURLGenerator
}()
NewSignedURLGenerator = func() SignedURLGenerator {
return ug
}
handler(t)
}
func testAdapterOperation(t *testing.T, tc adapterOperationTestCase, name string, expectedMethod string, expectedContentType string, operation func() *url.URL) {
t.Run(name, func(t *testing.T) {
runOnHijackedLogrusOutput(t, func(t *testing.T, output *bytes.Buffer) {
u := operation()
onMockedCredentialsResolver(t, func(t *testing.T) {
onMockedSigndeURLGenerator(t, tc, expectedMethod, expectedContentType, func(t *testing.T) {
u := operation()
assert.Regexp(t, "^https://storage.googleapis.com/test/key", u.String())
assert.Regexp(t, "Expires=\\d+", u.String())
assert.Contains(t, u.String(), "GoogleAccessId=test-access-id%40X.iam.gserviceaccount.com")
assert.Contains(t, u.String(), "Signature=")
assert.Empty(t, output.String())
if tc.expectedError != "" {
assert.Contains(t, output.String(), tc.expectedError)
return
}
require.Empty(t, output.String())
assert.Equal(t, tc.returnedURL, u.String())
})
})
})
})
}
func TestAdapterOperation(t *testing.T) {
config := defaultGCSCache()
build := defaultCacheBuild(config)
tests := map[string]adapterOperationTestCase{
"error-on-URL-signing": {
returnedURL: "",
returnedError: fmt.Errorf("test error"),
expectedError: "error while generating GCS pre-signed URL: test error",
},
"invalid-URL-returned": {
returnedURL: "://test",
returnedError: nil,
expectedError: "error while parsing generated URL: parse ://test: missing protocol scheme",
},
"valid-configuration": {
returnedURL: "https://storage.googleapis.com/test/key?Expires=123456789&GoogleAccessId=test-access-id%40X.iam.gserviceaccount.com&Signature=XYZ",
returnedError: nil,
expectedError: "",
},
}
adapter := new(Adapter)
adapter.SetBuild(build)
adapter.SetConfig(config)
adapter.SetObjectName("key")
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
config := defaultGCSCache()
testAdapterOperation(t, "GetDownloadURL", adapter.GetDownloadURL)
testAdapterOperation(t, "GetUploadURL", adapter.GetUploadURL)
adapter := new(Adapter)
adapter.SetBuild(defaultCacheBuild(config))
adapter.SetConfig(config)
adapter.SetObjectName(objectName)
testAdapterOperation(t, tc, "GetDownloadURL", http.MethodGet, "", adapter.GetDownloadURL)
testAdapterOperation(t, tc, "GetUploadURL", http.MethodPut, "application/octet-stream", adapter.GetUploadURL)
})
}
}
package gcs
import (
"encoding/json"
"fmt"
"io/ioutil"
"github.com/sirupsen/logrus"
"gitlab.com/gitlab-org/gitlab-runner/common"
)
type CredentialsResolver interface {
Credentials() *common.CacheGCSCredentials
Resolve() error
}
const TypeServiceAccount = "service_account"
type credentialsFile struct {
Type string `json:"type"`
ClientEmail string `json:"client_email"`
PrivateKey string `json:"private_key"`
}
type DefaultCredentialsResolver struct {
config *common.CacheGCSConfig
credentials *common.CacheGCSCredentials
}
func (cr *DefaultCredentialsResolver) Credentials() *common.CacheGCSCredentials {
return cr.credentials
}
func (cr *DefaultCredentialsResolver) Resolve() error {
if cr.config.CredentialsFile != "" {
return cr.readCredentialsFromFile()
}
return cr.readCredentialsFromConfig()
}
func (cr *DefaultCredentialsResolver) readCredentialsFromFile() error {
data, err := ioutil.ReadFile(cr.config.CredentialsFile)
if err != nil {
return fmt.Errorf("error while reading credentials file: %v", err)
}
var credentialsFileContent credentialsFile
err = json.Unmarshal(data, &credentialsFileContent)
if err != nil {
return fmt.Errorf("error while parsing credentials file: %v", err)
}
if credentialsFileContent.Type != TypeServiceAccount {
return fmt.Errorf("unsupported credentials file type: %s", credentialsFileContent.Type)
}
logrus.Debugln("Credentials loaded from file. Skipping direct settings from Runner configuration file")
cr.credentials.AccessID = credentialsFileContent.ClientEmail
cr.credentials.PrivateKey = credentialsFileContent.PrivateKey
return nil
}
func (cr *DefaultCredentialsResolver) readCredentialsFromConfig() error {
if cr.config.CacheGCSCredentials == nil {
return fmt.Errorf("GCS config present, but credentials are not configured")
}
cr.credentials.AccessID = cr.config.AccessID
cr.credentials.PrivateKey = cr.config.PrivateKey
return nil
}
func NewDefaultCredentialsResolver(config *common.CacheGCSConfig) (*DefaultCredentialsResolver, error) {
if config == nil {
return nil, fmt.Errorf("config can't be nil")
}
credentials := &DefaultCredentialsResolver{
config: config,
credentials: &common.CacheGCSCredentials{},
}
return credentials, nil
}
var NewCredentialsResolver = func(config *common.CacheGCSConfig) (CredentialsResolver, error) {
return NewDefaultCredentialsResolver(config)
}
package gcs
import (
"encoding/json"
"io/ioutil"
"syscall"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gitlab.com/gitlab-org/gitlab-runner/common"
)
var accessID2 = "test-access-id-2@X.iam.gserviceaccount.com"
type credentialsResolverTestCase struct {
config *common.CacheGCSConfig
credentialsFileContent *credentialsFile
credentialsFileDoesNotExist bool
credentialsFileWithInvalidJSON bool
errorExpectedOnInitialization bool
errorExpectedOnResolve bool
expectedCredentials *common.CacheGCSCredentials
}
func onStubbedCredentialsFile(t *testing.T, testCase credentialsResolverTestCase, handler func(t *testing.T, testCase credentialsResolverTestCase)) {
if testCase.credentialsFileContent != nil {
file, err := ioutil.TempFile("", "gcp-credentials-file")
require.NoError(t, err)
defer syscall.Unlink(file.Name())
switch {
case testCase.credentialsFileDoesNotExist:
syscall.Unlink(file.Name())
case testCase.credentialsFileWithInvalidJSON:
_, err = file.Write([]byte("a"))
require.NoError(t, err)
err = file.Close()
require.NoError(t, err)
default:
data, err := json.Marshal(testCase.credentialsFileContent)
require.NoError(t, err)
_, err = file.Write(data)
require.NoError(t, err)
err = file.Close()
require.NoError(t, err)
}
testCase.config.CredentialsFile = file.Name()
}
handler(t, testCase)
}
func getCredentialsConfig(accessID string, privateKey string) *common.CacheGCSConfig {
return &common.CacheGCSConfig{
CacheGCSCredentials: &common.CacheGCSCredentials{
AccessID: accessID,
PrivateKey: privateKey,
},
}
}
func getCredentialsFileContent(fileType string, clientEmail string, privateKey string) *credentialsFile {
return &credentialsFile{
Type: fileType,
ClientEmail: clientEmail,
PrivateKey: privateKey,
}
}
func getExpectedCredentials(accessID string, privateKey string) *common.CacheGCSCredentials {
return &common.CacheGCSCredentials{
AccessID: accessID,
PrivateKey: privateKey,
}
}
func TestDefaultCredentialsResolver(t *testing.T) {
cases := map[string]credentialsResolverTestCase{
"config is nil": {
config: nil,
credentialsFileContent: nil,
errorExpectedOnInitialization: true,
},
"credentials not set": {
config: &common.CacheGCSConfig{},
errorExpectedOnResolve: true,
},
"credentials direct in config": {
config: getCredentialsConfig(accessID, privateKey),
errorExpectedOnResolve: false,
expectedCredentials: getExpectedCredentials(accessID, privateKey),
},
"credentials in credentials file - service account file": {
config: &common.CacheGCSConfig{},
credentialsFileContent: getCredentialsFileContent(TypeServiceAccount, accessID, privateKey),
errorExpectedOnResolve: false,
expectedCredentials: getExpectedCredentials(accessID, privateKey),
},
"credentials in credentials file - unsupported type credentials file": {
config: &common.CacheGCSConfig{},
credentialsFileContent: getCredentialsFileContent("unknown_type", "", ""),
errorExpectedOnResolve: true,
},
"credentials in both places - credentials file takes precedence": {
config: getCredentialsConfig(accessID, privateKey),
credentialsFileContent: getCredentialsFileContent(TypeServiceAccount, accessID2, privateKey),
errorExpectedOnResolve: false,
expectedCredentials: getExpectedCredentials(accessID2, privateKey),
},
"credentials in non-existing credentials file": {
config: &common.CacheGCSConfig{},
credentialsFileContent: getCredentialsFileContent(TypeServiceAccount, accessID, privateKey),
credentialsFileDoesNotExist: true,
errorExpectedOnResolve: true,
},
"credentials in credentials file - invalid JSON": {
config: &common.CacheGCSConfig{},
credentialsFileContent: getCredentialsFileContent(TypeServiceAccount, accessID, privateKey),
credentialsFileWithInvalidJSON: true,
errorExpectedOnResolve: true,
},
}
for name, testCase := range cases {
t.Run(name, func(t *testing.T) {
onStubbedCredentialsFile(t, testCase, func(t *testing.T, testCase credentialsResolverTestCase) {
cr, err := NewCredentialsResolver(testCase.config)
if testCase.errorExpectedOnInitialization {
assert.Error(t, err)
return
}
require.NoError(t, err, "Error on resolver initialization is not expected")
err = cr.Resolve()
if testCase.errorExpectedOnResolve {
assert.Error(t, err)
return
}
require.NoError(t, err, "Error on credentials resolving is not expected")
assert.Equal(t, testCase.expectedCredentials, cr.Credentials())
})
})
}
}
// Code generated by mockery v1.0.0. DO NOT EDIT.
// This comment works around https://github.com/vektra/mockery/issues/155
package gcs
import common "gitlab.com/gitlab-org/gitlab-runner/common"
import mock "github.com/stretchr/testify/mock"
// MockCredentialsResolver is an autogenerated mock type for the CredentialsResolver type
type MockCredentialsResolver struct {
mock.Mock
}
// Credentials provides a mock function with given fields:
func (_m *MockCredentialsResolver) Credentials() *common.CacheGCSCredentials {
ret := _m.Called()
var r0 *common.CacheGCSCredentials
if rf, ok := ret.Get(0).(func() *common.CacheGCSCredentials); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*common.CacheGCSCredentials)
}
}
return r0
}
// Resolve provides a mock function with given fields:
func (_m *MockCredentialsResolver) Resolve() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// Code generated by mockery v1.0.0. DO NOT EDIT.
// This comment works around https://github.com/vektra/mockery/issues/155
package gcs
import mock "github.com/stretchr/testify/mock"
import storage "cloud.google.com/go/storage"
// MockSignedURLGenerator is an autogenerated mock type for the SignedURLGenerator type
type MockSignedURLGenerator struct {
mock.Mock
}
// GetSignedURL provides a mock function with given fields: bucket, name, opts
func (_m *MockSignedURLGenerator) GetSignedURL(bucket string, name string, opts *storage.SignedURLOptions) (string, error) {
ret := _m.Called(bucket, name, opts)
var r0 string
if rf, ok := ret.Get(0).(func(string, string, *storage.SignedURLOptions) string); ok {
r0 = rf(bucket, name, opts)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *storage.SignedURLOptions) error); ok {
r1 = rf(bucket, name, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment