Skip to content

Commit 010395c

Browse files
authored
Merge pull request #31 from fly-apps/config-api
WIP: Start adding config api
2 parents 4fd213a + 526e801 commit 010395c

File tree

10 files changed

+318
-16
lines changed

10 files changed

+318
-16
lines changed

cmd/start/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func main() {
6565
svisor.AddProcess("exporter", "postgres_exporter", supervisor.WithEnv(exporterEnv), supervisor.WithRestart(0, 1*time.Second))
6666

6767
svisor.StopOnSignal(syscall.SIGINT, syscall.SIGTERM)
68-
svisor.StartHttpListener()
68+
svisor.StartHttpListener(node)
6969

7070
if err := svisor.Run(); err != nil {
7171
fmt.Println(err)

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/pkg/errors v0.9.1
1212
github.com/pkg/term v1.1.0
1313
github.com/superfly/fly-checks v0.0.0-20221220181621-bcbf6f4dc6d7
14+
golang.org/x/exp v0.0.0-20230105202349-8879d0199aa3
1415
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
1516
)
1617

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWP
220220
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
221221
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
222222
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
223+
golang.org/x/exp v0.0.0-20230105202349-8879d0199aa3 h1:fJwx88sMf5RXwDwziL0/Mn9Wqs+efMSo/RYcL+37W9c=
224+
golang.org/x/exp v0.0.0-20230105202349-8879d0199aa3/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
223225
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
224226
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
225227
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=

pkg/api/handle_admin.go

Lines changed: 213 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
package api
22

33
import (
4-
"net/http"
5-
4+
"encoding/json"
5+
"fmt"
66
"github.com/fly-apps/postgres-flex/pkg/flypg"
7+
"github.com/fly-apps/postgres-flex/pkg/flypg/admin"
8+
"github.com/fly-apps/postgres-flex/pkg/flypg/state"
9+
"golang.org/x/exp/slices"
10+
"net/http"
11+
"strings"
712
)
813

914
func handleRole(w http.ResponseWriter, r *http.Request) {
@@ -39,3 +44,209 @@ func handleRole(w http.ResponseWriter, r *http.Request) {
3944

4045
renderJSON(w, res, http.StatusOK)
4146
}
47+
48+
type SettingsUpdate struct {
49+
Message string `json:"message"`
50+
RestartRequired bool `json:"restart_required"`
51+
}
52+
53+
func (s *Server) handleUpdatePostgresSettings(w http.ResponseWriter, r *http.Request) {
54+
conn, close, err := localConnection(r.Context(), "postgres")
55+
if err != nil {
56+
renderErr(w, err)
57+
return
58+
}
59+
defer close()
60+
61+
user := s.node.PGConfig.UserConfig()
62+
63+
var in map[string]interface{}
64+
65+
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
66+
renderErr(w, err)
67+
return
68+
}
69+
70+
for k, v := range in {
71+
exists, err := admin.SettingExists(r.Context(), conn, k)
72+
if err != nil {
73+
renderErr(w, err)
74+
return
75+
}
76+
if !exists {
77+
renderErr(w, fmt.Errorf("invalid config option: %s", k))
78+
return
79+
}
80+
user[k] = v
81+
}
82+
83+
s.node.PGConfig.SetUserConfig(user)
84+
85+
var requiresRestart []string
86+
87+
for k, _ := range user {
88+
restart, err := admin.SettingRequiresRestart(r.Context(), conn, k)
89+
if err != nil {
90+
renderErr(w, err)
91+
return
92+
}
93+
if restart {
94+
requiresRestart = append(requiresRestart, k)
95+
}
96+
}
97+
98+
res := &Response{Result: SettingsUpdate{
99+
Message: "Updated",
100+
RestartRequired: false,
101+
}}
102+
103+
if len(requiresRestart) > 0 {
104+
res = &Response{Result: SettingsUpdate{
105+
Message: fmt.Sprintf("Updated, but settings %s need a restart to apply", strings.Join(requiresRestart, ", ")),
106+
RestartRequired: true,
107+
}}
108+
}
109+
110+
renderJSON(w, res, http.StatusOK)
111+
}
112+
113+
func (s *Server) handleApplyConfig(w http.ResponseWriter, r *http.Request) {
114+
conn, close, err := localConnection(r.Context(), "postgres")
115+
if err != nil {
116+
renderErr(w, err)
117+
return
118+
}
119+
defer close()
120+
121+
consul, err := state.NewConsulClient()
122+
if err != nil {
123+
renderErr(w, err)
124+
return
125+
}
126+
127+
err = flypg.WriteUserConfig(s.node.PGConfig, consul)
128+
if err != nil {
129+
renderErr(w, err)
130+
return
131+
}
132+
133+
err = admin.ReloadPostgresConfig(r.Context(), conn)
134+
if err != nil {
135+
renderErr(w, err)
136+
return
137+
}
138+
}
139+
140+
type PGSettingsResponse struct {
141+
Settings []admin.PGSetting `json:"settings"`
142+
}
143+
144+
func (s *Server) handleViewPostgresSettings(w http.ResponseWriter, r *http.Request) {
145+
conn, close, err := localConnection(r.Context(), "postgres")
146+
if err != nil {
147+
renderErr(w, err)
148+
return
149+
}
150+
151+
defer close()
152+
internal := s.node.PGConfig.InternalConfig()
153+
user := s.node.PGConfig.UserConfig()
154+
155+
all := map[string]interface{}{}
156+
157+
for k, v := range internal {
158+
all[k] = v
159+
}
160+
for k, v := range user {
161+
all[k] = v
162+
}
163+
164+
var in []string
165+
166+
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
167+
renderErr(w, err)
168+
return
169+
}
170+
171+
var out []admin.PGSetting
172+
173+
for key, _ := range all {
174+
if slices.Contains(in, key) {
175+
setting, err := admin.GetSetting(r.Context(), conn, key)
176+
if err != nil {
177+
renderErr(w, err)
178+
return
179+
}
180+
out = append(out, *setting)
181+
}
182+
}
183+
184+
resp := &Response{Result: PGSettingsResponse{Settings: out}}
185+
renderJSON(w, resp, http.StatusOK)
186+
}
187+
188+
func (s *Server) handleViewBouncerSettings(w http.ResponseWriter, r *http.Request) {
189+
internal := s.node.PGBouncer.InternalConfig()
190+
user := s.node.PGBouncer.UserConfig()
191+
192+
all := map[string]interface{}{}
193+
194+
for k, v := range internal {
195+
all[k] = v
196+
}
197+
for k, v := range user {
198+
all[k] = v
199+
}
200+
201+
var in []string
202+
203+
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
204+
renderErr(w, err)
205+
return
206+
}
207+
208+
out := map[string]interface{}{}
209+
210+
for key, _ := range all {
211+
val, _ := all[key]
212+
if slices.Contains(in, key) {
213+
out[key] = val
214+
}
215+
}
216+
217+
resp := &Response{Result: out}
218+
renderJSON(w, resp, http.StatusOK)
219+
}
220+
221+
func (s *Server) handleViewRepmgrSettings(w http.ResponseWriter, r *http.Request) {
222+
internal := s.node.RepMgr.InternalConfig()
223+
user := s.node.RepMgr.UserConfig()
224+
225+
all := map[string]interface{}{}
226+
227+
for k, v := range internal {
228+
all[k] = v
229+
}
230+
for k, v := range user {
231+
all[k] = v
232+
}
233+
234+
var in []string
235+
236+
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
237+
renderErr(w, err)
238+
return
239+
}
240+
241+
out := map[string]interface{}{}
242+
243+
for key, _ := range all {
244+
val, _ := all[key]
245+
if slices.Contains(in, key) {
246+
out[key] = val
247+
}
248+
}
249+
250+
resp := &Response{Result: out}
251+
renderJSON(w, resp, http.StatusOK)
252+
}

