diff --git a/bigip/resource_bigip_as3.go b/bigip/resource_bigip_as3.go index e32bb0e24..140899d04 100644 --- a/bigip/resource_bigip_as3.go +++ b/bigip/resource_bigip_as3.go @@ -294,7 +294,7 @@ func resourceBigipAs3Read(ctx context.Context, d *schema.ResourceData, meta inte as3Json := d.Get("as3_json").(string) perappMode := d.Get("per_app_mode").(bool) log.Printf("[INFO] AS3 config:%+v", as3Json) - if d.Get("as3_json") != nil && !perappMode { + if d.Get("as3_json") != nil && !perappMode && d.Get("tenant_filter") == "" { tList, _, _ = client.GetTenantList(as3Json) if createdTenants != "" && createdTenants != tList { tList = createdTenants @@ -310,17 +310,7 @@ func resourceBigipAs3Read(ctx context.Context, d *schema.ResourceData, meta inte log.Printf("[DEBUG] Applications in AS3 get call : %s", applicationList) if name != "" { as3Resp, err := client.GetAs3(name, applicationList, d.Get("per_app_mode").(bool)) - if d.Get("per_app_mode").(bool) { - as3Json := make(map[string]interface{}) - _ = json.Unmarshal([]byte(as3Resp), &as3Json) - out, _ := json.Marshal(as3Json) - as3Dec := string(out) - applicationList = client.GetAppsList(fmt.Sprintf("%v", as3Dec)) - log.Printf("[DEBUG] Application List from retreived the GET call in Read function : %s", applicationList) - _ = d.Set("application_list", applicationList) - } - log.Printf("[DEBUG] AS3 json retreived from the GET call in Read function : %s", as3Resp) if err != nil { log.Printf("[ERROR] Unable to retrieve json ") if err.Error() == "unexpected end of JSON input" { @@ -336,7 +326,24 @@ func resourceBigipAs3Read(ctx context.Context, d *schema.ResourceData, meta inte // d.SetId("") return nil } - _ = d.Set("as3_json", as3Resp) + + if d.Get("per_app_mode").(bool) { + as3Json := make(map[string]interface{}) + filteredAs3Json := make(map[string]interface{}) + _ = json.Unmarshal([]byte(as3Resp), &as3Json) + for _, value := range strings.Split(applicationList, ",") { + log.Printf("[DEBUG] Fetching AS3 get for Application : %s", value) + filteredAs3Json[value] = as3Json[value] + } + filteredAs3Json["schemaVersion"] = as3Json["schemaVersion"] + out, _ := json.Marshal(filteredAs3Json) + filteredAs3String := string(out) + log.Printf("[DEBUG] AS3 GET call in Read function : %s", filteredAs3Json) + _ = d.Set("as3_json", filteredAs3String) + } else { + _ = d.Set("as3_json", as3Resp) + } + _ = d.Set("tenant_list", name) } else if d.Get("task_id") != nil { taskResponse, err := client.Getas3TaskResponse(d.Get("task_id").(string)) @@ -445,20 +452,33 @@ func resourceBigipAs3Delete(ctx context.Context, d *schema.ResourceData, meta in tList, _, _ = client.GetTenantList(d.Get("as3_json").(string)) } - if d.Id() != "" && tList != "" { + if d.Id() != "" && tList != "" && d.Get("tenant_filter") == "" { name = tList } else { name = d.Id() } log.Printf("[INFO] Deleting As3 config for tenants:%+v", name) - err, failedTenants := client.DeleteAs3Bigip(name) - if err != nil { - log.Printf("[ERROR] Unable to DeleteContext: %v :", err) - return diag.FromErr(err) - } - if failedTenants != "" { - _ = d.Set("tenant_list", name) - return resourceBigipAs3Read(ctx, d, meta) + if d.Get("per_app_mode").(bool) { + applicationList := d.Get("application_list").(string) + log.Printf("[INFO] Deleting As3 config for Applications:%+v", applicationList) + for _, appName := range strings.Split(applicationList, ",") { + log.Printf("[INFO] Deleting AS3 for Application : %s", appName) + err := client.DeletePerApplicationAs3Bigip(name, appName) + if err != nil { + log.Printf("[ERROR] Unable to DeleteContext: %v :", err) + return diag.FromErr(err) + } + } + } else { + err, failedTenants := client.DeleteAs3Bigip(name) + if err != nil { + log.Printf("[ERROR] Unable to DeleteContext: %v :", err) + return diag.FromErr(err) + } + if failedTenants != "" { + _ = d.Set("tenant_list", name) + return resourceBigipAs3Read(ctx, d, meta) + } } d.SetId("") return nil diff --git a/bigip/resource_bigip_as3_test.go b/bigip/resource_bigip_as3_test.go index c118cd804..c68c3bcab 100644 --- a/bigip/resource_bigip_as3_test.go +++ b/bigip/resource_bigip_as3_test.go @@ -14,6 +14,7 @@ import ( "net/http" "os" "regexp" + "strings" "testing" bigip "github.com/f5devcentral/go-bigip" @@ -98,6 +99,28 @@ resource "bigip_as3" "perapp2" { ignore_metadata = true } ` +var TestAs3PerAppResource = ` +resource "bigip_as3" "as3-example" { + tenant_name = "dmz" + as3_json = "${file("` + dir + `/../examples/as3/perApplication_example.json")}" +} +` +var TestAs3PerAppResource1 = ` +resource "bigip_as3" "as3-example1" { + tenant_name = "dmz" + as3_json = "${file("` + dir + `/../examples/as3/as3_per_app_example1.json")}" +} +` +var TestAs3PerAppResource2 = ` +resource "bigip_as3" "as3-example1" { + tenant_name = "dmz" + as3_json = "${file("` + dir + `/../examples/as3/as3_per_app_example1.json")}" +} +resource "bigip_as3" "as3-example2" { + tenant_name = "dmz" + as3_json = "${file("` + dir + `/../examples/as3/as3_per_app_example2.json")}" +} +` func TestAccBigipAs3_create_SingleTenant(t *testing.T) { resource.Test(t, resource.TestCase{ @@ -434,6 +457,46 @@ func testCheckAs3Exists(name string, exists bool) resource.TestCheckFunc { } } +func testCheckAS3AppExists(tenantName, appNames string, exists bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + clientBigip := testAccProvider.Meta().(*bigip.BigIP) + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} + client := &http.Client{Transport: tr} + + for _, appName := range strings.Split(appNames, ",") { + url := clientBigip.Host + "/mgmt/shared/appsvcs/declare/" + tenantName + "/applications/" + appName + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return fmt.Errorf("[ERROR] Error while creating http request with AS3 json: %v", err) + } + req.SetBasicAuth(clientBigip.User, clientBigip.Password) + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + return err + } + + defer func() { + if err := resp.Body.Close(); err != nil { + log.Printf("[DEBUG] Could not close the request to %s", url) + } + }() + var body bytes.Buffer + _, err = io.Copy(&body, resp.Body) + // body, err := ioutil.ReadAll(resp.Body) + bodyString := body.String() + if (resp.Status == "204 No Content" || err != nil || resp.StatusCode == 404) && exists { + return fmt.Errorf("[ERROR] Error while checking as3resource present in bigip :%s %v", bodyString, err) + } + } + + return nil + } +} + func TestAccBigipAs3_badJSON(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { @@ -464,3 +527,66 @@ func testCheckAs3Destroy(s *terraform.State) error { } return nil } + +func TestAccBigipPer_AppAs3_SingleTenant(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAcctPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testCheckAs3Destroy, + Steps: []resource.TestStep{ + { + Config: TestAs3PerAppResource, + Check: resource.ComposeTestCheckFunc( + testCheckAs3Exists("dmz", true), + testCheckAS3AppExists("dmz", "Application1,Application2", true), + ), + }, + }, + }) +} + +func TestAccBigipPer_AppAs3_update_addApplication(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAcctPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testCheckAs3Destroy, + Steps: []resource.TestStep{ + { + Config: TestAs3PerAppResource1, + + Check: resource.ComposeTestCheckFunc( + testCheckAs3Exists("dmz", true), + testCheckAS3AppExists("dmz", "path_app1", true), + ), + }, + { + Config: TestAs3PerAppResource2, + Check: resource.ComposeTestCheckFunc( + testCheckAs3Exists("dmz", true), + testCheckAS3AppExists("dmz", "path_app1,path_app2", true), + ), + }, + }, + }) +} + +// Per-App mode is disabled +func TestAccBigipPer_AppAs3_update_invalidJson(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAcctPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testCheckAs3Destroy, + Steps: []resource.TestStep{ + { + Config: TestAs3PerAppResource1, + ExpectError: regexp.MustCompile("Invalid request value"), + }, + }, + }) +} diff --git a/docs/resources/bigip_as3.md b/docs/resources/bigip_as3.md index 89a329d24..657498f6e 100644 --- a/docs/resources/bigip_as3.md +++ b/docs/resources/bigip_as3.md @@ -19,6 +19,13 @@ This resource is helpful to configure AS3 declarative JSON on BIG-IP. ~> For Deploying AS3 JSON in Per-App mode, this resource provided with a attribute [tenant_name](#tenant_name) to be passed to add application on specified tenant, else random tenant name will be generated. +As3 Declaration can be deployed in Traditional way as well as Per-Application Way : + +- Traditional Way - Entire Declaration needs to be passed in during the create and update call along with the tenant details in the declaration. +- Per-Application Way - Only application details needs to be passed in the as3_json. Tenant name needs to be passed else random tenant name will be generated. + +**Note:** : PerApplication needs to be turned `true` as a Prerequisite on the Big-IP (BIG-IP AS3 version >3.50) device. For details : + ## Example Usage ```hcl @@ -35,7 +42,33 @@ resource "bigip_as3" "as3-example1" { } ``` -## Example Usage for Per-App mode deployment +# Per-Application Deployment - Example Usage for json file with tenant name +resource "bigip_as3" "as3-example1" { + as3_json = file("perApplication_example.json") + tenant_name = "Test" +} + +# Per-Application Deployment - Example Usage for json file without tenant name - Tenant with Random name is generated in this case +resource "bigip_as3" "as3-example1" { + as3_json = file("perApplication_example.json") +} + +# Per-Application Deployment - Delete Example +resource "bigip_as3" "as3-example1" { + tenant_name = "Test" + as3_json = file("as3_per_app_example1.json") + +} + +resource "bigip_as3" "as3-example2" { + tenant_name = "Test" + as3_json = file("as3_per_app_example2.json") + +} + +On running above 2 resources , we will be able to deploy Applications - `path_app1 , path_app2` on Tenant `Test` +now, if we run `terraform destroy -target=bigip_as3.as3-example2` , only `path_app2` will be deleted from Tenant. + [perApplication as3](#perApplication_example) @@ -200,13 +233,12 @@ resource "bigip_as3" "as3-example1" { ```json { - "schemaVersion": "3.50.1", "Application1": { "class": "Application", "service": { "class": "Service_HTTP", "virtualAddresses": [ - "192.1.1.1" + "192.0.2.1" ], "pool": "pool" }, @@ -216,19 +248,19 @@ resource "bigip_as3" "as3-example1" { { "servicePort": 80, "serverAddresses": [ - "192.0.1.10", - "192.0.1.20" + "192.0.2.10", + "192.0.2.20" ] } ] } - }, + }, "Application2": { "class": "Application", "service": { "class": "Service_HTTP", "virtualAddresses": [ - "192.1.2.1" + "192.0.3.2" ], "pool": "pool" }, @@ -238,8 +270,8 @@ resource "bigip_as3" "as3-example1" { { "servicePort": 80, "serverAddresses": [ - "192.0.2.10", - "192.0.2.20" + "192.0.3.30", + "192.0.3.40" ] } ] diff --git a/examples/as3/as3_per_app_example1.json b/examples/as3/as3_per_app_example1.json new file mode 100644 index 000000000..d29e374e4 --- /dev/null +++ b/examples/as3/as3_per_app_example1.json @@ -0,0 +1,25 @@ +{ + "schemaVersion": "3.50.0", + "path_app1": { + "class": "Application", + "vs_name_app1": { + "class": "Service_HTTP", + "virtualAddresses": [ + "192.1.1.24" + ], + "pool": "pool" + }, + "pool": { + "class": "Pool", + "members": [ + { + "servicePort": 80, + "serverAddresses": [ + "192.20.1.10", + "192.30.1.20" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/examples/as3/as3_per_app_example2.json b/examples/as3/as3_per_app_example2.json new file mode 100644 index 000000000..8e061c5eb --- /dev/null +++ b/examples/as3/as3_per_app_example2.json @@ -0,0 +1,25 @@ +{ + "schemaVersion": "3.50.0", + "path_app2": { + "class": "Application", + "vs_name_app2": { + "class": "Service_HTTP", + "virtualAddresses": [ + "192.1.1.234" + ], + "pool": "pool" + }, + "pool": { + "class": "Pool", + "members": [ + { + "servicePort": 80, + "serverAddresses": [ + "12.20.1.10", + "12.30.1.20" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/examples/as3/bigip_as3.tf b/examples/as3/bigip_as3.tf index aabb5b3ab..fcd1dc624 100644 --- a/examples/as3/bigip_as3.tf +++ b/examples/as3/bigip_as3.tf @@ -25,4 +25,10 @@ resource "bigip_as3" "as3-example1" { depends_on = ["null_resource.install_as3"] } +// Per-Application Deployment example +resource "bigip_as3" "as3-example1" { + as3_json = file("perApplication_example.json") + tenant_name = "Test" +} + diff --git a/examples/as3/perApplication_example.json b/examples/as3/perApplication_example.json new file mode 100644 index 000000000..f18868345 --- /dev/null +++ b/examples/as3/perApplication_example.json @@ -0,0 +1,47 @@ +{ + "schemaVersion": "3.50.0", + "Application1": { + "class": "Application", + "service": { + "class": "Service_HTTP", + "virtualAddresses": [ + "192.0.2.1" + ], + "pool": "pool" + }, + "pool": { + "class": "Pool", + "members": [ + { + "servicePort": 80, + "serverAddresses": [ + "192.0.2.10", + "192.0.2.20" + ] + } + ] + } + }, + "Application2": { + "class": "Application", + "service": { + "class": "Service_HTTP", + "virtualAddresses": [ + "192.0.3.2" + ], + "pool": "pool" + }, + "pool": { + "class": "Pool", + "members": [ + { + "servicePort": 80, + "serverAddresses": [ + "192.0.3.30", + "192.0.3.40" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/vendor/github.com/f5devcentral/go-bigip/as3bigip.go b/vendor/github.com/f5devcentral/go-bigip/as3bigip.go index 1016b39da..6e714c573 100644 --- a/vendor/github.com/f5devcentral/go-bigip/as3bigip.go +++ b/vendor/github.com/f5devcentral/go-bigip/as3bigip.go @@ -580,6 +580,15 @@ func (b *BigIP) CheckSetting() (bool, error) { // return perAppDeploymentAllowed, nil } +func (b *BigIP) DeletePerApplicationAs3Bigip(tenantName string, applicationName string) error { + + _, err := b.deleteReq(uriMgmt, uriShared, uriAppsvcs, uriDeclare, tenantName, uriApplications, applicationName) + if err != nil { + return err + } + return nil +} + func (b *BigIP) AddServiceDiscoveryNodes(taskid string, config []interface{}) error { resp, err := b.postReq(config, uriMgmt, uriShared, "service-discovery", "task", taskid, "nodes") if err != nil {