Skip to content

Commit 26ec0cf

Browse files
Add server and user_mapping resources (#220)
* feat: Postgres foreign server * feat: Add user mapping resource * fix: wrong dependency * fix: wrong mapping * chore: document postgresql_server resource * chore: document postgresql_user_mapping resource * chore: replace wrong title * fix: ignore RDS tests * fix: documentation example using wrong resource * refactor: PR review changes request Co-authored-by: Cyril Gaudin <[email protected]>
1 parent 7716c76 commit 26ec0cf

9 files changed

+1473
-0
lines changed

postgresql/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const (
3939
featurePublication
4040
featurePubWithoutTruncate
4141
featureFunction
42+
featureServer
4243
)
4344

4445
var (
@@ -105,6 +106,8 @@ var (
105106
featurePublication: semver.MustParseRange(">=10.0.0"),
106107
// We do not support CREATE FUNCTION for Postgresql < 8.4
107108
featureFunction: semver.MustParseRange(">=8.4.0"),
109+
// CREATE SERVER support
110+
featureServer: semver.MustParseRange(">=10.0.0"),
108111
}
109112
)
110113

postgresql/provider.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ func Provider() *schema.Provider {
175175
"postgresql_schema": resourcePostgreSQLSchema(),
176176
"postgresql_role": resourcePostgreSQLRole(),
177177
"postgresql_function": resourcePostgreSQLFunction(),
178+
"postgresql_server": resourcePostgreSQLServer(),
179+
"postgresql_user_mapping": resourcePostgreSQLUserMapping(),
178180
},
179181

180182
ConfigureFunc: providerConfigure,
Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
package postgresql
2+
3+
import (
4+
"bytes"
5+
"database/sql"
6+
"fmt"
7+
"log"
8+
"strings"
9+
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
11+
"github.com/lib/pq"
12+
)
13+
14+
const (
15+
serverNameAttr = "server_name"
16+
serverTypeAttr = "server_type"
17+
serverVersionAttr = "server_version"
18+
serverOwnerAttr = "server_owner"
19+
serverFDWAttr = "fdw_name"
20+
serverOptionsAttr = "options"
21+
serverDropCascadeAttr = "drop_cascade"
22+
)
23+
24+
func resourcePostgreSQLServer() *schema.Resource {
25+
return &schema.Resource{
26+
Create: PGResourceFunc(resourcePostgreSQLServerCreate),
27+
Read: PGResourceFunc(resourcePostgreSQLServerRead),
28+
Update: PGResourceFunc(resourcePostgreSQLServerUpdate),
29+
Delete: PGResourceFunc(resourcePostgreSQLServerDelete),
30+
Importer: &schema.ResourceImporter{
31+
StateContext: schema.ImportStatePassthroughContext,
32+
},
33+
34+
Schema: map[string]*schema.Schema{
35+
serverNameAttr: {
36+
Type: schema.TypeString,
37+
Required: true,
38+
Description: "The name of the foreign server to be created",
39+
},
40+
serverTypeAttr: {
41+
Type: schema.TypeString,
42+
Optional: true,
43+
ForceNew: true,
44+
Description: "Optional server type, potentially useful to foreign-data wrappers",
45+
},
46+
serverVersionAttr: {
47+
Type: schema.TypeString,
48+
Optional: true,
49+
Description: "Optional server version, potentially useful to foreign-data wrappers.",
50+
},
51+
serverFDWAttr: {
52+
Type: schema.TypeString,
53+
Required: true,
54+
ForceNew: true,
55+
Description: "The name of the foreign-data wrapper that manages the server",
56+
},
57+
serverOwnerAttr: {
58+
Type: schema.TypeString,
59+
Optional: true,
60+
Computed: true,
61+
Description: "The user name of the new owner of the foreign server",
62+
},
63+
serverOptionsAttr: {
64+
Type: schema.TypeMap,
65+
Elem: &schema.Schema{
66+
Type: schema.TypeString,
67+
},
68+
Optional: true,
69+
Description: "This clause specifies the options for the server. The options typically define the connection details of the server, but the actual names and values are dependent on the server's foreign-data wrapper",
70+
},
71+
serverDropCascadeAttr: {
72+
Type: schema.TypeBool,
73+
Optional: true,
74+
Default: false,
75+
Description: "Automatically drop objects that depend on the server (such as user mappings), and in turn all objects that depend on those objects. Drop RESTRICT is the default",
76+
},
77+
},
78+
}
79+
}
80+
81+
func resourcePostgreSQLServerCreate(db *DBConnection, d *schema.ResourceData) error {
82+
if !db.featureSupported(featureServer) {
83+
return fmt.Errorf(
84+
"Foreign Server resource is not supported for this Postgres version (%s)",
85+
db.version,
86+
)
87+
}
88+
89+
serverName := d.Get(serverNameAttr).(string)
90+
91+
b := bytes.NewBufferString("CREATE SERVER ")
92+
fmt.Fprint(b, pq.QuoteIdentifier(serverName))
93+
94+
if v, ok := d.GetOk(serverTypeAttr); ok {
95+
fmt.Fprint(b, " TYPE ", pq.QuoteLiteral(v.(string)))
96+
}
97+
98+
if v, ok := d.GetOk(serverVersionAttr); ok {
99+
fmt.Fprint(b, " VERSION ", pq.QuoteLiteral(v.(string)))
100+
}
101+
102+
fmt.Fprint(b, " FOREIGN DATA WRAPPER ", pq.QuoteIdentifier(d.Get(serverFDWAttr).(string)))
103+
104+
if options, ok := d.GetOk(serverOptionsAttr); ok {
105+
fmt.Fprint(b, " OPTIONS ( ")
106+
cnt := 0
107+
len := len(options.(map[string]interface{}))
108+
for k, v := range options.(map[string]interface{}) {
109+
fmt.Fprint(b, " ", pq.QuoteIdentifier(k), " ", pq.QuoteLiteral(v.(string)))
110+
if cnt < len-1 {
111+
fmt.Fprint(b, ", ")
112+
}
113+
cnt++
114+
}
115+
fmt.Fprint(b, " ) ")
116+
}
117+
118+
txn, err := startTransaction(db.client, "")
119+
if err != nil {
120+
return err
121+
}
122+
defer deferredRollback(txn)
123+
124+
sql := b.String()
125+
if _, err := txn.Exec(sql); err != nil {
126+
return err
127+
}
128+
129+
if v, ok := d.GetOk(serverOwnerAttr); ok {
130+
currentUser, err := getCurrentUser(txn)
131+
if err != nil {
132+
return err
133+
}
134+
if v != currentUser {
135+
if err := setServerOwner(txn, d); err != nil {
136+
return err
137+
}
138+
}
139+
}
140+
141+
if err = txn.Commit(); err != nil {
142+
return fmt.Errorf("Error creating server: %w", err)
143+
}
144+
145+
d.SetId(d.Get(serverNameAttr).(string))
146+
147+
return resourcePostgreSQLServerReadImpl(db, d)
148+
}
149+
150+
func resourcePostgreSQLServerRead(db *DBConnection, d *schema.ResourceData) error {
151+
if !db.featureSupported(featureServer) {
152+
return fmt.Errorf(
153+
"Foreign Server resource is not supported for this Postgres version (%s)",
154+
db.version,
155+
)
156+
}
157+
158+
return resourcePostgreSQLServerReadImpl(db, d)
159+
}
160+
161+
func resourcePostgreSQLServerReadImpl(db *DBConnection, d *schema.ResourceData) error {
162+
serverName := d.Get(serverNameAttr).(string)
163+
txn, err := startTransaction(db.client, "")
164+
if err != nil {
165+
return err
166+
}
167+
defer deferredRollback(txn)
168+
169+
var serverType, serverVersion, serverOwner, serverFDW string
170+
var serverOptions []string
171+
query := `SELECT COALESCE(fs.srvtype, ''), COALESCE(fs.srvversion, ''), fs.srvowner::regrole, fs.srvoptions, w.fdwname ` +
172+
`FROM pg_foreign_server fs JOIN pg_foreign_data_wrapper w on w.oid = fs.srvfdw ` +
173+
`WHERE fs.srvname = $1`
174+
err = txn.QueryRow(query, serverName).Scan(&serverType, &serverVersion, &serverOwner, pq.Array(&serverOptions), &serverFDW)
175+
switch {
176+
case err == sql.ErrNoRows:
177+
log.Printf("[WARN] PostgreSQL foreign server (%s) not found", serverName)
178+
d.SetId("")
179+
return nil
180+
case err != nil:
181+
return fmt.Errorf("Error reading foreign server: %w", err)
182+
}
183+
184+
mappedOptions := make(map[string]interface{})
185+
for _, v := range serverOptions {
186+
pair := strings.Split(v, "=")
187+
mappedOptions[pair[0]] = pair[1]
188+
}
189+
190+
d.Set(serverNameAttr, serverName)
191+
d.Set(serverTypeAttr, serverType)
192+
d.Set(serverVersionAttr, serverVersion)
193+
d.Set(serverOwnerAttr, serverOwner)
194+
d.Set(serverOptionsAttr, mappedOptions)
195+
d.Set(serverFDWAttr, serverFDW)
196+
d.SetId(serverName)
197+
198+
return nil
199+
}
200+
201+
func resourcePostgreSQLServerDelete(db *DBConnection, d *schema.ResourceData) error {
202+
if !db.featureSupported(featureServer) {
203+
return fmt.Errorf(
204+
"Foreign Server resource is not supported for this Postgres version (%s)",
205+
db.version,
206+
)
207+
}
208+
209+
serverName := d.Get(serverNameAttr).(string)
210+
211+
txn, err := startTransaction(db.client, "")
212+
if err != nil {
213+
return err
214+
}
215+
defer deferredRollback(txn)
216+
217+
dropMode := "RESTRICT"
218+
if d.Get(serverDropCascadeAttr).(bool) {
219+
dropMode = "CASCADE"
220+
}
221+
222+
sql := fmt.Sprintf("DROP SERVER %s %s ", pq.QuoteIdentifier(serverName), dropMode)
223+
if _, err := txn.Exec(sql); err != nil {
224+
return err
225+
}
226+
227+
if err = txn.Commit(); err != nil {
228+
return fmt.Errorf("Error deleting server: %w", err)
229+
}
230+
231+
d.SetId("")
232+
233+
return nil
234+
}
235+
236+
func resourcePostgreSQLServerUpdate(db *DBConnection, d *schema.ResourceData) error {
237+
if !db.featureSupported(featureServer) {
238+
return fmt.Errorf(
239+
"Foreign Server resource is not supported for this Postgres version (%s)",
240+
db.version,
241+
)
242+
}
243+
244+
txn, err := startTransaction(db.client, "")
245+
if err != nil {
246+
return err
247+
}
248+
defer deferredRollback(txn)
249+
250+
if err := setServerNameIfChanged(txn, d); err != nil {
251+
return err
252+
}
253+
254+
if err := setServerOwnerIfChanged(txn, d); err != nil {
255+
return err
256+
}
257+
258+
if err := setServerVersionOptionsIfChanged(txn, d); err != nil {
259+
return err
260+
}
261+
262+
if err = txn.Commit(); err != nil {
263+
return fmt.Errorf("Error updating foreign server: %w", err)
264+
}
265+
266+
return resourcePostgreSQLServerReadImpl(db, d)
267+
}
268+
269+
func setServerVersionOptionsIfChanged(txn *sql.Tx, d *schema.ResourceData) error {
270+
if !d.HasChange(serverVersionAttr) && !d.HasChange(serverOptionsAttr) {
271+
return nil
272+
}
273+
274+
b := bytes.NewBufferString("ALTER SERVER ")
275+
serverName := d.Get(serverNameAttr).(string)
276+
277+
fmt.Fprintf(b, "%s ", pq.QuoteIdentifier(serverName))
278+
279+
if d.HasChange(serverVersionAttr) {
280+
fmt.Fprintf(b, "VERSION %s", pq.QuoteLiteral(d.Get(serverVersionAttr).(string)))
281+
}
282+
283+
if d.HasChange(serverOptionsAttr) {
284+
oldOptions, newOptions := d.GetChange(serverOptionsAttr)
285+
fmt.Fprint(b, " OPTIONS ( ")
286+
cnt := 0
287+
len := len(newOptions.(map[string]interface{}))
288+
toRemove := oldOptions.(map[string]interface{})
289+
for k, v := range newOptions.(map[string]interface{}) {
290+
operation := "ADD"
291+
if oldOptions.(map[string]interface{})[k] != nil {
292+
operation = "SET"
293+
delete(toRemove, k)
294+
}
295+
fmt.Fprintf(b, " %s %s %s ", operation, pq.QuoteIdentifier(k), pq.QuoteLiteral(v.(string)))
296+
if cnt < len-1 {
297+
fmt.Fprint(b, ", ")
298+
}
299+
cnt++
300+
}
301+
302+
for k := range toRemove {
303+
if cnt != 0 { // starting with 0 means to drop all the options. Cannot start with comma
304+
fmt.Fprint(b, " , ")
305+
}
306+
fmt.Fprintf(b, " DROP %s ", pq.QuoteIdentifier(k))
307+
cnt++
308+
}
309+
310+
fmt.Fprint(b, " ) ")
311+
}
312+
313+
sql := b.String()
314+
if _, err := txn.Exec(sql); err != nil {
315+
return fmt.Errorf("Error updating foreign server version and/or options: %w", err)
316+
}
317+
318+
return nil
319+
}
320+
321+
func setServerNameIfChanged(txn *sql.Tx, d *schema.ResourceData) error {
322+
if !d.HasChange(serverNameAttr) {
323+
return nil
324+
}
325+
326+
serverOldName, serverNewName := d.GetChange(serverNameAttr)
327+
328+
b := bytes.NewBufferString("ALTER SERVER ")
329+
fmt.Fprintf(b, "%s RENAME TO %s", pq.QuoteIdentifier(serverOldName.(string)), pq.QuoteIdentifier(serverNewName.(string)))
330+
331+
sql := b.String()
332+
if _, err := txn.Exec(sql); err != nil {
333+
return fmt.Errorf("Error updating foreign server name: %w", err)
334+
}
335+
336+
return nil
337+
}
338+
339+
func setServerOwnerIfChanged(txn *sql.Tx, d *schema.ResourceData) error {
340+
if !d.HasChange(serverOwnerAttr) {
341+
return nil
342+
}
343+
return setServerOwner(txn, d)
344+
}
345+
346+
func setServerOwner(txn *sql.Tx, d *schema.ResourceData) error {
347+
serverName := d.Get(serverNameAttr).(string)
348+
serverNewOwner := d.Get(serverOwnerAttr).(string)
349+
350+
b := bytes.NewBufferString("ALTER SERVER ")
351+
fmt.Fprintf(b, "%s OWNER TO %s", pq.QuoteIdentifier(serverName), pq.QuoteIdentifier(serverNewOwner))
352+
353+
sql := b.String()
354+
if _, err := txn.Exec(sql); err != nil {
355+
return fmt.Errorf("Error updating foreign server owner: %w", err)
356+
}
357+
358+
return nil
359+
}

0 commit comments

Comments
 (0)