Skip to content

Commit 4f46782

Browse files
feat(source/cloudsqladmin): Add cloud sql admin source (googleapis#1408)
## Description --- This PR introduces the `cloud-sql-admin` source, which provides a client for the Google Cloud SQL Admin API. ``` sources: my-cloud-sql-admin: kind: cloud-sql-admin ``` ## PR Checklist --- > Thank you for opening a Pull Request! Before submitting your PR, there are a > few things you can do to make sure it goes smoothly: - [x] Make sure you reviewed [CONTRIBUTING.md](https://github.com/googleapis/genai-toolbox/blob/main/CONTRIBUTING.md) - [x] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/genai-toolbox/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [x] Appropriate docs were updated (if necessary) - [ ] Make sure to add `!` if this involve a breaking change 🛠️ Fixes #<issue_number_goes_here>
1 parent bf6831f commit 4f46782

4 files changed

Lines changed: 289 additions & 0 deletions

File tree

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ import (
126126
_ "github.com/googleapis/genai-toolbox/internal/sources/bigquery"
127127
_ "github.com/googleapis/genai-toolbox/internal/sources/bigtable"
128128
_ "github.com/googleapis/genai-toolbox/internal/sources/clickhouse"
129+
_ "github.com/googleapis/genai-toolbox/internal/sources/cloudsqladmin"
129130
_ "github.com/googleapis/genai-toolbox/internal/sources/cloudsqlmssql"
130131
_ "github.com/googleapis/genai-toolbox/internal/sources/cloudsqlmysql"
131132
_ "github.com/googleapis/genai-toolbox/internal/sources/cloudsqlpg"
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
title: "Cloud SQL Admin"
3+
type: docs
4+
weight: 1
5+
description: >
6+
A "cloud-sql-admin" source provides a client for the Cloud SQL Admin API.
7+
aliases:
8+
- /resources/sources/cloud-sql-admin
9+
---
10+
11+
## About
12+
13+
The `cloud-sql-admin` source provides a client to interact with the [Google Cloud SQL Admin API](https://cloud.google.com/sql/docs/mysql/admin-api/v1). This allows tools to perform administrative tasks on Cloud SQL instances, such as creating users and databases.
14+
15+
Authentication can be handled in two ways:
16+
1. **Application Default Credentials (ADC):** By default, the source uses ADC to authenticate with the API.
17+
2. **Client-side OAuth:** If `useClientOAuth` is set to `true`, the source will expect an OAuth 2.0 access token to be provided by the client (e.g., a web browser) for each request.
18+
19+
## Example
20+
21+
```yaml
22+
sources:
23+
my-cloud-sql-admin:
24+
kind: cloud-sql-admin
25+
26+
my-oauth-cloud-sql-admin:
27+
kind: cloud-sql-admin
28+
useClientOAuth: true
29+
```
30+
31+
## Reference
32+
33+
| **field** | **type** | **required** | **description** |
34+
|----------------|:--------:|:------------:|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
35+
| kind | string | true | Must be "cloud-sql-admin". |
36+
| useClientOAuth | boolean | false | If true, the source will use client-side OAuth for authorization. Otherwise, it will use Application Default Credentials. Defaults to `false`. |
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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+
package cloudsqladmin
15+
16+
import (
17+
"context"
18+
"fmt"
19+
"net/http"
20+
21+
"github.com/goccy/go-yaml"
22+
"github.com/googleapis/genai-toolbox/internal/sources"
23+
"github.com/googleapis/genai-toolbox/internal/util"
24+
"go.opentelemetry.io/otel/trace"
25+
"golang.org/x/oauth2"
26+
"golang.org/x/oauth2/google"
27+
sqladmin "google.golang.org/api/sqladmin/v1"
28+
)
29+
30+
const SourceKind string = "cloud-sql-admin"
31+
32+
// validate interface
33+
var _ sources.SourceConfig = Config{}
34+
35+
func init() {
36+
if !sources.Register(SourceKind, newConfig) {
37+
panic(fmt.Sprintf("source kind %q already registered", SourceKind))
38+
}
39+
}
40+
41+
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources.SourceConfig, error) {
42+
actual := Config{Name: name}
43+
if err := decoder.DecodeContext(ctx, &actual); err != nil {
44+
return nil, err
45+
}
46+
return actual, nil
47+
}
48+
49+
type Config struct {
50+
Name string `yaml:"name" validate:"required"`
51+
Kind string `yaml:"kind" validate:"required"`
52+
UseClientOAuth bool `yaml:"useClientOAuth"`
53+
}
54+
55+
func (r Config) SourceConfigKind() string {
56+
return SourceKind
57+
}
58+
59+
// Initialize initializes a CloudSQL Admin Source instance.
60+
func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.Source, error) {
61+
ua, err := util.UserAgentFromContext(ctx)
62+
if err != nil {
63+
return nil, fmt.Errorf("error in User Agent retrieval: %s", err)
64+
}
65+
66+
var client *http.Client
67+
if r.UseClientOAuth {
68+
client = nil
69+
} else {
70+
// Use Application Default Credentials
71+
creds, err := google.FindDefaultCredentials(ctx, sqladmin.SqlserviceAdminScope)
72+
if err != nil {
73+
return nil, fmt.Errorf("failed to find default credentials: %w", err)
74+
}
75+
client = oauth2.NewClient(ctx, creds.TokenSource)
76+
}
77+
78+
s := &Source{
79+
Name: r.Name,
80+
Kind: SourceKind,
81+
BaseURL: "https://sqladmin.googleapis.com",
82+
Client: client,
83+
UserAgent: ua,
84+
UseClientOAuth: r.UseClientOAuth,
85+
}
86+
return s, nil
87+
}
88+
89+
var _ sources.Source = &Source{}
90+
91+
type Source struct {
92+
Name string `yaml:"name"`
93+
Kind string `yaml:"kind"`
94+
BaseURL string
95+
Client *http.Client
96+
UserAgent string
97+
UseClientOAuth bool
98+
}
99+
100+
func (s *Source) SourceKind() string {
101+
return SourceKind
102+
}
103+
104+
func (s *Source) GetClient(ctx context.Context, accessToken string) (*http.Client, error) {
105+
if s.UseClientOAuth {
106+
if accessToken == "" {
107+
return nil, fmt.Errorf("client-side OAuth is enabled but no access token was provided")
108+
}
109+
token := &oauth2.Token{AccessToken: accessToken}
110+
return oauth2.NewClient(ctx, oauth2.StaticTokenSource(token)), nil
111+
}
112+
return s.Client, nil
113+
}
114+
115+
func (s *Source) UseClientAuthorization() bool {
116+
return s.UseClientOAuth
117+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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 cloudsqladmin_test
16+
17+
import (
18+
"testing"
19+
20+
yaml "github.com/goccy/go-yaml"
21+
"github.com/google/go-cmp/cmp"
22+
"github.com/googleapis/genai-toolbox/internal/server"
23+
"github.com/googleapis/genai-toolbox/internal/sources"
24+
"github.com/googleapis/genai-toolbox/internal/sources/cloudsqladmin"
25+
"github.com/googleapis/genai-toolbox/internal/testutils"
26+
)
27+
28+
func TestParseFromYamlCloudSQLAdmin(t *testing.T) {
29+
t.Parallel()
30+
tcs := []struct {
31+
desc string
32+
in string
33+
want server.SourceConfigs
34+
}{
35+
{
36+
desc: "basic example",
37+
in: `
38+
sources:
39+
my-cloud-sql-admin-instance:
40+
kind: cloud-sql-admin
41+
`,
42+
want: map[string]sources.SourceConfig{
43+
"my-cloud-sql-admin-instance": cloudsqladmin.Config{
44+
Name: "my-cloud-sql-admin-instance",
45+
Kind: cloudsqladmin.SourceKind,
46+
UseClientOAuth: false,
47+
},
48+
},
49+
},
50+
{
51+
desc: "use client auth example",
52+
in: `
53+
sources:
54+
my-cloud-sql-admin-instance:
55+
kind: cloud-sql-admin
56+
useClientOAuth: true
57+
`,
58+
want: map[string]sources.SourceConfig{
59+
"my-cloud-sql-admin-instance": cloudsqladmin.Config{
60+
Name: "my-cloud-sql-admin-instance",
61+
Kind: cloudsqladmin.SourceKind,
62+
UseClientOAuth: true,
63+
},
64+
},
65+
},
66+
}
67+
for _, tc := range tcs {
68+
tc := tc
69+
t.Run(tc.desc, func(t *testing.T) {
70+
t.Parallel()
71+
got := struct {
72+
Sources server.SourceConfigs `yaml:"sources"`
73+
}{}
74+
// Parse contents
75+
err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got)
76+
if err != nil {
77+
t.Fatalf("unable to unmarshal: %s", err)
78+
}
79+
if !cmp.Equal(tc.want, got.Sources) {
80+
t.Fatalf("incorrect parse: want %v, got %v", tc.want, got.Sources)
81+
}
82+
})
83+
}
84+
}
85+
86+
func TestFailParseFromYaml(t *testing.T) {
87+
t.Parallel()
88+
tcs := []struct {
89+
desc string
90+
in string
91+
err string
92+
}{
93+
{
94+
desc: "extra field",
95+
in: `
96+
sources:
97+
my-cloud-sql-admin-instance:
98+
kind: cloud-sql-admin
99+
project: test-project
100+
`,
101+
err: `unable to parse source "my-cloud-sql-admin-instance" as "cloud-sql-admin": [2:1] unknown field "project"
102+
1 | kind: cloud-sql-admin
103+
> 2 | project: test-project
104+
^
105+
`,
106+
},
107+
{
108+
desc: "missing required field",
109+
in: `
110+
sources:
111+
my-cloud-sql-admin-instance:
112+
useClientOAuth: true
113+
`,
114+
err: "missing 'kind' field for source \"my-cloud-sql-admin-instance\"",
115+
},
116+
}
117+
for _, tc := range tcs {
118+
tc := tc
119+
t.Run(tc.desc, func(t *testing.T) {
120+
t.Parallel()
121+
got := struct {
122+
Sources server.SourceConfigs `yaml:"sources"`
123+
}{}
124+
// Parse contents
125+
err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got)
126+
if err == nil {
127+
t.Fatalf("expect parsing to fail")
128+
}
129+
errStr := err.Error()
130+
if errStr != tc.err {
131+
t.Fatalf("unexpected error: got %q, want %q", errStr, tc.err)
132+
}
133+
})
134+
}
135+
}

0 commit comments

Comments
 (0)