pkg/api/handler.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,21 @@ import (
1313

1414
const Port = 5500
1515

16-
func StartHttpServer() {
16+
type Server struct {
17+
node *flypg.Node
18+
}
19+
20+
func StartHttpServer(node *flypg.Node) {
21+
server := &Server{node: node}
1722
r := chi.NewMux()
1823

1924
r.Mount("/flycheck", flycheck.Handler())
20-
r.Mount("/commands", Handler())
25+
r.Mount("/commands", server.Handler())
2126

2227
http.ListenAndServe(fmt.Sprintf(":%d", Port), r)
2328
}
2429

25-
func Handler() http.Handler {
30+
func (s *Server) Handler() http.Handler {
2631
r := chi.NewRouter()
2732

2833
r.Route("/users", func(r chi.Router) {
@@ -41,9 +46,11 @@ func Handler() http.Handler {
4146

4247
r.Route("/admin", func(r chi.Router) {
4348
r.Get("/role", handleRole)
44-
// r.Get("/failover/trigger", handleFailoverTrigger)
45-
// r.Get("/settings/view", handleViewSettings)
46-
// r.Post("/settings/update", handleUpdateSettings)
49+
r.Get("/settings/view/postgres", s.handleViewPostgresSettings)
50+
r.Get("/settings/view/pgbouncer", s.handleViewBouncerSettings)
51+
r.Get("/settings/view/repmgr", s.handleViewRepmgrSettings)
52+
r.Post("/settings/update/postgres", s.handleUpdatePostgresSettings)
53+
r.Post("/settings/apply", s.handleApplyConfig)
4754
})
4855

4956
return r

pkg/flypg/admin/admin.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,53 @@ func SetConfigurationSetting(ctx context.Context, conn *pgx.Conn, key string, va
224224
_, err := conn.Exec(ctx, sql)
225225
return err
226226
}
227+
228+
func ReloadPostgresConfig(ctx context.Context, pg *pgx.Conn) error {
229+
sql := "SELECT pg_reload_conf()"
230+
231+
_, err := pg.Exec(ctx, sql)
232+
return err
233+
}
234+
235+
func SettingExists(ctx context.Context, pg *pgx.Conn, setting string) (bool, error) {
236+
sql := fmt.Sprintf("SELECT EXISTS(SELECT 1 FROM pg_settings WHERE name='%s')", setting)
237+
var out bool
238+
if err := pg.QueryRow(ctx, sql).Scan(&out); err != nil {
239+
return false, err
240+
}
241+
return out, nil
242+
}
243+
244+
func SettingRequiresRestart(ctx context.Context, pg *pgx.Conn, setting string) (bool, error) {
245+
sql := fmt.Sprintf("SELECT pending_restart FROM pg_settings WHERE name='%s'", setting)
246+
row := pg.QueryRow(ctx, sql)
247+
var out bool
248+
if err := row.Scan(&out); err != nil {
249+
return false, err
250+
}
251+
return out, nil
252+
}
253+
254+
type PGSetting struct {
255+
Name string `json:"name,omitempty"`
256+
Setting string `json:"setting,omitempty"`
257+
VarType *string `json:"vartype,omitempty"`
258+
MinVal *string `json:"min_val,omitempty"`
259+
MaxVal *string `json:"max_val,omitempty"`
260+
EnumVals *[]string `json:"enumvals,omitempty"`
261+
Context *string `json:"context,omitempty"`
262+
Unit *string `json:"unit,omitempty"`
263+
Desc *string `json:"short_desc,omitempty"`
264+
PendingChange *string `json:"pending_change,omitempty"`
265+
PendingRestart *bool `json:"pending_restart,omitempty"`
266+
}
267+
268+
func GetSetting(ctx context.Context, pg *pgx.Conn, setting string) (*PGSetting, error) {
269+
sql := fmt.Sprintf("SELECT name, setting, vartype, min_val, max_val, enumvals, context, unit, short_desc, pending_restart FROM pg_settings WHERE name='%s'", setting)
270+
row := pg.QueryRow(ctx, sql)
271+
out := PGSetting{}
272+
if err := row.Scan(&out.Name, &out.Setting, &out.VarType, &out.MinVal, &out.MaxVal, &out.EnumVals, &out.Context, &out.Unit, &out.Desc, &out.PendingRestart); err != nil {
273+
return nil, err
274+
}
275+
return &out, nil
276+
}

pkg/flypg/config.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ func SyncUserConfig(c Config, consul *state.ConsulClient) error {
3939
if err != nil {
4040
return fmt.Errorf("failed to pull config from consul: %s", err)
4141
}
42+
if cfg == nil {
43+
return nil
44+
}
4245
c.SetUserConfig(cfg)
4346

4447
if err := WriteConfigFiles(c); err != nil {
@@ -70,6 +73,9 @@ func pullFromConsul(c Config, consul *state.ConsulClient) (ConfigMap, error) {
7073
if err != nil {
7174
return nil, err
7275
}
76+
if configBytes == nil {
77+
return nil, nil
78+
}
7379

7480
var storeCfg ConfigMap
7581
if err = json.Unmarshal(configBytes, &storeCfg); err != nil {
@@ -91,16 +97,21 @@ func WriteConfigFiles(c Config) error {
9197
}
9298
defer userFile.Close()
9399

94-
for key, value := range c.InternalConfig() {
95-
entry := fmt.Sprintf("%s = %v\n", key, value)
96-
internalFile.Write([]byte(entry))
97-
}
100+
internal := c.InternalConfig()
98101

99102
for key, value := range c.UserConfig() {
100103
entry := fmt.Sprintf("%s = %v\n", key, value)
104+
if _, ok := internal[key]; ok {
105+
delete(internal, key)
106+
}
101107
userFile.Write([]byte(entry))
102108
}
103109

110+
for key, value := range internal {
111+
entry := fmt.Sprintf("%s = %v\n", key, value)
112+
internalFile.Write([]byte(entry))
113+
}
114+
104115
return nil
105116
}
106117

0 commit comments

Comments
 (0)