-
Notifications
You must be signed in to change notification settings - Fork 673
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(Cloud Databases): Create and Delete Logical Replication Slots for databases-for-postgresql #4116
feat(Cloud Databases): Create and Delete Logical Replication Slots for databases-for-postgresql #4116
Changes from 4 commits
7f636ce
dc7ed24
349dccb
4f61e10
7a2fac0
44dd3dc
6257092
23c3ce8
2b78695
700ae1c
e299e4c
84cf939
06aafc4
1a7625d
866c817
7a099a4
a8bcaf1
99334ca
8a74dbb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -338,7 +338,7 @@ func ResourceIBMDatabaseInstance() *schema.Resource { | |||
Description: "User name", | ||||
Type: schema.TypeString, | ||||
Required: true, | ||||
ValidateFunc: validation.StringLenBetween(5, 32), | ||||
ValidateFunc: validation.StringLenBetween(4, 32), | ||||
}, | ||||
"password": { | ||||
Description: "User password", | ||||
|
@@ -467,6 +467,29 @@ func ResourceIBMDatabaseInstance() *schema.Resource { | |||
}, | ||||
}, | ||||
}, | ||||
"logical_replication_slot": { | ||||
Type: schema.TypeSet, | ||||
Optional: true, | ||||
Elem: &schema.Resource{ | ||||
Schema: map[string]*schema.Schema{ | ||||
"name": { | ||||
Description: "Logical Replication Slot name", | ||||
Type: schema.TypeString, | ||||
Required: true, | ||||
}, | ||||
"database_name": { | ||||
Description: "Logical Replication Slot name", | ||||
Type: schema.TypeString, | ||||
Required: true, | ||||
}, | ||||
"plugin_type": { | ||||
Description: "Logical Replication Slot name", | ||||
Type: schema.TypeString, | ||||
Required: true, | ||||
}, | ||||
}, | ||||
}, | ||||
}, | ||||
"group": { | ||||
Type: schema.TypeSet, | ||||
Optional: true, | ||||
|
@@ -1561,35 +1584,39 @@ func resourceIBMDatabaseInstanceCreate(context context.Context, d *schema.Resour | |||
|
||||
for _, user := range userList.(*schema.Set).List() { | ||||
userEl := user.(map[string]interface{}) | ||||
createDatabaseUserRequestUserModel := &clouddatabasesv5.User{ | ||||
Username: core.StringPtr(userEl["name"].(string)), | ||||
Password: core.StringPtr(userEl["password"].(string)), | ||||
} | ||||
if userEl["name"].(string) == "repl" && (strings.Contains(serviceName, "postgresql") || strings.Contains(serviceName, "enterprisedb")) { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of a special case for the |
||||
updateReplUser(userEl["password"].(string), instanceID, userEl["type"].(string), meta, d) | ||||
} else { | ||||
createDatabaseUserRequestUserModel := &clouddatabasesv5.User{ | ||||
Username: core.StringPtr(userEl["name"].(string)), | ||||
Password: core.StringPtr(userEl["password"].(string)), | ||||
} | ||||
|
||||
// User Role only for ops_manager user type | ||||
if userEl["type"].(string) == "ops_manager" && userEl["role"].(string) != "" { | ||||
createDatabaseUserRequestUserModel.Role = core.StringPtr(userEl["role"].(string)) | ||||
} | ||||
// User Role only for ops_manager user type | ||||
if userEl["type"].(string) == "ops_manager" && userEl["role"].(string) != "" { | ||||
createDatabaseUserRequestUserModel.Role = core.StringPtr(userEl["role"].(string)) | ||||
} | ||||
|
||||
instanceId := d.Id() | ||||
createDatabaseUserOptions := &clouddatabasesv5.CreateDatabaseUserOptions{ | ||||
ID: &instanceId, | ||||
UserType: core.StringPtr(userEl["type"].(string)), | ||||
User: createDatabaseUserRequestUserModel, | ||||
} | ||||
instanceId := d.Id() | ||||
createDatabaseUserOptions := &clouddatabasesv5.CreateDatabaseUserOptions{ | ||||
ID: &instanceId, | ||||
UserType: core.StringPtr(userEl["type"].(string)), | ||||
User: createDatabaseUserRequestUserModel, | ||||
} | ||||
|
||||
createDatabaseUserResponse, response, err := cloudDatabasesClient.CreateDatabaseUser(createDatabaseUserOptions) | ||||
createDatabaseUserResponse, response, err := cloudDatabasesClient.CreateDatabaseUser(createDatabaseUserOptions) | ||||
|
||||
if err != nil { | ||||
return diag.FromErr(fmt.Errorf("CreateDatabaseUser (%s) failed %s\n%s", userEl["name"], err, response)) | ||||
} | ||||
if err != nil { | ||||
return diag.FromErr(fmt.Errorf("CreateDatabaseUser (%s) failed %s\n%s", userEl["name"], err, response)) | ||||
} | ||||
|
||||
taskID := *createDatabaseUserResponse.Task.ID | ||||
taskID := *createDatabaseUserResponse.Task.ID | ||||
|
||||
_, err = waitForDatabaseTaskComplete(taskID, d, meta, d.Timeout(schema.TimeoutCreate)) | ||||
if err != nil { | ||||
return diag.FromErr(fmt.Errorf( | ||||
"[ERROR] Error waiting for update of database (%s) user (%s) create task to complete: %s", d.Id(), userEl["name"], err)) | ||||
_, err = waitForDatabaseTaskComplete(taskID, d, meta, d.Timeout(schema.TimeoutCreate)) | ||||
if err != nil { | ||||
return diag.FromErr(fmt.Errorf( | ||||
"[ERROR] Error waiting for update of database (%s) user (%s) create task to complete: %s", d.Id(), userEl["name"], err)) | ||||
} | ||||
} | ||||
} | ||||
} | ||||
|
@@ -2169,6 +2196,7 @@ func resourceIBMDatabaseInstanceUpdate(context context.Context, d *schema.Resour | |||
} | ||||
|
||||
for _, change := range userChanges { | ||||
service := d.Get("service").(string) | ||||
// Update Database User password only | ||||
if change.Old != nil && change.New != nil { | ||||
// No change | ||||
|
@@ -2209,6 +2237,14 @@ func resourceIBMDatabaseInstanceUpdate(context context.Context, d *schema.Resour | |||
change.Old = nil | ||||
} | ||||
|
||||
newName, ok := change.New["name"] | ||||
isPgOrEdb := strings.Contains(service, "postgresql") || strings.Contains(service, "enterprisedb") | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PG only There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Logical Replication is PG only :D But the repl user exists in both edb and pg. So we need to handle it for both cases. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than create a special case for the
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Smart! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Went ahead and did it! |
||||
if ok && isPgOrEdb && newName.(string) == "repl" { | ||||
updateReplUser(change.New["password"].(string), instanceID, change.New["type"].(string), meta, d) | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if this method is necessary can it be a generic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done :D |
||||
|
||||
continue | ||||
} | ||||
|
||||
// Delete Old User | ||||
if change.Old != nil { | ||||
deleteDatabaseUserOptions := &clouddatabasesv5.DeleteDatabaseUserOptions{ | ||||
|
@@ -2267,6 +2303,85 @@ func resourceIBMDatabaseInstanceUpdate(context context.Context, d *schema.Resour | |||
} | ||||
} | ||||
|
||||
if d.HasChange("logical_replication_slot") { | ||||
cloudDatabasesClient, err := meta.(conns.ClientSession).CloudDatabasesV5() | ||||
if err != nil { | ||||
return diag.FromErr(fmt.Errorf("[ERROR] Error getting database client settings: %s", err)) | ||||
} | ||||
|
||||
oldSlots, newSlots := d.GetChange("logical_replication_slot") | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we can't update anything, we don't need to track changes to the state. Something like this may be more straightforwardL oldList, newList := d.GetChange("logical_replication_slot")
if oldList == nil {
oldList = new(schema.Set)
}
if newList == nil {
newList = new(schema.Set)
}
os := oldList.(*schema.Set)
ns := newList.(*schema.Set)
remove := os.Difference(ns).List()
add := ns.Difference(os).List()
if len(add) > 0 {
for _, slot := range add {
addSlot := slot.(map[string]interface{})
logicalReplicationSlot := &clouddatabasesv5.LogicalReplicationSlot{
Name: core.StringPtr(addSlot["name"].(string)),
DatabaseName: core.StringPtr(addSlot["database_name"].(string)),
PluginType: core.StringPtr(addSlot["plugin_type"].(string)),
}
// ...
}
}
if len(remove) > 0 {
for _, slot := range remove {
removeSlot := slot.(map[string]interface{})
// ...
}
} |
||||
slotChanges := make(map[string]*userChange) | ||||
slotKey := func(raw map[string]interface{}) string { | ||||
return fmt.Sprintf("%s-%s", raw["name"].(string), raw["database_name"].(string)) | ||||
} | ||||
|
||||
for _, raw := range oldSlots.(*schema.Set).List() { | ||||
user := raw.(map[string]interface{}) | ||||
k := slotKey(user) | ||||
slotChanges[k] = &userChange{Old: user} | ||||
} | ||||
|
||||
for _, raw := range newSlots.(*schema.Set).List() { | ||||
user := raw.(map[string]interface{}) | ||||
k := slotKey(user) | ||||
if _, ok := slotChanges[k]; !ok { | ||||
slotChanges[k] = &userChange{} | ||||
} | ||||
slotChanges[k].New = user | ||||
} | ||||
|
||||
for _, change := range slotChanges { | ||||
// Create New Logical Rep Slot | ||||
if change.New != nil { | ||||
logicalReplicationSlot := &clouddatabasesv5.LogicalReplicationSlot{ | ||||
Name: core.StringPtr(change.New["name"].(string)), | ||||
DatabaseName: core.StringPtr(change.New["database_name"].(string)), | ||||
PluginType: core.StringPtr(change.New["plugin_type"].(string)), | ||||
} | ||||
|
||||
createLogicalReplicationOptions := &clouddatabasesv5.CreateLogicalReplicationSlotOptions{ | ||||
ID: &instanceID, | ||||
LogicalReplicationSlot: logicalReplicationSlot, | ||||
} | ||||
|
||||
createLogicalRepSlotResponse, response, err := cloudDatabasesClient.CreateLogicalReplicationSlot(createLogicalReplicationOptions) | ||||
if err != nil { | ||||
return diag.FromErr(fmt.Errorf("[ERROR] CreateLogicalReplicationSlot (%s) failed %s\n%s", *createLogicalReplicationOptions.LogicalReplicationSlot.Name, err, response)) | ||||
} | ||||
|
||||
taskID := *createLogicalRepSlotResponse.Task.ID | ||||
_, err = waitForDatabaseTaskComplete(taskID, d, meta, d.Timeout(schema.TimeoutUpdate)) | ||||
if err != nil { | ||||
return diag.FromErr(fmt.Errorf( | ||||
"[ERROR] Error waiting for database (%s) logical replication slot (%s) create task to complete: %s", instanceID, *createLogicalReplicationOptions.LogicalReplicationSlot.Name, err)) | ||||
} | ||||
} | ||||
|
||||
// Delete Old Logical Rep Slot | ||||
if change.Old != nil { | ||||
deleteDatabaseUserOptions := &clouddatabasesv5.DeleteLogicalReplicationSlotOptions{ | ||||
ID: &instanceID, | ||||
Name: core.StringPtr(change.Old["name"].(string)), | ||||
} | ||||
|
||||
deleteDatabaseUserResponse, response, err := cloudDatabasesClient.DeleteLogicalReplicationSlot(deleteDatabaseUserOptions) | ||||
|
||||
if err != nil { | ||||
return diag.FromErr(fmt.Errorf( | ||||
"[ERROR] DeleteDatabaseUser (%s) failed %s\n%s", *deleteDatabaseUserOptions.Name, err, response)) | ||||
} | ||||
|
||||
taskID := *deleteDatabaseUserResponse.Task.ID | ||||
_, err = waitForDatabaseTaskComplete(taskID, d, meta, d.Timeout(schema.TimeoutUpdate)) | ||||
|
||||
if err != nil { | ||||
return diag.FromErr(fmt.Errorf( | ||||
"[ERROR] Error waiting for database (%s) logical replication slot (%s) delete task to complete: %s", icdId, *deleteDatabaseUserOptions.Name, err)) | ||||
} | ||||
} | ||||
} | ||||
} | ||||
|
||||
return resourceIBMDatabaseInstanceRead(context, d, meta) | ||||
} | ||||
|
||||
|
@@ -2939,3 +3054,35 @@ func checkV5Groups(_ context.Context, diff *schema.ResourceDiff, meta interface{ | |||
|
||||
return nil | ||||
} | ||||
|
||||
func updateReplUser(password string, instanceID string, userType string, meta interface{}, d *schema.ResourceData) diag.Diagnostics { | ||||
cloudDatabasesClient, err := meta.(conns.ClientSession).CloudDatabasesV5() | ||||
passwordSettingUser := &clouddatabasesv5.APasswordSettingUser{ | ||||
Password: core.StringPtr(password), | ||||
} | ||||
|
||||
changeUserPasswordOptions := &clouddatabasesv5.ChangeUserPasswordOptions{ | ||||
ID: &instanceID, | ||||
UserType: core.StringPtr(userType), | ||||
Username: core.StringPtr("repl"), | ||||
User: passwordSettingUser, | ||||
} | ||||
|
||||
changeUserPasswordResponse, response, err := cloudDatabasesClient.ChangeUserPassword(changeUserPasswordOptions) | ||||
|
||||
if response.StatusCode != 404 { | ||||
if err != nil { | ||||
return diag.FromErr(fmt.Errorf("[ERROR] ChangeUserPassword (%s) failed %s\n%s", *changeUserPasswordOptions.Username, err, response)) | ||||
} | ||||
|
||||
taskID := *changeUserPasswordResponse.Task.ID | ||||
_, err = waitForDatabaseTaskComplete(taskID, d, meta, d.Timeout(schema.TimeoutUpdate)) | ||||
|
||||
if err != nil { | ||||
return diag.FromErr(fmt.Errorf( | ||||
"[ERROR] Error waiting for database (%s) user (%s) password update task to complete: %s", instanceID, *changeUserPasswordOptions.Username, err)) | ||||
} | ||||
} | ||||
|
||||
return nil | ||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should have validation to enforce only
postgresql
deployments can uselogical_replication_slot
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Took your recommendation and added it here:
terraform-provider-ibm/ibm/service/database/resource_ibm_database.go
Line 2301 in 7a2fac0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we might want to do this in a
CustomizeDiff
function since this will blow up during runtime rather than during the plan stageThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done :D