Skip to content
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

Deprecate Whitelist for IBM-cloud-databases. Introduce allowlisting #3852

Merged
merged 20 commits into from
Nov 3, 2022
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
740b229
works with local vendor changes.
omaraibrahim Jun 8, 2022
5c2a8a1
working using cloud databases v5 now
omaraibrahim Jun 16, 2022
53c40e1
Merge branch 'IBM-Cloud:master' into icd-allowlist
omaraibrahim Jun 16, 2022
992c917
updated deprecation message
omaraibrahim Jun 16, 2022
975774a
data_source_ibm_database and some tests
omaraibrahim Jun 17, 2022
8dc4474
Merge branch 'icd-allowlist' of https://github.com/omaraibrahim/terra…
omaraibrahim Jun 17, 2022
ef0a37f
modified the rest of the resource tests
omaraibrahim Jun 17, 2022
f635561
fixed calling d.getOK for computed variable
omaraibrahim Jun 21, 2022
a241f8d
Merge branch 'master' of https://github.com/IBM-Cloud/terraform-provi…
omaraibrahim Jun 28, 2022
a75bb41
Merge branch 'master' of https://github.com/IBM-Cloud/terraform-provi…
omaraibrahim Aug 30, 2022
fb40772
potential import issue fix
omaraibrahim Aug 30, 2022
d84700d
Merge branch 'master' of https://github.com/IBM-Cloud/terraform-provi…
omaraibrahim Sep 7, 2022
cb2a144
Merge branch 'master' of https://github.com/IBM-Cloud/terraform-provi…
omaraibrahim Sep 12, 2022
22a4573
added migration test
omaraibrahim Sep 13, 2022
26fe2bc
Merge branch 'master' of https://github.com/IBM-Cloud/terraform-provi…
omaraibrahim Oct 28, 2022
470cee1
fixed migration bug
omaraibrahim Oct 28, 2022
05da7d3
fixed allowlist removal bug
omaraibrahim Nov 2, 2022
44017d4
setting import state verify to false due to terraform bug
omaraibrahim Nov 2, 2022
b7773ac
removed extra logs
omaraibrahim Nov 2, 2022
9845af3
updated docs
omaraibrahim Nov 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions ibm/flex/structures.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/IBM-Cloud/bluemix-go/models"
"github.com/IBM-Cloud/container-services-go-sdk/kubernetesserviceapiv1"
"github.com/IBM-Cloud/terraform-provider-ibm/ibm/conns"
"github.com/IBM/cloud-databases-go-sdk/clouddatabasesv5"
"github.com/IBM/go-sdk-core/v5/core"
"github.com/IBM/ibm-cos-sdk-go-config/resourceconfigurationv1"
"github.com/IBM/ibm-cos-sdk-go/service/s3"
Expand Down Expand Up @@ -1614,6 +1615,19 @@ func ExpandWhitelist(whiteList *schema.Set) (whitelist []icdv4.WhitelistEntry) {
return
}

// IBM Cloud Databases
func ExpandAllowlist(allowList *schema.Set) (allowlist []clouddatabasesv5.AllowlistEntry) {
for _, iface := range allowList.List() {
alItem := iface.(map[string]interface{})
alEntry := &clouddatabasesv5.AllowlistEntry{
Address: core.StringPtr(alItem["address"].(string)),
Description: core.StringPtr(alItem["description"].(string)),
}
allowlist = append(allowlist, *alEntry)
}
return
}

