package collect import ( "bytes" "context" "encoding/json" "fmt" "os" "path/filepath" "regexp" "github.com/jackc/pgx/v5" "github.com/pkg/errors" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/klog/v2" ) type CollectPostgres struct { Collector *troubleshootv1beta2.Database BundlePath string Namespace string ClientConfig *rest.Config Client kubernetes.Interface Context context.Context RBACErrors } func (c *CollectPostgres) Title() string { return getCollectorName(c) } func (c *CollectPostgres) IsExcluded() (bool, error) { return isExcluded(c.Collector.Exclude) } func (c *CollectPostgres) createConnectConfig() (*pgx.ConnConfig, error) { if c.Collector.URI == "" { return nil, errors.New("postgres uri cannot be empty") } if c.Collector.TLS != nil { klog.V(2).Infof("Connecting to postgres with TLS client config") // Set the libpq TLS environment variables since pgx parses them to // create the TLS configuration (tls.Config instance) to connect with // https://www.postgresql.org/docs/current/libpq-envars.html caCert, clientCert, clientKey, err := getTLSParamTriplet(c.Context, c.Client, c.Collector.TLS) if err != nil { return nil, err } // Drop the TLS params to files and set the paths to their // respective environment variables // The environment variables are unset after the connection config // is created. Their respective files are deleted as well. tmpdir, err := os.MkdirTemp("", "ts-postgres-collector") if err != nil { return nil, errors.Wrap(err, "failed to create temp dir to store postgres collector TLS files") } defer os.RemoveAll(tmpdir) if caCert != "" { caCertPath := filepath.Join(tmpdir, "ca.crt") err = os.WriteFile(caCertPath, []byte(caCert), 0644) if err != nil { return nil, errors.Wrap(err, "failed to write ca cert to file") } err = os.Setenv("PGSSLROOTCERT", caCertPath) if err != nil { return nil, errors.Wrap(err, "failed to set PGSSLROOTCERT environment variable") } klog.V(2).Infof("'PGSSLROOTCERT' environment variable set to %q", caCertPath) defer os.Unsetenv("PGSSLROOTCERT") } if clientCert != "" { clientCertPath := filepath.Join(tmpdir, "client.crt") err = os.WriteFile(clientCertPath, []byte(clientCert), 0644) if err != nil { return nil, errors.Wrap(err, "failed to write client cert to file") } err = os.Setenv("PGSSLCERT", clientCertPath) if err != nil { return nil, errors.Wrap(err, "failed to set PGSSLCERT environment variable") } klog.V(2).Infof("'PGSSLCERT' environment variable set to %q", clientCertPath) defer os.Unsetenv("PGSSLCERT") } if clientKey != "" { clientKeyPath := filepath.Join(tmpdir, "client.key") err = os.WriteFile(clientKeyPath, []byte(clientKey), 0600) if err != nil { return nil, errors.Wrap(err, "failed to write client key to file") } err = os.Setenv("PGSSLKEY", clientKeyPath) if err != nil { return nil, errors.Wrap(err, "failed to set PGSSLKEY environment variable") } klog.V(2).Infof("'PGSSLKEY' environment variable set to %q", clientKeyPath) defer os.Unsetenv("PGSSLKEY") } } cfg, err := pgx.ParseConfig(c.Collector.URI) if err != nil { return nil, errors.Wrap(err, "failed to parse postgres config") } klog.V(2).Infof("Successfully parsed postgres config") return cfg, nil } func (c *CollectPostgres) connect() (*pgx.Conn, error) { connCfg, err := c.createConnectConfig() if err != nil { return nil, err } conn, err := pgx.ConnectConfig(c.Context, connCfg) if err != nil { return nil, err } return conn, nil } func (c *CollectPostgres) Collect(progressChan chan<- interface{}) (CollectorResult, error) { databaseConnection := DatabaseConnection{} conn, err := c.connect() if err != nil { klog.V(2).Infof("Postgres connection error: %s", err.Error()) databaseConnection.Error = err.Error() } else { klog.V(2).Info("Successfully connected to postgres") defer conn.Close(c.Context) query := `select version()` row := conn.QueryRow(c.Context, query) version := "" if err := row.Scan(&version); err != nil { databaseConnection.Error = err.Error() } else { databaseConnection.IsConnected = true postgresVersion, err := parsePostgresVersion(version) if err != nil { databaseConnection.Version = "Unknown" databaseConnection.Error = err.Error() } else { databaseConnection.Version = postgresVersion } } } b, err := json.Marshal(databaseConnection) if err != nil { return nil, errors.Wrap(err, "failed to marshal database connection") } collectorName := c.Collector.CollectorName if collectorName == "" { collectorName = "postgres" } output := NewResult() output.SaveResult(c.BundlePath, fmt.Sprintf("postgres/%s.json", collectorName), bytes.NewBuffer(b)) return output, nil } func parsePostgresVersion(postgresVersion string) (string, error) { re := regexp.MustCompile("PostgreSQL ([0-9.]*)") matches := re.FindStringSubmatch(postgresVersion) if len(matches) < 2 { return "", errors.Errorf("postgres version did not match regex: %q", postgresVersion) } return matches[1], nil }