From 5bf54dd295f1b260237ac7298fb30b24ec1cc0a0 Mon Sep 17 00:00:00 2001 From: Augustin Husson Date: Mon, 18 May 2026 11:50:45 +0200 Subject: [PATCH 1/2] [FEATURE] Add proxy definitions Signed-off-by: Augustin Husson --- cue/common/regexp_go_gen.cue | 8 + cue/common/regexp_patch.cue | 24 + cue/common/url_go_gen.cue | 10 + cue/common/url_patch.cue | 21 + cue/datasource/proxy/http/http_go_gen.cue | 17 + cue/datasource/proxy/http/http_patch.cue | 50 ++ cue/datasource/proxy/proxy_go_gen.cue | 11 + cue/datasource/proxy/sql/sql_go_gen.cue | 58 +++ cue/datasource/proxy/sql/sql_patch.cue | 60 +++ go/common/regexp.go | 83 ++++ go/common/url.go | 128 +++++ go/datasource/proxy/http/http.go | 126 +++++ go/datasource/proxy/http/http_test.go | 241 +++++++++ go/datasource/proxy/proxy.go | 20 + go/datasource/proxy/sql/sql.go | 209 ++++++++ go/datasource/proxy/sql/sql_test.go | 570 ++++++++++++++++++++++ ts/src/datasource/proxy/http.ts | 35 ++ ts/src/datasource/proxy/sql.ts | 54 ++ 18 files changed, 1725 insertions(+) create mode 100644 cue/common/regexp_go_gen.cue create mode 100644 cue/common/regexp_patch.cue create mode 100644 cue/common/url_go_gen.cue create mode 100644 cue/common/url_patch.cue create mode 100644 cue/datasource/proxy/http/http_go_gen.cue create mode 100644 cue/datasource/proxy/http/http_patch.cue create mode 100644 cue/datasource/proxy/proxy_go_gen.cue create mode 100644 cue/datasource/proxy/sql/sql_go_gen.cue create mode 100644 cue/datasource/proxy/sql/sql_patch.cue create mode 100644 go/common/regexp.go create mode 100644 go/common/url.go create mode 100644 go/datasource/proxy/http/http.go create mode 100644 go/datasource/proxy/http/http_test.go create mode 100644 go/datasource/proxy/proxy.go create mode 100644 go/datasource/proxy/sql/sql.go create mode 100644 go/datasource/proxy/sql/sql_test.go create mode 100644 ts/src/datasource/proxy/http.ts create mode 100644 ts/src/datasource/proxy/sql.ts diff --git a/cue/common/regexp_go_gen.cue b/cue/common/regexp_go_gen.cue new file mode 100644 index 0000000..269129f --- /dev/null +++ b/cue/common/regexp_go_gen.cue @@ -0,0 +1,8 @@ +// Code generated by cue get go. DO NOT EDIT. + +//cue:generate cue get go github.com/perses/spec/go/common + +package common + +// Regexp encapsulates a regexp.Regexp and makes it JSON/YAML marshalable. +#Regexp: _ diff --git a/cue/common/regexp_patch.cue b/cue/common/regexp_patch.cue new file mode 100644 index 0000000..e9dab4d --- /dev/null +++ b/cue/common/regexp_patch.cue @@ -0,0 +1,24 @@ +// Copyright The Perses Authors +// 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. + +// NB: This file complements the regexp_go_gen.cue file generated by +// `cue get go` to add the missing constraints lost in the translation +// process. This should no longer be needed at some point hopefully, but for +// the moment, because of a technical limitation in the CUE translation +// process, a top-value (= "any") gets generated instead of a proper def for +// any type that defines a custom UnmarshallJSON or UnmarshallYAML. +// For more info see https://github.com/cue-lang/cue/issues/2466. + +package common + +#Regexp: string diff --git a/cue/common/url_go_gen.cue b/cue/common/url_go_gen.cue new file mode 100644 index 0000000..2fce926 --- /dev/null +++ b/cue/common/url_go_gen.cue @@ -0,0 +1,10 @@ +// Code generated by cue get go. DO NOT EDIT. + +//cue:generate cue get go github.com/perses/spec/go/common + +package common + +// +kubebuilder:validation:Schemaless +// +kubebuilder:validation:Type=string +// +kubebuilder:validation:Format=uri +#URL: _ diff --git a/cue/common/url_patch.cue b/cue/common/url_patch.cue new file mode 100644 index 0000000..8852959 --- /dev/null +++ b/cue/common/url_patch.cue @@ -0,0 +1,21 @@ +// Copyright The Perses Authors +// 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. + +// NB: This file complements the url_go_gen.cue file generated by +// `cue get go` to add the missing constraints lost in the translation +// process. Indeed in Go, the URL pattern is not enforced by typing but +// by using a custom unmarshalling process, that doesn't get converted. + +package common + +#URL: =~"^https?:\/\/[^\\s\/$.?#].[^\\s]*$" diff --git a/cue/datasource/proxy/http/http_go_gen.cue b/cue/datasource/proxy/http/http_go_gen.cue new file mode 100644 index 0000000..a343f46 --- /dev/null +++ b/cue/datasource/proxy/http/http_go_gen.cue @@ -0,0 +1,17 @@ +// Code generated by cue get go. DO NOT EDIT. + +//cue:generate cue get go github.com/perses/spec/go/datasource/proxy/http + +package http + +import "github.com/perses/spec/cue/datasource/proxy" + +#AllowedEndpoint: _ + +#Config: _ + +// Proxy is the HTTP proxy definition proposed in Perses. +// In case you are defining a datasource that will work with the Perses backend, then you will need to use this definition. +#Proxy: proxy.#Proxy + +#ProxyKindName: "httpproxy" diff --git a/cue/datasource/proxy/http/http_patch.cue b/cue/datasource/proxy/http/http_patch.cue new file mode 100644 index 0000000..5bb4fca --- /dev/null +++ b/cue/datasource/proxy/http/http_patch.cue @@ -0,0 +1,50 @@ +// Copyright The Perses Authors +// 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. + +// NB: This file complements the http_go_gen.cue file generated by +// `cue get go` to add the missing constraints lost in the translation +// process. This should no longer be needed at some point hopefully, but for +// the moment, because of a technical limitation in the CUE translation +// process, a top-value (= "any") gets generated instead of a proper def for +// any type that defines a custom UnmarshallJSON or UnmarshallYAML. +// For more info see https://github.com/cue-lang/cue/issues/2466. + +package http + +import ( + "github.com/perses/spec/cue/common" + "github.com/perses/spec/cue/datasource/proxy" + ) + +#AllowedEndpoint: { + endpointPattern: string @go(EndpointPattern) + method: "POST" | "PUT" | "PATCH" | "GET" | "DELETE" @go(Method) +} + +#Config: { + // url is the url of the datasource. It is not the url of the proxy. + // The Perses server is the proxy, so it needs to know where to redirect the request. + url: common.#URL @go(URL) + // allowedEndpoints is a list of tuples of http methods and http endpoints that will be accessible. + // Leave it empty if you don't want to restrict the access to the datasource. + allowedEndpoints?: [...#AllowedEndpoint] @go(AllowedEndpoints) + // headers can be used to provide additional headers that need to be forwarded when requesting the datasource + headers?: {[string]: string} @go(Headers) + // secret is the name of the secret that should be used for the proxy or discovery configuration + // It will contain any sensitive information such as password, token, certificate. + secret?: string @go(Secret) +} + +#Proxy: proxy.#Proxy & { + kind: "HTTPProxy" @go(Kind) +} diff --git a/cue/datasource/proxy/proxy_go_gen.cue b/cue/datasource/proxy/proxy_go_gen.cue new file mode 100644 index 0000000..329952b --- /dev/null +++ b/cue/datasource/proxy/proxy_go_gen.cue @@ -0,0 +1,11 @@ +// Code generated by cue get go. DO NOT EDIT. + +//cue:generate cue get go github.com/perses/spec/go/datasource/proxy + +package proxy + +// Proxy is the generic struct of the proxy definition +#Proxy: { + kind: string @go(Kind) + spec: _ @go(Spec,T) +} diff --git a/cue/datasource/proxy/sql/sql_go_gen.cue b/cue/datasource/proxy/sql/sql_go_gen.cue new file mode 100644 index 0000000..c3467f6 --- /dev/null +++ b/cue/datasource/proxy/sql/sql_go_gen.cue @@ -0,0 +1,58 @@ +// Code generated by cue get go. DO NOT EDIT. + +//cue:generate cue get go github.com/perses/spec/go/datasource/proxy/sql + +package sql + +import ( + "github.com/perses/spec/cue/common" + "github.com/perses/spec/cue/datasource/proxy" +) + +// Driver the SQL driver to use +#Driver: string // #enumDriver + +#enumDriver: + #DriverMySQL | + #DriverMariaDB | + #DriverPostgreSQL + +#DriverMySQL: #Driver & "mysql" +#DriverMariaDB: #Driver & "mariadb" +#DriverPostgreSQL: #Driver & "postgres" + +// SSLMode postgres ssl modes +#SSLMode: string // #enumSSLMode + +#enumSSLMode: + #SSLModeDisable | + #SSLModeAllow | + #SSLModePreferable | + #SSLModeRequire | + #SSLModeVerifyFull | + #SSLModeVerifyCA + +#SSLModeDisable: #SSLMode & "disable" +#SSLModeAllow: #SSLMode & "allow" +#SSLModePreferable: #SSLMode & "prefer" +#SSLModeRequire: #SSLMode & "require" +#SSLModeVerifyFull: #SSLMode & "verify-full" +#SSLModeVerifyCA: #SSLMode & "verify-ca" + +#MySQLConfig: { + params?: {[string]: string} @go(Params,map[string]string) + maxAllowedPacket?: int @go(MaxAllowedPacket) + timeout?: common.#DurationString @go(Timeout) + readTimeout?: common.#DurationString @go(ReadTimeout) + writeTimeout?: common.#DurationString @go(WriteTimeout) +} + +#PostgresConfig: _ + +#Config: _ + +// Proxy is the SQL proxy definition proposed in Perses. +// In case you are defining a datasource that will work with the Perses backend, then you will need to use this definition. +#Proxy: proxy.#Proxy + +#ProxyKindName: "sqlproxy" diff --git a/cue/datasource/proxy/sql/sql_patch.cue b/cue/datasource/proxy/sql/sql_patch.cue new file mode 100644 index 0000000..d050ead --- /dev/null +++ b/cue/datasource/proxy/sql/sql_patch.cue @@ -0,0 +1,60 @@ +// Copyright The Perses Authors +// 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. + +// NB: This file complements the http_go_gen.cue file generated by +// `cue get go` to add the missing constraints lost in the translation +// process. This should no longer be needed at some point hopefully, but for +// the moment, because of a technical limitation in the CUE translation +// process, a top-value (= "any") gets generated instead of a proper def for +// any type that defines a custom UnmarshallJSON or UnmarshallYAML. +// For more info see https://github.com/cue-lang/cue/issues/2466. + +package sql + +import ( + "github.com/perses/spec/cue/common" + "github.com/perses/spec/cue/datasource/proxy" +) + +#PostgresConfig: { + // options specifies command-line options to send to the server at connection start + options?: string @go(Options,string) + // max_conns is the maximum size of the pool + max_conns?: number @go(MaxConns,int) + // connect_timeout the timeout value used for socket connect operations. + connect_timeout?: common.#DurationString @go(ConnectTimeout) + // prepare_threshold specifies the number of PreparedStatement executions that must occur before the driver begins using server-side prepared statements. + prepare_threshold?: number @go(PrepareThreshold,int) + // ssl_mode to use when connecting to postgres + ssl_mode?: #enumSSLMode @go(SSLMode,int) +} + +#Config: { + driver: "mysql" | "postgres" + // host is the hostname and port of the datasource. It is not the hostname of the proxy. + // The Perses server is the proxy, so it needs to know where to redirect the request. + host: string + // database is the name of the database to connect to + database: string + // secret is the name of the secret that should be used for the proxy or discovery configuration + // It will contain any sensitive information such as username, password, token, certificate. + secret?: string + // mysql specific driver configurations + mysql?: #MySQLConfig + // postgres specific driver configurations + postgres?: #PostgresConfig +} + +#Proxy: proxy.#Proxy & { + kind: "SQLProxy" @go(Kind) +} diff --git a/go/common/regexp.go b/go/common/regexp.go new file mode 100644 index 0000000..2e3beb1 --- /dev/null +++ b/go/common/regexp.go @@ -0,0 +1,83 @@ +// Copyright The Perses Authors +// 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 common + +import ( + "encoding/json" + "regexp" +) + +// Regexp encapsulates a regexp.Regexp and makes it JSON/YAML marshalable. +type Regexp struct { + *regexp.Regexp + original string +} + +// NewRegexp creates a new anchored Regexp and returns an error if the +// passed-in regular expression does not compile. +func NewRegexp(s string) (Regexp, error) { + regex, err := regexp.Compile(s) + return Regexp{ + Regexp: regex, + original: s, + }, err +} + +// MustNewRegexp works like NewRegexp, but panics if the regular expression does not compile. +func MustNewRegexp(s string) Regexp { + re, err := NewRegexp(s) + if err != nil { + panic(err) + } + return re +} + +func (re *Regexp) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + return re.validate(s) +} + +func (re *Regexp) UnmarshalYAML(unmarshal func(any) error) error { + var s string + if err := unmarshal(&s); err != nil { + return err + } + return re.validate(s) +} + +func (re Regexp) MarshalJSON() ([]byte, error) { + if len(re.original) > 0 { + return json.Marshal(re.original) + } + return nil, nil +} + +func (re Regexp) MarshalYAML() (any, error) { + if len(re.original) > 0 { + return re.original, nil + } + return nil, nil +} + +func (re *Regexp) validate(s string) error { + r, err := NewRegexp(s) + if err != nil { + return err + } + *re = r + return nil +} diff --git a/go/common/url.go b/go/common/url.go new file mode 100644 index 0000000..d2582ff --- /dev/null +++ b/go/common/url.go @@ -0,0 +1,128 @@ +// Copyright The Perses Authors +// 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 common + +import ( + "encoding/json" + "net/url" + "path" +) + +func ParseURL(rawURL string) (*URL, error) { + u, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + return &URL{URL: u}, nil +} + +func MustParseURL(rawURL string) *URL { + u, err := url.Parse(rawURL) + if err != nil { + panic(err) + } + return &URL{URL: u} +} + +func NewURL(u *URL, paths ...string) *URL { + result := &URL{ + URL: &url.URL{}, + } + if u == nil { + return result + } + *result.URL = *u.URL + if len(paths) > 0 { + result.Path = path.Join(append([]string{u.Path}, paths...)...) + } + return result +} + +// +kubebuilder:validation:Schemaless +// +kubebuilder:validation:Type=string +// +kubebuilder:validation:Format=uri +type URL struct { + *url.URL `json:"-" yaml:"-"` +} + +func (u *URL) IsNilOrEmpty() bool { + return u.URL == nil || u.URL.String() == "" +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface for URLs. +func (u *URL) UnmarshalYAML(unmarshal func(any) error) error { + var s string + if err := unmarshal(&s); err != nil { + return err + } + + urlp, err := url.Parse(s) + if err != nil { + return err + } + u.URL = urlp + return nil +} + +// MarshalYAML implements the yaml.Marshaler interface for URLs. +func (u URL) MarshalYAML() (any, error) { + if u.URL != nil { + return u.String(), nil + } + return nil, nil +} + +// UnmarshalJSON implements the json.Marshaler interface for URL. +func (u *URL) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + urlp, err := url.Parse(s) + if err != nil { + return err + } + u.URL = urlp + return nil +} + +// MarshalJSON implements the json.Marshaler interface for URL. +func (u URL) MarshalJSON() ([]byte, error) { + if u.URL != nil { + return json.Marshal(u.URL.String()) + } + return []byte("null"), nil +} + +// MarshalText implements the encoding.TextMarshaler interface. +func (u *URL) MarshalText() ([]byte, error) { + return []byte(u.URL.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +func (u *URL) UnmarshalText(text []byte) error { + urlp, err := url.Parse(string(text)) + if err != nil { + return err + } + u.URL = urlp + return nil +} + +func (u *URL) String() string { + if u == nil || u.URL == nil { + return "" + } + return u.URL.String() +} diff --git a/go/datasource/proxy/http/http.go b/go/datasource/proxy/http/http.go new file mode 100644 index 0000000..7193ffe --- /dev/null +++ b/go/datasource/proxy/http/http.go @@ -0,0 +1,126 @@ +// Copyright The Perses Authors +// 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 http + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/perses/spec/go/common" + "github.com/perses/spec/go/datasource/proxy" +) + +type AllowedEndpoint struct { + EndpointPattern common.Regexp `json:"endpointPattern" yaml:"endpointPattern"` + Method string `json:"method" yaml:"method"` +} + +func (h *AllowedEndpoint) UnmarshalJSON(data []byte) error { + var tmp AllowedEndpoint + type plain AllowedEndpoint + if err := json.Unmarshal(data, (*plain)(&tmp)); err != nil { + return err + } + if err := (&tmp).validate(); err != nil { + return err + } + *h = tmp + return nil +} + +func (h *AllowedEndpoint) UnmarshalYAML(unmarshal func(any) error) error { + var tmp AllowedEndpoint + type plain AllowedEndpoint + if err := unmarshal((*plain)(&tmp)); err != nil { + return err + } + if err := (&tmp).validate(); err != nil { + return err + } + *h = tmp + return nil +} + +func (h *AllowedEndpoint) validate() error { + if len(h.Method) == 0 { + return fmt.Errorf("HTTP method cannot be empty") + } + if h.EndpointPattern.Regexp == nil { + return fmt.Errorf("HTTP endpoint pattern cannot be empty") + } + if h.Method != http.MethodGet && + h.Method != http.MethodPost && + h.Method != http.MethodDelete && + h.Method != http.MethodPut && + h.Method != http.MethodPatch { + return fmt.Errorf("%q is not a valid http method. Current supported HTTP method: %s, %s, %s, %s, %s", h.Method, http.MethodGet, http.MethodPost, http.MethodDelete, http.MethodPut, http.MethodPatch) + } + return nil +} + +type Config struct { + // URL is the url required to contact the datasource + URL *common.URL `json:"url" yaml:"url"` + // AllowedEndpoints is a list of tuple of http method and http endpoint that will be accessible. + // If not set, then everything is accessible. + AllowedEndpoints []AllowedEndpoint `json:"allowedEndpoints,omitempty" yaml:"allowedEndpoints,omitempty"` + // Headers can be used to provide additional header that needs to be forwarded when requesting the datasource + // When defined, it's impossible to set the value of Access with 'browser' + Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"` + // Secret is the name of the secret that should be used for the proxy or discovery configuration + // It will contain any sensitive information such as password, token, certificate. + Secret string `json:"secret,omitempty" yaml:"secret,omitempty"` +} + +func (h *Config) UnmarshalJSON(data []byte) error { + var tmp Config + type plain Config + if err := json.Unmarshal(data, (*plain)(&tmp)); err != nil { + return err + } + if err := (&tmp).validate(); err != nil { + return err + } + *h = tmp + return nil +} + +func (h *Config) UnmarshalYAML(unmarshal func(any) error) error { + var tmp Config + type plain Config + if err := unmarshal((*plain)(&tmp)); err != nil { + return err + } + if err := (&tmp).validate(); err != nil { + return err + } + *h = tmp + return nil +} + +func (h *Config) validate() error { + if h.URL == nil { + return fmt.Errorf("url cannot be empty") + } + return nil +} + +// Proxy is the HTTP proxy definition proposed in Perses. +// In case you are defining a datasource that will work with the Perses backend, then you will need to use this definition. +type Proxy proxy.Proxy[Config] + +const ( + ProxyKindName = "httpproxy" +) diff --git a/go/datasource/proxy/http/http_test.go b/go/datasource/proxy/http/http_test.go new file mode 100644 index 0000000..84913ec --- /dev/null +++ b/go/datasource/proxy/http/http_test.go @@ -0,0 +1,241 @@ +// Copyright The Perses Authors +// 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 http + +import ( + "encoding/json" + "net/http" + "net/url" + "testing" + + "github.com/perses/spec/go/common" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" +) + +func TestUnmarshalJSONConfig(t *testing.T) { + testSuite := []struct { + title string + jason string + result Config + }{ + { + title: "basic config", + jason: ` +{ + "url": "http://localhost:9090" +} +`, + result: Config{ + URL: &common.URL{ + URL: &url.URL{ + Scheme: "http", + Host: "localhost:9090", + }, + }, + }, + }, + } + for _, test := range testSuite { + t.Run(test.title, func(t *testing.T) { + result := Config{} + assert.NoError(t, json.Unmarshal([]byte(test.jason), &result)) + assert.Equal(t, test.result, result) + }) + } +} + +func TestUnmarshalYAMLConfig(t *testing.T) { + testSuite := []struct { + title string + yamele string + result Config + }{ + { + title: "basic config", + yamele: ` +url: "http://localhost:9090" +`, + result: Config{ + URL: &common.URL{ + URL: &url.URL{ + Scheme: "http", + Host: "localhost:9090", + }, + }, + }, + }, + } + for _, test := range testSuite { + t.Run(test.title, func(t *testing.T) { + result := Config{} + assert.NoError(t, yaml.Unmarshal([]byte(test.yamele), &result)) + assert.Equal(t, test.result, result) + }) + } +} + +func TestUnmarshalJSONAllowedEndpoint(t *testing.T) { + testSuite := []struct { + title string + jason string + result AllowedEndpoint + }{ + { + title: "simple endpoint", + jason: ` +{ + "endpointPattern": "/api/v1/labels", + "method": "POST" +} +`, + result: AllowedEndpoint{ + EndpointPattern: common.MustNewRegexp("/api/v1/labels"), + Method: http.MethodPost, + }, + }, + { + title: "complex endpoint patter", + jason: ` +{ + "endpointPattern": "^/?api/v./[a-zA-Z0-9]$", + "method": "POST" +} +`, + result: AllowedEndpoint{ + EndpointPattern: common.MustNewRegexp("^/?api/v./[a-zA-Z0-9]$"), + Method: http.MethodPost, + }, + }, + } + for _, test := range testSuite { + t.Run(test.title, func(t *testing.T) { + result := AllowedEndpoint{} + assert.NoError(t, json.Unmarshal([]byte(test.jason), &result)) + assert.Equal(t, test.result, result) + }) + } +} + +func TestUnmarshalYAMLAllowedEndpoint(t *testing.T) { + testSuite := []struct { + title string + yamele string + result AllowedEndpoint + }{ + { + title: "simple endpoint", + yamele: ` +endpointPattern: "/api/v1/labels" +method: "POST" +`, + result: AllowedEndpoint{ + EndpointPattern: common.MustNewRegexp("/api/v1/labels"), + Method: http.MethodPost, + }, + }, + { + title: "complex endpoint patter", + yamele: ` +endpointPattern: "^/?api/v./[a-zA-Z0-9]$" +method: "POST" +`, + result: AllowedEndpoint{ + EndpointPattern: common.MustNewRegexp("^/?api/v./[a-zA-Z0-9]$"), + Method: http.MethodPost, + }, + }, + } + for _, test := range testSuite { + t.Run(test.title, func(t *testing.T) { + result := AllowedEndpoint{} + assert.NoError(t, yaml.Unmarshal([]byte(test.yamele), &result)) + assert.Equal(t, test.result, result) + }) + } +} + +func TestMarshalJSONAllowedEndpoint(t *testing.T) { + testSuite := []struct { + title string + allowedEndpoint AllowedEndpoint + result string + }{ + { + title: "simple endpoint", + allowedEndpoint: AllowedEndpoint{ + EndpointPattern: common.MustNewRegexp("/api/v1/labels"), + Method: http.MethodPost, + }, + result: `{ + "endpointPattern": "/api/v1/labels", + "method": "POST" +}`, + }, + { + title: "complex endpoint patter", + allowedEndpoint: AllowedEndpoint{ + EndpointPattern: common.MustNewRegexp("^/?api/v./[a-zA-Z0-9]$"), + Method: http.MethodPost, + }, + result: `{ + "endpointPattern": "^/?api/v./[a-zA-Z0-9]$", + "method": "POST" +}`, + }, + } + for _, test := range testSuite { + t.Run(test.title, func(t *testing.T) { + data, err := json.MarshalIndent(test.allowedEndpoint, "", " ") + assert.NoError(t, err) + assert.Equal(t, test.result, string(data)) + }) + } +} + +func TestMarshalYAMLAllowedEndpoint(t *testing.T) { + testSuite := []struct { + title string + allowedEndpoint AllowedEndpoint + result string + }{ + { + title: "simple endpoint", + allowedEndpoint: AllowedEndpoint{ + EndpointPattern: common.MustNewRegexp("/api/v1/labels"), + Method: http.MethodPost, + }, + result: `endpointPattern: /api/v1/labels +method: POST +`, + }, + { + title: "complex endpoint patter", + allowedEndpoint: AllowedEndpoint{ + EndpointPattern: common.MustNewRegexp("^/?api/v./[a-zA-Z0-9]$"), + Method: http.MethodPost, + }, + result: `endpointPattern: ^/?api/v./[a-zA-Z0-9]$ +method: POST +`, + }, + } + for _, test := range testSuite { + t.Run(test.title, func(t *testing.T) { + data, err := yaml.Marshal(test.allowedEndpoint) + assert.NoError(t, err) + assert.Equal(t, test.result, string(data)) + }) + } +} diff --git a/go/datasource/proxy/proxy.go b/go/datasource/proxy/proxy.go new file mode 100644 index 0000000..30d02c9 --- /dev/null +++ b/go/datasource/proxy/proxy.go @@ -0,0 +1,20 @@ +// Copyright The Perses Authors +// 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 proxy + +// Proxy is the generic struct of the proxy definition +type Proxy[T any] struct { + Kind string `json:"kind" yaml:"kind"` + Spec T `json:"spec" yaml:"spec"` +} diff --git a/go/datasource/proxy/sql/sql.go b/go/datasource/proxy/sql/sql.go new file mode 100644 index 0000000..12db8ca --- /dev/null +++ b/go/datasource/proxy/sql/sql.go @@ -0,0 +1,209 @@ +// Copyright The Perses Authors +// 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 sql + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/perses/spec/go/common" + "github.com/perses/spec/go/datasource/proxy" +) + +// Driver the SQL driver to use +type Driver string + +const ( + DriverMySQL Driver = "mysql" + DriverMariaDB Driver = "mariadb" + DriverPostgreSQL Driver = "postgres" +) + +// SSLMode postgres ssl modes +type SSLMode string + +const ( + SSLModeDisable SSLMode = "disable" + SSLModeAllow SSLMode = "allow" + SSLModePreferable SSLMode = "prefer" + SSLModeRequire SSLMode = "require" + SSLModeVerifyFull SSLMode = "verify-full" + SSLModeVerifyCA SSLMode = "verify-ca" +) + +type MySQLConfig struct { + Params map[string]string `json:"params,omitempty" yaml:"params,omitempty"` + MaxAllowedPacket int `json:"maxAllowedPacket,omitempty" yaml:"maxAllowedPacket,omitempty"` + Timeout common.DurationString `json:"timeout,omitempty" yaml:"timeout,omitempty"` + ReadTimeout common.DurationString `json:"readTimeout,omitempty" yaml:"readTimeout,omitempty"` + WriteTimeout common.DurationString `json:"writeTimeout,omitempty" yaml:"writeTimeout,omitempty"` +} + +type PostgresConfig struct { + // MaxConns is the maximum size of the pool + MaxConns int32 `json:"maxConns,omitempty" yaml:"maxConns,omitempty"` + // ConnectTimeout the timeout value used for socket connect operations. + ConnectTimeout common.DurationString `json:"connectTimeout,omitempty" yaml:"connectTimeout,omitempty"` + // PrepareThreshold specifies the number of PreparedStatement executions that must occur before the driver begins using server-side prepared statements. + PrepareThreshold *int `json:"prepareThreshold,omitempty" yaml:"prepareThreshold,omitempty"` + // SSLMode to use when connecting to postgres + SSLMode SSLMode `json:"sslMode,omitempty" yaml:"sslMode,omitempty"` + // Options specifies command-line options to send to the server at connection start + Options string `json:"options,omitempty" yaml:"options,omitempty"` +} + +func (p *PostgresConfig) UnmarshalJSON(data []byte) error { + var tmp PostgresConfig + type plain PostgresConfig + if err := json.Unmarshal(data, (*plain)(&tmp)); err != nil { + return err + } + if err := (&tmp).validate(); err != nil { + return err + } + *p = tmp + return nil +} + +func (p *PostgresConfig) UnmarshalYAML(unmarshal func(any) error) error { + var tmp PostgresConfig + type plain PostgresConfig + if err := unmarshal((*plain)(&tmp)); err != nil { + return err + } + if err := (&tmp).validate(); err != nil { + return err + } + *p = tmp + return nil +} + +func (p *PostgresConfig) validate() error { + if p.SSLMode != "" { + switch p.SSLMode { + case SSLModeDisable, + SSLModeAllow, + SSLModePreferable, + SSLModeRequire, + SSLModeVerifyFull, + SSLModeVerifyCA: + default: + return fmt.Errorf("unknown ssl mode %s", p.SSLMode) + } + } + return nil +} + +type Config struct { + Driver Driver `json:"driver" yaml:"driver"` + // Host is the hostname required to contact the datasource + Host string `json:"host" yaml:"host"` + // Database is the database for the datasource + Database string `json:"database" yaml:"database"` + // Secret is the name of the secret that should be used for the proxy or discovery configuration + // It will contain any sensitive information such as username, password, token, certificate. + Secret string `json:"secret,omitempty" yaml:"secret,omitempty"` + // MySQL specific driver config + MySQL *MySQLConfig `json:"mysql,omitempty" yaml:"mysql,omitempty"` + // MariaDB specific driver config (uses same structure as MySQL since MariaDB is MySQL-compatible) + MariaDB *MySQLConfig `json:"mariadb,omitempty" yaml:"mariadb,omitempty"` + // Postgres specific driver config + Postgres *PostgresConfig `json:"postgres,omitempty" yaml:"postgres,omitempty"` +} + +func (s *Config) UnmarshalJSON(data []byte) error { + var tmp Config + type plain Config + if err := json.Unmarshal(data, (*plain)(&tmp)); err != nil { + return err + } + if err := (&tmp).validate(); err != nil { + return err + } + *s = tmp + return nil +} + +func (s *Config) UnmarshalYAML(unmarshal func(any) error) error { + var tmp Config + type plain Config + if err := unmarshal((*plain)(&tmp)); err != nil { + return err + } + if err := (&tmp).validate(); err != nil { + return err + } + *s = tmp + return nil +} + +func (s *Config) validate() error { + if err := s.verifySupportedDriver(); err != nil { + return err + } + + if s.Host == "" { + return errors.New("host cannot be empty") + } + + if s.Database == "" { + return errors.New("database cannot be empty") + } + + return nil +} + +func (s *Config) verifySupportedDriver() error { + if s.Driver == "" { + if s.MySQL != nil { + s.Driver = DriverMySQL + } else if s.MariaDB != nil { + s.Driver = DriverMariaDB + } else if s.Postgres != nil { + s.Driver = DriverPostgreSQL + } + } + + // Varify if the driver is still empty + if s.Driver == "" { + return errors.New("driver is required") + } + + if s.Driver != DriverMySQL && s.Driver != DriverMariaDB && s.Driver != DriverPostgreSQL { + return fmt.Errorf("driver %s is not supported", s.Driver) + } + + if s.Driver == DriverMySQL && (s.MariaDB != nil || s.Postgres != nil) { + return errors.New("driver mysql cannot be set if mariaDB or postgres config is set ") + } + + if s.Driver == DriverMariaDB && (s.MySQL != nil || s.Postgres != nil) { + return errors.New("driver mariaDB cannot be set if mysql or postgres config is set ") + } + + if s.Driver == DriverPostgreSQL && (s.MySQL != nil || s.MariaDB != nil) { + return errors.New("driver postgres cannot be set if mysql or mariaDB config is set ") + } + + return nil +} + +// Proxy is the SQL proxy definition proposed in Perses. +// In case you are defining a datasource that will work with the Perses backend, then you will need to use this definition. +type Proxy proxy.Proxy[Config] + +const ( + ProxyKindName = "sqlproxy" +) diff --git a/go/datasource/proxy/sql/sql_test.go b/go/datasource/proxy/sql/sql_test.go new file mode 100644 index 0000000..87fa2df --- /dev/null +++ b/go/datasource/proxy/sql/sql_test.go @@ -0,0 +1,570 @@ +// Copyright The Perses Authors +// 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 sql + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" +) + +func TestUnmarshalJSONConfig(t *testing.T) { + testSuite := []struct { + title string + jason string + result Config + expectErr bool + }{ + { + title: "basic postgres config", + jason: ` +{ + "driver": "postgres", + "host": "localhost:5432", + "database": "test", + "postgres": { + "sslMode": "disable" + } +} +`, + result: Config{ + Driver: DriverPostgreSQL, + Host: "localhost:5432", + Database: "test", + Postgres: &PostgresConfig{ + SSLMode: SSLModeDisable, + }, + }, + }, + { + title: "mysql config", + jason: ` +{ + "driver": "mysql", + "host": "localhost:3306", + "database": "testdb", + "secret": "mysql-secret" +} +`, + result: Config{ + Driver: DriverMySQL, + Host: "localhost:3306", + Database: "testdb", + Secret: "mysql-secret", + }, + }, + { + title: "mariadb config", + jason: ` +{ + "driver": "mariadb", + "host": "localhost:3306", + "database": "testdb", + "secret": "mariadb-secret" +} +`, + result: Config{ + Driver: DriverMariaDB, + Host: "localhost:3306", + Database: "testdb", + Secret: "mariadb-secret", + }, + }, + { + title: "mariadb config with params", + jason: ` +{ + "driver": "mariadb", + "host": "localhost:3307", + "database": "testdb", + "mariadb": { + "params": { + "charset": "utf8mb4", + "collation": "utf8mb4_unicode_ci" + }, + "maxAllowedPacket": 33554432, + "timeout": "20s", + "readTimeout": "15s", + "writeTimeout": "15s" + } +} +`, + result: Config{ + Driver: DriverMariaDB, + Host: "localhost:3307", + Database: "testdb", + MariaDB: &MySQLConfig{ + Params: map[string]string{ + "charset": "utf8mb4", + "collation": "utf8mb4_unicode_ci", + }, + MaxAllowedPacket: 33554432, + Timeout: "20s", + ReadTimeout: "15s", + WriteTimeout: "15s", + }, + }, + }, + { + title: "mysql config with params", + jason: ` +{ + "driver": "mysql", + "host": "localhost:3306", + "database": "testdb", + "mysql": { + "params": { + "charset": "utf8mb4", + "parseTime": "true" + }, + "maxAllowedPacket": 67108864, + "timeout": "20s", + "readTimeout": "15s", + "writeTimeout": "15s" + } +} +`, + result: Config{ + Driver: DriverMySQL, + Host: "localhost:3306", + Database: "testdb", + MySQL: &MySQLConfig{ + Params: map[string]string{ + "charset": "utf8mb4", + "parseTime": "true", + }, + MaxAllowedPacket: 67108864, + Timeout: "20s", + ReadTimeout: "15s", + WriteTimeout: "15s", + }, + }, + }, + { + title: "postgres config with all options", + jason: ` +{ + "driver": "postgres", + "host": "localhost:5432", + "database": "test", + "postgres": { + "maxConns": 50, + "connectTimeout": "5m", + "prepareThreshold": 5, + "sslMode": "require", + "options": "-c search_path=myschema" + } +} +`, + result: Config{ + Driver: DriverPostgreSQL, + Host: "localhost:5432", + Database: "test", + Postgres: &PostgresConfig{ + MaxConns: 50, + ConnectTimeout: "5m", + PrepareThreshold: func() *int { i := 5; return &i }(), + SSLMode: SSLModeRequire, + Options: "-c search_path=myschema", + }, + }, + }, + { + title: "invalid SSL mode", + jason: ` +{ + "driver": "postgres", + "host": "localhost:5432", + "database": "test", + "postgres": { + "sslMode": "notreal" + } +} +`, + expectErr: true, + }, + { + title: "missing driver", + jason: ` +{ + "host": "localhost:5432", + "database": "test" +} +`, + expectErr: true, + }, + { + title: "missing host", + jason: ` +{ + "driver": "postgres", + "database": "test" +} +`, + expectErr: true, + }, + { + title: "missing database", + jason: ` +{ + "driver": "postgres", + "host": "localhost:5432" +} +`, + expectErr: true, + }, + { + title: "unsupported driver", + jason: ` +{ + "driver": "oracle", + "host": "localhost:1521", + "database": "test" +} +`, + expectErr: true, + }, + } + for _, test := range testSuite { + t.Run(test.title, func(t *testing.T) { + result := Config{} + err := json.Unmarshal([]byte(test.jason), &result) + if test.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, test.result, result) + } + }) + } +} + +func TestUnmarshalYAMLConfig(t *testing.T) { + testSuite := []struct { + title string + yamele string + result Config + expectErr bool + }{ + { + title: "basic postgres config", + yamele: ` +driver: postgres +host: localhost:5432 +database: test +postgres: + sslMode: disable +`, + result: Config{ + Driver: DriverPostgreSQL, + Host: "localhost:5432", + Database: "test", + Postgres: &PostgresConfig{ + SSLMode: SSLModeDisable, + }, + }, + }, + { + title: "mysql config", + yamele: ` +driver: mysql +host: localhost:3306 +database: testdb +secret: mysql-secret +`, + result: Config{ + Driver: DriverMySQL, + Host: "localhost:3306", + Database: "testdb", + Secret: "mysql-secret", + }, + }, + { + title: "mariadb config", + yamele: ` +driver: mariadb +host: localhost:3306 +database: testdb +secret: mariadb-secret +`, + result: Config{ + Driver: DriverMariaDB, + Host: "localhost:3306", + Database: "testdb", + Secret: "mariadb-secret", + }, + }, + { + title: "mariadb config with params", + yamele: ` +driver: mariadb +host: localhost:3307 +database: testdb +mariadb: + params: + charset: utf8mb4 + collation: utf8mb4_unicode_ci + maxAllowedPacket: 33554432 + timeout: 20s + readTimeout: 15s + writeTimeout: 15s +`, + result: Config{ + Driver: DriverMariaDB, + Host: "localhost:3307", + Database: "testdb", + MariaDB: &MySQLConfig{ + Params: map[string]string{ + "charset": "utf8mb4", + "collation": "utf8mb4_unicode_ci", + }, + MaxAllowedPacket: 33554432, + Timeout: "20s", + ReadTimeout: "15s", + WriteTimeout: "15s", + }, + }, + }, + { + title: "mysql config with params", + yamele: ` +driver: mysql +host: localhost:3306 +database: testdb +mysql: + params: + charset: utf8mb4 + parseTime: "true" + maxAllowedPacket: 67108864 + timeout: 30s + readTimeout: 10s + writeTimeout: 10s +`, + result: Config{ + Driver: DriverMySQL, + Host: "localhost:3306", + Database: "testdb", + MySQL: &MySQLConfig{ + Params: map[string]string{ + "charset": "utf8mb4", + "parseTime": "true", + }, + MaxAllowedPacket: 67108864, + Timeout: "30s", + ReadTimeout: "10s", + WriteTimeout: "10s", + }, + }, + }, + { + title: "postgres config with all SSL modes", + yamele: ` +driver: postgres +host: localhost:5432 +database: test +postgres: + sslMode: verify-full +`, + result: Config{ + Driver: DriverPostgreSQL, + Host: "localhost:5432", + Database: "test", + Postgres: &PostgresConfig{ + SSLMode: SSLModeVerifyFull, + }, + }, + }, + { + title: "invalid SSL mode in yaml", + yamele: ` +driver: postgres +host: localhost:5432 +database: test +postgres: + sslMode: invalid +`, + expectErr: true, + }, + { + title: "missing driver in yaml", + yamele: ` +host: localhost:5432 +database: test +`, + expectErr: true, + }, + } + for _, test := range testSuite { + t.Run(test.title, func(t *testing.T) { + result := Config{} + err := yaml.Unmarshal([]byte(test.yamele), &result) + if test.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, test.result, result) + } + }) + } +} + +func TestValidateConfig(t *testing.T) { + testSuite := []struct { + name string + config Config + expectErrMsg string + }{ + { + name: "valid mysql config", + config: Config{ + Driver: DriverMySQL, + Host: "localhost:3306", + Database: "test", + }, + }, + { + name: "valid mariadb config", + config: Config{ + Driver: DriverMariaDB, + Host: "localhost:3306", + Database: "test", + }, + }, + { + name: "valid postgres config", + config: Config{ + Driver: DriverPostgreSQL, + Host: "localhost:5432", + Database: "test", + }, + }, + { + name: "missing driver", + config: Config{ + Host: "localhost:5432", + Database: "test", + }, + expectErrMsg: "driver is required", + }, + { + name: "unsupported driver", + config: Config{ + Driver: "mssql", + Host: "localhost:1433", + Database: "test", + }, + expectErrMsg: "not supported", + }, + { + name: "missing host", + config: Config{ + Driver: DriverMySQL, + Database: "test", + }, + expectErrMsg: "host cannot be empty", + }, + { + name: "missing database", + config: Config{ + Driver: DriverMySQL, + Host: "localhost:3306", + }, + expectErrMsg: "database cannot be empty", + }, + } + + for _, test := range testSuite { + t.Run(test.name, func(t *testing.T) { + err := test.config.validate() + if len(test.expectErrMsg) > 0 { + assert.Error(t, err) + assert.Contains(t, err.Error(), test.expectErrMsg) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestPostgresConfigValidation(t *testing.T) { + testSuite := []struct { + name string + config PostgresConfig + expectErrMsg string + }{ + { + name: "valid disable ssl mode", + config: PostgresConfig{ + SSLMode: SSLModeDisable, + }, + }, + { + name: "valid allow ssl mode", + config: PostgresConfig{ + SSLMode: SSLModeAllow, + }, + }, + { + name: "valid prefer ssl mode", + config: PostgresConfig{ + SSLMode: SSLModePreferable, + }, + }, + { + name: "valid require ssl mode", + config: PostgresConfig{ + SSLMode: SSLModeRequire, + }, + }, + { + name: "valid verify-full ssl mode", + config: PostgresConfig{ + SSLMode: SSLModeVerifyFull, + }, + }, + { + name: "valid verify-ca ssl mode", + config: PostgresConfig{ + SSLMode: SSLModeVerifyCA, + }, + }, + { + name: "invalid ssl mode", + config: PostgresConfig{ + SSLMode: "invalid-mode", + }, + expectErrMsg: "unknown ssl mode", + }, + { + name: "empty ssl mode is valid", + config: PostgresConfig{ + SSLMode: "", + }, + }, + } + + for _, test := range testSuite { + t.Run(test.name, func(t *testing.T) { + err := test.config.validate() + if len(test.expectErrMsg) > 0 { + assert.Error(t, err) + assert.Contains(t, err.Error(), test.expectErrMsg) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/ts/src/datasource/proxy/http.ts b/ts/src/datasource/proxy/http.ts new file mode 100644 index 0000000..79c6778 --- /dev/null +++ b/ts/src/datasource/proxy/http.ts @@ -0,0 +1,35 @@ +// Copyright The Perses Authors +// 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. + +export interface HTTPProxy { + kind: 'HTTPProxy'; + spec: HTTPProxySpec; +} +export interface HTTPProxySpec { + // url is the url of the datasource. It is not the url of the proxy. + // The Perses server is the proxy, so it needs to know where to redirect the request. + url: string; + // allowedEndpoints is a list of tuples of http methods and http endpoints that will be accessible. + // Leave it empty if you don't want to restrict the access to the datasource. + allowedEndpoints?: HTTPAllowedEndpoint[]; + // headers can be used to provide additional headers that need to be forwarded when requesting the datasource + headers?: Record; + // secret is the name of the secret that should be used for the proxy or discovery configuration + // It will contain any sensitive information such as password, token, certificate. + secret?: string; +} + +export interface HTTPAllowedEndpoint { + endpointPattern: string; + method: string; +} diff --git a/ts/src/datasource/proxy/sql.ts b/ts/src/datasource/proxy/sql.ts new file mode 100644 index 0000000..6ee5599 --- /dev/null +++ b/ts/src/datasource/proxy/sql.ts @@ -0,0 +1,54 @@ +// Copyright The Perses Authors +// 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. + +import { DurationString } from '@perses-dev/spec'; + +export interface SQLProxy { + kind: 'sqlproxy'; + spec: SQLProxySpec; +} + +export interface SQLProxySpec { + driver: 'mysql' | 'mariadb' | 'postgres'; + // host is the hostname required to contact the datasource + host: string; + // database is the database for the datasource + database: string; + // secret is the name of the secret that should be used for the proxy or discovery configuration + // It will contain any sensitive information such as username, password, token, certificate. + secret?: string; + // MySQL specific driver config + mysql?: MySQLConfig; + // MariaDB specific driver config (uses same structure as MySQL since MariaDB is MySQL-compatible) + mariadb?: MySQLConfig; + // Postgres specific driver config + postgres?: PostgresConfig; +} + +export interface MySQLConfig { + params?: Record; + maxAllowedPacket?: number; + timeout?: DurationString; + readTimeout?: DurationString; + writeTimeout?: DurationString; +} + +export interface PostgresConfig { + // maxConns is the maximum size of the pool + maxConns?: number; + // connectTimeout the timeout value used for socket connect operations. + connectTimeout?: DurationString; + // PrepareThreshold specifies the number of PreparedStatement executions that must occur before the driver begins using server-side prepared statements. + prepareThreshold?: DurationString; + sslMode?: 'disable' | 'allow' | 'prefer' | 'require' | 'verify-full' | 'verify-ca'; +} From fbf35a671b3550e9e7ebc89caa178d19c373407c Mon Sep 17 00:00:00 2001 From: Augustin Husson Date: Thu, 21 May 2026 10:06:22 +0200 Subject: [PATCH 2/2] fix review Signed-off-by: Augustin Husson --- cue/datasource/proxy/sql/sql_patch.cue | 4 ++-- go/datasource/proxy/sql/sql.go | 2 +- ts/src/datasource/index.ts | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cue/datasource/proxy/sql/sql_patch.cue b/cue/datasource/proxy/sql/sql_patch.cue index d050ead..29a3ea1 100644 --- a/cue/datasource/proxy/sql/sql_patch.cue +++ b/cue/datasource/proxy/sql/sql_patch.cue @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// NB: This file complements the http_go_gen.cue file generated by +// NB: This file complements the sql_go_gen.cue file generated by // `cue get go` to add the missing constraints lost in the translation // process. This should no longer be needed at some point hopefully, but for // the moment, because of a technical limitation in the CUE translation @@ -40,7 +40,7 @@ import ( } #Config: { - driver: "mysql" | "postgres" + driver: "mysql" | "postgres" | "mariadb" // host is the hostname and port of the datasource. It is not the hostname of the proxy. // The Perses server is the proxy, so it needs to know where to redirect the request. host: string diff --git a/go/datasource/proxy/sql/sql.go b/go/datasource/proxy/sql/sql.go index 12db8ca..4bf7b7c 100644 --- a/go/datasource/proxy/sql/sql.go +++ b/go/datasource/proxy/sql/sql.go @@ -176,7 +176,7 @@ func (s *Config) verifySupportedDriver() error { } } - // Varify if the driver is still empty + // Verify if the driver is still empty if s.Driver == "" { return errors.New("driver is required") } diff --git a/ts/src/datasource/index.ts b/ts/src/datasource/index.ts index d6874e8..7d8c8d5 100644 --- a/ts/src/datasource/index.ts +++ b/ts/src/datasource/index.ts @@ -12,3 +12,5 @@ // limitations under the License. export * from './datasource'; +export * from './proxy/http'; +export * from './proxy/sql';