Skip to content

Commit db514f3

Browse files
authored
Merge pull request #3 from fly-apps/flyadmin-cmds
Add flyadmin commands
2 parents f102c61 + a6cd4cd commit db514f3

File tree

3 files changed

+254
-1
lines changed

3 files changed

+254
-1
lines changed

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ FROM golang:1.16 as flyutil
66
WORKDIR /go/src/github.com/fly-examples/fly-postgres
77
COPY . .
88

9-
RUN CGO_ENABLED=0 GOOS=linux go build -v -o /fly/bin/start ./cmd
9+
RUN CGO_ENABLED=0 GOOS=linux go build -v -o /fly/bin/flyadmin ./cmd/flyadmin
10+
RUN CGO_ENABLED=0 GOOS=linux go build -v -o /fly/bin/start ./cmd/start
1011

1112
FROM postgres:${PG_VERSION}
1213
ENV PGDATA=/data/pg_data

cmd/flyadmin/main.go

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"os"
8+
9+
"github.com/jackc/pgx/v4"
10+
)
11+
12+
type cmd func(pg *pgx.Conn, input map[string]interface{}) (result interface{}, err error)
13+
14+
func main() {
15+
app := os.Getenv("FLY_APP_NAME")
16+
hostname := fmt.Sprintf("%s.internal:5432", app)
17+
conn, err := openConnection(hostname)
18+
if err != nil {
19+
fmt.Fprintf(os.Stderr, "error connecting to postgres: %s\n", err)
20+
os.Exit(1)
21+
}
22+
defer conn.Close(context.Background())
23+
24+
if len(os.Args) == 1 {
25+
fmt.Fprintln(os.Stderr, "subcommand required")
26+
os.Exit(1)
27+
}
28+
29+
command := os.Args[1]
30+
input := map[string]interface{}{}
31+
32+
if len(os.Args) > 2 && os.Args[2] != "" {
33+
if err := json.Unmarshal([]byte(os.Args[2]), &input); err != nil {
34+
fmt.Fprintf(os.Stderr, "error decoding json input: %s\n", err)
35+
os.Exit(1)
36+
}
37+
}
38+
39+
commands := map[string]cmd{
40+
"database-list": listDatabases,
41+
"database-create": createDatabase,
42+
"database-delete": deleteDatabase,
43+
"user-list": listUsers,
44+
"user-create": createUser,
45+
"user-delete": deleteUser,
46+
"grant-access": grantAccess,
47+
"revoke-access": revokeAccess,
48+
"grant-superuser": grantSuperuser,
49+
"revoke-superuser": revokeSuperuser,
50+
}
51+
52+
cmd := commands[command]
53+
if cmd == nil {
54+
fmt.Fprintf(os.Stderr, "unknown command '%s'\n", command)
55+
os.Exit(1)
56+
}
57+
58+
output, err := cmd(conn, input)
59+
resp := response{
60+
Result: output,
61+
}
62+
if err != nil {
63+
resp.Error = err.Error()
64+
}
65+
66+
if err := json.NewEncoder(os.Stdout).Encode(resp); err != nil {
67+
fmt.Fprintf(os.Stderr, "error marshaling response '%s'\n", err)
68+
os.Exit(1)
69+
}
70+
}
71+
72+
type response struct {
73+
Result interface{} `json:"result"`
74+
Error string `json:"error,omitempty"`
75+
}
76+
77+
func listDatabases(pg *pgx.Conn, input map[string]interface{}) (interface{}, error) {
78+
sql := `
79+
SELECT d.datname,
80+
(SELECT array_agg(u.usename::text order by u.usename)
81+
from pg_user u
82+
where has_database_privilege(u.usename, d.datname, 'CONNECT')) as allowed_users
83+
from pg_database d where d.datistemplate = false
84+
order by d.datname;
85+
`
86+
87+
rows, err := pg.Query(context.Background(), sql)
88+
if err != nil {
89+
return nil, err
90+
}
91+
defer rows.Close()
92+
93+
values := []dbInfo{}
94+
95+
for rows.Next() {
96+
di := dbInfo{}
97+
if err := rows.Scan(&di.Name, &di.Users); err != nil {
98+
return nil, err
99+
}
100+
values = append(values, di)
101+
}
102+
103+
return values, nil
104+
}
105+
106+
type userInfo struct {
107+
Username string `json:"username"`
108+
SuperUser bool `json:"superuser"`
109+
Databases []string `json:"databases"`
110+
}
111+
112+
type dbInfo struct {
113+
Name string `json:"name"`
114+
Users []string `json:"users"`
115+
}
116+
117+
func listUsers(pg *pgx.Conn, input map[string]interface{}) (interface{}, error) {
118+
sql := `
119+
select u.usename,
120+
usesuper as superuser,
121+
(select array_agg(d.datname::text order by d.datname)
122+
from pg_database d
123+
WHERE datistemplate = false
124+
AND has_database_privilege(u.usename, d.datname, 'CONNECT')
125+
) as allowed_databases
126+
from pg_user u
127+
order by u.usename
128+
`
129+
130+
rows, err := pg.Query(context.Background(), sql)
131+
if err != nil {
132+
return nil, err
133+
}
134+
defer rows.Close()
135+
136+
values := []userInfo{}
137+
138+
for rows.Next() {
139+
ui := userInfo{}
140+
if err := rows.Scan(&ui.Username, &ui.SuperUser, &ui.Databases); err != nil {
141+
return nil, err
142+
}
143+
values = append(values, ui)
144+
}
145+
146+
return values, nil
147+
}
148+
149+
func createUser(pg *pgx.Conn, input map[string]interface{}) (interface{}, error) {
150+
sql := fmt.Sprintf(`CREATE USER %s WITH LOGIN PASSWORD '%s'`, input["username"], input["password"])
151+
152+
_, err := pg.Exec(context.Background(), sql)
153+
if err != nil {
154+
return false, err
155+
}
156+
157+
if val, ok := input["superuser"]; ok && val == true {
158+
return grantSuperuser(pg, input)
159+
}
160+
161+
return true, nil
162+
}
163+
164+
func deleteUser(pg *pgx.Conn, input map[string]interface{}) (interface{}, error) {
165+
sql := fmt.Sprintf(`DROP USER IF EXISTS %s`, input["username"])
166+
167+
_, err := pg.Exec(context.Background(), sql)
168+
if err != nil {
169+
return false, err
170+
}
171+
172+
return true, nil
173+
}
174+
175+
func createDatabase(pg *pgx.Conn, input map[string]interface{}) (interface{}, error) {
176+
sql := fmt.Sprintf("CREATE DATABASE %s;", input["name"])
177+
178+
_, err := pg.Exec(context.Background(), sql)
179+
if err != nil {
180+
return false, err
181+
}
182+
183+
return true, nil
184+
}
185+
186+
func deleteDatabase(pg *pgx.Conn, input map[string]interface{}) (interface{}, error) {
187+
sql := fmt.Sprintf("DROP DATABASE %s;", input["name"])
188+
189+
_, err := pg.Exec(context.Background(), sql)
190+
if err != nil {
191+
return false, err
192+
}
193+
194+
return true, nil
195+
}
196+
197+
func grantAccess(pg *pgx.Conn, input map[string]interface{}) (interface{}, error) {
198+
sql := fmt.Sprintf("GRANT ALL PRIVILEGES ON DATABASE %s TO %s", input["database"], input["username"])
199+
200+
_, err := pg.Exec(context.Background(), sql)
201+
if err != nil {
202+
return false, err
203+
}
204+
205+
return true, nil
206+
}
207+
208+
func revokeAccess(pg *pgx.Conn, input map[string]interface{}) (interface{}, error) {
209+
sql := fmt.Sprintf("REVOKE ALL PRIVILEGES ON DATABASE %s FROM %s", input["database"], input["username"])
210+
211+
_, err := pg.Exec(context.Background(), sql)
212+
if err != nil {
213+
return false, err
214+
}
215+
216+
return true, nil
217+
}
218+
219+
func grantSuperuser(pg *pgx.Conn, input map[string]interface{}) (interface{}, error) {
220+
sql := fmt.Sprintf("ALTER USER %s WITH SUPERUSER;", input["username"])
221+
222+
_, err := pg.Exec(context.Background(), sql)
223+
if err != nil {
224+
return false, err
225+
}
226+
227+
return true, nil
228+
}
229+
230+
func revokeSuperuser(pg *pgx.Conn, input map[string]interface{}) (interface{}, error) {
231+
sql := fmt.Sprintf("ALTER USER %s WITH NOSUPERUSER;", input["username"])
232+
233+
_, err := pg.Exec(context.Background(), sql)
234+
if err != nil {
235+
return false, err
236+
}
237+
238+
return true, nil
239+
}
240+
241+
func openConnection(hostname string) (*pgx.Conn, error) {
242+
url := fmt.Sprintf("postgres://%s/postgres", hostname)
243+
conf, err := pgx.ParseConfig(url)
244+
245+
if err != nil {
246+
return nil, err
247+
}
248+
conf.User = "flypgadmin"
249+
conf.Password = os.Getenv("SU_PASSWORD")
250+
251+
return pgx.ConnectConfig(context.Background(), conf)
252+
}
File renamed without changes.

0 commit comments

Comments
 (0)