mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-02-19 03:14:29 -05:00
Compare commits
7 Commits
dependabot
...
fix-param-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17b875da3f | ||
|
|
fd22009322 | ||
|
|
d6af2907fd | ||
|
|
3684efd512 | ||
|
|
7a6d0c12e9 | ||
|
|
57b77bca09 | ||
|
|
276cf604a2 |
2
.github/workflows/link_checker.yaml
vendored
2
.github/workflows/link_checker.yaml
vendored
@@ -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 }}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
116
internal/server/static/js/resize.js
Normal file
116
internal/server/static/js/resize.js
Normal file
@@ -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`;
|
||||
}
|
||||
|
||||
@@ -22,12 +22,16 @@
|
||||
<script type="module" src="/ui/js/tools.js"></script>
|
||||
<script src="/ui/js/navbar.js"></script>
|
||||
<script src="/ui/js/mainContent.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
<script type="module">
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const navbarContainer = document.getElementById('navbar-container');
|
||||
const activeNav = navbarContainer.getAttribute('data-active-nav');
|
||||
renderNavbar('navbar-container', activeNav);
|
||||
renderMainContent('main-content-container', 'tool-display-area', getToolInstructions())
|
||||
renderMainContent('main-content-container', 'tool-display-area', getToolInstructions());
|
||||
|
||||
// Initialize resize functionality
|
||||
const { initializeResize } = await import('/ui/js/resize.js');
|
||||
initializeResize();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -29,12 +29,16 @@
|
||||
<script type="module" src="/ui/js/toolsets.js"></script>
|
||||
<script src="/ui/js/navbar.js"></script>
|
||||
<script src="/ui/js/mainContent.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
<script type="module">
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const navbarContainer = document.getElementById('navbar-container');
|
||||
const activeNav = navbarContainer.getAttribute('data-active-nav');
|
||||
renderNavbar('navbar-container', activeNav);
|
||||
renderMainContent('main-content-container', 'tool-display-area', getToolsetInstructions());
|
||||
|
||||
// Initialize resize functionality
|
||||
const { initializeResize } = await import('/ui/js/resize.js');
|
||||
initializeResize();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -144,7 +144,7 @@ func ParseParams(ps Parameters, data map[string]any, claimsMap map[string]map[st
|
||||
// parse non auth-required parameter
|
||||
var ok bool
|
||||
v, ok = data[name]
|
||||
if !ok {
|
||||
if !ok || v == nil {
|
||||
v = p.GetDefault()
|
||||
// if the parameter is required and no value given, throw an error
|
||||
if CheckParamRequired(p.GetRequired(), v) {
|
||||
|
||||
@@ -2347,3 +2347,23 @@ func TestCheckParamRequired(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseParams_ExplicitNullForRequiredParam(t *testing.T) {
|
||||
// Define a required string parameter
|
||||
params := parameters.Parameters{
|
||||
parameters.NewStringParameter("required_param", "this is required"),
|
||||
}
|
||||
|
||||
// Input map with explicit nil
|
||||
input := map[string]any{
|
||||
"required_param": nil,
|
||||
}
|
||||
|
||||
// Call ParseParams
|
||||
_, err := parameters.ParseParams(params, input, nil)
|
||||
|
||||
// Expect an error because the parameter is required
|
||||
if err == nil {
|
||||
t.Errorf("ParseParams allowed explicit nil for required parameter, expected error")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user