feat(tools/alloydb-create-user): Add new custom tool kind for AlloyDB create user (#1380)

## Description

---
This pull request introduces a new custom tool kind
`alloydb-create-user` that allows users to create a new database user
for an AlloyDB cluster.
### Example Configuration

```yaml
tools:
  alloydb_create_user:
    kind: alloydb-create-user
    source: my-http-source
    description: Use this tool to create a new database user for an AlloyDB cluster.
```

### Example Request
``` 
curl -X POST http://127.0.0.1:5000/api/tool/alloydb_create_user/invoke \
-H "Content-Type: application/json" \
-d '{
    "project": "example-project",
    "location": "us-central1",
    "cluster": "example-cluster",
    "user": "my-new-db-user",
    "userType": "ALLOYDB_BUILT_IN",
    "password": "my-password",
    "databaseRoles": ["alloydbsuperuser"]
}'
```

## 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)
- [ ] 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
- [x] Ensure the tests and linter pass
- [x] Code coverage does not decrease (if any source code was changed)
- [x] Appropriate docs were updated (if necessary)
- [x] Make sure to add `!` if this involve a breaking change

🛠️ Fixes #<issue_number_goes_here>

---------

Co-authored-by: Averi Kitsch <akitsch@google.com>
This commit is contained in:
Sri Varshitha
2025-09-17 03:23:10 +05:30
committed by GitHub
parent 6c140d718a
commit ab3fd261af
6 changed files with 613 additions and 51 deletions

View File

@@ -42,6 +42,7 @@ import (
"github.com/googleapis/genai-toolbox/internal/util"
// Import tool packages for side effect of registration
_ "github.com/googleapis/genai-toolbox/internal/tools/alloydb/alloydbcreateuser"
_ "github.com/googleapis/genai-toolbox/internal/tools/alloydb/alloydbgetcluster"
_ "github.com/googleapis/genai-toolbox/internal/tools/alloydb/alloydbgetinstance"
_ "github.com/googleapis/genai-toolbox/internal/tools/alloydb/alloydbgetuser"

View File

@@ -0,0 +1,47 @@
---
title: "alloydb-create-user"
type: docs
weight: 2
description: >
The "alloydb-create-user" tool creates a new database user within a specified AlloyDB cluster.
aliases:
- /resources/tools/alloydb-create-user
---
## About
The `alloydb-create-user` tool creates a new database user (`ALLOYDB_BUILT_IN` or `ALLOYDB_IAM_USER`) within a specified cluster. It is compatible with [alloydb-admin](../../sources/alloydb-admin.md) source.
**Permissions & APIs Required:**
Before using, ensure the following on your GCP project:
1. The [AlloyDB API](https://console.cloud.google.com/apis/library/alloydb.googleapis.com) is enabled.
2. The user or service account executing the tool has the following IAM roles:
- `roles/alloydb.admin`: To create and manage AlloyDB users.
The tool takes the following input parameters:
| Parameter | Type | Description | Required |
| :--- | :--- | :--- | :--- |
| `project` | string | The GCP project ID where the cluster exists. | Yes |
| `cluster` | string | The ID of the existing cluster where the user will be created. | Yes |
| `location` | string | The GCP location where the cluster exists (e.g., `us-central1`). | Yes |
| `user` | string | The name for the new user. Must be unique within the cluster. | Yes |
| `userType` | string | The type of user. Valid values: `ALLOYDB_BUILT_IN` and `ALLOYDB_IAM_USER`. `ALLOYDB_IAM_USER` is recommended. | Yes |
| `password` | string | A secure password for the user. Required only if `userType` is `ALLOYDB_BUILT_IN`. | No |
| `databaseRoles` | array(string) | Optional. A list of database roles to grant to the new user (e.g., `pg_read_all_data`). | No |
## Example
```yaml
tools:
create_user:
kind: alloydb-create-user
source: alloydb-admin-source
description: Use this tool to create a new database user for an AlloyDB cluster.
```
## Reference
| **field** | **type** | **required** | **description** |
|-------------|:------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------|
| kind | string | true | Must be alloydb-create-user. |
| source | string | true | The name of an `alloydb-admin` source. |
| description | string | false | Description of the tool that is passed to the agent. |

View File

@@ -143,52 +143,9 @@ tools:
source: alloydb-admin-source
description: "Lists all database users within a specific AlloyDB cluster."
create_user:
kind: http
source: alloydb-api-source
method: POST
path: /v1/projects/{{.projectId}}/locations/{{.locationId}}/clusters/{{.clusterId}}/users
description: "Creates a new database user in an AlloyDB cluster. Takes the new user's name and a secure password. Optionally, a list of database roles can be assigned."
pathParams:
- name: projectId
type: string
description: "The GCP project ID."
- name: locationId
type: string
description: "The location of the cluster (e.g., 'us-central1')."
default: us-central1
- name: clusterId
type: string
description: "The ID of the cluster where the user will be created."
queryParams:
- name: userId
type: string
description: "The name for the new user. Must be unique within the cluster."
requestBody: |
{
"userType": "{{.userType}}"
{{- if eq .userType "ALLOYDB_BUILT_IN" -}}
, "password": "{{.password}}"
{{- end -}}
{{- if .databaseRoles }}
, "databaseRoles": [{{range $i, $role := .databaseRoles}}{{if $i}},{{end}}"{{$role}}"{{end}}]
{{- end }}
}
bodyParams:
- name: password
type: string
description: "A secure password for the new user. Required only for ALLOYDB_BUILT_IN userType."
required: false
- name: databaseRoles
type: array
description: "Optional. A list of database roles to grant to the new user (e.g., ['pg_read_all_data']). If not specified, the user will have no roles."
items:
name: role
type: string
description: "A single database role to grant to the user (e.g., 'pg_read_all_data')."
- name: userType
type: string
description: "The type of user to create. Valid values are: USER_TYPE_UNSPECIFIED, ALLOYDB_BUILT_IN, ALLOYDB_IAM_USER."
default: "ALLOYDB_BUILT_IN"
kind: alloydb-create-user
source: alloydb-admin-source
description: "Creates a new database user in an AlloyDB cluster. Takes the new user's name and a secure password. Optionally, a list of database roles can be assigned. Always ask the user for the type of user to create. ALLOYDB_IAM_USER is recommended."
get_cluster:
kind: alloydb-get-cluster
source: alloydb-admin-source

View File

@@ -0,0 +1,212 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package alloydbcreateuser
import (
"context"
"fmt"
yaml "github.com/goccy/go-yaml"
"github.com/googleapis/genai-toolbox/internal/sources"
alloydbadmin "github.com/googleapis/genai-toolbox/internal/sources/alloydbadmin"
"github.com/googleapis/genai-toolbox/internal/tools"
"google.golang.org/api/alloydb/v1"
)
const kind string = "alloydb-create-user"
func init() {
if !tools.Register(kind, newConfig) {
panic(fmt.Sprintf("tool kind %q already registered", kind))
}
}
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) {
actual := Config{Name: name}
if err := decoder.DecodeContext(ctx, &actual); err != nil {
return nil, err
}
return actual, nil
}
// Configuration for the create-user tool.
type Config struct {
Name string `yaml:"name" validate:"required"`
Kind string `yaml:"kind" validate:"required"`
Source string `yaml:"source" validate:"required"`
Description string `yaml:"description"`
AuthRequired []string `yaml:"authRequired"`
}
// validate interface
var _ tools.ToolConfig = Config{}
// ToolConfigKind returns the kind of the tool.
func (cfg Config) ToolConfigKind() string {
return kind
}
// Initialize initializes the tool from the configuration.
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
rawS, ok := srcs[cfg.Source]
if !ok {
return nil, fmt.Errorf("source %q not found", cfg.Source)
}
s, ok := rawS.(*alloydbadmin.Source)
if !ok {
return nil, fmt.Errorf("invalid source for %q tool: source kind must be `alloydb-admin`", kind)
}
allParameters := tools.Parameters{
tools.NewStringParameter("project", "The GCP project ID."),
tools.NewStringParameter("location", "The location of the cluster (e.g., 'us-central1')."),
tools.NewStringParameter("cluster", "The ID of the cluster where the user will be created."),
tools.NewStringParameter("user", "The name for the new user. Must be unique within the cluster."),
tools.NewStringParameterWithRequired("password", "A secure password for the new user. Required only for ALLOYDB_BUILT_IN userType.", false),
tools.NewArrayParameterWithDefault("databaseRoles", []any{}, "Optional. A list of database roles to grant to the new user (e.g., ['pg_read_all_data']).", tools.NewStringParameter("role", "A single database role to grant to the user (e.g., 'pg_read_all_data').")),
tools.NewStringParameter("userType", "The type of user to create. Valid values are: ALLOYDB_BUILT_IN and ALLOYDB_IAM_USER. ALLOYDB_IAM_USER is recommended."),
}
paramManifest := allParameters.Manifest()
inputSchema := allParameters.McpManifest()
inputSchema.Required = []string{"project", "location", "cluster", "user", "userType"}
description := cfg.Description
if description == "" {
description = "Creates a new AlloyDB user within a cluster. Takes the new user's name and a secure password. Optionally, a list of database roles can be assigned. Always ask the user for the type of user to create. ALLOYDB_IAM_USER is recommended."
}
mcpManifest := tools.McpManifest{
Name: cfg.Name,
Description: description,
InputSchema: inputSchema,
}
return Tool{
Name: cfg.Name,
Kind: kind,
Source: s,
AllParams: allParameters,
manifest: tools.Manifest{Description: description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired},
mcpManifest: mcpManifest,
}, nil
}
// Tool represents the create-user tool.
type Tool struct {
Name string `yaml:"name"`
Kind string `yaml:"kind"`
Description string `yaml:"description"`
Source *alloydbadmin.Source
AllParams tools.Parameters `yaml:"allParams"`
manifest tools.Manifest
mcpManifest tools.McpManifest
}
// Invoke executes the tool's logic.
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken tools.AccessToken) (any, error) {
paramsMap := params.AsMap()
project, ok := paramsMap["project"].(string)
if !ok || project == "" {
return nil, fmt.Errorf("invalid or missing 'project' parameter; expected a non-empty string")
}
location, ok := paramsMap["location"].(string)
if !ok || location == "" {
return nil, fmt.Errorf("invalid or missing'location' parameter; expected a non-empty string")
}
cluster, ok := paramsMap["cluster"].(string)
if !ok || cluster == "" {
return nil, fmt.Errorf("invalid or missing 'cluster' parameter; expected a non-empty string")
}
userID, ok := paramsMap["user"].(string)
if !ok || userID == "" {
return nil, fmt.Errorf("invalid or missing 'user' parameter; expected a non-empty string")
}
userType, ok := paramsMap["userType"].(string)
if !ok || (userType != "ALLOYDB_BUILT_IN" && userType != "ALLOYDB_IAM_USER") {
return nil, fmt.Errorf("invalid or missing 'userType' parameter; expected 'ALLOYDB_BUILT_IN' or 'ALLOYDB_IAM_USER'")
}
service, err := t.Source.GetService(ctx, string(accessToken))
if err != nil {
return nil, err
}
urlString := fmt.Sprintf("projects/%s/locations/%s/clusters/%s", project, location, cluster)
// Build the request body using the type-safe User struct.
user := &alloydb.User{
UserType: userType,
}
if userType == "ALLOYDB_BUILT_IN" {
password, ok := paramsMap["password"].(string)
if !ok || password == "" {
return nil, fmt.Errorf("password is required when userType is ALLOYDB_BUILT_IN")
}
user.Password = password
}
if dbRolesRaw, ok := paramsMap["databaseRoles"].([]any); ok && len(dbRolesRaw) > 0 {
var roles []string
for _, r := range dbRolesRaw {
if role, ok := r.(string); ok {
roles = append(roles, role)
}
}
if len(roles) > 0 {
user.DatabaseRoles = roles
}
}
// The Create API returns a long-running operation.
resp, err := service.Projects.Locations.Clusters.Users.Create(urlString, user).UserId(userID).Do()
if err != nil {
return nil, fmt.Errorf("error creating AlloyDB user: %w", err)
}
return resp, nil
}
// ParseParams parses the parameters for the tool.
func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
return tools.ParseParams(t.AllParams, data, claims)
}
// Manifest returns the tool's manifest.
func (t Tool) Manifest() tools.Manifest {
return t.manifest
}
// McpManifest returns the tool's MCP manifest.
func (t Tool) McpManifest() tools.McpManifest {
return t.mcpManifest
}
// Authorized checks if the tool is authorized.
func (t Tool) Authorized(verifiedAuthServices []string) bool {
return true
}
func (t Tool) RequiresClientAuthorization() bool {
return t.Source.UseClientAuthorization()
}