// Cloud Internet Services
func FlattenWhitelist(whitelist icdv4.Whitelist) []map[string]interface{} {
entries := make([]map[string]interface{}, len(whitelist.WhitelistEntrys), len(whitelist.WhitelistEntrys))
Expand All @@ -1627,6 +1641,19 @@ func FlattenWhitelist(whitelist icdv4.Whitelist) []map[string]interface{} {
return entries
}

// Cloud Internet Services
func FlattenGetAllowlist(allowlist clouddatabasesv5.GetAllowlistResponse) []map[string]interface{} {
entries := make([]map[string]interface{}, len(allowlist.IPAddresses), len(allowlist.IPAddresses))
for i, allowlistEntry := range allowlist.IPAddresses {
l := map[string]interface{}{
"address": allowlistEntry.Address,
"description": allowlistEntry.Description,
}
entries[i] = l
}
return entries
}

func expandStringMap(inVal interface{}) map[string]string {
outVal := make(map[string]string)
if inVal == nil {
Expand Down
33 changes: 33 additions & 0 deletions ibm/service/database/data_source_ibm_database.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/IBM-Cloud/bluemix-go/models"
"github.com/IBM-Cloud/terraform-provider-ibm/ibm/conns"
"github.com/IBM-Cloud/terraform-provider-ibm/ibm/flex"
"github.com/IBM/cloud-databases-go-sdk/clouddatabasesv5"
)

func DataSourceIBMDatabaseInstance() *schema.Resource {
Expand Down Expand Up @@ -227,6 +228,25 @@ func DataSourceIBMDatabaseInstance() *schema.Resource {
},
},
},
Deprecated: "The whitelist field is deprecated please use allowlist",
},
"allowlist": {
Type: schema.TypeSet,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"address": {
Description: "Allowlist IP address in CIDR notation",
Type: schema.TypeString,
Computed: true,
},
"description": {
Description: "Unique white list description",
Type: schema.TypeString,
Computed: true,
},
},
},
},
"groups": {
Type: schema.TypeList,
Expand Down Expand Up @@ -706,6 +726,19 @@ func dataSourceIBMDatabaseInstanceRead(d *schema.ResourceData, meta interface{})
}
d.Set("whitelist", flex.FlattenWhitelist(whitelist))

cloudDatabasesClient, err := meta.(conns.ClientSession).CloudDatabasesV5()
alEntry := &clouddatabasesv5.GetAllowlistOptions{
ID: &instance.ID,
}

allowlist, _, err := cloudDatabasesClient.GetAllowlist(alEntry)

if err != nil {
return fmt.Errorf("[ERROR] Error getting database allowlist: %s", err)
}

d.Set("allowlist", flex.FlattenGetAllowlist(*allowlist))

