diff --git a/.github/workflows/link_checker.yaml b/.github/workflows/link_checker.yaml index 308d482ca17..b08757dc628 100644 --- a/.github/workflows/link_checker.yaml +++ b/.github/workflows/link_checker.yaml @@ -114,7 +114,7 @@ jobs: - name: Create PR Comment if: env.HAS_CHANGES == 'true' && steps.lychee-check.outcome == 'failure' - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4 + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5 with: comment-id: ${{ steps.find-comment.outputs.comment-id }} issue-number: ${{ github.event.pull_request.number }} diff --git a/docs/en/getting-started/quickstart/js/genkit/package-lock.json b/docs/en/getting-started/quickstart/js/genkit/package-lock.json index cdb5744245c..436ecf3b6f6 100644 --- a/docs/en/getting-started/quickstart/js/genkit/package-lock.json +++ b/docs/en/getting-started/quickstart/js/genkit/package-lock.json @@ -3231,9 +3231,10 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", diff --git a/docs/en/resources/sources/postgres.md b/docs/en/resources/sources/postgres.md index ed7c77aeee2..c42d8dca644 100644 --- a/docs/en/resources/sources/postgres.md +++ b/docs/en/resources/sources/postgres.md @@ -133,3 +133,4 @@ instead of hardcoding your secrets into the configuration file. | user | string | true | Name of the Postgres user to connect as (e.g. "my-pg-user"). | | password | string | true | Password of the Postgres user (e.g. "my-password"). | | queryParams | map[string]string | false | Raw query to be added to the db connection string. | +| queryExecMode | string | false | pgx query execution mode. Valid values: `cache_statement` (default), `cache_describe`, `describe_exec`, `exec`, `simple_protocol`. Useful with connection poolers that don't support prepared statement caching. | diff --git a/docs/en/resources/sources/redis.md b/docs/en/resources/sources/redis.md index 51c8cfde00e..62ca36be704 100644 --- a/docs/en/resources/sources/redis.md +++ b/docs/en/resources/sources/redis.md @@ -4,8 +4,8 @@ linkTitle: "Redis" type: docs weight: 1 description: > - Redis is a in-memory data structure store. - + Redis is a in-memory data structure store. + --- ## About @@ -44,6 +44,9 @@ password: ${MY_AUTH_STRING} # Omit this field if you don't have a password. # database: 0 # clusterEnabled: false # useGCPIAM: false +# tls: +# enabled: false +# insecureSkipVerify: false ``` {{< notice tip >}} @@ -61,7 +64,7 @@ Here is an example tools.yaml config with [AUTH][auth] enabled: ```yaml kind: sources name: my-redis-cluster-instance -type: memorystore-redis +type: redis address: - 127.0.0.1:6379 password: ${MY_AUTH_STRING} @@ -78,7 +81,7 @@ using IAM authentication: ```yaml kind: sources name: my-redis-cluster-instance -type: memorystore-redis +type: redis address: - 127.0.0.1:6379 useGCPIAM: true @@ -89,14 +92,16 @@ clusterEnabled: true ## Reference -| **field** | **type** | **required** | **description** | -|----------------|:--------:|:------------:|---------------------------------------------------------------------------------------------------------------------------------| -| type | string | true | Must be "memorystore-redis". | -| address | string | true | Primary endpoint for the Memorystore Redis instance to connect to. | -| username | string | false | If you are using a non-default user, specify the user name here. If you are using Memorystore for Redis, leave this field blank | -| password | string | false | If you have [Redis AUTH][auth] enabled, specify the AUTH string here | -| database | int | false | The Redis database to connect to. Not applicable for cluster enabled instances. The default database is `0`. | -| clusterEnabled | bool | false | Set it to `true` if using a Redis Cluster instance. Defaults to `false`. | -| useGCPIAM | string | false | Set it to `true` if you are using GCP's IAM authentication. Defaults to `false`. | +| **field** | **type** | **required** | **description** | +|------------------------|:--------:|:------------:|-----------------------------------------------------------------------------------------------------------------------------------------------| +| type | string | true | Must be "redis". | +| address | string | true | Primary endpoint for the Memorystore Redis instance to connect to. | +| username | string | false | If you are using a non-default user, specify the user name here. If you are using Memorystore for Redis, leave this field blank | +| password | string | false | If you have [Redis AUTH][auth] enabled, specify the AUTH string here | +| database | int | false | The Redis database to connect to. Not applicable for cluster enabled instances. The default database is `0`. | +| tls.enabled | bool | false | Set it to `true` to enable TLS for the Redis connection. Defaults to `false`. | +| tls.insecureSkipVerify | bool | false | Set it to `true` to skip TLS certificate verification. **Warning:** This is insecure and not recommended for production. Defaults to `false`. | +| clusterEnabled | bool | false | Set it to `true` if using a Redis Cluster instance. Defaults to `false`. | +| useGCPIAM | bool | false | Set it to `true` if you are using GCP's IAM authentication. Defaults to `false`. | [auth]: https://cloud.google.com/memorystore/docs/redis/about-redis-auth diff --git a/docs/en/samples/pre_post_processing/js/langchain/package-lock.json b/docs/en/samples/pre_post_processing/js/langchain/package-lock.json index 8eed2a79ea8..27ed1a7601e 100644 --- a/docs/en/samples/pre_post_processing/js/langchain/package-lock.json +++ b/docs/en/samples/pre_post_processing/js/langchain/package-lock.json @@ -217,13 +217,13 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", - "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, @@ -1598,17 +1598,6 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", - "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", - "license": "ISC", - "optional": true, - "peer": true, - "peerDependencies": { - "zod": "^3.25 || ^4" - } } } } diff --git a/internal/server/static/css/style.css b/internal/server/static/css/style.css index 47d8a35974b..b0c1968c1b6 100644 --- a/internal/server/static/css/style.css +++ b/internal/server/static/css/style.css @@ -87,8 +87,29 @@ body { display: flex; flex-direction: column; padding: 15px; - align-items: center; + align-items: stretch; position: relative; + min-width: 200px; + max-width: 50vw; + overflow: visible; + box-sizing: border-box; +} + +.resize-handle { + position: absolute; + right: -2px; + top: 0; + bottom: 0; + width: 4px; + cursor: ew-resize; + background-color: transparent; + z-index: 10; + transition: background-color 0.2s ease; +} + +.resize-handle:hover, +.resize-handle.active { + background-color: var(--toolbox-blue); } .nav-logo { @@ -626,10 +647,13 @@ body { .search-container { display: flex; width: 100%; + min-width: 0; margin-bottom: 15px; + box-sizing: border-box; #toolset-search-input { - flex-grow: 1; + flex: 1; + min-width: 0; padding: 10px 12px; border: 1px solid #ccc; border-radius: 20px 0 0 20px; @@ -637,6 +661,7 @@ body { font-family: inherit; font-size: 0.9em; color: var(--text-primary-gray); + box-sizing: border-box; &:focus { outline: none; diff --git a/internal/server/static/js/resize.js b/internal/server/static/js/resize.js new file mode 100644 index 00000000000..f2ff490fe33 --- /dev/null +++ b/internal/server/static/js/resize.js @@ -0,0 +1,116 @@ +// 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. + +const STORAGE_KEY = 'toolbox-second-nav-width'; +const DEFAULT_WIDTH = 250; +const MIN_WIDTH = 200; +const MAX_WIDTH_PERCENT = 50; + +/** + * Creates and attaches a resize handle to the second navigation panel + */ +export function initializeResize() { + const secondNav = document.querySelector('.second-nav'); + if (!secondNav) { + return; + } + + // Create resize handle + const resizeHandle = document.createElement('div'); + resizeHandle.className = 'resize-handle'; + resizeHandle.setAttribute('aria-label', 'Resize panel'); + secondNav.appendChild(resizeHandle); + + // Load saved width or use default + let initialWidth = DEFAULT_WIDTH; + try { + const savedWidth = localStorage.getItem(STORAGE_KEY); + if (savedWidth) { + const parsed = parseInt(savedWidth, 10); + if (!isNaN(parsed) && parsed >= MIN_WIDTH) { + initialWidth = parsed; + } + } + } catch (e) { + // localStorage may be unavailable in private browsing mode + console.warn('Failed to load saved panel width:', e); + } + setPanelWidth(secondNav, initialWidth); + + // Setup resize functionality + let startX = 0; + let startWidth = 0; + + const onMouseMove = (e) => { + const deltaX = e.clientX - startX; + const newWidth = startWidth + deltaX; + const maxWidth = (window.innerWidth * MAX_WIDTH_PERCENT) / 100; + + const clampedWidth = Math.max(MIN_WIDTH, Math.min(newWidth, maxWidth)); + setPanelWidth(secondNav, clampedWidth); + }; + + const onMouseUp = () => { + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + + resizeHandle.classList.remove('active'); + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + + // Save width to localStorage + try { + localStorage.setItem(STORAGE_KEY, secondNav.offsetWidth.toString()); + } catch (e) { + // localStorage may be unavailable in private browsing mode + console.warn('Failed to save panel width:', e); + } + }; + + resizeHandle.addEventListener('mousedown', (e) => { + startX = e.clientX; + startWidth = secondNav.offsetWidth; + resizeHandle.classList.add('active'); + document.body.style.cursor = 'ew-resize'; + document.body.style.userSelect = 'none'; + e.preventDefault(); + + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + }); + + // Handle window resize to enforce max width + window.addEventListener('resize', () => { + const currentWidth = secondNav.offsetWidth; + const maxWidth = (window.innerWidth * MAX_WIDTH_PERCENT) / 100; + + if (currentWidth > maxWidth) { + setPanelWidth(secondNav, maxWidth); + try { + localStorage.setItem(STORAGE_KEY, maxWidth.toString()); + } catch (e) { + // localStorage may be unavailable in private browsing mode + console.warn('Failed to save panel width:', e); + } + } + }); +} + +/** + * Sets the width of the panel and updates flex property + */ +function setPanelWidth(panel, width) { + panel.style.flex = `0 0 ${width}px`; +} + diff --git a/internal/server/static/tools.html b/internal/server/static/tools.html index c618c5111b4..f461cc80ad5 100644 --- a/internal/server/static/tools.html +++ b/internal/server/static/tools.html @@ -22,12 +22,16 @@ - diff --git a/internal/server/static/toolsets.html b/internal/server/static/toolsets.html index adcd5fdfde8..84907222603 100644 --- a/internal/server/static/toolsets.html +++ b/internal/server/static/toolsets.html @@ -29,12 +29,16 @@ - diff --git a/internal/sources/postgres/postgres.go b/internal/sources/postgres/postgres.go index 65d7a17b75d..b2ed44a95d4 100644 --- a/internal/sources/postgres/postgres.go +++ b/internal/sources/postgres/postgres.go @@ -24,6 +24,7 @@ import ( "github.com/googleapis/genai-toolbox/internal/sources" "github.com/googleapis/genai-toolbox/internal/util" "github.com/googleapis/genai-toolbox/internal/util/orderedmap" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "go.opentelemetry.io/otel/trace" ) @@ -48,14 +49,15 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources } type Config struct { - Name string `yaml:"name" validate:"required"` - Type string `yaml:"type" validate:"required"` - Host string `yaml:"host" validate:"required"` - Port string `yaml:"port" validate:"required"` - User string `yaml:"user" validate:"required"` - Password string `yaml:"password" validate:"required"` - Database string `yaml:"database" validate:"required"` - QueryParams map[string]string `yaml:"queryParams"` + Name string `yaml:"name" validate:"required"` + Type string `yaml:"type" validate:"required"` + Host string `yaml:"host" validate:"required"` + Port string `yaml:"port" validate:"required"` + User string `yaml:"user" validate:"required"` + Password string `yaml:"password" validate:"required"` + Database string `yaml:"database" validate:"required"` + QueryParams map[string]string `yaml:"queryParams"` + QueryExecMode string `yaml:"queryExecMode" validate:"omitempty,oneof=cache_statement cache_describe describe_exec exec simple_protocol"` } func (r Config) SourceConfigType() string { @@ -63,7 +65,7 @@ func (r Config) SourceConfigType() string { } func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.Source, error) { - pool, err := initPostgresConnectionPool(ctx, tracer, r.Name, r.Host, r.Port, r.User, r.Password, r.Database, r.QueryParams) + pool, err := initPostgresConnectionPool(ctx, tracer, r.Name, r.Host, r.Port, r.User, r.Password, r.Database, r.QueryParams, r.QueryExecMode) if err != nil { return nil, fmt.Errorf("unable to create pool: %w", err) } @@ -126,7 +128,7 @@ func (s *Source) RunSQL(ctx context.Context, statement string, params []any) (an return out, nil } -func initPostgresConnectionPool(ctx context.Context, tracer trace.Tracer, name, host, port, user, pass, dbname string, queryParams map[string]string) (*pgxpool.Pool, error) { +func initPostgresConnectionPool(ctx context.Context, tracer trace.Tracer, name, host, port, user, pass, dbname string, queryParams map[string]string, queryExecMode string) (*pgxpool.Pool, error) { //nolint:all // Reassigned ctx ctx, span := sources.InitConnectionSpan(ctx, tracer, SourceType, name) defer span.End() @@ -150,7 +152,18 @@ func initPostgresConnectionPool(ctx context.Context, tracer trace.Tracer, name, Path: dbname, RawQuery: ConvertParamMapToRawQuery(queryParams), } - pool, err := pgxpool.New(ctx, url.String()) + config, err := pgxpool.ParseConfig(url.String()) + if err != nil { + return nil, fmt.Errorf("unable to parse connection uri: %w", err) + } + + execMode, err := ParseQueryExecMode(queryExecMode) + if err != nil { + return nil, err + } + config.ConnConfig.DefaultQueryExecMode = execMode + + pool, err := pgxpool.NewWithConfig(ctx, config) if err != nil { return nil, fmt.Errorf("unable to create connection pool: %w", err) } @@ -165,3 +178,20 @@ func ConvertParamMapToRawQuery(queryParams map[string]string) string { } return strings.Join(queryArray, "&") } + +func ParseQueryExecMode(queryExecMode string) (pgx.QueryExecMode, error) { + switch queryExecMode { + case "", "cache_statement": + return pgx.QueryExecModeCacheStatement, nil + case "cache_describe": + return pgx.QueryExecModeCacheDescribe, nil + case "describe_exec": + return pgx.QueryExecModeDescribeExec, nil + case "exec": + return pgx.QueryExecModeExec, nil + case "simple_protocol": + return pgx.QueryExecModeSimpleProtocol, nil + default: + return 0, fmt.Errorf("invalid queryExecMode %q: must be one of %q, %q, %q, %q, or %q", queryExecMode, "cache_statement", "cache_describe", "describe_exec", "exec", "simple_protocol") + } +} diff --git a/internal/sources/postgres/postgres_test.go b/internal/sources/postgres/postgres_test.go index 4bcde0420e5..0441f356a3d 100644 --- a/internal/sources/postgres/postgres_test.go +++ b/internal/sources/postgres/postgres_test.go @@ -25,6 +25,7 @@ import ( "github.com/googleapis/genai-toolbox/internal/sources" "github.com/googleapis/genai-toolbox/internal/sources/postgres" "github.com/googleapis/genai-toolbox/internal/testutils" + "github.com/jackc/pgx/v5" ) func TestParseFromYamlPostgres(t *testing.T) { @@ -88,6 +89,32 @@ func TestParseFromYamlPostgres(t *testing.T) { }, }, }, + { + desc: "example with query exec mode", + in: ` + kind: sources + name: my-pg-instance + type: postgres + host: my-host + port: my-port + database: my_db + user: my_user + password: my_pass + queryExecMode: simple_protocol + `, + want: map[string]sources.SourceConfig{ + "my-pg-instance": postgres.Config{ + Name: "my-pg-instance", + Type: postgres.SourceType, + Host: "my-host", + Port: "my-port", + Database: "my_db", + User: "my_user", + Password: "my_pass", + QueryExecMode: "simple_protocol", + }, + }, + }, } for _, tc := range tcs { t.Run(tc.desc, func(t *testing.T) { @@ -137,6 +164,21 @@ func TestFailParseFromYaml(t *testing.T) { `, err: "error unmarshaling sources: unable to parse source \"my-pg-instance\" as \"postgres\": Key: 'Config.Password' Error:Field validation for 'Password' failed on the 'required' tag", }, + { + desc: "invalid query exec mode", + in: ` + kind: sources + name: my-pg-instance + type: postgres + host: my-host + port: my-port + database: my_db + user: my_user + password: my_pass + queryExecMode: invalid_mode + `, + err: "error unmarshaling sources: unable to parse source \"my-pg-instance\" as \"postgres\": [6:16] Key: 'Config.QueryExecMode' Error:Field validation for 'QueryExecMode' failed on the 'oneof' tag\n 3 | name: my-pg-instance\n 4 | password: my_pass\n 5 | port: my-port\n> 6 | queryExecMode: invalid_mode\n ^\n 7 | type: postgres\n 8 | user: my_user", + }, } for _, tc := range tcs { t.Run(tc.desc, func(t *testing.T) { @@ -193,3 +235,32 @@ func TestConvertParamMapToRawQuery(t *testing.T) { }) } } + +func TestParseQueryExecMode(t *testing.T) { + tcs := []struct { + desc string + in string + want pgx.QueryExecMode + wantErr bool + }{ + {desc: "empty (default)", in: "", want: pgx.QueryExecModeCacheStatement}, + {desc: "cache_statement", in: "cache_statement", want: pgx.QueryExecModeCacheStatement}, + {desc: "cache_describe", in: "cache_describe", want: pgx.QueryExecModeCacheDescribe}, + {desc: "describe_exec", in: "describe_exec", want: pgx.QueryExecModeDescribeExec}, + {desc: "exec", in: "exec", want: pgx.QueryExecModeExec}, + {desc: "simple_protocol", in: "simple_protocol", want: pgx.QueryExecModeSimpleProtocol}, + {desc: "invalid mode", in: "invalid_mode", wantErr: true}, + } + + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + got, err := postgres.ParseQueryExecMode(tc.in) + if (err != nil) != tc.wantErr { + t.Fatalf("parseQueryExecMode() error = %v, wantErr %v", err, tc.wantErr) + } + if !tc.wantErr && got != tc.want { + t.Errorf("parseQueryExecMode() = %v, want %v", got, tc.want) + } + }) + } +} diff --git a/internal/sources/redis/redis.go b/internal/sources/redis/redis.go index e2a28c52a7e..2a0b4f81a44 100644 --- a/internal/sources/redis/redis.go +++ b/internal/sources/redis/redis.go @@ -15,6 +15,7 @@ package redis import ( "context" + "crypto/tls" "fmt" "time" @@ -44,14 +45,20 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources } type Config struct { - Name string `yaml:"name" validate:"required"` - Type string `yaml:"type" validate:"required"` - Address []string `yaml:"address" validate:"required"` - Username string `yaml:"username"` - Password string `yaml:"password"` - Database int `yaml:"database"` - UseGCPIAM bool `yaml:"useGCPIAM"` - ClusterEnabled bool `yaml:"clusterEnabled"` + Name string `yaml:"name" validate:"required"` + Type string `yaml:"type" validate:"required"` + Address []string `yaml:"address" validate:"required"` + Username string `yaml:"username"` + Password string `yaml:"password"` + Database int `yaml:"database"` + UseGCPIAM bool `yaml:"useGCPIAM"` + ClusterEnabled bool `yaml:"clusterEnabled"` + TLS TLSConfig `yaml:"tls"` +} + +type TLSConfig struct { + Enabled bool `yaml:"enabled"` + InsecureSkipVerify bool `yaml:"insecureSkipVerify"` } func (r Config) SourceConfigType() string { @@ -91,6 +98,13 @@ func initRedisClient(ctx context.Context, r Config) (RedisClient, error) { } } + var tlsConfig *tls.Config + if r.TLS.Enabled { + tlsConfig = &tls.Config{ + InsecureSkipVerify: r.TLS.InsecureSkipVerify, + } + } + var client RedisClient var err error if r.ClusterEnabled { @@ -104,6 +118,7 @@ func initRedisClient(ctx context.Context, r Config) (RedisClient, error) { CredentialsProviderContext: authFn, Username: r.Username, Password: r.Password, + TLSConfig: tlsConfig, }) err = clusterClient.ForEachShard(ctx, func(ctx context.Context, shard *redis.Client) error { return shard.Ping(ctx).Err() @@ -125,6 +140,7 @@ func initRedisClient(ctx context.Context, r Config) (RedisClient, error) { CredentialsProviderContext: authFn, Username: r.Username, Password: r.Password, + TLSConfig: tlsConfig, }) _, err = standaloneClient.Ping(ctx).Result() if err != nil { diff --git a/internal/sources/redis/redis_test.go b/internal/sources/redis/redis_test.go index 63e2d01beec..ce44c53afbf 100644 --- a/internal/sources/redis/redis_test.go +++ b/internal/sources/redis/redis_test.go @@ -63,6 +63,9 @@ func TestParseFromYamlRedis(t *testing.T) { database: 1 useGCPIAM: true clusterEnabled: true + tls: + enabled: true + insecureSkipVerify: true `, want: map[string]sources.SourceConfig{ "my-redis-instance": redis.Config{ @@ -73,6 +76,10 @@ func TestParseFromYamlRedis(t *testing.T) { Database: 1, ClusterEnabled: true, UseGCPIAM: true, + TLS: redis.TLSConfig{ + Enabled: true, + InsecureSkipVerify: true, + }, }, }, },