View File

@@ -0,0 +1,94 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package alloydbcreateuser_test
import (
"testing"
yaml "github.com/goccy/go-yaml"
"github.com/google/go-cmp/cmp"
"github.com/googleapis/genai-toolbox/internal/server"
"github.com/googleapis/genai-toolbox/internal/testutils"
alloydbcreateuser "github.com/googleapis/genai-toolbox/internal/tools/alloydb/alloydbcreateuser"
)
func TestParseFromYaml(t *testing.T) {
ctx, err := testutils.ContextWithNewLogger()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
tcs := []struct {
desc string
in string
want server.ToolConfigs
}{
{
desc: "basic example",
in: `
tools:
create-my-user:
kind: alloydb-create-user
source: my-alloydb-admin-source
description: some description
`,
want: server.ToolConfigs{
"create-my-user": alloydbcreateuser.Config{
Name: "create-my-user",
Kind: "alloydb-create-user",
Source: "my-alloydb-admin-source",
Description: "some description",
AuthRequired: []string{},
},
},
},
{
desc: "with auth required",
in: `
tools:
create-my-user-auth:
kind: alloydb-create-user
source: my-alloydb-admin-source
description: some description
authRequired:
- my-google-auth-service
- other-auth-service
`,
want: server.ToolConfigs{
"create-my-user-auth": alloydbcreateuser.Config{
Name: "create-my-user-auth",
Kind: "alloydb-create-user",
Source: "my-alloydb-admin-source",
Description: "some description",
AuthRequired: []string{"my-google-auth-service", "other-auth-service"},
},
},
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
got := struct {
Tools server.ToolConfigs `yaml:"tools"`
}{}
// Parse contents
err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got)
if err != nil {
t.Fatalf("unable to unmarshal: %s", err)
}
if diff := cmp.Diff(tc.want, got.Tools); diff != "" {
t.Fatalf("incorrect parse: diff %v", diff)
}
})
}
}

