diff --git a/managed/cmd/pmm-managed/main.go b/managed/cmd/pmm-managed/main.go index d2749a36d7..824b77bcee 100644 --- a/managed/cmd/pmm-managed/main.go +++ b/managed/cmd/pmm-managed/main.go @@ -729,7 +729,6 @@ func main() { //nolint:gocognit,maintidx,cyclop clickhouseAddrF := kingpin.Flag("clickhouse-addr", "Clickhouse database address").Default("127.0.0.1:9000").Envar("PMM_CLICKHOUSE_ADDR").String() clickhouseUsernameF := kingpin.Flag("clickhouse-username", "Clickhouse database user").Default("default").Envar("PMM_CLICKHOUSE_USER").String() clickhousePasswordF := kingpin.Flag("clickhouse-password", "Clickhouse database user password").Default("clickhouse").Envar("PMM_CLICKHOUSE_PASSWORD").String() - watchtowerHostF := kingpin.Flag("watchtower-host", "Watchtower host").Default("http://watchtower:8080").Envar("PMM_WATCHTOWER_HOST").URL() // Nomad garbage collection flags @@ -824,15 +823,18 @@ func main() { //nolint:gocognit,maintidx,cyclop grafanadb.DSN.DB = "grafana" grafanadb.DSN.Params = q.Encode() - chURI := url.URL{ - Scheme: "tcp", - User: url.UserPassword(*clickhouseUsernameF, *clickhousePasswordF), - Host: *clickhouseAddrF, - Path: *clickHouseDatabaseF, + chParams, err := models.NewClickHouseParams( + *clickhouseAddrF, + *clickHouseDatabaseF, + *clickhouseUsernameF, + *clickhousePasswordF, + ) + if err != nil { + l.Panicf("cannot load clickhouse params: %+v", err) } qanDB := ds.QanDBSelect - qanDB.DSN = chURI.String() + qanDB.DSN = chParams.URL().String() ds.VM.Address = *victoriaMetricsURLF @@ -882,7 +884,7 @@ func main() { //nolint:gocognit,maintidx,cyclop cleaner := clean.New(db) externalRules := vmalert.NewExternalRules() - vmdb, err := victoriametrics.NewVictoriaMetrics(*victoriaMetricsConfigF, db, vmParams, haService) + vmdb, err := victoriametrics.NewVictoriaMetrics(*victoriaMetricsConfigF, db, vmParams, chParams, haService) if err != nil { l.Panicf("VictoriaMetrics service problem: %+v", err) } @@ -1022,7 +1024,7 @@ func main() { //nolint:gocognit,maintidx,cyclop versionCache := versioncache.New(db, versioner) dumpService := dump.New(db, &dump.URLs{ - ClickhouseURL: chURI.String(), + ClickhouseURL: chParams.URL().String(), VMURL: *victoriaMetricsURLF, }) diff --git a/managed/models/agent_model.go b/managed/models/agent_model.go index acdb3dd839..3ac53c06b4 100644 --- a/managed/models/agent_model.go +++ b/managed/models/agent_model.go @@ -843,7 +843,7 @@ func (a *Agent) ExporterURL(q *reform.Querier) (string, error) { username := pointer.GetString(a.Username) password := pointer.GetString(a.Password) - host := "127.0.0.1" + host := localhost if !a.ExporterOptions.PushMetrics { node, err := FindNodeByID(q, *a.RunsOnNodeID) if err != nil { diff --git a/managed/models/clickhouse_params.go b/managed/models/clickhouse_params.go new file mode 100644 index 0000000000..041f040fef --- /dev/null +++ b/managed/models/clickhouse_params.go @@ -0,0 +1,72 @@ +// Copyright (C) 2023 Percona LLC +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package models + +import ( + "fmt" + "net" + "net/url" + "strconv" +) + +// ClickHouseParams represents ClickHouse server params. +type ClickHouseParams struct { + url *url.URL +} + +// ExternalClickHouse returns true if ClickHouse is configured externally. +func (p *ClickHouseParams) ExternalClickHouse() bool { + return !internalAddr(p.url.Hostname()) +} + +// URL returns the ClickHouse URL. +func (p *ClickHouseParams) URL() *url.URL { + u := *p.url + return &u +} + +// NewClickHouseParams returns validated ClickHouse configuration params, +// or an error if any required field is missing or malformed. +func NewClickHouseParams(addr, dbName, dbUsername, dbPassword string) (*ClickHouseParams, error) { + if addr == "" { + return nil, fmt.Errorf("addr is required") + } + host, port, err := net.SplitHostPort(addr) + if err != nil { + return nil, fmt.Errorf("invalid addr %q: %w", addr, err) + } + if host == "" { + return nil, fmt.Errorf("invalid addr %q: empty host", addr) + } + if _, err := strconv.ParseUint(port, 10, 16); err != nil { + return nil, fmt.Errorf("invalid port in addr %q: %w", addr, err) + } + if dbName == "" { + return nil, fmt.Errorf("database name is required") + } + if dbUsername == "" { + return nil, fmt.Errorf("username is required") + } + + return &ClickHouseParams{ + url: &url.URL{ + Scheme: "tcp", + User: url.UserPassword(dbUsername, dbPassword), + Host: addr, + Path: dbName, + }, + }, nil +} diff --git a/managed/models/clickhouse_params_test.go b/managed/models/clickhouse_params_test.go new file mode 100644 index 0000000000..4b133ba057 --- /dev/null +++ b/managed/models/clickhouse_params_test.go @@ -0,0 +1,94 @@ +// Copyright (C) 2023 Percona LLC +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package models + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewClickHouseParams(t *testing.T) { + t.Run("valid", func(t *testing.T) { + p, err := NewClickHouseParams("127.0.0.1:9000", "pmm", "default", "clickhouse") + require.NoError(t, err) + assert.Equal(t, "tcp://default:clickhouse@127.0.0.1:9000/pmm", p.URL().String()) + }) + + t.Run("valid empty password", func(t *testing.T) { + _, err := NewClickHouseParams("127.0.0.1:9000", "pmm", "default", "") + require.NoError(t, err) + }) + + errCases := []struct { + name string + addr string + dbName string + dbUsername string + dbPassword string + wantErrSub string + }{ + {"empty addr", "", "pmm", "default", "clickhouse", "addr is required"}, + {"missing port", "127.0.0.1", "pmm", "default", "clickhouse", "invalid addr"}, + {"empty host", ":9000", "pmm", "default", "clickhouse", "empty host"}, + {"non numeric port", "localhost:abc", "pmm", "default", "clickhouse", "invalid port"}, + {"port out of range", "localhost:99999", "pmm", "default", "clickhouse", "invalid port"}, + {"empty db name", "127.0.0.1:9000", "", "default", "clickhouse", "database name is required"}, + {"empty username", "127.0.0.1:9000", "pmm", "", "clickhouse", "username is required"}, + } + for _, tc := range errCases { + t.Run(tc.name, func(t *testing.T) { + _, err := NewClickHouseParams(tc.addr, tc.dbName, tc.dbUsername, tc.dbPassword) + require.Error(t, err) + assert.ErrorContains(t, err, tc.wantErrSub) + }) + } +} + +func TestCHParamsExternalClickHouse(t *testing.T) { + cases := []struct { + name string + addr string + want bool + }{ + {"loopback", "127.0.0.1:9000", false}, + {"localhost", "localhost:9000", false}, + {"external host", "ch-01.test.net:9000", true}, + {"wildcard", "0.0.0.0:9000", true}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + p, err := NewClickHouseParams(tc.addr, "pmm", "default", "clickhouse") + require.NoError(t, err) + assert.Equal(t, tc.want, p.ExternalClickHouse()) + }) + } +} + +func TestCHParamsURL(t *testing.T) { + t.Run("simple", func(t *testing.T) { + p, err := NewClickHouseParams("127.0.0.1:9000", "pmm", "default", "clickhouse") + require.NoError(t, err) + assert.Equal(t, "tcp://default:clickhouse@127.0.0.1:9000/pmm", p.URL().String()) + }) + + t.Run("password with special chars", func(t *testing.T) { + p, err := NewClickHouseParams("127.0.0.1:9000", "pmm", "default", "p@ss/word") + require.NoError(t, err) + assert.Equal(t, "tcp://default:p%40ss%2Fword@127.0.0.1:9000/pmm", p.URL().String()) + }) +} diff --git a/managed/models/common_helpers.go b/managed/models/common_helpers.go index 0dd59dabb6..8078815d3b 100644 --- a/managed/models/common_helpers.go +++ b/managed/models/common_helpers.go @@ -32,3 +32,13 @@ func NewInvalidArgumentError(format string, a ...any) *InvalidArgumentError { func (e *InvalidArgumentError) Error() string { return "invalid argument: " + e.Details } + +// localhost is the IPv4 loopback address used by PMM Server's +// co-located services. +const localhost = "127.0.0.1" + +// internalAddr reports whether host refers to PMM's built-in, +// co-located services. +func internalAddr(host string) bool { + return host == localhost || host == "localhost" +} diff --git a/managed/models/database.go b/managed/models/database.go index 60a5547bb3..f94177a1a5 100644 --- a/managed/models/database.go +++ b/managed/models/database.go @@ -1569,7 +1569,7 @@ func setupPMMServerHAAgents(q *reform.Querier, params SetupDBParams) error { node, err := createNodeWithID(q, nodeID, GenericNodeType, &CreateNodeParams{ NodeName: params.HANodeID, - Address: "127.0.0.1", + Address: localhost, CustomLabels: labels, IsPMMServerNode: true, }) @@ -1600,7 +1600,7 @@ func setupPMMServerAgents(q *reform.Querier, params SetupDBParams) error { // create PMM Server Node and associated Agents node, err := createNodeWithID(q, PMMServerNodeID, GenericNodeType, &CreateNodeParams{ NodeName: "pmm-server", - Address: "127.0.0.1", + Address: localhost, IsPMMServerNode: true, }) if err != nil { diff --git a/managed/models/victoriametrics_params.go b/managed/models/victoriametrics_params.go index c76166eebe..0c0dc0ec57 100644 --- a/managed/models/victoriametrics_params.go +++ b/managed/models/victoriametrics_params.go @@ -103,7 +103,7 @@ func (vmp *VictoriaMetricsParams) loadVMAlertParams() error { // ExternalVM returns true if VictoriaMetrics is configured to run externally. func (vmp *VictoriaMetricsParams) ExternalVM() bool { - return vmp.url.Hostname() != "127.0.0.1" + return !internalAddr(vmp.url.Hostname()) } // URL returns the base URL for VictoriaMetrics. diff --git a/managed/services/victoriametrics/prometheus.go b/managed/services/victoriametrics/prometheus.go index 8026645881..807e068a5d 100644 --- a/managed/services/victoriametrics/prometheus.go +++ b/managed/services/victoriametrics/prometheus.go @@ -87,7 +87,7 @@ func AddScrapeConfigs(l *logrus.Entry, cfg *config.Config, q *reform.Querier, // } switch { case pushMetrics: - paramsHost = "127.0.0.1" + paramsHost = localhost case agent.PMMAgentID != nil: pmmAgentNode = &models.Node{NodeID: pointer.GetString(pmmAgent.RunsOnNodeID)} if err = q.Reload(pmmAgentNode); err != nil { @@ -272,7 +272,8 @@ func addInternalServicesToScrape(s models.MetricsResolutions, svc *Service, pmmS scrapeConfigForQANAPI2(s.MR, pmmServerNodeName), } - if svc.params.ExternalVM() { + if svc.chParams.ExternalClickHouse() { + svc.l.Warnf("Skip internal ClickHouse scrape config, ClickHouse is configured to run externally.") return cfg } diff --git a/managed/services/victoriametrics/scrape_configs.go b/managed/services/victoriametrics/scrape_configs.go index 32ff757067..5887620d5c 100644 --- a/managed/services/victoriametrics/scrape_configs.go +++ b/managed/services/victoriametrics/scrape_configs.go @@ -55,7 +55,7 @@ func scrapeConfigForClickhouse(mr time.Duration, pmmServerNodeName string) *conf MetricsPath: "/metrics", ServiceDiscoveryConfig: config.ServiceDiscoveryConfig{ StaticConfigs: []*config.Group{{ - Targets: []string{"127.0.0.1:9363"}, + Targets: []string{localhost + ":9363"}, Labels: map[string]string{"instance": pmmServerNodeName}, }}, }, @@ -70,7 +70,7 @@ func scrapeConfigForGrafana(interval time.Duration, pmmServerNodeName string) *c MetricsPath: "/graph/metrics", ServiceDiscoveryConfig: config.ServiceDiscoveryConfig{ StaticConfigs: []*config.Group{{ - Targets: []string{"127.0.0.1:3000"}, + Targets: []string{localhost + ":3000"}, Labels: map[string]string{"instance": pmmServerNodeName}, }}, }, @@ -85,7 +85,7 @@ func scrapeConfigForPMMManaged(interval time.Duration, pmmServerNodeName string) MetricsPath: "/debug/metrics", ServiceDiscoveryConfig: config.ServiceDiscoveryConfig{ StaticConfigs: []*config.Group{{ - Targets: []string{"127.0.0.1:7773"}, + Targets: []string{localhost + ":7773"}, Labels: map[string]string{"instance": pmmServerNodeName}, }}, }, @@ -100,7 +100,7 @@ func scrapeConfigForQANAPI2(interval time.Duration, pmmServerNodeName string) *c MetricsPath: "/debug/metrics", ServiceDiscoveryConfig: config.ServiceDiscoveryConfig{ StaticConfigs: []*config.Group{{ - Targets: []string{"127.0.0.1:9933"}, + Targets: []string{localhost + ":9933"}, Labels: map[string]string{"instance": pmmServerNodeName}, }}, }, @@ -116,7 +116,7 @@ func scrapeConfigForNomadServer(resolution time.Duration, pmmServerNodeName stri Scheme: "https", ServiceDiscoveryConfig: config.ServiceDiscoveryConfig{ StaticConfigs: []*config.Group{{ - Targets: []string{"127.0.0.1:4646"}, + Targets: []string{localhost + ":4646"}, Labels: map[string]string{"instance": pmmServerNodeName}, }}, }, diff --git a/managed/services/victoriametrics/victoriametrics.go b/managed/services/victoriametrics/victoriametrics.go index 5af81891f8..df36170b4f 100644 --- a/managed/services/victoriametrics/victoriametrics.go +++ b/managed/services/victoriametrics/victoriametrics.go @@ -49,6 +49,8 @@ const ( victoriametricsDir = "/srv/victoriametrics" victoriametricsDataDir = "/srv/victoriametrics/data" dirPerm = os.FileMode(0o775) + + localhost = "127.0.0.1" ) var checkFailedRE = regexp.MustCompile(`(?s)cannot unmarshal data: (.+)`) @@ -60,7 +62,8 @@ type Service struct { baseURL *url.URL client *http.Client - params *models.VictoriaMetricsParams + params *models.VictoriaMetricsParams + chParams *models.ClickHouseParams l *logrus.Entry reloadCh chan struct{} @@ -68,11 +71,20 @@ type Service struct { } // NewVictoriaMetrics creates new VictoriaMetrics service. -func NewVictoriaMetrics(scrapeConfigPath string, db *reform.DB, params *models.VictoriaMetricsParams, haService haService) (*Service, error) { +func NewVictoriaMetrics( + scrapeConfigPath string, + db *reform.DB, + params *models.VictoriaMetricsParams, + chParams *models.ClickHouseParams, + haService haService, +) (*Service, error) { u, err := url.Parse(params.URL()) if err != nil { return nil, err } + if chParams == nil { + return nil, fmt.Errorf("ClickHouse params is required") + } return &Service{ scrapeConfigPath: scrapeConfigPath, @@ -80,6 +92,7 @@ func NewVictoriaMetrics(scrapeConfigPath string, db *reform.DB, params *models.V baseURL: u, client: &http.Client{}, // TODO instrument with utils/irt; see vmalert package https://jira.percona.com/browse/PMM-7229 params: params, + chParams: chParams, l: logrus.WithField("component", "victoriametrics"), reloadCh: make(chan struct{}, 1), haService: haService, @@ -435,7 +448,7 @@ func scrapeConfigForVMAlert(interval time.Duration, pmmServerNodeName string) *c ServiceDiscoveryConfig: config.ServiceDiscoveryConfig{ StaticConfigs: []*config.Group{ { - Targets: []string{"127.0.0.1:8880"}, + Targets: []string{localhost + ":8880"}, Labels: map[string]string{"instance": pmmServerNodeName}, }, }, diff --git a/managed/services/victoriametrics/victoriametrics_test.go b/managed/services/victoriametrics/victoriametrics_test.go index cdd4a4e146..77f67e2452 100644 --- a/managed/services/victoriametrics/victoriametrics_test.go +++ b/managed/services/victoriametrics/victoriametrics_test.go @@ -45,10 +45,13 @@ func setup(t *testing.T) (*reform.DB, *Service, []byte) { vmParams, err := models.NewVictoriaMetricsParams(models.BasePrometheusConfigPath, models.VMBaseURL) check.NoError(err) + chParams, err := models.NewClickHouseParams("127.0.0.1:9000", "pmm", "default", "clickhouse") + check.NoError(err) + mockHaService := newMockHaService(t) mockHaService.On("Params").Return(&models.HAParams{Enabled: false, NodeID: "pmm-ha-service-0"}).Maybe() mockHaService.On("IsLeader").Return(true).Maybe() - svc, err := NewVictoriaMetrics(configPath, db, vmParams, mockHaService) + svc, err := NewVictoriaMetrics(configPath, db, vmParams, chParams, mockHaService) check.NoError(err) original, err := os.ReadFile(configPath) @@ -991,3 +994,66 @@ scrape_configs: require.NoError(t, err) assert.Equal(t, expected, string(newcfg), "actual:\n%s", newcfg) } + +func TestVMConfig_OmitsClickhouseScrape(t *testing.T) { + newSvc := func(t *testing.T, chParams *models.ClickHouseParams, vmURL string) (*reform.DB, *Service) { + t.Helper() + sqlDB := testdb.Open(t, models.SkipFixtures, nil) + db := reform.NewDB(sqlDB, postgresql.Dialect, reform.NewPrintfLogger(t.Logf)) + vmParams, err := models.NewVictoriaMetricsParams(models.BasePrometheusConfigPath, vmURL) + require.NoError(t, err) + + mockHaService := newMockHaService(t) + mockHaService.On("Params").Return(&models.HAParams{Enabled: false, NodeID: "pmm-ha-service-0"}).Maybe() + mockHaService.On("IsLeader").Return(true).Maybe() + + svc, err := NewVictoriaMetrics(configPath, db, vmParams, chParams, mockHaService) + require.NoError(t, err) + require.NoError(t, svc.IsReady(t.Context())) + t.Cleanup(func() { _ = db.DBInterface().(*sql.DB).Close() }) + return db, svc + } + + newCHParams := func(t *testing.T, addr string) *models.ClickHouseParams { + t.Helper() + chp, err := models.NewClickHouseParams(addr, "pmm", "default", "clickhouse") + require.NoError(t, err) + return chp + } + + cases := []struct { + name string + addr string + wantClickhouse bool + }{ + {"internal enabled positive control", "127.0.0.1:9000", true}, + {"external addr skips scrape", "ch.external:9000", false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, svc := newSvc(t, newCHParams(t, tc.addr), models.VMBaseURL) + cfg, err := svc.marshalConfig(svc.loadBaseConfig()) + require.NoError(t, err) + assert.Contains(t, string(cfg), "job_name: grafana") + assert.Contains(t, string(cfg), "job_name: pmm-managed") + assert.Contains(t, string(cfg), "job_name: qan-api2") + if tc.wantClickhouse { + assert.Contains(t, string(cfg), "127.0.0.1:9363") + assert.Contains(t, string(cfg), "job_name: clickhouse") + } else { + assert.NotContains(t, string(cfg), "127.0.0.1:9363") + assert.NotContains(t, string(cfg), "job_name: clickhouse") + } + }) + } +} + +func TestNewVictoriaMetrics_NilClickHouseParams(t *testing.T) { + vmParams, err := models.NewVictoriaMetricsParams(models.BasePrometheusConfigPath, models.VMBaseURL) + require.NoError(t, err) + + svc, err := NewVictoriaMetrics(configPath, nil, vmParams, nil, newMockHaService(t)) + require.Error(t, err) + assert.Nil(t, svc) + assert.Contains(t, err.Error(), "ClickHouse params is required") +}