mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-11 08:28:11 -05:00
feat(prebuilt/cloudsqlpg): add cloud sql pg create instance tool (#1403)
## Description --- This pull request introduces a new tool, `cloud-sql-postgres-create-instance`, which allows users to create Cloud SQL PG instances directly from the toolbox. The tool is designed to simplify the instance creation process by providing sensible defaults while still offering flexibility for advanced configurations. __Key Features of the New Tool:__ - __Simplified Instance Creation:__ The tool introduces an `editionPreset` parameter that can be set to either `"Production"` or `"Development"`. This allows users to easily create instances with appropriate settings for their environment without needing to specify low-level configuration details. - __Production Preset:__ Configures a high-availability, performance-optimized instance. - __Development Preset:__ Configures a cost-effective, general-purpose instance suitable for testing and development. ## 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 - [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) - [ ] Make sure to add `!` if this involve a breaking change 🛠️ Fixes #<issue_number_goes_here>
This commit is contained in:
committed by
GitHub
parent
677254e6d9
commit
d30249961b
@@ -67,6 +67,7 @@ import (
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudsql/cloudsqlgetinstances"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudsql/cloudsqllistinstances"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudsql/cloudsqlwaitforoperation"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudsqlpg/cloudsqlpgcreateinstances"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/couchbase"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/dataplex/dataplexlookupentry"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/dataplex/dataplexsearchaspecttypes"
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: cloud-sql-postgres-create-instance
|
||||
type: docs
|
||||
weight: 10
|
||||
description: Create a Cloud SQL for PostgreSQL instance.
|
||||
---
|
||||
|
||||
The `cloud-sql-postgres-create-instance` tool creates a Cloud SQL for PostgreSQL instance using the Cloud SQL Admin API.
|
||||
|
||||
{{< notice info >}}
|
||||
This tool uses a `source` of kind `cloud-sql-admin`.
|
||||
{{< /notice >}}
|
||||
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
create-sql-instance:
|
||||
kind: cloud-sql-postgres-create-instance
|
||||
source: cloud-sql-admin-source
|
||||
description: "Creates a Postgres instance using `Production` and `Development` presets. For the `Development` template, it chooses a 2 vCPU, 16 GiB RAM, 100 GiB SSD configuration with Non-HA/zonal availability. For the `Production` template, it chooses an 8 vCPU, 64 GiB RAM, 250 GiB SSD configuration with HA/regional availability. The Enterprise Plus edition is used in both cases. The default database version is `POSTGRES_17`. The agent should ask the user if they want to use a different version."
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
### Tool Configuration
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
| ----------- | :------: | :----------: | ------------------------------------------------ |
|
||||
| kind | string | true | Must be "cloud-sql-postgres-create-instance". |
|
||||
| source | string | true | The name of the `cloud-sql-admin` source to use. |
|
||||
| description | string | false | A description of the tool. |
|
||||
|
||||
### Tool Inputs
|
||||
|
||||
| **parameter** | **type** | **required** | **description** |
|
||||
| --------------- | :------: | :----------: | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| project | string | true | The project ID. |
|
||||
| name | string | true | The name of the instance. |
|
||||
| databaseVersion | string | false | The database version for Postgres. If not specified, defaults to the latest available version (e.g., POSTGRES_17). |
|
||||
| rootPassword | string | true | The root password for the instance. |
|
||||
| editionPreset | string | false | The edition of the instance. Can be `Production` or `Development`. This determines the default machine type and availability. Defaults to `Development`. |
|
||||
@@ -0,0 +1,206 @@
|
||||
// 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 cloudsqlpgcreateinstances
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources/cloudsqladmin"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
sqladmin "google.golang.org/api/sqladmin/v1"
|
||||
)
|
||||
|
||||
const kind string = "cloud-sql-postgres-create-instance"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Config defines the configuration for the create-instances tool.
|
||||
type Config struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Description string `yaml:"description"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
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("no source named %q configured", cfg.Source)
|
||||
}
|
||||
s, ok := rawS.(*cloudsqladmin.Source)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid source for %q tool: source kind must be `cloud-sql-admin`", kind)
|
||||
}
|
||||
|
||||
allParameters := tools.Parameters{
|
||||
tools.NewStringParameter("project", "The project ID"),
|
||||
tools.NewStringParameter("name", "The name of the instance"),
|
||||
tools.NewStringParameterWithDefault("databaseVersion", "POSTGRES_17", "The database version for Postgres. If not specified, defaults to the latest available version (e.g., POSTGRES_17)."),
|
||||
tools.NewStringParameter("rootPassword", "The root password for the instance"),
|
||||
tools.NewStringParameterWithDefault("editionPreset", "Development", "The edition of the instance. Can be `Production` or `Development`. This determines the default machine type and availability. Defaults to `Development`."),
|
||||
}
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
inputSchema := allParameters.McpManifest()
|
||||
inputSchema.Required = []string{"project", "name", "editionPreset", "rootPassword"}
|
||||
|
||||
description := cfg.Description
|
||||
if description == "" {
|
||||
description = "Creates a Postgres instance using `Production` and `Development` presets. For the `Development` template, it chooses a 2 vCPU, 16 GiB RAM, 100 GiB SSD configuration with Non-HA/zonal availability. For the `Production` template, it chooses an 8 vCPU, 64 GiB RAM, 250 GiB SSD configuration with HA/regional availability. The Enterprise Plus edition is used in both cases. The default database version is `POSTGRES_17`. The agent should ask the user if they want to use a different version."
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: description,
|
||||
InputSchema: inputSchema,
|
||||
}
|
||||
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Source: s,
|
||||
AllParams: allParameters,
|
||||
manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired},
|
||||
mcpManifest: mcpManifest,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Tool represents the create-instances tool.
|
||||
type Tool struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
Description string `yaml:"description"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
|
||||
Source *cloudsqladmin.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 {
|
||||
return nil, fmt.Errorf("missing 'project' parameter")
|
||||
}
|
||||
name, ok := paramsMap["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing 'name' parameter")
|
||||
}
|
||||
dbVersion, ok := paramsMap["databaseVersion"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing 'databaseVersion' parameter")
|
||||
}
|
||||
rootPassword, ok := paramsMap["rootPassword"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing 'rootPassword' parameter")
|
||||
}
|
||||
editionPreset, ok := paramsMap["editionPreset"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing 'editionPreset' parameter")
|
||||
}
|
||||
|
||||
settings := sqladmin.Settings{}
|
||||
switch strings.ToLower(editionPreset) {
|
||||
case "production":
|
||||
settings.AvailabilityType = "REGIONAL"
|
||||
settings.Edition = "ENTERPRISE_PLUS"
|
||||
settings.Tier = "db-perf-optimized-N-8"
|
||||
settings.DataDiskSizeGb = 250
|
||||
settings.DataDiskType = "PD_SSD"
|
||||
case "development":
|
||||
settings.AvailabilityType = "ZONAL"
|
||||
settings.Edition = "ENTERPRISE_PLUS"
|
||||
settings.Tier = "db-perf-optimized-N-2"
|
||||
settings.DataDiskSizeGb = 100
|
||||
settings.DataDiskType = "PD_SSD"
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid 'editionPreset': %q. Must be either 'Production' or 'Development'", editionPreset)
|
||||
}
|
||||
|
||||
instance := sqladmin.DatabaseInstance{
|
||||
Name: name,
|
||||
DatabaseVersion: dbVersion,
|
||||
RootPassword: rootPassword,
|
||||
Settings: &settings,
|
||||
Project: project,
|
||||
}
|
||||
|
||||
service, err := t.Source.GetService(ctx, string(accessToken))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := service.Instances.Insert(project, &instance).Do()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating instance: %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()
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// 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 cloudsqlpgcreateinstances_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"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools/cloudsqlpg/cloudsqlpgcreateinstances"
|
||||
)
|
||||
|
||||
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-instance-tool:
|
||||
kind: cloud-sql-postgres-create-instance
|
||||
description: a test description
|
||||
source: a-source
|
||||
`,
|
||||
want: server.ToolConfigs{
|
||||
"create-instance-tool": cloudsqlpgcreateinstances.Config{
|
||||
Name: "create-instance-tool",
|
||||
Kind: "cloud-sql-postgres-create-instance",
|
||||
Description: "a test description",
|
||||
Source: "a-source",
|
||||
AuthRequired: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
277
tests/cloudsqlpg/cloud_sql_pg_create_instances_test.go
Normal file
277
tests/cloudsqlpg/cloud_sql_pg_create_instances_test.go
Normal file
@@ -0,0 +1,277 @@
|
||||
// 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 cloudsqlpg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/googleapis/genai-toolbox/internal/testutils"
|
||||
"github.com/googleapis/genai-toolbox/tests"
|
||||
"google.golang.org/api/sqladmin/v1"
|
||||
)
|
||||
|
||||
var (
|
||||
createInstanceToolKind = "cloud-sql-postgres-create-instance"
|
||||
)
|
||||
|
||||
type createInstanceTransport struct {
|
||||
transport http.RoundTripper
|
||||
url *url.URL
|
||||
}
|
||||
|
||||
func (t *createInstanceTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if strings.HasPrefix(req.URL.String(), "https://sqladmin.googleapis.com") {
|
||||
req.URL.Scheme = t.url.Scheme
|
||||
req.URL.Host = t.url.Host
|
||||
}
|
||||
return t.transport.RoundTrip(req)
|
||||
}
|
||||
|
||||
type masterHandler struct {
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func (h *masterHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.HasPrefix(r.Header.Get("User-Agent"), "genai-toolbox/") {
|
||||
h.t.Errorf("unexpected User-Agent: got %q", r.Header.Get("User-Agent"))
|
||||
}
|
||||
var body sqladmin.DatabaseInstance
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
h.t.Fatalf("failed to decode request body: %v", err)
|
||||
}
|
||||
|
||||
instanceName := body.Name
|
||||
if instanceName == "" {
|
||||
http.Error(w, "missing instance name", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var expectedBody sqladmin.DatabaseInstance
|
||||
var response any
|
||||
var statusCode int
|
||||
|
||||
switch instanceName {
|
||||
case "instance1":
|
||||
expectedBody = sqladmin.DatabaseInstance{
|
||||
Project: "p1",
|
||||
Name: "instance1",
|
||||
DatabaseVersion: "POSTGRES_15",
|
||||
RootPassword: "password123",
|
||||
Settings: &sqladmin.Settings{
|
||||
AvailabilityType: "REGIONAL",
|
||||
Edition: "ENTERPRISE_PLUS",
|
||||
Tier: "db-perf-optimized-N-8",
|
||||
DataDiskSizeGb: 250,
|
||||
DataDiskType: "PD_SSD",
|
||||
},
|
||||
}
|
||||
response = map[string]any{"name": "op1", "status": "PENDING"}
|
||||
statusCode = http.StatusOK
|
||||
case "instance2":
|
||||
expectedBody = sqladmin.DatabaseInstance{
|
||||
Project: "p2",
|
||||
Name: "instance2",
|
||||
DatabaseVersion: "POSTGRES_17",
|
||||
RootPassword: "password456",
|
||||
Settings: &sqladmin.Settings{
|
||||
AvailabilityType: "ZONAL",
|
||||
Edition: "ENTERPRISE_PLUS",
|
||||
Tier: "db-perf-optimized-N-2",
|
||||
DataDiskSizeGb: 100,
|
||||
DataDiskType: "PD_SSD",
|
||||
},
|
||||
}
|
||||
response = map[string]any{"name": "op2", "status": "RUNNING"}
|
||||
statusCode = http.StatusOK
|
||||
default:
|
||||
http.Error(w, fmt.Sprintf("unhandled instance name: %s", instanceName), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if expectedBody.Project != body.Project {
|
||||
h.t.Errorf("unexpected project: got %q, want %q", body.Project, expectedBody.Project)
|
||||
}
|
||||
if expectedBody.Name != body.Name {
|
||||
h.t.Errorf("unexpected name: got %q, want %q", body.Name, expectedBody.Name)
|
||||
}
|
||||
if expectedBody.DatabaseVersion != body.DatabaseVersion {
|
||||
h.t.Errorf("unexpected databaseVersion: got %q, want %q", body.DatabaseVersion, expectedBody.DatabaseVersion)
|
||||
}
|
||||
if expectedBody.RootPassword != body.RootPassword {
|
||||
h.t.Errorf("unexpected rootPassword: got %q, want %q", body.RootPassword, expectedBody.RootPassword)
|
||||
}
|
||||
if diff := cmp.Diff(expectedBody.Settings, body.Settings); diff != "" {
|
||||
h.t.Errorf("unexpected request body settings (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(statusCode)
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateInstanceToolEndpoints(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
handler := &masterHandler{t: t}
|
||||
server := httptest.NewServer(handler)
|
||||
defer server.Close()
|
||||
|
||||
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 = &createInstanceTransport{
|
||||
transport: originalTransport,
|
||||
url: serverURL,
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
http.DefaultClient.Transport = originalTransport
|
||||
})
|
||||
|
||||
var args []string
|
||||
toolsFile := getCreateInstanceToolsConfig()
|
||||
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
|
||||
if err != nil {
|
||||
t.Fatalf("command initialization returned an error: %s", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
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
|
||||
toolName string
|
||||
body string
|
||||
want string
|
||||
expectError bool
|
||||
errorStatus int
|
||||
}{
|
||||
{
|
||||
name: "successful creation - production",
|
||||
toolName: "create-instance-prod",
|
||||
body: `{"project": "p1", "name": "instance1", "databaseVersion": "POSTGRES_15", "rootPassword": "password123", "editionPreset": "Production"}`,
|
||||
want: `{"name":"op1","status":"PENDING"}`,
|
||||
},
|
||||
{
|
||||
name: "successful creation - development",
|
||||
toolName: "create-instance-dev",
|
||||
body: `{"project": "p2", "name": "instance2", "rootPassword": "password456", "editionPreset": "Development"}`,
|
||||
want: `{"name":"op2","status":"RUNNING"}`,
|
||||
},
|
||||
{
|
||||
name: "missing required parameter",
|
||||
toolName: "create-instance-prod",
|
||||
body: `{"name": "instance1"}`,
|
||||
expectError: true,
|
||||
errorStatus: http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
api := fmt.Sprintf("http://127.0.0.1:5000/api/tool/%s/invoke", tc.toolName)
|
||||
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()
|
||||
|
||||
if tc.expectError {
|
||||
if resp.StatusCode != tc.errorStatus {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("expected status %d but got %d: %s", tc.errorStatus, resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
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.NewDecoder(resp.Body).Decode(&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: %v", err)
|
||||
}
|
||||
if err := json.Unmarshal([]byte(tc.want), &want); err != nil {
|
||||
t.Fatalf("failed to unmarshal want: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("unexpected result: got %+v, want %+v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getCreateInstanceToolsConfig() map[string]any {
|
||||
return map[string]any{
|
||||
"sources": map[string]any{
|
||||
"my-cloud-sql-source": map[string]any{
|
||||
"kind": "cloud-sql-admin",
|
||||
},
|
||||
},
|
||||
"tools": map[string]any{
|
||||
"create-instance-prod": map[string]any{
|
||||
"kind": createInstanceToolKind,
|
||||
"source": "my-cloud-sql-source",
|
||||
},
|
||||
"create-instance-dev": map[string]any{
|
||||
"kind": createInstanceToolKind,
|
||||
"source": "my-cloud-sql-source",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user