View File

@@ -21,6 +21,8 @@ import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"reflect"
"regexp"
@@ -36,11 +38,12 @@ import (
)
var (
AlloyDBProject = os.Getenv("ALLOYDB_PROJECT")
AlloyDBLocation = os.Getenv("ALLOYDB_REGION")
AlloyDBCluster = os.Getenv("ALLOYDB_CLUSTER")
AlloyDBInstance = os.Getenv("ALLOYDB_INSTANCE")
AlloyDBUser = os.Getenv("ALLOYDB_POSTGRES_USER")
AlloyDBCreateUserToolKind = "alloydb-create-user"
AlloyDBProject = os.Getenv("ALLOYDB_PROJECT")
AlloyDBLocation = os.Getenv("ALLOYDB_REGION")
AlloyDBCluster = os.Getenv("ALLOYDB_CLUSTER")
AlloyDBInstance = os.Getenv("ALLOYDB_INSTANCE")
AlloyDBUser = os.Getenv("ALLOYDB_POSTGRES_USER")
)
func getAlloyDBVars(t *testing.T) map[string]string {
@@ -971,3 +974,251 @@ func runAlloyDBGetUserTest(t *testing.T, vars map[string]string) {
})
}
}
type mockAlloyDBTransport struct {
transport http.RoundTripper
url *url.URL
}
func (t *mockAlloyDBTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if strings.HasPrefix(req.URL.String(), "https://alloydb.googleapis.com") {
req.URL.Scheme = t.url.Scheme
req.URL.Host = t.url.Host
}
return t.transport.RoundTrip(req)
}
type mockAlloyDBHandler struct {
t *testing.T
idParam string
}
func (h *mockAlloyDBHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.UserAgent(), "genai-toolbox/") {
h.t.Errorf("User-Agent header not found")
}
id := r.URL.Query().Get(h.idParam)
var response string
var statusCode int
switch id {
case "u1-iam-success":
response = `{
"databaseRoles": ["alloydbiamuser", "alloydbsuperuser"],
"name": "projects/p1/locations/l1/clusters/c1/users/u1-iam-success",
"userType": "ALLOYDB_IAM_USER"
}`
statusCode = http.StatusOK
case "u2-builtin-success":
response = `{
"databaseRoles": ["alloydbsuperuser"],
"name": "projects/p1/locations/l1/clusters/c1/users/u2-builtin-success",
"userType": "ALLOYDB_BUILT_IN"
}`
statusCode = http.StatusOK
case "u3-api-failure":
response = `{"error":{"message":"user internal api error"}}`
statusCode = http.StatusInternalServerError
default:
http.Error(w, fmt.Sprintf("unhandled %s in mock server: %s", h.idParam, id), http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
if _, err := w.Write([]byte(response)); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func setupTestServer(t *testing.T, idParam string) func() {
handler := &mockAlloyDBHandler{t: t, idParam: idParam}
server := httptest.NewServer(handler)
serverURL, err := url.Parse(server.URL)
if err != nil {
t.Fatalf("failed to parse server URL: %v", err)
}
originalTransport := http.DefaultClient.Transport
if originalTransport == nil {
originalTransport = http.DefaultTransport
}
http.DefaultClient.Transport = &mockAlloyDBTransport{
transport: originalTransport,
url: serverURL,
}
return func() {
server.Close()
http.DefaultClient.Transport = originalTransport
}
}
func TestAlloyDBCreateUser(t *testing.T) {
cleanup := setupTestServer(t, "userId")
defer cleanup()
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
var args []string
toolsFile := getAlloyDBCreateToolsConfig()
cmd, cleanupCmd, err := tests.StartCmd(ctx, toolsFile, args...)
if err != nil {
t.Fatalf("command initialization returned an error: %v", err)
}
defer cleanupCmd()
waitCtx, cancelWait := context.WithTimeout(ctx, 10*time.Second)
defer cancelWait()
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
if err != nil {
t.Logf("toolbox command logs: \n%s", out)
t.Fatalf("toolbox didn't start successfully: %s", err)
}
tcs := []struct {
name string
body string
want string
wantStatusCode int
}{
{
name: "successful creation IAM user",
body: `{"project": "p1", "location": "l1", "cluster": "c1", "user": "u1-iam-success", "userType": "ALLOYDB_IAM_USER"}`,
want: `{
"databaseRoles": ["alloydbiamuser", "alloydbsuperuser"],
"name": "projects/p1/locations/l1/clusters/c1/users/u1-iam-success",
"userType": "ALLOYDB_IAM_USER"
}`,
wantStatusCode: http.StatusOK,
},
{
name: "successful creation builtin user",
body: `{"project": "p1", "location": "l1", "cluster": "c1", "user": "u2-builtin-success", "userType": "ALLOYDB_BUILT_IN", "password": "pass123"}`,
want: `{
"databaseRoles": ["alloydbsuperuser"],
"name": "projects/p1/locations/l1/clusters/c1/users/u2-builtin-success",
"userType": "ALLOYDB_BUILT_IN"
}`,
wantStatusCode: http.StatusOK,
},
{
name: "api failure",
body: `{"project": "p1", "location": "l1", "cluster": "c1", "user": "u3-api-failure", "userType": "ALLOYDB_IAM_USER"}`,
want: "user internal api error",
wantStatusCode: http.StatusBadRequest,
},
{
name: "missing project",
body: `{"location": "l1", "cluster": "c1", "user": "u-fail", "userType": "ALLOYDB_IAM_USER"}`,
want: `parameter \"project\" is required`,
wantStatusCode: http.StatusBadRequest,
},
{
name: "missing cluster",
body: `{"project": "p1", "location": "l1", "user": "u-fail", "userType": "ALLOYDB_IAM_USER"}`,
want: `parameter \"cluster\" is required`,
wantStatusCode: http.StatusBadRequest,
},
{
name: "missing location",
body: `{"project": "p1", "cluster": "c1", "user": "u-fail", "userType": "ALLOYDB_IAM_USER"}`,
want: `parameter \"location\" is required`,
wantStatusCode: http.StatusBadRequest,
},
{
name: "missing user",
body: `{"project": "p1", "location": "l1", "cluster": "c1", "userType": "ALLOYDB_IAM_USER"}`,
want: `parameter \"user\" is required`,
wantStatusCode: http.StatusBadRequest,
},
{
name: "missing userType",
body: `{"project": "p1", "location": "l1", "cluster": "c1", "user": "u-fail"}`,
want: `parameter \"userType\" is required`,
wantStatusCode: http.StatusBadRequest,
},
{
name: "missing password for builtin user",
body: `{"project": "p1", "location": "l1", "cluster": "c1", "user": "u-fail", "userType": "ALLOYDB_BUILT_IN"}`,
want: `password is required when userType is ALLOYDB_BUILT_IN`,
wantStatusCode: http.StatusBadRequest,
},
{
name: "invalid userType",
body: `{"project": "p1", "location": "l1", "cluster": "c1", "user": "u-fail", "userType": "invalid"}`,
want: `invalid or missing 'userType' parameter; expected 'ALLOYDB_BUILT_IN' or 'ALLOYDB_IAM_USER'`,
wantStatusCode: http.StatusBadRequest,
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
api := "http://127.0.0.1:5000/api/tool/alloydb-create-user/invoke"
req, err := http.NewRequest(http.MethodPost, api, bytes.NewBufferString(tc.body))
if err != nil {
t.Fatalf("unable to create request: %s", err)
}
req.Header.Add("Content-type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("unable to send request: %s", err)
}
defer resp.Body.Close()
bodyBytes, _ := io.ReadAll(resp.Body)
if tc.wantStatusCode != http.StatusOK {
if tc.want != "" && !bytes.Contains(bodyBytes, []byte(tc.want)) {
t.Fatalf("expected error response to contain %q, but got: %s", tc.want, string(bodyBytes))
}
return
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
}
var result struct {
Result string `json:"result"`
}
if err := json.Unmarshal(bodyBytes, &result); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
var got, want map[string]any
if err := json.Unmarshal([]byte(result.Result), &got); err != nil {
t.Fatalf("failed to unmarshal result string: %v. Result: %s", err, result.Result)
}
if err := json.Unmarshal([]byte(tc.want), &want); err != nil {
t.Fatalf("failed to unmarshal want string: %v. Want: %s", err, tc.want)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("unexpected result map (-want +got):\n%s", diff)
}
})
}
}
func getAlloyDBCreateToolsConfig() map[string]any {
return map[string]any{
"sources": map[string]any{
"my-alloydb-source": map[string]any{
"kind": "alloydb-admin",
},
},
"tools": map[string]any{
"alloydb-create-user": map[string]any{
"kind": "alloydb-create-user",
"description": "create user",
"source": "my-alloydb-source",
},
},
}
}