diff --git a/examples/ibm-event-streams/README.md b/examples/ibm-event-streams/README.md index 2e3dab13b1..99282dadb0 100644 --- a/examples/ibm-event-streams/README.md +++ b/examples/ibm-event-streams/README.md @@ -160,7 +160,34 @@ resource "ibm_resource_tag" "tag_example_on_es" { } ``` -#### Scenario 6: Connect to an existing Event Streams instance and its topics. +#### Scenario 6: Set default and user quotas on an existing Event Streams instance. + +This code sets the default quota to 32768 bytes/second for producers and 16384 bytes/second for consumers. +It sets a quota for user `iam-ServiceId-00001111-2222-3333-4444-555566667777` to 65536 bytes/second for producers and no limit (-1) for consumers. +For more information on quotas, see [Setting Kafka quotas](https://cloud.ibm.com/docs/EventStreams?topic=EventStreams-enabling_kafka_quotas). + +```terraform +data "ibm_resource_instance" "es_instance_6" { + name = "terraform-integration-6" + resource_group_id = data.ibm_resource_group.group.id +} + +resource "ibm_event_streams_quota" "default_quota" { + resource_instance_id = data.ibm_resource_instance.es_instance_6.id + entity = "default" + producer_byte_rate = 32768 + consumer_byte_rate = 16384 +} + +resource "ibm_event_streams_quota" "user00001111_quota" { + resource_instance_id = data.ibm_resource_instance.es_instance_6.id + entity = "iam-ServiceId-00001111-2222-3333-4444-555566667777" + producer_byte_rate = 65536 + consumer_byte_rate = -1 +} +``` + +#### Scenario 7: Connect to an existing Event Streams instance and its topics. This scenario uses a fictitious `"kafka_consumer_app"` resource to demonstrate how a consumer application could be configured. The resource uses three configuration properties: @@ -177,22 +204,22 @@ The topic names can be provided as strings, or can be taken from topic data sour ```terraform # Use an existing instance -data "ibm_resource_instance" "es_instance_6" { - name = "terraform-integration-6" +data "ibm_resource_instance" "es_instance_7" { + name = "terraform-integration-7" resource_group_id = data.ibm_resource_group.group.id } # Use an existing topic on that instance -data "ibm_event_streams_topic" "es_topic_6" { - resource_instance_id = data.ibm_resource_instance.es_instance_6.id +data "ibm_event_streams_topic" "es_topic_7" { + resource_instance_id = data.ibm_resource_instance.es_instance_7.id name = "my-es-topic" } # The FICTITIOUS consumer application, configured with brokers, API key, and topics resource "kafka_consumer_app" "es_kafka_app" { - bootstrap_server = lookup(data.ibm_resource_instance.es_instance_4.extensions, "kafka_brokers_sasl", []) + bootstrap_server = lookup(data.ibm_resource_instance.es_instance_7.extensions, "kafka_brokers_sasl", []) apikey = var.es_reader_api_key - topics = [data.ibm_event_streams_topic.es_topic_4.name] + topics = [data.ibm_event_streams_topic.es_topic_7.name] } ``` diff --git a/ibm/conns/config.go b/ibm/conns/config.go index 83ae73f383..5b002295ba 100644 --- a/ibm/conns/config.go +++ b/ibm/conns/config.go @@ -120,6 +120,7 @@ import ( "github.com/IBM/continuous-delivery-go-sdk/cdtektonpipelinev2" "github.com/IBM/continuous-delivery-go-sdk/cdtoolchainv2" "github.com/IBM/event-notifications-go-admin-sdk/eventnotificationsv1" + "github.com/IBM/eventstreams-go-sdk/pkg/adminrestv1" "github.com/IBM/eventstreams-go-sdk/pkg/schemaregistryv1" "github.com/IBM/ibm-hpcs-uko-sdk/ukov4" "github.com/IBM/logs-go-sdk/logsv0" @@ -299,6 +300,7 @@ type ClientSession interface { AtrackerV2() (*atrackerv2.AtrackerV2, error) MetricsRouterV3() (*metricsrouterv3.MetricsRouterV3, error) ESschemaRegistrySession() (*schemaregistryv1.SchemaregistryV1, error) + ESadminRestSession() (*adminrestv1.AdminrestV1, error) ContextBasedRestrictionsV1() (*contextbasedrestrictionsv1.ContextBasedRestrictionsV1, error) SecurityAndComplianceCenterV3() (*scc.SecurityAndComplianceCenterApiV3, error) CdToolchainV2() (*cdtoolchainv2.CdToolchainV2, error) @@ -614,6 +616,9 @@ type clientSession struct { esSchemaRegistryClient *schemaregistryv1.SchemaregistryV1 esSchemaRegistryErr error + esAdminRestClient *adminrestv1.AdminrestV1 + esAdminRestErr error + // Security and Compliance Center (SCC) securityAndComplianceCenterClient *scc.SecurityAndComplianceCenterApiV3 securityAndComplianceCenterClientErr error @@ -1212,6 +1217,10 @@ func (session clientSession) ESschemaRegistrySession() (*schemaregistryv1.Schema return session.esSchemaRegistryClient, session.esSchemaRegistryErr } +func (session clientSession) ESadminRestSession() (*adminrestv1.AdminrestV1, error) { + return session.esAdminRestClient, session.esAdminRestErr +} + // Security and Compliance center Admin API func (session clientSession) SecurityAndComplianceCenterV3() (*scc.SecurityAndComplianceCenterApiV3, error) { return session.securityAndComplianceCenterClient, session.securityAndComplianceCenterClientErr @@ -3325,6 +3334,20 @@ func (c *Config) ClientSession() (interface{}, error) { }) } + esAdminRestV1Options := &adminrestv1.AdminrestV1Options{ + Authenticator: authenticator, + } + session.esAdminRestClient, err = adminrestv1.NewAdminrestV1(esAdminRestV1Options) + if err != nil { + session.esAdminRestErr = fmt.Errorf("[ERROR] Error occured while configuring Event Streams admin rest: %q", err) + } + if session.esAdminRestClient != nil && session.esAdminRestClient.Service != nil { + session.esAdminRestClient.Service.EnableRetries(c.RetryCount, c.RetryDelay) + session.esAdminRestClient.SetDefaultHeaders(gohttp.Header{ + "X-Original-User-Agent": {fmt.Sprintf("terraform-provider-ibm/%s", version.Version)}, + }) + } + // Construct an "options" struct for creating the service client. var cdToolchainClientURL string if c.Visibility == "private" || c.Visibility == "public-and-private" { diff --git a/ibm/provider/provider.go b/ibm/provider/provider.go index a8da05c7ba..8f65fc116c 100644 --- a/ibm/provider/provider.go +++ b/ibm/provider/provider.go @@ -354,6 +354,7 @@ func Provider() *schema.Provider { "ibm_dns_secondary": classicinfrastructure.DataSourceIBMDNSSecondary(), "ibm_event_streams_topic": eventstreams.DataSourceIBMEventStreamsTopic(), "ibm_event_streams_schema": eventstreams.DataSourceIBMEventStreamsSchema(), + "ibm_event_streams_quota": eventstreams.DataSourceIBMEventStreamsQuota(), "ibm_hpcs": hpcs.DataSourceIBMHPCS(), "ibm_hpcs_managed_key": hpcs.DataSourceIbmManagedKey(), "ibm_hpcs_key_template": hpcs.DataSourceIbmKeyTemplate(), @@ -1102,6 +1103,7 @@ func Provider() *schema.Provider { "ibm_dns_record": classicinfrastructure.ResourceIBMDNSRecord(), "ibm_event_streams_topic": eventstreams.ResourceIBMEventStreamsTopic(), "ibm_event_streams_schema": eventstreams.ResourceIBMEventStreamsSchema(), + "ibm_event_streams_quota": eventstreams.ResourceIBMEventStreamsQuota(), "ibm_firewall": classicinfrastructure.ResourceIBMFirewall(), "ibm_firewall_policy": classicinfrastructure.ResourceIBMFirewallPolicy(), "ibm_hpcs": hpcs.ResourceIBMHPCS(), diff --git a/ibm/service/eventstreams/data_source_ibm_event_streams_quota.go b/ibm/service/eventstreams/data_source_ibm_event_streams_quota.go new file mode 100644 index 0000000000..b521d261ca --- /dev/null +++ b/ibm/service/eventstreams/data_source_ibm_event_streams_quota.go @@ -0,0 +1,129 @@ +// Copyright IBM Corp. 2024 All Rights Reserved. +// Licensed under the Mozilla Public License v2.0 + +package eventstreams + +import ( + "context" + "fmt" + "log" + "strings" + + "github.com/IBM-Cloud/terraform-provider-ibm/ibm/conns" + "github.com/IBM-Cloud/terraform-provider-ibm/ibm/flex" + "github.com/IBM/eventstreams-go-sdk/pkg/adminrestv1" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// A quota in an Event Streams service instance. +// The ID is the CRN with the last two components "quota:entity". +func DataSourceIBMEventStreamsQuota() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceIBMEventStreamsQuotaRead, + + Schema: map[string]*schema.Schema{ + "resource_instance_id": { + Type: schema.TypeString, + Required: true, + Description: "The ID or CRN of the Event Streams service instance", + }, + "entity": { + Type: schema.TypeString, + Required: true, + Description: "The entity for which the quota is set; 'default' or IAM ID", + }, + "producer_byte_rate": { + Type: schema.TypeInt, + Computed: true, + Description: "The producer quota in bytes per second, -1 means no quota", + }, + "consumer_byte_rate": { + Type: schema.TypeInt, + Computed: true, + Description: "The consumer quota in bytes per second, -1 means no quota", + }, + }, + } +} + +// read quota properties using the admin-rest API +func dataSourceIBMEventStreamsQuotaRead(context context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + adminrestClient, instanceCRN, entity, err := getQuotaClientInstanceEntity(d, meta) + if err != nil { + tfErr := flex.TerraformErrorf(err, "Error getting Event Streams instance", "ibm_event_streams_quota", "read") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + + getQuotaOptions := &adminrestv1.GetQuotaOptions{} + getQuotaOptions.SetEntityName(entity) + quota, response, err := adminrestClient.GetQuotaWithContext(context, getQuotaOptions) + if err != nil { + var tfErr *flex.TerraformProblem + if response != nil && response.StatusCode == 404 { + tfErr = flex.TerraformErrorf(err, fmt.Sprintf("Quota for '%s' does not exist", entity), "ibm_event_streams_quota", "read") + } else { + tfErr = flex.TerraformErrorf(err, fmt.Sprintf("GetQuota failed with response: %s", response), "ibm_event_streams_quota", "read") + } + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + + d.Set("resource_instance_id", instanceCRN) + d.Set("entity", entity) + d.Set("producer_byte_rate", getQuotaValue(quota.ProducerByteRate)) + d.Set("consumer_byte_rate", getQuotaValue(quota.ConsumerByteRate)) + d.SetId(getQuotaID(instanceCRN, entity)) + + return nil +} + +// Returns +// admin-rest client (set to use the service instance) +// CRN for the service instance +// entity name +// Any error that occurred +func getQuotaClientInstanceEntity(d *schema.ResourceData, meta interface{}) (*adminrestv1.AdminrestV1, string, string, error) { + adminrestClient, err := meta.(conns.ClientSession).ESadminRestSession() + if err != nil { + return nil, "", "", err + } + instanceCRN := d.Get("resource_instance_id").(string) + if instanceCRN == "" { // importing + id := d.Id() + crnSegments := strings.Split(id, ":") + if len(crnSegments) != 10 || crnSegments[8] != "quota" || crnSegments[9] == "" { + return nil, "", "", fmt.Errorf("ID '%s' is not a quota resource", id) + } + entity := crnSegments[9] + crnSegments[8] = "" + crnSegments[9] = "" + instanceCRN = strings.Join(crnSegments, ":") + d.Set("resource_instance_id", instanceCRN) + d.Set("entity", entity) + } + + instance, err := getInstanceDetails(instanceCRN, meta) + if err != nil { + return nil, "", "", err + } + adminURL := instance.Extensions["kafka_http_url"].(string) + adminrestClient.SetServiceURL(adminURL) + return adminrestClient, instanceCRN, d.Get("entity").(string), nil +} + +func getQuotaID(instanceCRN string, entity string) string { + crnSegments := strings.Split(instanceCRN, ":") + crnSegments[8] = "quota" + crnSegments[9] = entity + return strings.Join(crnSegments, ":") +} + +// admin-rest API returns nil for undefined rate, convert that to -1 +func getQuotaValue(v *int64) int { + if v == nil { + return -1 + } + return int(*v) +} diff --git a/ibm/service/eventstreams/data_source_ibm_event_streams_quota_test.go b/ibm/service/eventstreams/data_source_ibm_event_streams_quota_test.go new file mode 100644 index 0000000000..8bff410848 --- /dev/null +++ b/ibm/service/eventstreams/data_source_ibm_event_streams_quota_test.go @@ -0,0 +1,84 @@ +// Copyright IBM Corp. 2024 All Rights Reserved. +// Licensed under the Mozilla Public License v2.0 + +package eventstreams_test + +import ( + "fmt" + "strings" + "testing" + + acc "github.com/IBM-Cloud/terraform-provider-ibm/ibm/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +const ( + // Data source test requires MZR instance have this quota with producer rate 10000, consumer rate 20000 + testQuotaEntity1 = "iam-ServiceId-00001111-2222-3333-4444-555566667777" + // Data source test requires MZR instance have this quota with producer rate 4096, consumer rate not defined + testQuotaEntity2 = "iam-ServiceId-77776666-5555-4444-3333-222211110000" + // Resource test requires MZR instance NOT have a quota for this + testQuotaEntity3 = "iam-ServiceId-99998888-7777-6666-5555-444433332222" + // Resource test requires MZR instance NOT have a quota for this + testQuotaEntity4 = "default" +) + +func TestAccIBMEventStreamsQuotaDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + Providers: acc.TestAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCheckIBMEventStreamsQuotaDataSourceConfig(getTestInstanceName(mzrKey), testQuotaEntity1), + Check: resource.ComposeTestCheckFunc( + testAccCheckIBMEventStreamsQuotaDataSourceProperties("data.ibm_event_streams_quota.es_quota", testQuotaEntity1, "10000", "20000"), + ), + }, + { + Config: testAccCheckIBMEventStreamsQuotaDataSourceConfig(getTestInstanceName(mzrKey), testQuotaEntity2), + Check: resource.ComposeTestCheckFunc( + testAccCheckIBMEventStreamsQuotaDataSourceProperties("data.ibm_event_streams_quota.es_quota", testQuotaEntity2, "4096", "-1"), + ), + }, + }, + }) +} + +func testAccCheckIBMEventStreamsQuotaDataSourceConfig(instanceName string, entity string) string { + return fmt.Sprintf(` + data "ibm_resource_group" "group" { + is_default=true + } + data "ibm_resource_instance" "es_instance" { + resource_group_id = data.ibm_resource_group.group.id + name = "%s" + } + data "ibm_event_streams_quota" "es_quota" { + resource_instance_id = data.ibm_resource_instance.es_instance.id + entity = "%s" + }`, instanceName, entity) +} + +// check properties of the terraform data source object +func testAccCheckIBMEventStreamsQuotaDataSourceProperties(name string, entity string, producerRate string, consumerRate string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + quotaID := rs.Primary.ID + if quotaID == "" { + return fmt.Errorf("[ERROR] Quota ID is not set") + } + if !strings.HasSuffix(quotaID, fmt.Sprintf(":quota:%s", entity)) { + return fmt.Errorf("[ERROR] Quota ID for %s not expected CRN", quotaID) + } + if producerRate != rs.Primary.Attributes["producer_byte_rate"] { + return fmt.Errorf("[ERROR] Quota for %s producer_byte_rate = %s, expected %s", entity, rs.Primary.Attributes["producer_byte_rate"], producerRate) + } + if consumerRate != rs.Primary.Attributes["consumer_byte_rate"] { + return fmt.Errorf("[ERROR] Quota for %s consumer_byte_rate = %s, expected %s", entity, rs.Primary.Attributes["consumer_byte_rate"], consumerRate) + } + return nil + } +} diff --git a/ibm/service/eventstreams/resource_ibm_event_streams_quota.go b/ibm/service/eventstreams/resource_ibm_event_streams_quota.go new file mode 100644 index 0000000000..be80fdf728 --- /dev/null +++ b/ibm/service/eventstreams/resource_ibm_event_streams_quota.go @@ -0,0 +1,166 @@ +// Copyright IBM Corp. 2024 All Rights Reserved. +// Licensed under the Mozilla Public License v2.0 + +package eventstreams + +import ( + "context" + "fmt" + "log" + + "github.com/IBM-Cloud/terraform-provider-ibm/ibm/flex" + "github.com/IBM/eventstreams-go-sdk/pkg/adminrestv1" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +// A quota in an Event Streams service instance. +// The ID is the CRN with the last two components "quota:entity". +// The producer_byte_rate and consumer_byte_rate are the two quota properties, and must be at least -1; +// -1 means no quota applied. +func ResourceIBMEventStreamsQuota() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceIBMEventStreamsQuotaCreate, + ReadContext: resourceIBMEventStreamsQuotaRead, + UpdateContext: resourceIBMEventStreamsQuotaUpdate, + DeleteContext: resourceIBMEventStreamsQuotaDelete, + Importer: &schema.ResourceImporter{}, + + Schema: map[string]*schema.Schema{ + "resource_instance_id": { + Type: schema.TypeString, + Description: "The ID or the CRN of the Event Streams service instance", + Required: true, + ForceNew: true, + }, + "entity": { + Type: schema.TypeString, + Required: true, + Description: "The entity for which the quota is set; 'default' or IAM ID", + }, + "producer_byte_rate": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntAtLeast(-1), + Description: "The producer quota in bytes per second, -1 means no quota", + }, + "consumer_byte_rate": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntAtLeast(-1), + Description: "The consumer quota in bytes per second, -1 means no quota", + }, + }, + } +} + +func resourceIBMEventStreamsQuotaCreate(context context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + adminrestClient, instanceCRN, entity, err := getQuotaClientInstanceEntity(d, meta) + if err != nil { + tfErr := flex.TerraformErrorf(err, "Error getting Event Streams instance", "ibm_event_streams_quota", "create") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + + createQuotaOptions := &adminrestv1.CreateQuotaOptions{} + createQuotaOptions.SetEntityName(entity) + pbr := d.Get("producer_byte_rate").(int) + cbr := d.Get("consumer_byte_rate").(int) + if pbr == -1 && cbr == -1 { + return diag.FromErr(fmt.Errorf("Quota for %s cannot be created: producer_byte_rate and consumer_byte_rate are both -1 (no quota)", entity)) + } + if pbr != -1 { + createQuotaOptions.SetProducerByteRate(int64(pbr)) + } + if cbr != -1 { + createQuotaOptions.SetConsumerByteRate(int64(cbr)) + } + + response, err := adminrestClient.CreateQuotaWithContext(context, createQuotaOptions) + if err != nil { + tfErr := flex.TerraformErrorf(err, fmt.Sprintf("CreateQuota failed with response: %s", response), "ibm_event_streams_quota", "create") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + d.SetId(getQuotaID(instanceCRN, entity)) + + return resourceIBMEventStreamsQuotaRead(context, d, meta) +} + +func resourceIBMEventStreamsQuotaRead(context context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + adminrestClient, instanceCRN, entity, err := getQuotaClientInstanceEntity(d, meta) + if err != nil { + tfErr := flex.TerraformErrorf(err, "Error getting Event Streams instance", "ibm_event_streams_quota", "read") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + + getQuotaOptions := &adminrestv1.GetQuotaOptions{} + getQuotaOptions.SetEntityName(entity) + quota, response, err := adminrestClient.GetQuotaWithContext(context, getQuotaOptions) + if err != nil || quota == nil { + d.SetId("") + var tfErr *flex.TerraformProblem + if response != nil && response.StatusCode == 404 { + tfErr = flex.TerraformErrorf(err, fmt.Sprintf("Quota for '%s' does not exist", entity), "ibm_event_streams_quota", "read") + } else { + tfErr = flex.TerraformErrorf(err, fmt.Sprintf("GetQuota failed with response: %s", response), "ibm_event_streams_quota", "read") + } + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + + d.Set("resource_instance_id", instanceCRN) + d.Set("entity", entity) + d.Set("producer_byte_rate", getQuotaValue(quota.ProducerByteRate)) + d.Set("consumer_byte_rate", getQuotaValue(quota.ConsumerByteRate)) + d.SetId(getQuotaID(instanceCRN, entity)) + + return nil +} + +func resourceIBMEventStreamsQuotaUpdate(context context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + if d.HasChange("producer_byte_rate") || d.HasChange("consumer_byte_rate") { + adminrestClient, _, entity, err := getQuotaClientInstanceEntity(d, meta) + if err != nil { + tfErr := flex.TerraformErrorf(err, "Error getting Event Streams instance", "ibm_event_streams_quota", "update") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + + updateQuotaOptions := &adminrestv1.UpdateQuotaOptions{} + updateQuotaOptions.SetEntityName(entity) + updateQuotaOptions.SetProducerByteRate(int64(d.Get("producer_byte_rate").(int))) + updateQuotaOptions.SetConsumerByteRate(int64(d.Get("consumer_byte_rate").(int))) + + response, err := adminrestClient.UpdateQuotaWithContext(context, updateQuotaOptions) + if err != nil { + tfErr := flex.TerraformErrorf(err, fmt.Sprintf("UpdateQuota failed with response: %s", response), "ibm_event_streams_quota", "update") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + } + return resourceIBMEventStreamsQuotaRead(context, d, meta) +} + +func resourceIBMEventStreamsQuotaDelete(context context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + adminrestClient, _, entity, err := getQuotaClientInstanceEntity(d, meta) + if err != nil { + tfErr := flex.TerraformErrorf(err, "Error getting Event Streams instance", "ibm_event_streams_quota", "delete") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + + deleteQuotaOptions := &adminrestv1.DeleteQuotaOptions{} + deleteQuotaOptions.SetEntityName(entity) + + response, err := adminrestClient.DeleteQuotaWithContext(context, deleteQuotaOptions) + if err != nil { + tfErr := flex.TerraformErrorf(err, fmt.Sprintf("DeleteQuota failed with response: %s", response), "ibm_event_streams_quota", "delete") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + d.SetId("") + return nil +} diff --git a/ibm/service/eventstreams/resource_ibm_event_streams_quota_test.go b/ibm/service/eventstreams/resource_ibm_event_streams_quota_test.go new file mode 100644 index 0000000000..83b1f5733d --- /dev/null +++ b/ibm/service/eventstreams/resource_ibm_event_streams_quota_test.go @@ -0,0 +1,138 @@ +// Copyright IBM Corp. 2024 All Rights Reserved. +// Licensed under the Mozilla Public License v2.0 + +package eventstreams_test + +import ( + "fmt" + "strings" + "testing" + + acc "github.com/IBM-Cloud/terraform-provider-ibm/ibm/acctest" + "github.com/IBM-Cloud/terraform-provider-ibm/ibm/conns" + "github.com/IBM/eventstreams-go-sdk/pkg/adminrestv1" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccIBMEventStreamsQuotaResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + Providers: acc.TestAccProviders, + CheckDestroy: testAccCheckIBMEventStreamsQuotasDeletedFromInstance(testQuotaEntity3, testQuotaEntity4), + Steps: []resource.TestStep{ + { + Config: testAccCheckIBMEventStreamsQuotaResourceConfig(getTestInstanceName(mzrKey), 0, testQuotaEntity3, 2048, 1024), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckIBMEventStreamsQuotaResourceProperties("ibm_event_streams_quota.es_quota0", testQuotaEntity3), + testAccCheckIBMEventStreamsQuotaWasSetInInstance(testQuotaEntity3, 2048, 1024), + resource.TestCheckResourceAttrSet("ibm_event_streams_quota.es_quota0", "id"), + resource.TestCheckResourceAttr("ibm_event_streams_quota.es_quota0", "producer_byte_rate", "2048"), + resource.TestCheckResourceAttr("ibm_event_streams_quota.es_quota0", "consumer_byte_rate", "1024"), + ), + }, + { + Config: testAccCheckIBMEventStreamsQuotaResourceConfig(getTestInstanceName(mzrKey), 1, testQuotaEntity4, 100000000, -1), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckIBMEventStreamsQuotaResourceProperties("ibm_event_streams_quota.es_quota1", testQuotaEntity4), + testAccCheckIBMEventStreamsQuotaWasSetInInstance(testQuotaEntity4, 100000000, -1), + resource.TestCheckResourceAttrSet("ibm_event_streams_quota.es_quota1", "id"), + resource.TestCheckResourceAttr("ibm_event_streams_quota.es_quota1", "producer_byte_rate", "100000000"), + resource.TestCheckResourceAttr("ibm_event_streams_quota.es_quota1", "consumer_byte_rate", "-1"), + ), + }, + { + ResourceName: "ibm_event_streams_quota.es_quota1", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckIBMEventStreamsQuotaResourceConfig(instanceName string, tnum int, entity string, producerRate int, consumerRate int) string { + return fmt.Sprintf(` + data "ibm_resource_group" "group" { + is_default=true + } + data "ibm_resource_instance" "es_instance" { + resource_group_id = data.ibm_resource_group.group.id + name = "%s" + } + resource "ibm_event_streams_quota" "es_quota%d" { + resource_instance_id = data.ibm_resource_instance.es_instance.id + entity = "%s" + producer_byte_rate = %d + consumer_byte_rate = %d + }`, instanceName, tnum, entity, producerRate, consumerRate) +} + +// check properties of the terraform resource object +func testAccCheckIBMEventStreamsQuotaResourceProperties(name string, entity string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + quotaID := rs.Primary.ID + if quotaID == "" { + return fmt.Errorf("[ERROR] Quota ID is not set") + } + if !strings.HasSuffix(quotaID, fmt.Sprintf(":quota:%s", entity)) { + return fmt.Errorf("[ERROR] Quota ID %s not expected CRN", quotaID) + } + return nil + } +} + +// go to the Event Streams instance and check the quota has been set +func testAccCheckIBMEventStreamsQuotaWasSetInInstance(entity string, producerRate int, consumerRate int) resource.TestCheckFunc { + return func(s *terraform.State) error { + adminClient, err := acc.TestAccProvider.Meta().(conns.ClientSession).ESadminRestSession() + if err != nil { + return fmt.Errorf("[ERROR] ESadminRestSession returned %v", err) + } + entityName := entity + qd, _, err := adminClient.GetQuota(&adminrestv1.GetQuotaOptions{EntityName: &entityName}) + if err != nil { + return fmt.Errorf("[ERROR] GetQuota returned %v", err) + } + qdp := testGetQuotaValue(qd.ProducerByteRate) + if producerRate != qdp { + return fmt.Errorf("[ERROR] quota producer byte rate expected %d, got %d", producerRate, qdp) + } + qdc := testGetQuotaValue(qd.ConsumerByteRate) + if consumerRate != qdc { + return fmt.Errorf("[ERROR] quota consumer byte rate expected %d, got %d", consumerRate, qdc) + } + return nil + } +} + +// go to the Event Streams instance and check the quota has been destroyed +func testAccCheckIBMEventStreamsQuotasDeletedFromInstance(entities ...string) func(*terraform.State) error { + return func(s *terraform.State) error { + adminClient, err := acc.TestAccProvider.Meta().(conns.ClientSession).ESadminRestSession() + if err != nil { + return fmt.Errorf("[ERROR] ESadminRestSession returned %v", err) + } + for _, entity := range entities { + entityName := entity + qd, response, err := adminClient.GetQuota(&adminrestv1.GetQuotaOptions{EntityName: &entityName}) + if err == nil { + return fmt.Errorf("[ERROR] Expected no quota for %s, but GetQuota succeeded (%d,%d)", entity, testGetQuotaValue(qd.ProducerByteRate), testGetQuotaValue(qd.ConsumerByteRate)) + } + if response != nil && response.StatusCode != 404 { + return fmt.Errorf("[ERROR] Expected 404 NotFound for %s, but GetQuota response was %d", entity, response.StatusCode) + } + } + return nil + } +} + +func testGetQuotaValue(v *int64) int { + if v == nil { + return -1 + } + return int(*v) +} diff --git a/website/docs/d/event_streams_quota.html.markdown b/website/docs/d/event_streams_quota.html.markdown new file mode 100644 index 0000000000..fed93b90e8 --- /dev/null +++ b/website/docs/d/event_streams_quota.html.markdown @@ -0,0 +1,55 @@ +--- +subcategory: "Event Streams" +layout: "ibm" +page_title: "IBM: ibm_event_streams_quota" +description: |- + Get information about a quota of an IBM Event Streams service instance. +--- + +# ibm_event_streams_quota + +Retrieve information about a quota of an Event Streams instance. Both the default quota and user quotas may be managed. Quotas are only available on Event Streams Enterprise plan service instances. For more information about Event Streams quotas, see [Setting Kafka quotas](https://cloud.ibm.com/docs/EventStreams?topic=EventStreams-enabling_kafka_quotas). + +## Example usage + +To retrieve the default quota: + +```terraform +data "ibm_resource_instance" "es_instance" { + name = "terraform-integration" + resource_group_id = data.ibm_resource_group.group.id +} + +data "ibm_event_streams_quota" "es_quota_default" { + resource_instance_id = data.ibm_resource_instance.es_instance.id + entity = "default" +} +``` + +To retrieve a user quota, for a user with the given IAM ID: + +```terraform +data "ibm_resource_instance" "es_instance" { + name = "terraform-integration" + resource_group_id = data.ibm_resource_group.group.id +} + +data "ibm_event_streams_quota" "es_quota_user" { + resource_instance_id = data.ibm_resource_instance.es_instance.id + entity = "iam-ServiceId-00001111-2222-3333-4444-555566667777" +} + +## Argument reference + +You must specify the following arguments for this data source. + +- `resource_instance_id` - (Required, String) The ID or CRN of the Event Streams service instance. +- `entity` - (Required, String) Either `default` to set the default quota, or an IAM ID for a user quota. + +## Attribute reference + +After your data source is created, you can read values from the listed arguments and the following attributes. + +- `id` - (String) The ID of the quota in CRN format. The last field of the CRN is either `default`, or the IAM ID of the user. For example, `crn:v1:bluemix:public:messagehub:us-south:a/6db1b0d0b5c54ee5c201552547febcd8:ffffffff-eeee-dddd-cccc-bbbbaaaa9999:quota:default`, or `crn:v1:bluemix:public:messagehub:us-south:a/6db1b0d0b5c54ee5c201552547febcd8:ffffffff-eeee-dddd-cccc-bbbbaaaa9999:quota:iam-ServiceId-00001111-2222-3333-4444-555566667777`. +- `producer_byte_rate` - (Integer) The producer quota in bytes/second. If no producer quota is set, this is -1. +- `consumer_byte_rate` - (Integer) The consumer quota in bytes/second. If no consumer quota is set, this is -1. diff --git a/website/docs/r/event_streams_quota.html.markdown b/website/docs/r/event_streams_quota.html.markdown new file mode 100644 index 0000000000..d1aa40b38a --- /dev/null +++ b/website/docs/r/event_streams_quota.html.markdown @@ -0,0 +1,96 @@ +--- +subcategory: "Event Streams" +layout: "ibm" +page_title: "IBM: event_streams_quota" +description: |- + Manages a quota of an IBM Event Streams service instance. +--- + +# ibm_event_streams_quota + +Create, update or delete a quota of an Event Streams service instance. Both the default quota and user quotas may be managed. Quotas are only available on Event Streams Enterprise plan service instances. For more information about Event Streams quotas, see [Setting Kafka quotas](https://cloud.ibm.com/docs/EventStreams?topic=EventStreams-enabling_kafka_quotas). + +## Example usage + +### Sample 1: Create an Event Streams service instance and apply a default quota + +Using `entity = default` in the quota resource sets the default quota, which applies to all users for which no user quota has been set. + +```terraform +resource "ibm_resource_instance" "es_instance_1" { + name = "terraform-integration-1" + service = "messagehub" + plan = "enterprise-3nodes-2tb" + location = "us-south" + resource_group_id = data.ibm_resource_group.group.id + + timeouts { + create = "3h" + update = "1h" + delete = "15m" + } +} + +resource "ibm_event_streams_quota" "es_quota_1" { + resource_instance_id = ibm_resource_instance.es_instance_1.id + entity = "default" + producer_byte_rate = 16384 + consumer_byte_rate = 32768 +} + +``` + +### Sample 2: Create a user quota on an existing Event Streams instance + +The quota is set for the user with the given IAM ID. The producer rate is limited, the consumer rate is unlimited (-1). + +```terraform +data "ibm_resource_instance" "es_instance_2" { + name = "terraform-integration-2" + resource_group_id = data.ibm_resource_group.group.id +} + +resource "ibm_event_streams_quota" "es_quota_2" { + resource_instance_id = ibm_resource_instance.es_instance_2.id + entity = "iam-ServiceId-00001111-2222-3333-4444-555566667777" + producer_byte_rate = 16384 + consumer_byte_rate = -1 +} + +``` + +## Argument reference + +You must specify the following arguments for this resource. + +- `resource_instance_id` - (Required, String) The ID or the CRN of the Event Streams service instance. +- `entity` - (Required, String) Either `default` to set the default quota, or an IAM ID for a user quota. +- `producer_byte_rate` - (Required, Integer) The producer quota in bytes/second. Use -1 for no quota. +- `consumer_byte_rate` - (Required, Integer) The consumer quota in bytes/second. Use -1 for no quota. + +## Attribute reference + +After your resource is created, you can read values from the listed arguments and the following attributes. + +- `id` - (String) The ID of the quota in CRN format. The last field of the CRN is either `default`, or the IAM ID of the user. See the examples in the import section. + +## Import + +The `ibm_event_streams_quota` resource can be imported by using the ID in CRN format. The three colon-separated parameters of the `CRN` are: + - instance CRN = CRN of the Event Streams instance + - resource type = quota + - quota entity = `default` or the IAM ID of the user + +**Syntax** + +``` +$ terraform import ibm_event_streams_quota.es_quota + +``` + +**Examples** + +``` +$ terraform import ibm_event_streams_quota.es_quota_default crn:v1:bluemix:public:messagehub:us-south:a/6db1b0d0b5c54ee5c201552547febcd8:ffffffff-eeee-dddd-cccc-bbbbaaaa9999:quota:default +$ terraform import ibm_event_streams_quota.es_quota_user crn:v1:bluemix:public:messagehub:us-south:a/6db1b0d0b5c54ee5c201552547febcd8:ffffffff-eeee-dddd-cccc-bbbbaaaa9999:quota:iam-ServiceId-00001111-2222-3333-4444-555566667777 +```