Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release v2023.08.15.1 [skip pd_pr] #1572

Merged
merged 9 commits into from
Aug 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 56 additions & 3 deletions cmd/tidb-dashboard/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package main
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"net/http"
Expand Down Expand Up @@ -66,16 +67,19 @@ func NewCLIConfig() *DashboardCLIConfig {
flag.BoolVar(&cfg.CoreConfig.EnableTelemetry, "telemetry", cfg.CoreConfig.EnableTelemetry, "allow telemetry")
flag.BoolVar(&cfg.CoreConfig.EnableExperimental, "experimental", cfg.CoreConfig.EnableExperimental, "allow experimental features")
flag.StringVar(&cfg.CoreConfig.FeatureVersion, "feature-version", cfg.CoreConfig.FeatureVersion, "target TiDB version for standalone mode")
flag.IntVar(&cfg.CoreConfig.NgmTimeout, "ngm-timeout", cfg.CoreConfig.NgmTimeout, "timeout secs for accessing the ngm API")

showVersion := flag.BoolP("version", "v", false, "print version information and exit")

clusterCaPath := flag.String("cluster-ca", "", "(TLS between components of the TiDB cluster) path of file that contains list of trusted SSL CAs")
clusterCertPath := flag.String("cluster-cert", "", "(TLS between components of the TiDB cluster) path of file that contains X509 certificate in PEM format")
clusterKeyPath := flag.String("cluster-key", "", "(TLS between components of the TiDB cluster) path of file that contains X509 key in PEM format")
clusterAllowedNames := flag.String("cluster-allowed-names", "", "comma-delimited list of acceptable peer certificate SAN identities")

tidbCaPath := flag.String("tidb-ca", "", "(TLS for MySQL client) path of file that contains list of trusted SSL CAs")
tidbCertPath := flag.String("tidb-cert", "", "(TLS for MySQL client) path of file that contains X509 certificate in PEM format")
tidbKeyPath := flag.String("tidb-key", "", "(TLS for MySQL client) path of file that contains X509 key in PEM format")
tidbAllowedNames := flag.String("tidb-allowed-names", "", "comma-delimited list of acceptable peer certificate SAN identities")

// debug for keyvisual,hide help information
flag.Int64Var(&cfg.KVFileStartTime, "keyviz-file-start", 0, "(debug) start time for file range in file mode")
Expand All @@ -94,13 +98,13 @@ func NewCLIConfig() *DashboardCLIConfig {

// setup TLS config for TiDB components
if len(*clusterCaPath) != 0 && len(*clusterCertPath) != 0 && len(*clusterKeyPath) != 0 {
cfg.CoreConfig.ClusterTLSConfig = buildTLSConfig(clusterCaPath, clusterKeyPath, clusterCertPath)
cfg.CoreConfig.ClusterTLSConfig = buildTLSConfig(clusterCaPath, clusterKeyPath, clusterCertPath, clusterAllowedNames)
}

// setup TLS config for MySQL client
// See https://github.com/pingcap/docs/blob/7a62321b3ce9318cbda8697503c920b2a01aeb3d/how-to/secure/enable-tls-clients.md#enable-authentication
if (len(*tidbCertPath) != 0 && len(*tidbKeyPath) != 0) || len(*tidbCaPath) != 0 {
cfg.CoreConfig.TiDBTLSConfig = buildTLSConfig(tidbCaPath, tidbKeyPath, tidbCertPath)
cfg.CoreConfig.TiDBTLSConfig = buildTLSConfig(tidbCaPath, tidbKeyPath, tidbCertPath, tidbAllowedNames)
}

if err := cfg.CoreConfig.NormalizePDEndPoint(); err != nil {
Expand Down Expand Up @@ -135,16 +139,65 @@ func getContext() context.Context {
return ctx
}

func buildTLSConfig(caPath, keyPath, certPath *string) *tls.Config {
func buildTLSConfig(caPath, keyPath, certPath, allowedNames *string) *tls.Config {
tlsInfo := transport.TLSInfo{
TrustedCAFile: *caPath,
KeyFile: *keyPath,
CertFile: *certPath,
}

tlsConfig, err := tlsInfo.ClientConfig()
if err != nil {
log.Fatal("Failed to load certificates", zap.Error(err))
}

// Disable the default server verification routine in favor of a manually defined connection
// verification callback. The custom verification process verifies that the server
// certificate is issued by a trusted root CA, and that the peer certificate identities
// matches at least one entry specified in verifyNames (if specified). This is required
// because tidb-dashboard directs requests to a loopback-bound forwarding proxy, which would
// otherwise cause server hostname verification to fail.
tlsConfig.InsecureSkipVerify = true
tlsConfig.VerifyConnection = func(state tls.ConnectionState) error {
opts := x509.VerifyOptions{
Intermediates: x509.NewCertPool(),
Roots: tlsConfig.RootCAs,
}

for _, cert := range state.PeerCertificates[1:] {
opts.Intermediates.AddCert(cert)
}

_, err := state.PeerCertificates[0].Verify(opts)

// Optionally verify the peer SANs when available. If no peer identities are
// provided, simply reuse the verification result of the CA verification.
if err != nil || *allowedNames == "" {
return err
}

for _, name := range strings.Split(*allowedNames, ",") {
for _, dns := range state.PeerCertificates[0].DNSNames {
if name == dns {
return nil
}
}

for _, uri := range state.PeerCertificates[0].URIs {
if name == uri.String() {
return nil
}
}
}

return fmt.Errorf(
"no SANs in server certificate (%v, %v) match allowed names %v",
state.PeerCertificates[0].DNSNames,
state.PeerCertificates[0].URIs,
strings.Split(*allowedNames, ","),
)
}

return tlsConfig
}

Expand Down
1 change: 1 addition & 0 deletions pkg/apiserver/debugapi/endpoint/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ import (
var (
ErrNS = errorx.NewNamespace("debug_api.endpoint")
ErrUnknownComponent = ErrNS.NewType("unknown_component")
ErrInvalidEndpoint = ErrNS.NewType("invalid_endpoint")
)
82 changes: 79 additions & 3 deletions pkg/apiserver/debugapi/endpoint/payload.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@
package endpoint

import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"regexp"

"go.etcd.io/etcd/clientv3"

"github.com/pingcap/tidb-dashboard/pkg/pd"
"github.com/pingcap/tidb-dashboard/pkg/utils/topology"
"github.com/pingcap/tidb-dashboard/util/client/httpclient"
"github.com/pingcap/tidb-dashboard/util/client/pdclient"
"github.com/pingcap/tidb-dashboard/util/client/tidbclient"
Expand Down Expand Up @@ -110,8 +115,6 @@ func (r *RequestPayloadResolver) ResolvePayload(payload RequestPayload) (*Resolv
return nil, rest.ErrBadRequest.New("Unknown API endpoint '%s'", payload.API)
}

// TODO: Verify host and port

resolvedPayload := &ResolvedRequestPayload{
api: api,
host: payload.Host,
Expand Down Expand Up @@ -171,7 +174,18 @@ type ResolvedRequestPayload struct {
queryValues url.Values
}

func (p *ResolvedRequestPayload) SendRequestAndPipe(clientsToUse HTTPClients, w io.Writer) (respNoBody *http.Response, err error) {
func (p *ResolvedRequestPayload) SendRequestAndPipe(
ctx context.Context,
clientsToUse HTTPClients,
etcdClient *clientv3.Client,
pdClient *pd.Client,
w io.Writer,
) (respNoBody *http.Response, err error) {
if etcdClient != nil && pdClient != nil { // It can only be false in tests.
if err := p.verifyEndpoint(ctx, etcdClient, pdClient); err != nil {
return nil, err
}
}
httpClient := clientsToUse.GetHTTPClientByNodeKind(p.api.Component)
if httpClient == nil {
return nil, ErrUnknownComponent.New("Unknown component '%s'", p.api.Component)
Expand All @@ -189,3 +203,65 @@ func (p *ResolvedRequestPayload) SendRequestAndPipe(clientsToUse HTTPClients, w
_, respNoBody, err = resp.PipeBody(w)
return
}

func (p *ResolvedRequestPayload) verifyEndpoint(ctx context.Context, etcdClient *clientv3.Client, pdClient *pd.Client) error {
switch p.api.Component {
case topo.KindTiDB:
infos, err := topology.FetchTiDBTopology(ctx, etcdClient)
if err != nil {
return ErrInvalidEndpoint.Wrap(err, "failed to fetch tidb topology")
}
matched := false
for _, info := range infos {
if info.IP == p.host && info.StatusPort == uint(p.port) {
matched = true
break
}
}
if !matched {
return ErrInvalidEndpoint.New("invalid endpoint '%s:%d'", p.host, p.port)
}
case topo.KindTiKV, topo.KindTiFlash:
tikvInfos, tiflashInfos, err := topology.FetchStoreTopology(pdClient)
if err != nil {
return ErrInvalidEndpoint.Wrap(err, "failed to fetch store topology")
}
matched := false
if p.api.Component == topo.KindTiKV {
for _, info := range tikvInfos {
if info.IP == p.host && info.StatusPort == uint(p.port) {
matched = true
break
}
}
} else {
for _, info := range tiflashInfos {
if info.IP == p.host && info.StatusPort == uint(p.port) {
matched = true
break
}
}
}
if !matched {
return ErrInvalidEndpoint.New("invalid endpoint '%s:%d'", p.host, p.port)
}
case topo.KindPD:
infos, err := topology.FetchPDTopology(pdClient)
if err != nil {
return ErrInvalidEndpoint.Wrap(err, "failed to fetch pd topology")
}
matched := false
for _, info := range infos {
if info.IP == p.host && info.Port == uint(p.port) {
matched = true
break
}
}
if !matched {
return ErrInvalidEndpoint.New("invalid endpoint '%s:%d'", p.host, p.port)
}
default:
return ErrUnknownComponent.New("Unknown component '%s'", p.api.Component)
}
return nil
}
3 changes: 2 additions & 1 deletion pkg/apiserver/debugapi/endpoint/payload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package endpoint

import (
"bytes"
"context"
"fmt"
"net"
"net/http"
Expand Down Expand Up @@ -287,7 +288,7 @@ func TestResolvedRequestPayload(t *testing.T) {
}

buf := bytes.Buffer{}
_, err := rp.SendRequestAndPipe(clients, &buf)
_, err := rp.SendRequestAndPipe(context.Background(), clients, nil, nil, &buf)

assert.Nil(t, err)
assert.Equal(t, "/abc\nhello\n", buf.String())
Expand Down
10 changes: 9 additions & 1 deletion pkg/apiserver/debugapi/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import (
"time"

"github.com/gin-gonic/gin"
"go.etcd.io/etcd/clientv3"
"go.uber.org/fx"

"github.com/pingcap/tidb-dashboard/pkg/apiserver/debugapi/endpoint"
"github.com/pingcap/tidb-dashboard/pkg/apiserver/user"
"github.com/pingcap/tidb-dashboard/pkg/pd"
"github.com/pingcap/tidb-dashboard/util/client/pdclient"
"github.com/pingcap/tidb-dashboard/util/client/tidbclient"
"github.com/pingcap/tidb-dashboard/util/client/tiflashclient"
Expand All @@ -37,10 +39,14 @@ type ServiceParams struct {
TiDBStatusClient *tidbclient.StatusClient
TiKVStatusClient *tikvclient.StatusClient
TiFlashStatusClient *tiflashclient.StatusClient
EtcdClient *clientv3.Client
PDClient *pd.Client
}

type Service struct {
httpClients endpoint.HTTPClients
etcdClient *clientv3.Client
pdClient *pd.Client
resolver *endpoint.RequestPayloadResolver
fSwap *fileswap.Handler
}
Expand All @@ -54,6 +60,8 @@ func newService(p ServiceParams) *Service {
}
return &Service{
httpClients: httpClients,
etcdClient: p.EtcdClient,
pdClient: p.PDClient,
resolver: endpoint.NewRequestPayloadResolver(apiEndpoints, httpClients),
fSwap: fileswap.New(),
}
Expand Down Expand Up @@ -110,7 +118,7 @@ func (s *Service) RequestEndpoint(c *gin.Context) {
_ = writer.Close()
}()

resp, err := resolved.SendRequestAndPipe(s.httpClients, writer)
resp, err := resolved.SendRequestAndPipe(c.Request.Context(), s.httpClients, s.etcdClient, s.pdClient, writer)
if err != nil {
rest.Error(c, err)
return
Expand Down
2 changes: 1 addition & 1 deletion pkg/apiserver/metrics/prom_resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func normalizeCustomizedPromAddress(addr string) (string, error) {
return "", fmt.Errorf("invalid Prometheus address format")
}
// Normalize the address, remove unnecessary parts.
addr = fmt.Sprintf("%s://%s", u.Scheme, u.Host)
addr = fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, strings.TrimSuffix(u.Path, "/"))
return addr, nil
}

Expand Down
24 changes: 24 additions & 0 deletions pkg/apiserver/metrics/prom_resolve_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2023 PingCAP, Inc. Licensed under Apache-2.0.

package metrics

import (
"testing"

"github.com/stretchr/testify/require"
)

// https://github.com/pingcap/tidb-dashboard/issues/1560
func Test_normalizeCustomizedPromAddress(t *testing.T) {
addr, err := normalizeCustomizedPromAddress("http://infra-tidb-monitoring-shadow2-prod-0a01da41:9090")
require.NoError(t, err)
require.Equal(t, "http://infra-tidb-monitoring-shadow2-prod-0a01da41:9090", addr)

addr, err = normalizeCustomizedPromAddress("http://infra-tidb-monitoring-shadow2-prod-0a01da41:9090/")
require.NoError(t, err)
require.Equal(t, "http://infra-tidb-monitoring-shadow2-prod-0a01da41:9090", addr)

addr, err = normalizeCustomizedPromAddress("http://infra-tidb-monitoring-shadow2-prod-0a01da41:9090/_/tsdb/")
require.NoError(t, err)
require.Equal(t, "http://infra-tidb-monitoring-shadow2-prod-0a01da41:9090/_/tsdb", addr)
}
37 changes: 20 additions & 17 deletions pkg/apiserver/statement/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,27 +44,30 @@ func newService(p ServiceParams, ff *featureflag.Registry) *Service {

func registerRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
endpoint := r.Group("/statements")
endpoint.Use(auth.MWAuthRequired())
endpoint.Use(utils.MWConnectTiDB(s.params.TiDBClient))
{
endpoint.GET("/download", s.downloadHandler)
endpoint.POST("/download/token", s.downloadTokenHandler)

endpoint.GET("/config", s.configHandler)
endpoint.POST("/config", auth.MWRequireWritePriv(), s.modifyConfigHandler)
endpoint.GET("/stmt_types", s.stmtTypesHandler)
endpoint.GET("/list", s.listHandler)
endpoint.GET("/plans", s.plansHandler)
endpoint.GET("/plan/detail", s.planDetailHandler)

endpoint.GET("/available_fields", s.getAvailableFields)

binding := endpoint.Group("/plan/binding")
binding.Use(s.planBindingFeatureFlag.VersionGuard())
endpoint.Use(auth.MWAuthRequired())
endpoint.Use(utils.MWConnectTiDB(s.params.TiDBClient))
{
binding.GET("", s.getPlanBindingHandler)
binding.POST("", s.createPlanBindingHandler)
binding.DELETE("", s.dropPlanBindingHandler)
endpoint.POST("/download/token", s.downloadTokenHandler)

endpoint.GET("/config", s.configHandler)
endpoint.POST("/config", auth.MWRequireWritePriv(), s.modifyConfigHandler)
endpoint.GET("/stmt_types", s.stmtTypesHandler)
endpoint.GET("/list", s.listHandler)
endpoint.GET("/plans", s.plansHandler)
endpoint.GET("/plan/detail", s.planDetailHandler)

endpoint.GET("/available_fields", s.getAvailableFields)

binding := endpoint.Group("/plan/binding")
binding.Use(s.planBindingFeatureFlag.VersionGuard())
{
binding.GET("", s.getPlanBindingHandler)
binding.POST("", s.createPlanBindingHandler)
binding.DELETE("", s.dropPlanBindingHandler)
}
}
}
}
Expand Down
Loading
Loading