connectionEndpoint := "public"
if instance.Parameters != nil {
if endpoint, ok := instance.Parameters["service-endpoints"]; ok {
Expand Down
1 change: 1 addition & 0 deletions ibm/service/database/data_source_ibm_database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func TestAccIBMDatabaseDataSource_basic(t *testing.T) {
resource.TestCheckResourceAttr(dataName, "members_memory_allocation_mb", "2048"),
resource.TestCheckResourceAttr(dataName, "members_disk_allocation_mb", "10240"),
resource.TestCheckResourceAttr(dataName, "whitelist.#", "0"),
resource.TestCheckResourceAttr(dataName, "allowlist.#", "0"),
resource.TestCheckResourceAttr(dataName, "connectionstrings.#", "1"),
resource.TestCheckResourceAttr(dataName, "connectionstrings.0.name", "admin"),
resource.TestCheckResourceAttr(dataName, "connectionstrings.0.hosts.#", "1"),
Expand Down
149 changes: 145 additions & 4 deletions ibm/service/database/resource_ibm_database.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,29 @@ func ResourceIBMDatabaseInstance() *schema.Resource {
},
},
},
Deprecated: "Whitelist is deprecated please use allowlist",
ConflictsWith: []string{"allowlist"},
},
"allowlist": {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we set the v5 API response of allowlist to whitelist (because the schema looks same for both) it just flatten and setting the same

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @hkantare!

They're two different apis endpoints, and despite them being very similar, I think it's safer to keep them separate. Whitelist is depreciated and will be removed soon as well. Hence, I think a separation of concerns is better. Here are the api docs for both. #3852 (comment)

Thanks!
Omar

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes ...v4/v5 API are two different endpoint with same model structure and functionality ...cant we just handle in the backend (i mean resource/datasource) to migrate to V5 AP's without any change to schema.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hkantare so the responses are the same yes :D But the supporting functionality is not. Meaning, the autogenerated allowlist functions utilize allowlist structs the use pointers. For example, https://github.com/IBM/cloud-databases-go-sdk/blob/de91b81e136fd4c53c0000e87932aaf150658a33/clouddatabasesv5/cloud_databases_v5.go#L1848

In contrast the whitelist functions utilizes a whitelist struct that does not use pointers. An example of the functions that consume structs that are shaped differently can be found here: https://github.com/IBM-Cloud/bluemix-go/blob/d538cb4fd9bece50f7b012496df63b3f62d58662/api/icd/icdv4/whitelist.go#L38

So despite achieving the same result, the code is structured very differently and the structs that allowlist relies on are different than whitelist. For example, AllowlistEntry is written as follows: https://github.com/IBM/cloud-databases-go-sdk/blob/de91b81e136fd4c53c0000e87932aaf150658a33/clouddatabasesv5/cloud_databases_v5.go#L2046
While WhitelistEntry is written as follows https://github.com/IBM-Cloud/bluemix-go/blob/d538cb4fd9bece50f7b012496df63b3f62d58662/api/icd/icdv4/whitelist.go#L13

My concern of just setting the v5 response of allowlist to whitelist is we'll have to do lots of downstream changes that will break things or cause unexpected bugs.

Copy link
Collaborator Author

@omaraibrahim omaraibrahim Sep 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I also tested the instances using import and it works :)

Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"address": {
Description: "Allowlist IP address in CIDR notation",
Type: schema.TypeString,
Optional: true,
ValidateFunc: validate.ValidateCIDR,
},
"description": {
Description: "Unique allow list description",
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.StringLenBetween(1, 32),
},
},
},
ConflictsWith: []string{"whitelist"},
},
"group": {
Type: schema.TypeSet,
Expand Down Expand Up @@ -1476,6 +1499,36 @@ func resourceIBMDatabaseInstanceCreate(context context.Context, d *schema.Resour
"[ERROR] Error waiting for update of database (%s) whitelist task to complete: %s", icdId, err))
}
}
} else if al, ok := d.GetOk("allowlist"); ok {
cloudDatabasesClient, err := meta.(conns.ClientSession).CloudDatabasesV5()

if err != nil {
return diag.FromErr(fmt.Errorf("[ERROR] Error getting database client settings: %s", err))
}

add := flex.ExpandAllowlist(al.(*schema.Set))
for _, entry := range add {
holdEntry := &clouddatabasesv5.AllowlistEntry{
Address: core.StringPtr(*entry.Address),
Description: core.StringPtr(*entry.Description),
}
alEntry := &clouddatabasesv5.AddAllowlistEntryOptions{
ID: &instanceID,
IPAddress: holdEntry,
}
addAllowListResponse, _, err := cloudDatabasesClient.AddAllowlistEntry(alEntry)
if err != nil {
return diag.FromErr(fmt.Errorf(
"[ERROR] Error updating database allowlist entry: (%s)", err))
}

taskID := *addAllowListResponse.Task.ID
_, err = waitForDatabaseTaskComplete(taskID, d, meta, d.Timeout(schema.TimeoutUpdate))
if err != nil {
return diag.FromErr(fmt.Errorf(
"[ERROR] Error waiting for update of database (%s) allowlist task to complete: %s", instanceID, err))
}
}
}
if cpuRecord, ok := d.GetOk("auto_scaling.0.cpu"); ok {
params := icdv4.AutoscalingSetGroup{}
Expand Down Expand Up @@ -1710,11 +1763,25 @@ func resourceIBMDatabaseInstanceRead(context context.Context, d *schema.Resource
}
d.Set("auto_scaling", flattenICDAutoScalingGroup(autoSclaingGroup))

whitelist, err := icdClient.Whitelists().GetWhitelist(icdId)
if err != nil {
return diag.FromErr(fmt.Errorf("[ERROR] Error getting database whitelist: %s", err))
if _, ok := d.GetOk("whitelist"); ok {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Resource Read also don't use d.Get operations because the import functionality doesn't work

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @hkantare!

Thanks for taking a look at the PR. I really appreciate it! After spending a few days trying to incorporate this requested change, I am not convinced that it is a good idea. Removing the operation actually causes bugs as seen below:

test_working_directory=/var/folders/zl/1pk63bj95xx56m3vvm0yn35m0000gn/T/plugintest824757667
  error=
  | After applying this test step and performing a `terraform refresh`, the plan was not empty.
  | stdout
  | 
  | 
  | Terraform used the selected providers to generate the following execution
  | plan. Resource actions are indicated with the following symbols:
  |   ~ update in-place
  | 
  | Terraform will perform the following actions:
  | 
  |   # ibm_database.tf-edb-58 will be updated in-place
  |   ~ resource "ibm_database" "tf-edb-58" {
  |         id                           = "crn:v1:bluemix:public:databases-for-enterprisedb:eu-gb:a/40ddc34a953a8c02f10987b59085b60e:c1ff0800-8682-4b6b-a8b7-5fa8df912a31::"
  |         name                         = "tf-edb-58"
  |         tags                         = [
  |             "one:two",
  |         ]
  |         # (25 unchanged attributes hidden)
  | 
  | 
  | 
  | 
  | 
  |       - whitelist {
  |           - address     = "172.168.1.2/32" -> null
  |           - description = "desc1" -> null
  |         }
  |         # (4 unchanged blocks hidden)
  |     }
  | 
  | Plan: 0 to add, 1 to change, 0 to destroy.
  
    resource_ibm_database_edb_test.go:25: Step 1/4 error: After applying this test step and performing a `terraform refresh`, the plan was not empty.
        stdout
        
        
        Terraform used the selected providers to generate the following execution
        plan. Resource actions are indicated with the following symbols:
          ~ update in-place
        
        Terraform will perform the following actions:
        
          # ibm_database.tf-edb-58 will be updated in-place
          ~ resource "ibm_database" "tf-edb-58" {
                id                           = "crn:v1:bluemix:public:databases-for-enterprisedb:eu-gb:a/40ddc34a953a8c02f10987b59085b60e:c1ff0800-8682-4b6b-a8b7-5fa8df912a31::"
                name                         = "tf-edb-58"
                tags                         = [
                    "one:two",
                ]
                # (25 unchanged attributes hidden)
        
        
        
        
        
              - whitelist {
                  - address     = "172.168.1.2/32" -> null
                  - description = "desc1" -> null
                }
                # (4 unchanged blocks hidden)
            }
        
        Plan: 0 to add, 1 to change, 0 to destroy.

Here's my explanation as to why d.GetOK is actually vital:

  1. getOK does work in resource read (I've confirmed this). In general we use many operations associated with the resourceDataStructure throughout the function and this isn't any different really.
  2. By taking it away, we end up setting whitelist to a value. The reason begin is the api commands for allowlist and whitelist update the same source. We do not want to set our whitelist to anything if allowlist is used, and vice versa.
  3. By not doing the above we end up setting whitelist to some value. This value then gets updated to null down the line. This is bad and can cause unintended bugs like overwriting data.
  4. Also note, whitelist and allowlist are special cases where both apis do the same exact thing. This is highly unusual, but it is what it is.

Hence, the best was to approach this is the original way which is check if allowlist or whitelist is used. Then proceed to do the api calls depending on which one is used. Both can never be set at the same time due to the conflict code, and we’ve established that d.getOK does work perfectly fine.

Thanks!
Omar

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for explanation
getOK does work in resource read but it fails during the import scenario. Let say you have created a database outside Terraform and want to import and manage in future using Terraform. We need to import the resource. (https://learn.hashicorp.com/tutorials/terraform/state-import?in=terraform/state&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS)
For import we first create a empty resource block
resource "ibm_database" "mydb" {
}
Run terraform import ibm_database.mydb
Import also calls the same resource read since the block is empty d.GetOk fails for both whitelist and allowlist

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hkantare thank you very very much for the explanation :D I've gone ahead and update the code so that if whitelist isn't there we always use allowlist. I follow the terraform docs here: https://www.terraform.io/plugin/sdkv2/best-practices/deprecations#renaming-an-optional-attribute

So if d.getOK("whitelist") fails we'll always use allowlist. lmk what you think :D

whitelist, err := icdClient.Whitelists().GetWhitelist(icdId)
if err != nil {
return diag.FromErr(fmt.Errorf("[ERROR] Error getting database whitelist: %s", err))
}
d.Set("whitelist", flex.FlattenWhitelist(whitelist))
} else if _, ok := d.GetOk("allowlist"); ok {
cloudDatabasesClient, err := meta.(conns.ClientSession).CloudDatabasesV5()
alEntry := &clouddatabasesv5.GetAllowlistOptions{
ID: &instanceID,
}

allowlist, _, err := cloudDatabasesClient.GetAllowlist(alEntry)
if err != nil {
return diag.FromErr(fmt.Errorf("[ERROR] Error getting database allowlist: %s", err))
}

d.Set("allowlist", flex.FlattenGetAllowlist(*allowlist))
}
d.Set("whitelist", flex.FlattenWhitelist(whitelist))

var connectionStrings []flex.CsEntry
//ICD does not implement a GetUsers API. Users populated from tf configuration.
Expand Down Expand Up @@ -2103,6 +2170,80 @@ func resourceIBMDatabaseInstanceUpdate(context context.Context, d *schema.Resour
"[ERROR] Error waiting for database (%s) whitelist delete task to complete for ipAddress %s : %s", icdId, ipAddress, err))
}

}
}
} else if d.HasChange("allowlist") {
cloudDatabasesClient, err := meta.(conns.ClientSession).CloudDatabasesV5()

if err != nil {
return diag.FromErr(fmt.Errorf("[ERROR] Error getting database client settings: %s", err))
}

oldList, newList := d.GetChange("allowlist")
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 _, entry := range add {
newEntry := entry.(map[string]interface{})
holdEntry := &clouddatabasesv5.AllowlistEntry{
Address: core.StringPtr(newEntry["address"].(string)),
Description: core.StringPtr(newEntry["description"].(string)),
}
alEntry := &clouddatabasesv5.AddAllowlistEntryOptions{
ID: &instanceID,
IPAddress: holdEntry,
}
addAllowListResponse, response, err := cloudDatabasesClient.AddAllowlistEntry(alEntry)
if err != nil {
return diag.FromErr(fmt.Errorf(
"[ERROR] DeleteAllowlistEntry (%s) failed %s\n%s", *addAllowListResponse.Task.Description, err, response))
}

taskID := *addAllowListResponse.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) allowlist delete task to complete for ipAddress %s : %s", instanceID, *addAllowListResponse.Task.Description, err))
}

}

}

if len(remove) > 0 {
for _, entry := range remove {
newEntry := entry.(map[string]interface{})
holdEntry := &clouddatabasesv5.AllowlistEntry{
Address: core.StringPtr(newEntry["address"].(string)),
Description: core.StringPtr(newEntry["description"].(string)),
}
alEntry := &clouddatabasesv5.DeleteAllowlistEntryOptions{
ID: &instanceID,
Ipaddress: holdEntry.Address,
}

deleteAllowListResponse, response, err := cloudDatabasesClient.DeleteAllowlistEntry(alEntry)
if err != nil {
return diag.FromErr(fmt.Errorf(
"[ERROR] DeleteAllowlistEntry (%s) failed %s\n%s", *deleteAllowListResponse.Task.Description, err, response))
}

taskID := *deleteAllowListResponse.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) allowlist delete task to complete for ipAddress %s : %s", instanceID, *deleteAllowListResponse.Task.Description, err))
}

}
}
}
Expand Down
Loading