From 325d4806ed62489cf8fc0f1146f8231a35a0133e Mon Sep 17 00:00:00 2001
From: Garet Halliday <me@garet.holiday>
Date: Fri, 15 Sep 2023 15:07:13 -0500
Subject: [PATCH] do auto discovery working, just need replicas

---
 .../modes/digitalocean_discovery/config.go    | 130 ++++++++++++++++++
 .../modes/digitalocean_discovery/database.go  |  54 ++++++++
 2 files changed, 184 insertions(+)
 create mode 100644 lib/gat/modes/digitalocean_discovery/config.go
 create mode 100644 lib/gat/modes/digitalocean_discovery/database.go

diff --git a/lib/gat/modes/digitalocean_discovery/config.go b/lib/gat/modes/digitalocean_discovery/config.go
new file mode 100644
index 00000000..efaabc78
--- /dev/null
+++ b/lib/gat/modes/digitalocean_discovery/config.go
@@ -0,0 +1,130 @@
+package digitalocean_discovery
+
+import (
+	"crypto/tls"
+	"encoding/json"
+	"errors"
+	"net"
+	"net/http"
+	"net/url"
+	"strconv"
+
+	"gfx.cafe/util/go/gun"
+	"tuxpa.in/a/zlog/log"
+
+	"pggat/lib/auth/credentials"
+	"pggat/lib/bouncer"
+	"pggat/lib/bouncer/backends/v0"
+	"pggat/lib/bouncer/frontends/v0"
+	"pggat/lib/gat"
+	"pggat/lib/gat/pool"
+	"pggat/lib/gat/pool/dialer"
+	"pggat/lib/gat/pool/pools/transaction"
+	"pggat/lib/gat/pool/recipe"
+	"pggat/lib/util/flip"
+	"pggat/lib/util/strutil"
+)
+
+type Config struct {
+	APIKey string `env:"PGGAT_DO_API_KEY"`
+}
+
+func Load() (Config, error) {
+	var conf Config
+	gun.Load(&conf)
+	if conf.APIKey == "" {
+		return Config{}, errors.New("expected auth token")
+	}
+
+	return conf, nil
+}
+
+func (T *Config) ListenAndServe() error {
+	dest, err := url.Parse("https://api.digitalocean.com/v2/databases")
+	if err != nil {
+		return err
+	}
+
+	req := http.Request{
+		Method: http.MethodGet,
+		URL:    dest,
+		Header: http.Header{
+			"Content-Type":  []string{"application/json"},
+			"Authorization": []string{"Bearer " + T.APIKey},
+		},
+	}
+
+	resp, err := http.DefaultClient.Do(&req)
+	if err != nil {
+		return err
+	}
+
+	var r ListClustersResponse
+	err = json.NewDecoder(resp.Body).Decode(&r)
+	if err != nil {
+		return err
+	}
+
+	var m gat.PoolsMap
+
+	for _, cluster := range r.Databases {
+		if cluster.Engine != "pg" {
+			continue
+		}
+
+		for _, user := range cluster.Users {
+			creds := credentials.Cleartext{
+				Username: user.Name,
+				Password: user.Password,
+			}
+
+			for _, dbname := range cluster.DBNames {
+				p := pool.NewPool(transaction.Apply(pool.Options{
+					Credentials: creds,
+					TrackedParameters: []strutil.CIString{
+						strutil.MakeCIString("client_encoding"),
+						strutil.MakeCIString("datestyle"),
+						strutil.MakeCIString("timezone"),
+						strutil.MakeCIString("standard_conforming_strings"),
+						strutil.MakeCIString("application_name"),
+					},
+				}))
+				p.AddRecipe("do", recipe.NewRecipe(recipe.Options{
+					Dialer: dialer.Net{
+						Network: "tcp",
+						Address: net.JoinHostPort(cluster.Connection.Host, strconv.Itoa(cluster.Connection.Port)),
+						AcceptOptions: backends.AcceptOptions{
+							SSLMode: bouncer.SSLModeRequire,
+							SSLConfig: &tls.Config{
+								InsecureSkipVerify: true,
+							},
+							Credentials: creds,
+							Database:    dbname,
+						},
+					},
+				}))
+
+				m.Add(user.Name, dbname, p)
+			}
+		}
+	}
+
+	var b flip.Bank
+
+	b.Queue(func() error {
+		log.Print("listening on :5432")
+		return gat.ListenAndServe("tcp", ":5432", frontends.AcceptOptions{
+			AllowedStartupOptions: []strutil.CIString{
+				strutil.MakeCIString("client_encoding"),
+				strutil.MakeCIString("datestyle"),
+				strutil.MakeCIString("timezone"),
+				strutil.MakeCIString("standard_conforming_strings"),
+				strutil.MakeCIString("application_name"),
+				strutil.MakeCIString("extra_float_digits"),
+				strutil.MakeCIString("options"),
+			},
+		}, &m)
+	})
+
+	return b.Wait()
+}
diff --git a/lib/gat/modes/digitalocean_discovery/database.go b/lib/gat/modes/digitalocean_discovery/database.go
new file mode 100644
index 00000000..af84cebe
--- /dev/null
+++ b/lib/gat/modes/digitalocean_discovery/database.go
@@ -0,0 +1,54 @@
+package digitalocean_discovery
+
+import (
+	"time"
+
+	"github.com/google/uuid"
+)
+
+type Connection struct {
+	URI      string `env:"uri"`
+	Database string `env:"database"`
+	Host     string `env:"host"`
+	Port     int    `env:"port"`
+	User     string `env:"user"`
+	Password string `env:"password"`
+	SSL      bool   `env:"ssl"`
+}
+
+type User struct {
+	Name     string `json:"name"`
+	Role     string `json:"role"`
+	Password string `json:"password"`
+}
+
+type MaintenanceWindow struct {
+	Day         string   `json:"day"`
+	Hour        string   `json:"hour"`
+	Pending     bool     `json:"pending"`
+	Description []string `json:"description"`
+}
+
+type Database struct {
+	ID                       uuid.UUID  `json:"id"`
+	Name                     string     `json:"name"`
+	Engine                   string     `json:"engine"`
+	Version                  string     `json:"version"`
+	Connection               Connection `json:"connection"`
+	PrivateConnection        Connection `json:"private_connection"`
+	Users                    []User     `json:"users"`
+	DBNames                  []string   `json:"db_names"`
+	NumNodes                 int        `json:"num_nodes"`
+	Region                   string     `json:"region"`
+	Status                   string     `json:"online"`
+	CreatedAt                time.Time  `json:"created_at"`
+	Size                     string     `json:"size"`
+	Tags                     []string   `json:"tags"`
+	PrivateNetworkUUID       uuid.UUID  `json:"private_network_uuid"`
+	VersionEndOfLife         time.Time  `json:"version_end_of_life"`
+	VersionEndOfAvailability time.Time  `json:"version_end_of_availability"`
+}
+
+type ListClustersResponse struct {
+	Databases []Database `json:"databases"`
+}
-- 
GitLab