From 975a2f71dc39a388220c74207ea8bbe57ab4a71c Mon Sep 17 00:00:00 2001 From: Dmitrii Shelomentsev Date: Tue, 21 Jun 2022 15:43:47 +0400 Subject: [PATCH] GCLOUD2-6855: Added DDoS protection support to provider --- .../gcore_ddos_profile_template.md | 70 +++ docs/resources/gcore_ddos_protection.md | 73 +++ .../data-source.tf | 21 + .../resources/gcore_ddos_profile/import.sh | 2 + .../resources/gcore_ddos_profile/resource.tf | 15 + ...data_source_gcore_ddos_profile_template.go | 189 +++++++ ...source_gcore_ddos_profile_template_test.go | 89 ++++ gcore/provider.go | 56 +- gcore/resource_gcore_ddos_protection.go | 483 ++++++++++++++++++ gcore/resource_gcore_ddos_protection_test.go | 128 +++++ gcore/resource_gcore_servergroup.go | 2 +- gcore/utils.go | 43 ++ go.mod | 2 +- go.sum | 4 +- 14 files changed, 1146 insertions(+), 31 deletions(-) create mode 100644 docs/data-sources/gcore_ddos_profile_template.md create mode 100644 docs/resources/gcore_ddos_protection.md create mode 100644 examples/data-sources/gcore_ddos_profile_template/data-source.tf create mode 100644 examples/resources/gcore_ddos_profile/import.sh create mode 100644 examples/resources/gcore_ddos_profile/resource.tf create mode 100644 gcore/data_source_gcore_ddos_profile_template.go create mode 100644 gcore/data_source_gcore_ddos_profile_template_test.go create mode 100644 gcore/resource_gcore_ddos_protection.go create mode 100644 gcore/resource_gcore_ddos_protection_test.go diff --git a/docs/data-sources/gcore_ddos_profile_template.md b/docs/data-sources/gcore_ddos_profile_template.md new file mode 100644 index 0000000..6851458 --- /dev/null +++ b/docs/data-sources/gcore_ddos_profile_template.md @@ -0,0 +1,70 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "gcore_ddos_profile_template Data Source - terraform-provider-gcorelabs" +subcategory: "" +description: |- + Represents list of available DDoS protection profile templates +--- + +# gcore_ddos_profile_template (Data Source) + +Represents list of available DDoS protection profile templates + +## Example Usage + +```terraform +provider gcore { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "gcore_project" "pr" { + name = "test" +} + +data "gcore_region" "rg" { + name = "ED-10 Preprod" +} + +data "gcore_ddos_profile_template" "template" { + template_id = 63 + region_id = data.gcore_region.rg.id + project_id = data.gcore_project.pr.id +} + +output "view" { + value = data.gcore_ddos_profile_template.template +} +``` + + +## Schema + +### Optional + +- `name` (String) Template name +- `project_id` (Number) +- `project_name` (String) +- `region_id` (Number) +- `region_name` (String) +- `template_id` (Number) Template id + +### Read-Only + +- `description` (String) Template description +- `fields` (List of Object) Additional fields (see [below for nested schema](#nestedatt--fields)) +- `id` (String) The ID of this resource. + + +### Nested Schema for `fields` + +Read-Only: + +- `default` (String) +- `description` (String) +- `field_type` (String) +- `id` (Number) +- `name` (String) +- `required` (Boolean) +- `validation_schema` (String) + + diff --git a/docs/resources/gcore_ddos_protection.md b/docs/resources/gcore_ddos_protection.md new file mode 100644 index 0000000..5bd7926 --- /dev/null +++ b/docs/resources/gcore_ddos_protection.md @@ -0,0 +1,73 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "gcore_ddos_protection Resource - terraform-provider-gcorelabs" +subcategory: "" +description: |- + Represents DDoS protection profile +--- + +# gcore_ddos_protection (Resource) + +Represents DDoS protection profile + + + + +## Schema + +### Required + +- `bm_instance_id` (String) +- `ip_address` (String) IP address +- `profile_template` (Number) Profile template ID + +### Optional + +- `active` (Boolean) Activate profile +- `bgp` (Boolean) Activate BGP protocol +- `fields` (Block List) (see [below for nested schema](#nestedblock--fields)) +- `last_updated` (String) +- `project_id` (Number) +- `project_name` (String) +- `region_id` (Number) +- `region_name` (String) + +### Read-Only + +- `id` (String) The ID of this resource. +- `price` (String) +- `protocols` (List of Object) List of protocols (see [below for nested schema](#nestedatt--protocols)) +- `site` (String) + + +### Nested Schema for `fields` + +Required: + +- `base_field` (Number) + +Optional: + +- `field_value` (String) Complex value. Only one of 'value' or 'field_value' must be specified. +- `value` (String) Basic type value. Only one of 'value' or 'field_value' must be specified. + +Read-Only: + +- `default` (String) +- `description` (String) Field description +- `field_type` (String) +- `id` (Number) The ID of this resource. +- `name` (String) +- `required` (Boolean) +- `validation_schema` (String) Json schema to validate field_values + + + +### Nested Schema for `protocols` + +Read-Only: + +- `port` (String) +- `protocols` (List of String) + + diff --git a/examples/data-sources/gcore_ddos_profile_template/data-source.tf b/examples/data-sources/gcore_ddos_profile_template/data-source.tf new file mode 100644 index 0000000..d818264 --- /dev/null +++ b/examples/data-sources/gcore_ddos_profile_template/data-source.tf @@ -0,0 +1,21 @@ +provider gcore { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "gcore_project" "pr" { + name = "test" +} + +data "gcore_region" "rg" { + name = "ED-10 Preprod" +} + +data "gcore_ddos_profile_template" "template" { + template_id = 63 + region_id = data.gcore_region.rg.id + project_id = data.gcore_project.pr.id +} + +output "view" { + value = data.gcore_ddos_profile_template.template +} \ No newline at end of file diff --git a/examples/resources/gcore_ddos_profile/import.sh b/examples/resources/gcore_ddos_profile/import.sh new file mode 100644 index 0000000..3e67e10 --- /dev/null +++ b/examples/resources/gcore_ddos_profile/import.sh @@ -0,0 +1,2 @@ +# import using :: format +terraform import gcore_ddos_protection.test 1:6:1337 \ No newline at end of file diff --git a/examples/resources/gcore_ddos_profile/resource.tf b/examples/resources/gcore_ddos_profile/resource.tf new file mode 100644 index 0000000..f36c40f --- /dev/null +++ b/examples/resources/gcore_ddos_profile/resource.tf @@ -0,0 +1,15 @@ +provider gcore { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "gcore_ddos_protection" "ddos_protection" { + project_id = 1 + region_id = 1 + profile_template = 63 + ip_address = "10.94.77.72" + bm_instance_id = "99cd3a2d-607f-4fbb-91d9-01fe926b1e7f" + fields { + base_field = 118 + field_value = [33033] + } +} \ No newline at end of file diff --git a/gcore/data_source_gcore_ddos_profile_template.go b/gcore/data_source_gcore_ddos_profile_template.go new file mode 100644 index 0000000..48cc064 --- /dev/null +++ b/gcore/data_source_gcore_ddos_profile_template.go @@ -0,0 +1,189 @@ +package gcore + +import ( + "context" + "log" + "strconv" + + "github.com/G-Core/gcorelabscloud-go/gcore/ddos/v1/ddos" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const ( + ddosTemplatesPoint = "ddos/profile-templates" +) + +func dataSourceDDoSProfileTemplate() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceDDoSProfileTemplateRead, + Description: "Represents list of available DDoS protection profile templates", + Schema: map[string]*schema.Schema{ + "project_id": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ExactlyOneOf: []string{ + "project_id", + "project_name", + }, + }, + "region_id": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ExactlyOneOf: []string{ + "region_id", + "region_name", + }, + }, + "region_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ExactlyOneOf: []string{ + "region_id", + "region_name", + }, + }, + "project_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ExactlyOneOf: []string{ + "project_id", + "project_name", + }, + }, + "template_id": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "Template id", + ExactlyOneOf: []string{ + "template_id", + "name", + }, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Template name", + ExactlyOneOf: []string{ + "template_id", + "name", + }, + }, + "fields": &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Description: "Additional fields", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": &schema.Schema{ + Type: schema.TypeInt, + Computed: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: "Field name", + }, + "field_type": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: "Field type", + }, + "required": &schema.Schema{ + Type: schema.TypeBool, + Computed: true, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: "Field description", + }, + "default": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "validation_schema": { + Type: schema.TypeString, + Computed: true, + Description: "Json schema to validate field_values", + }, + }, + }, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Description: "Template description", + Computed: true, + }, + }, + } +} + +func dataSourceDDoSProfileTemplateRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Starts DDoS protection profile template reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, ddosTemplatesPoint, versionPointV1) + if err != nil { + return diag.FromErr(err) + } + + id := d.Get("template_id").(int) + name := d.Get("name").(string) + templates, err := ddos.ListAllProfileTemplates(client) + if err != nil { + return diag.FromErr(err) + } + + var found bool + var template ddos.ProfileTemplate + for _, t := range templates { + if t.ID == id { + template = t + found = true + break + } + + if t.Name == name { + template = t + found = true + break + } + } + + if !found { + return diag.Errorf("DDoS protection profile template not found not by ID %d nor by name %s", id, name) + } + + d.SetId(strconv.Itoa(template.ID)) + d.Set("name", template.Name) + d.Set("description", template.Description) + fields := make([]map[string]interface{}, len(template.Fields)) + for i, f := range template.Fields { + field := map[string]interface{}{ + "field_type": f.FieldType, + "required": f.Required, + "id": f.ID, + "default": f.Default, + "description": f.Description, + "name": f.Name, + "validation_schema": string(f.ValidationSchema), + } + + fields[i] = field + } + if err := d.Set("fields", fields); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish DDoS protection profile template reading") + return diags +} diff --git a/gcore/data_source_gcore_ddos_profile_template_test.go b/gcore/data_source_gcore_ddos_profile_template_test.go new file mode 100644 index 0000000..da48b2e --- /dev/null +++ b/gcore/data_source_gcore_ddos_profile_template_test.go @@ -0,0 +1,89 @@ +//go:build cloud +// +build cloud + +package gcore + +import ( + "fmt" + "strconv" + "testing" + + "github.com/G-Core/gcorelabscloud-go/gcore/ddos/v1/ddos" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDDoSProtectionProfileTemplatesTest(t *testing.T) { + cfg, err := createTestConfig() + if err != nil { + t.Fatal(err) + } + + client, err := CreateTestClient(cfg.Provider, ddosTemplatesPoint, versionPointV1) + if err != nil { + t.Fatal(err) + } + + templates, err := ddos.ListAllProfileTemplates(client) + if err != nil { + t.Fatal(err) + } + + if len(templates) == 0 { + t.Fatal("templates not found: templates list empty") + } + + var template *ddos.ProfileTemplate + for _, tmp := range templates { + if len(tmp.Fields) > 0 { + template = &tmp + break + } + } + + if template == nil { + t.Fatal("templates not found: there are no templates with non-empty fields") + } + + fullName := "data.gcore_ddos_profile_template.acctest" + tplByName := func(name string) string { + return fmt.Sprintf(` + data "gcore_ddos_profile_template" "acctest" { + %s + %s + name = "%s" + } + `, projectInfo(), regionInfo(), name) + } + tplByID := func(id int) string { + return fmt.Sprintf(` + data "gcore_ddos_profile_template" "acctest" { + %s + %s + template_id = "%d" + } + `, projectInfo(), regionInfo(), id) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: tplByID(template.ID), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(fullName), + resource.TestCheckResourceAttr(fullName, "name", template.Name), + resource.TestCheckResourceAttr(fullName, "id", strconv.Itoa(template.ID)), + ), + }, + { + Config: tplByName(template.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(fullName), + resource.TestCheckResourceAttr(fullName, "name", template.Name), + resource.TestCheckResourceAttr(fullName, "id", strconv.Itoa(template.ID)), + ), + }, + }, + }) +} diff --git a/gcore/provider.go b/gcore/provider.go index 09c7869..82bca54 100644 --- a/gcore/provider.go +++ b/gcore/provider.go @@ -156,35 +156,37 @@ func Provider() *schema.Provider { "gcore_cdn_rule": resourceCDNRule(), "gcore_cdn_sslcert": resourceCDNCert(), lifecyclePolicyResource: resourceLifecyclePolicy(), + "gcore_ddos_protection": resourceDDoSProtection(), }, DataSourcesMap: map[string]*schema.Resource{ - "gcore_project": dataSourceProject(), - "gcore_region": dataSourceRegion(), - "gcore_securitygroup": dataSourceSecurityGroup(), - "gcore_image": dataSourceImage(), - "gcore_volume": dataSourceVolume(), - "gcore_network": dataSourceNetwork(), - "gcore_subnet": dataSourceSubnet(), - "gcore_router": dataSourceRouter(), - "gcore_loadbalancer": dataSourceLoadBalancer(), - "gcore_loadbalancerv2": dataSourceLoadBalancerV2(), - "gcore_lblistener": dataSourceLBListener(), - "gcore_lbpool": dataSourceLBPool(), - "gcore_instance": dataSourceInstance(), - "gcore_floatingip": dataSourceFloatingIP(), - "gcore_storage_s3": dataSourceStorageS3(), - "gcore_storage_s3_bucket": dataSourceStorageS3Bucket(), - "gcore_storage_sftp": dataSourceStorageSFTP(), - "gcore_storage_sftp_key": dataSourceStorageSFTPKey(), - "gcore_reservedfixedip": dataSourceReservedFixedIP(), - "gcore_servergroup": dataSourceServerGroup(), - "gcore_k8s": dataSourceK8s(), - "gcore_k8s_pool": dataSourceK8sPool(), - "gcore_secret": dataSourceSecret(), - "gcore_laas_hosts": dataSourceLaaSHosts(), - "gcore_laas_status": dataSourceLaaSStatus(), - "gcore_faas_namespace": dataSourceFaaSNamespace(), - "gcore_faas_function": dataSourceFaaSFunction(), + "gcore_project": dataSourceProject(), + "gcore_region": dataSourceRegion(), + "gcore_securitygroup": dataSourceSecurityGroup(), + "gcore_image": dataSourceImage(), + "gcore_volume": dataSourceVolume(), + "gcore_network": dataSourceNetwork(), + "gcore_subnet": dataSourceSubnet(), + "gcore_router": dataSourceRouter(), + "gcore_loadbalancer": dataSourceLoadBalancer(), + "gcore_loadbalancerv2": dataSourceLoadBalancerV2(), + "gcore_lblistener": dataSourceLBListener(), + "gcore_lbpool": dataSourceLBPool(), + "gcore_instance": dataSourceInstance(), + "gcore_floatingip": dataSourceFloatingIP(), + "gcore_storage_s3": dataSourceStorageS3(), + "gcore_storage_s3_bucket": dataSourceStorageS3Bucket(), + "gcore_storage_sftp": dataSourceStorageSFTP(), + "gcore_storage_sftp_key": dataSourceStorageSFTPKey(), + "gcore_reservedfixedip": dataSourceReservedFixedIP(), + "gcore_servergroup": dataSourceServerGroup(), + "gcore_k8s": dataSourceK8s(), + "gcore_k8s_pool": dataSourceK8sPool(), + "gcore_secret": dataSourceSecret(), + "gcore_laas_hosts": dataSourceLaaSHosts(), + "gcore_laas_status": dataSourceLaaSStatus(), + "gcore_faas_namespace": dataSourceFaaSNamespace(), + "gcore_faas_function": dataSourceFaaSFunction(), + "gcore_ddos_profile_template": dataSourceDDoSProfileTemplate(), }, ConfigureContextFunc: providerConfigure, } diff --git a/gcore/resource_gcore_ddos_protection.go b/gcore/resource_gcore_ddos_protection.go new file mode 100644 index 0000000..0a413d4 --- /dev/null +++ b/gcore/resource_gcore_ddos_protection.go @@ -0,0 +1,483 @@ +package gcore + +import ( + "context" + "fmt" + "log" + "strconv" + "time" + + "github.com/G-Core/gcorelabscloud-go/gcore/ddos/v1/ddos" + "github.com/G-Core/gcorelabscloud-go/gcore/task/v1/tasks" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const ddosProfileCreatingTimeout int = 1200 +const ddosProfileDeletingTimeout int = 1200 +const ddosProfileUpdatingTimeout int = 1200 +const ddosProfilePoint = "ddos/profiles" + +func resourceDDoSProtection() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceDDoSProtectionCreate, + ReadContext: resourceDDoSProtectionRead, + UpdateContext: resourceDDoSProtectionUpdate, + DeleteContext: resourceDDoSProtectionDelete, + Description: "Represents DDoS protection profile", + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + projectID, regionID, profileID, err := ImportStringParser(d.Id()) + + if err != nil { + return nil, err + } + d.Set("project_id", projectID) + d.Set("region_id", regionID) + d.SetId(profileID) + + return []*schema.ResourceData{d}, nil + }, + }, + Schema: map[string]*schema.Schema{ + "project_id": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ExactlyOneOf: []string{ + "project_id", + "project_name", + }, + }, + "region_id": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ExactlyOneOf: []string{ + "region_id", + "region_name", + }, + }, + "project_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ExactlyOneOf: []string{ + "project_id", + "project_name", + }, + }, + "region_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ExactlyOneOf: []string{ + "region_id", + "region_name", + }, + }, + "ip_address": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "IP address", + }, + "profile_template": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + Description: "Profile template ID", + }, + "site": { + Type: schema.TypeString, + Computed: true, + }, + "active": { + Type: schema.TypeBool, + Description: "Activate profile", + Optional: true, + Default: true, + }, + "bgp": { + Type: schema.TypeBool, + Description: "Activate BGP protocol", + Optional: true, + Default: true, + }, + "price": { + Type: schema.TypeString, + Computed: true, + }, + "fields": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeInt, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "field_type": { + Type: schema.TypeString, + Computed: true, + }, + "base_field": { + Type: schema.TypeInt, + Required: true, + }, + "value": { + Type: schema.TypeString, + Optional: true, + Description: "Basic type value. Only one of 'value' or 'field_value' must be specified.", + }, + "description": { + Type: schema.TypeString, + Computed: true, + Description: "Field description", + }, + "default": { + Type: schema.TypeString, + Computed: true, + }, + "required": { + Type: schema.TypeBool, + Computed: true, + }, + "field_value": { + Type: schema.TypeString, + Optional: true, + Description: "Complex value. Only one of 'value' or 'field_value' must be specified.", + }, + "validation_schema": { + Type: schema.TypeString, + Computed: true, + Description: "Json schema to validate field_values", + }, + }, + }, + }, + "bm_instance_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "protocols": { + Type: schema.TypeList, + Computed: true, + Description: "List of protocols", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "port": { + Type: schema.TypeString, + Computed: true, + }, + "protocols": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + }, + }, + }, + }, + "last_updated": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + } +} + +func resourceDDoSProtectionCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start DDoS protection profile creating") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, ddosProfilePoint, versionPointV1) + if err != nil { + return diag.FromErr(err) + } + + createOpts := ddos.CreateProfileOpts{} + + createOpts.IPAddress = d.Get("ip_address").(string) + createOpts.ProfileTemplate = d.Get("profile_template").(int) + createOpts.BaremetalInstanceID = d.Get("bm_instance_id").(string) + fields := d.Get("fields") + if len(fields.([]interface{})) > 0 { + createOpts.Fields, err = extractProfileFieldsMap(fields.([]interface{})) + if err != nil { + return diag.FromErr(err) + } + } else { + createOpts.Fields = make([]ddos.ProfileField, 0) + } + + results, err := ddos.CreateProfile(client, createOpts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + log.Printf("[DEBUG] Task id (%s)", taskID) + profileID, err := tasks.WaitTaskAndReturnResult(client, taskID, true, ddosProfileCreatingTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + Profile, err := ddos.ExtractProfileIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrive DDoS protection profile ID from task info %w", err) + } + + return Profile, nil + }) + if err != nil { + return diag.FromErr(err) + } + + var options ddos.ActivateProfileOpts + options.Active = d.Get("active").(bool) + options.BGP = d.Get("bgp").(bool) + if !options.Active || !options.BGP { + id, _ := strconv.Atoi(profileID.(string)) + if results, err = ddos.ActivateProfile(client, id, options).Extract(); err != nil { + return diag.FromErr(err) + } + + taskID = results.Tasks[0] + if err = tasks.WaitForStatus(client, string(taskID), + tasks.TaskStateFinished, ddosProfileCreatingTimeout, true); err != nil { + log.Printf("[DEBUG] failed to activate/disactivate profile: %v", err) + } + } + + if err != nil { + id, _ := strconv.Atoi(profileID.(string)) + results, err = ddos.DeleteProfile(client, id).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID = results.Tasks[0] + if err = tasks.WaitForStatus(client, string(taskID), + tasks.TaskStateFinished, ddosProfileCreatingTimeout, true); err != nil { + return diag.FromErr(err) + } + } + + log.Printf("[DEBUG] DDoS protection profile id (%s)", profileID) + d.SetId(profileID.(string)) + resourceDDoSProtectionRead(ctx, d, m) + + log.Println("[DEBUG] Finish DDoS protection profile creating") + return diags +} + +func resourceDDoSProtectionRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start DDoS protection profile reading") + log.Printf("[DEBUG] Start DDoS protection profile reading %s", d.State()) + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + profileID, err := strconv.Atoi(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + log.Printf("[DEBUG] DDoS profile id = %d", profileID) + + client, err := CreateClient(provider, d, ddosProfilePoint, versionPointV1) + if err != nil { + return diag.FromErr(err) + } + + profiles, err := ddos.ListAllProfiles(client) + if err != nil { + return diag.FromErr(err) + } + + var profile ddos.Profile + var found bool + for _, f := range profiles { + if f.ID == profileID { + profile = f + found = true + break + } + } + + if !found { + return diag.Errorf("DDoS protection profile id = %d not found", profileID) + } + + d.Set("ip_address", profile.IPAddress) + d.Set("profile_template", profile.ProfileTemplate) + d.Set("bgp", profile.Options.BGP) + d.Set("price", profile.Options.Price) + d.Set("active", profile.Options.Active) + d.Set("site", profile.Site) + fields := make([]map[string]interface{}, len(profile.Fields)) + for i, f := range profile.Fields { + field := map[string]interface{}{ + "id": f.ID, + "name": f.Name, + "field_type": f.FieldType, + "base_field": f.BaseField, + "value": f.Value, + "description": f.Description, + "default": f.Default, + "required": f.Required, + "field_value": string(f.FieldValue), + "validation_schema": string(f.ValidationSchema), + } + + fields[i] = field + } + if err := d.Set("fields", fields); err != nil { + return diag.FromErr(err) + } + + protocols := make([]map[string]interface{}, len(profile.Protocols)) + for i, p := range profile.Protocols { + protocol := map[string]interface{}{ + "port": p.Port, + "protocols": p.Protocols, + } + + protocols[i] = protocol + } + if err := d.Set("protocols", protocols); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish DDoS protection profile reading") + return diags +} + +func resourceDDoSProtectionUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start DDoS protection profile updating") + profileID, err := strconv.Atoi(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + log.Printf("[DEBUG] DDoS protection profile id = %d", profileID) + config := m.(*Config) + provider := config.Provider + client, err := CreateClient(provider, d, ddosProfilePoint, versionPointV1) + if err != nil { + return diag.FromErr(err) + } + updateOpts := ddos.UpdateProfileOpts{} + + var profileChanged bool + + updateOpts.ProfileTemplate = d.Get("profile_template").(int) + if d.HasChange("profile_template") { + profileChanged = true + } + + updateOpts.BaremetalInstanceID = d.Get("bm_instance_id").(string) + if d.HasChange("bm_instance_id") { + profileChanged = true + } + + updateOpts.IPAddress = d.Get("ip_address").(string) + if d.HasChange("ip_address") { + profileChanged = true + } + + fs := d.Get("fields") + updateOpts.Fields = make([]ddos.ProfileField, 0) + if len(fs.([]interface{})) > 0 { + fields, err := extractProfileFieldsMap(fs.([]interface{})) + if err != nil { + return diag.FromErr(err) + } + updateOpts.Fields = fields + } + if d.HasChange("fields") { + profileChanged = true + } + + var ( + activateOpts ddos.ActivateProfileOpts + optionsChanged bool + ) + + if d.HasChange("bgp") || d.HasChange("active") { + optionsChanged = true + activateOpts.BGP = d.Get("bgp").(bool) + activateOpts.Active = d.Get("active").(bool) + } + + if profileChanged { + log.Println("[DEBUG] Profile changed: updating profile") + results, err := ddos.UpdateProfile(client, profileID, updateOpts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + log.Printf("[DEBUG] Task id (%s)", taskID) + if err := tasks.WaitForStatus(client, string(taskID), + tasks.TaskStateFinished, ddosProfileUpdatingTimeout, true); err != nil { + return diag.FromErr(err) + } + } + + if optionsChanged { + log.Println("[DEBUG] Profile options changed: updating profile options") + results, err := ddos.ActivateProfile(client, profileID, activateOpts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + log.Printf("[DEBUG] Task id (%s)", taskID) + if err := tasks.WaitForStatus(client, string(taskID), + tasks.TaskStateFinished, ddosProfileUpdatingTimeout, true); err != nil { + return diag.FromErr(err) + } + } + + d.Set("last_updated", time.Now().Format(time.RFC850)) + log.Println("[DEBUG] Finish DDoS protection profile updating") + return resourceDDoSProtectionRead(ctx, d, m) +} + +func resourceDDoSProtectionDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start DDoS protection profile deleting") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + profileID, err := strconv.Atoi(d.Id()) + if err != nil { + return diag.FromErr(err) + } + log.Printf("[DEBUG] DDoS profile id = %d", profileID) + + client, err := CreateClient(provider, d, ddosProfilePoint, versionPointV1) + if err != nil { + return diag.FromErr(err) + } + + results, err := ddos.DeleteProfile(client, profileID).Extract() + if err != nil { + return diag.FromErr(err) + } + taskID := results.Tasks[0] + log.Printf("[DEBUG] Task id (%s)", taskID) + err = tasks.WaitForStatus(client, string(taskID), tasks.TaskStateFinished, ddosProfileDeletingTimeout, true) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + + log.Println("[DEBUG] Finish DDoS protection profile deleting") + return diags +} diff --git a/gcore/resource_gcore_ddos_protection_test.go b/gcore/resource_gcore_ddos_protection_test.go new file mode 100644 index 0000000..7ffd73f --- /dev/null +++ b/gcore/resource_gcore_ddos_protection_test.go @@ -0,0 +1,128 @@ +//go:build cloud +// +build cloud + +package gcore + +import ( + "fmt" + "strconv" + "testing" + + "github.com/G-Core/gcorelabscloud-go/gcore/ddos/v1/ddos" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccDDoSProfile(t *testing.T) { + type Params struct { + ProfileTemplate string + ProfileFields string + BGP bool + } + + profileTmpl := func(params *Params) string { + template := ` + resource "gcore_baremetal" "bm" { + %[1]s + %[2]s + name = "baremetal_acctest" + flavor_id = "bm1-hf-medium-fake" + image_id = "570fb9a3-5074-4539-b0d0-ec49f8c463aa" + interface { + type = "external" + is_parent = "true" + } + } + + resource "gcore_ddos_protection" "acctest" { + %[1]s + %[2]s + ip_address = gcore_baremetal.bm.addresses.0.net.0.addr + profile_template = %[3]s + bm_instance_id = gcore_baremetal.bm.id + active = true + bgp = %[4]t + fields { + base_field = 118 + field_value = "%[5]s" + } + } +` + return fmt.Sprintf(template, projectInfo(), regionInfo(), params.ProfileTemplate, params.BGP, params.ProfileFields) + } + + createParams := Params{ + ProfileTemplate: "63", + ProfileFields: "[33033]", + BGP: false, + } + + updateParams := Params{ + ProfileTemplate: createParams.ProfileTemplate, + ProfileFields: "[33031,33034]", + BGP: true, + } + + fullName := "gcore_ddos_protection.acctest" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: checkDestroyDDoSProtectionProfile, + Steps: []resource.TestStep{ + { + Config: profileTmpl(&createParams), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(fullName), + resource.TestCheckResourceAttr(fullName, "profile_template", createParams.ProfileTemplate), + resource.TestCheckResourceAttrSet(fullName, "ip_address"), + resource.TestCheckResourceAttr(fullName, "active", "true"), + resource.TestCheckResourceAttr(fullName, "bgp", "false"), + resource.TestCheckResourceAttr(fullName, "fields.0.field_value", createParams.ProfileFields), + ), + }, + { + Config: profileTmpl(&updateParams), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(fullName), + resource.TestCheckResourceAttr(fullName, "profile_template", updateParams.ProfileTemplate), + resource.TestCheckResourceAttrSet(fullName, "ip_address"), + resource.TestCheckResourceAttr(fullName, "active", "true"), + resource.TestCheckResourceAttr(fullName, "bgp", "true"), + resource.TestCheckResourceAttr(fullName, "fields.0.field_value", updateParams.ProfileFields), + ), + }, + }, + }) +} + +func checkDestroyDDoSProtectionProfile(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + client, err := CreateTestClient(config.Provider, ddosProfilePoint, versionPointV1) + if err != nil { + return err + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "gcore_ddos_protection" { + continue + } + + id, err := strconv.Atoi(rs.Primary.ID) + if err != nil { + return err + } + + profiles, err := ddos.ListAllProfiles(client) + if err != nil { + return err + } + + for _, profile := range profiles { + if profile.ID == id { + return fmt.Errorf("ddos protection profile still exists") + } + } + } + + return nil +} diff --git a/gcore/resource_gcore_servergroup.go b/gcore/resource_gcore_servergroup.go index 6fc4d18..7dcab78 100644 --- a/gcore/resource_gcore_servergroup.go +++ b/gcore/resource_gcore_servergroup.go @@ -178,7 +178,7 @@ func resourceServerGroupDelete(ctx context.Context, d *schema.ResourceData, m in return diag.FromErr(err) } - err = servergroups.Delete(client, d.Id()).ExtractErr() + err = servergroups.Delete(client, d.Id()).Err if err != nil { return diag.FromErr(err) } diff --git a/gcore/utils.go b/gcore/utils.go index 5c763aa..04dde5c 100644 --- a/gcore/utils.go +++ b/gcore/utils.go @@ -18,6 +18,7 @@ import ( gcdn "github.com/G-Core/gcorelabscdn-go" gcorecloud "github.com/G-Core/gcorelabscloud-go" gc "github.com/G-Core/gcorelabscloud-go/gcore" + "github.com/G-Core/gcorelabscloud-go/gcore/ddos/v1/ddos" "github.com/G-Core/gcorelabscloud-go/gcore/instance/v1/instances" "github.com/G-Core/gcorelabscloud-go/gcore/instance/v1/types" "github.com/G-Core/gcorelabscloud-go/gcore/loadbalancer/v1/lbpools" @@ -733,6 +734,48 @@ func extractListenerIntoMap(listener *listeners.Listener) map[string]interface{} return l } +func StringToRawJsonHookFunc() mapstructure.DecodeHookFuncType { + return func( + f reflect.Type, + t reflect.Type, + data interface{}) (interface{}, error) { + if f.Kind() != reflect.String { + return data, nil + } + + if t == reflect.TypeOf(json.RawMessage{}) { + s := data.(string) + return json.RawMessage(s), nil + } + + return data, nil + } +} + +func extractProfileFieldsMap(fs []interface{}) ([]ddos.ProfileField, error) { + config := &mapstructure.DecoderConfig{ + TagName: "json", + DecodeHook: StringToRawJsonHookFunc(), + } + fields := make([]ddos.ProfileField, len(fs)) + for i, f := range fs { + m := f.(map[string]interface{}) + var field ddos.ProfileField + err := MapStructureDecoder(&field, &m, config) + if err != nil { + return nil, err + } + // Ignore all non-mutable fields + fields[i] = ddos.ProfileField{ + Value: field.Value, + BaseField: field.BaseField, + FieldValue: field.FieldValue, + } + } + + return fields, nil +} + func getUniqueID(d *schema.ResourceData) string { return fmt.Sprintf( "%d%d%s%s", diff --git a/go.mod b/go.mod index 5b86e3a..43838fb 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/G-Core/gcore-dns-sdk-go v0.2.3 github.com/G-Core/gcore-storage-sdk-go v0.1.34 github.com/G-Core/gcorelabscdn-go v0.1.25 - github.com/G-Core/gcorelabscloud-go v0.5.25 + github.com/G-Core/gcorelabscloud-go v0.5.31 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/terraform-plugin-sdk/v2 v2.10.1 github.com/mitchellh/mapstructure v1.5.0 diff --git a/go.sum b/go.sum index 82f5d42..b96d251 100644 --- a/go.sum +++ b/go.sum @@ -85,8 +85,8 @@ github.com/G-Core/gcore-storage-sdk-go v0.1.34 h1:0GPQfz1kA6mQi6fiisGsh0Um4H9PZe github.com/G-Core/gcore-storage-sdk-go v0.1.34/go.mod h1:BUAEZZZJJt/+luRFunqziv3+JnbVMLbQXDWz9kV8Te8= github.com/G-Core/gcorelabscdn-go v0.1.25 h1:iZ89q+x3Lk48IdLJJzp5OJyD+NENiye0XpxNjhX857E= github.com/G-Core/gcorelabscdn-go v0.1.25/go.mod h1:iSGXaTvZBzDHQW+rKFS918BgFVpONcyLEijwh8WsXpE= -github.com/G-Core/gcorelabscloud-go v0.5.25 h1:GpAeUPIfcl/yBthYRxiKLlLq1vKift28xHK5+mA5Iz8= -github.com/G-Core/gcorelabscloud-go v0.5.25/go.mod h1:EFyllSaVpPPIXnU35ALXXXQJhPRmwjShgOGHqHnb2Xk= +github.com/G-Core/gcorelabscloud-go v0.5.31 h1:cbGJVaBf4zUK/CXM8rKEGhfl68sq4QJpGwEwmAM4tV4= +github.com/G-Core/gcorelabscloud-go v0.5.31/go.mod h1:tizV2NaASUpPI6NfJVIOM5yBI8MOriAzSHA/5K1m6aU= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=