diff --git a/internal/services/eventgrid/client/client.go b/internal/services/eventgrid/client/client.go index 38625e8021ab..04b83989cfc1 100644 --- a/internal/services/eventgrid/client/client.go +++ b/internal/services/eventgrid/client/client.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/go-azure-sdk/resource-manager/eventgrid/2023-12-15-preview/namespaces" eventgrid_v2025_02_15 "github.com/hashicorp/go-azure-sdk/resource-manager/eventgrid/2025-02-15" + "github.com/hashicorp/go-azure-sdk/resource-manager/eventgrid/2025-02-15/namespacetopics" "github.com/hashicorp/go-azure-sdk/sdk/client/resourcemanager" "github.com/hashicorp/terraform-provider-azurerm/internal/common" ) @@ -15,7 +16,8 @@ import ( type Client struct { *eventgrid_v2025_02_15.Client - NamespacesClient *namespaces.NamespacesClient + NamespacesClient *namespaces.NamespacesClient + NamespaceTopicsClient *namespacetopics.NamespaceTopicsClient } func NewClient(o *common.ClientOptions) (*Client, error) { @@ -25,6 +27,12 @@ func NewClient(o *common.ClientOptions) (*Client, error) { } o.Configure(NamespacesClient.Client, o.Authorizers.ResourceManager) + NamespaceTopicsClient, err := namespacetopics.NewNamespaceTopicsClientWithBaseURI(o.Environment.ResourceManager) + if err != nil { + return nil, fmt.Errorf("building Namespace Topics Client: %+v", err) + } + o.Configure(NamespaceTopicsClient.Client, o.Authorizers.ResourceManager) + client, err := eventgrid_v2025_02_15.NewClientWithBaseURI(o.Environment.ResourceManager, func(c *resourcemanager.Client) { o.Configure(c, o.Authorizers.ResourceManager) }) @@ -32,7 +40,8 @@ func NewClient(o *common.ClientOptions) (*Client, error) { return nil, fmt.Errorf("building EventGrid client: %+v", err) } return &Client{ - NamespacesClient: NamespacesClient, - Client: client, + NamespacesClient: NamespacesClient, + NamespaceTopicsClient: NamespaceTopicsClient, + Client: client, }, nil } diff --git a/internal/services/eventgrid/eventgrid_namespace_topic_resource.go b/internal/services/eventgrid/eventgrid_namespace_topic_resource.go new file mode 100644 index 000000000000..f1d7a435e96f --- /dev/null +++ b/internal/services/eventgrid/eventgrid_namespace_topic_resource.go @@ -0,0 +1,226 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package eventgrid + +import ( + "context" + "fmt" + "regexp" + "time" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" + "github.com/hashicorp/go-azure-sdk/resource-manager/eventgrid/2025-02-15/namespaces" + "github.com/hashicorp/go-azure-sdk/resource-manager/eventgrid/2025-02-15/namespacetopics" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" +) + +//go:generate go run ../../tools/generator-tests resourceidentity -resource-name eventgrid_namespace_topic -properties "name" -compare-values "subscription_id:eventgrid_namespace_id,resource_group_name:eventgrid_namespace_id,namespace_name:eventgrid_namespace_id" + +var ( + _ sdk.ResourceWithUpdate = EventGridNamespaceTopicResource{} + _ sdk.ResourceWithIdentity = EventGridNamespaceTopicResource{} +) + +type EventGridNamespaceTopicResource struct{} + +type EventGridNamespaceTopicResourceModel struct { + Name string `tfschema:"name"` + EventgridNamespaceId string `tfschema:"eventgrid_namespace_id"` + EventRetentionInDays int64 `tfschema:"event_retention_in_days"` +} + +func (r EventGridNamespaceTopicResource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringIsNotEmpty, + validation.StringMatch( + regexp.MustCompile("^[a-zA-Z0-9-]{3,50}$"), + "Event Grid Namespace Topic name must be 3 - 50 characters long, contain only letters, numbers and hyphens.", + ), + ), + }, + + "eventgrid_namespace_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: namespacetopics.ValidateNamespaceID, + }, + + "event_retention_in_days": { + Type: pluginsdk.TypeInt, + Optional: true, + Default: 7, + ValidateFunc: validation.IntBetween(1, 7), + }, + } +} + +func (r EventGridNamespaceTopicResource) ModelObject() interface{} { + return &EventGridNamespaceTopicResourceModel{} +} + +func (r EventGridNamespaceTopicResource) ResourceType() string { + return "azurerm_eventgrid_namespace_topic" +} + +func (r EventGridNamespaceTopicResource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{} +} + +func (r EventGridNamespaceTopicResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.EventGrid.NamespaceTopicsClient + + var model EventGridNamespaceTopicResourceModel + if err := metadata.Decode(&model); err != nil { + return fmt.Errorf("decoding: %+v", err) + } + + namespaceId, err := namespaces.ParseNamespaceID(model.EventgridNamespaceId) + if err != nil { + return err + } + + id := namespacetopics.NewNamespaceTopicID(namespaceId.SubscriptionId, namespaceId.ResourceGroupName, namespaceId.NamespaceName, model.Name) + + existing, err := client.Get(ctx, id) + if err != nil { + if !response.WasNotFound(existing.HttpResponse) { + return fmt.Errorf("checking for presence of existing %s: %s", id, err) + } + } + + if !response.WasNotFound(existing.HttpResponse) { + return metadata.ResourceRequiresImport(r.ResourceType(), id) + } + + namespaceTopic := namespacetopics.NamespaceTopic{ + Name: pointer.To(model.Name), + Properties: &namespacetopics.NamespaceTopicProperties{ + EventRetentionInDays: pointer.To(model.EventRetentionInDays), + InputSchema: pointer.To(namespacetopics.EventInputSchemaCloudEventSchemaVOneZero), + PublisherType: pointer.To(namespacetopics.PublisherTypeCustom), + }, + } + + if err = client.CreateOrUpdateThenPoll(ctx, id, namespaceTopic); err != nil { + return fmt.Errorf("creating %s: %+v", id, err) + } + + metadata.SetID(id) + return pluginsdk.SetResourceIdentityData(metadata.ResourceData, &id) + }, + } +} + +func (r EventGridNamespaceTopicResource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.EventGrid.NamespaceTopicsClient + + var model EventGridNamespaceTopicResourceModel + if err := metadata.Decode(&model); err != nil { + return fmt.Errorf("decoding: %+v", err) + } + + id, err := namespacetopics.ParseNamespaceTopicID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + payload := namespacetopics.NamespaceTopicUpdateParameters{ + Properties: &namespacetopics.NamespaceTopicUpdateParameterProperties{}, + } + + if metadata.ResourceData.HasChange("event_retention_in_days") { + payload.Properties.EventRetentionInDays = pointer.To(model.EventRetentionInDays) + } + + if err = client.UpdateThenPoll(ctx, *id, payload); err != nil { + return fmt.Errorf("updating %s: %+v", id, err) + } + + return nil + }, + } +} + +func (r EventGridNamespaceTopicResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.EventGrid.NamespaceTopicsClient + + id, err := namespacetopics.ParseNamespaceTopicID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + resp, err := client.Get(ctx, *id) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return metadata.MarkAsGone(id) + } + return fmt.Errorf("retrieving %s: %+v", *id, err) + } + + state := EventGridNamespaceTopicResourceModel{ + Name: id.TopicName, + EventgridNamespaceId: namespacetopics.NewNamespaceID(id.SubscriptionId, id.ResourceGroupName, id.NamespaceName).ID(), + } + + if model := resp.Model; model != nil { + if props := model.Properties; props != nil { + state.EventRetentionInDays = pointer.From(props.EventRetentionInDays) + } + } + + if err := pluginsdk.SetResourceIdentityData(metadata.ResourceData, id); err != nil { + return err + } + + return metadata.Encode(&state) + }, + } +} + +func (r EventGridNamespaceTopicResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.EventGrid.NamespaceTopicsClient + + id, err := namespacetopics.ParseNamespaceTopicID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + if err := client.DeleteThenPoll(ctx, *id); err != nil { + return fmt.Errorf("deleting %s: %v", *id, err) + } + + return nil + }, + } +} + +func (r EventGridNamespaceTopicResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return namespacetopics.ValidateNamespaceTopicID +} + +func (r EventGridNamespaceTopicResource) Identity() resourceids.ResourceId { + return new(namespacetopics.NamespaceTopicId) +} diff --git a/internal/services/eventgrid/eventgrid_namespace_topic_resource_identity_gen_test.go b/internal/services/eventgrid/eventgrid_namespace_topic_resource_identity_gen_test.go new file mode 100644 index 000000000000..f243b072a133 --- /dev/null +++ b/internal/services/eventgrid/eventgrid_namespace_topic_resource_identity_gen_test.go @@ -0,0 +1,40 @@ +// Copyright IBM Corp. 2014, 2025 +// SPDX-License-Identifier: MPL-2.0 + +package eventgrid_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" + customstatecheck "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/statecheck" +) + +func TestAccEventgridNamespaceTopic_resourceIdentity(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_eventgrid_namespace_topic", "test") + r := EventgridNamespaceTopicResource{} + + checkedFields := map[string]struct{}{ + "name": {}, + "namespace_name": {}, + "resource_group_name": {}, + "subscription_id": {}, + } + + data.ResourceIdentityTest(t, []acceptance.TestStep{ + { + Config: r.basic(data), + ConfigStateChecks: []statecheck.StateCheck{ + customstatecheck.ExpectAllIdentityFieldsAreChecked("azurerm_eventgrid_namespace_topic.test", checkedFields), + statecheck.ExpectIdentityValueMatchesStateAtPath("azurerm_eventgrid_namespace_topic.test", tfjsonpath.New("name"), tfjsonpath.New("name")), + customstatecheck.ExpectStateContainsIdentityValueAtPath("azurerm_eventgrid_namespace_topic.test", tfjsonpath.New("namespace_name"), tfjsonpath.New("eventgrid_namespace_id")), + customstatecheck.ExpectStateContainsIdentityValueAtPath("azurerm_eventgrid_namespace_topic.test", tfjsonpath.New("resource_group_name"), tfjsonpath.New("eventgrid_namespace_id")), + customstatecheck.ExpectStateContainsIdentityValueAtPath("azurerm_eventgrid_namespace_topic.test", tfjsonpath.New("subscription_id"), tfjsonpath.New("eventgrid_namespace_id")), + }, + }, + data.ImportBlockWithResourceIdentityStep(false), + data.ImportBlockWithIDStep(false), + }, false) +} diff --git a/internal/services/eventgrid/eventgrid_namespace_topic_resource_test.go b/internal/services/eventgrid/eventgrid_namespace_topic_resource_test.go new file mode 100644 index 000000000000..c9c85d015b9f --- /dev/null +++ b/internal/services/eventgrid/eventgrid_namespace_topic_resource_test.go @@ -0,0 +1,153 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package eventgrid_test + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-sdk/resource-manager/eventgrid/2025-02-15/namespacetopics" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" + "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" +) + +type EventgridNamespaceTopicResource struct{} + +func TestAccEventgridNamespaceTopic_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_eventgrid_namespace_topic", "test") + r := EventgridNamespaceTopicResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccEventgridNamespaceTopic_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_eventgrid_namespace_topic", "test") + r := EventgridNamespaceTopicResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.RequiresImportErrorStep(r.requiresImport), + }) +} + +func TestAccEventgridNamespaceTopic_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_eventgrid_namespace_topic", "test") + r := EventgridNamespaceTopicResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccEventgridNamespaceTopic_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_eventgrid_namespace_topic", "test") + r := EventgridNamespaceTopicResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func (r EventgridNamespaceTopicResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := namespacetopics.ParseNamespaceTopicID(state.ID) + if err != nil { + return nil, err + } + + resp, err := clients.EventGrid.NamespaceTopicsClient.Get(ctx, *id) + if err != nil { + return nil, fmt.Errorf("retrieving %s: %+v", *id, err) + } + + return pointer.To(resp.Model != nil), nil +} + +func (r EventgridNamespaceTopicResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_eventgrid_namespace_topic" "test" { + name = "acctest-egnt-%d" + eventgrid_namespace_id = azurerm_eventgrid_namespace.test.id +} +`, r.template(data), data.RandomInteger) +} + +func (r EventgridNamespaceTopicResource) requiresImport(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_eventgrid_namespace_topic" "import" { + name = azurerm_eventgrid_namespace_topic.test.name + eventgrid_namespace_id = azurerm_eventgrid_namespace_topic.test.eventgrid_namespace_id +} +`, r.basic(data)) +} + +func (r EventgridNamespaceTopicResource) complete(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_eventgrid_namespace_topic" "test" { + name = "acctest-egnt-%d" + eventgrid_namespace_id = azurerm_eventgrid_namespace.test.id + event_retention_in_days = 1 +} +`, r.template(data), data.RandomInteger) +} + +func (r EventgridNamespaceTopicResource) template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%[1]d" + location = "%[2]s" +} + +resource "azurerm_eventgrid_namespace" "test" { + name = "acctest-egn-%[1]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +} diff --git a/internal/services/eventgrid/registration.go b/internal/services/eventgrid/registration.go index f127bf1953a5..6e68f5de615f 100644 --- a/internal/services/eventgrid/registration.go +++ b/internal/services/eventgrid/registration.go @@ -43,6 +43,7 @@ func (r Registration) DataSources() []sdk.DataSource { func (r Registration) Resources() []sdk.Resource { return []sdk.Resource{ EventGridNamespaceResource{}, + EventGridNamespaceTopicResource{}, EventGridPartnerConfigurationResource{}, EventGridPartnerNamespaceResource{}, EventGridPartnerRegistrationResource{}, diff --git a/website/docs/r/eventgrid_namespace_topic.html.markdown b/website/docs/r/eventgrid_namespace_topic.html.markdown new file mode 100644 index 000000000000..54d659d605c3 --- /dev/null +++ b/website/docs/r/eventgrid_namespace_topic.html.markdown @@ -0,0 +1,73 @@ +--- +subcategory: "Messaging" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_eventgrid_namespace_topic" +description: |- + Manages an Event Grid Namespace Topic. +--- + +# azurerm_eventgrid_namespace_topic + +Manages an Event Grid Namespace Topic. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "example-resources" + location = "West Europe" +} + +resource "azurerm_eventgrid_namespace" "example" { + name = "my-eventgrid-namespace" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name +} + +resource "azurerm_eventgrid_namespace_topic" "example" { + name = "topic-namespace-example" + eventgrid_namespace_id = azurerm_eventgrid_namespace.test.id + event_retention_in_days = 1 +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `name` - (Required) The name which should be used for this Event Grid Namespace Topic. Changing this forces a new resource to be created. + +* `eventgrid_namespace_id` - (Required) The ID of the Event Grid Namespace. Changing this forces a new resource to be created. + +--- + +* `event_retention_in_days` - (Optional) Event retention for the namespace topic expressed in days. Defaults to `7`. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the Event Grid Namespace Topic. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://developer.hashicorp.com/terraform/language/resources/configure#define-operation-timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the Event Grid Namespace Topic. +* `read` - (Defaults to 5 minutes) Used when retrieving the Event Grid Namespace Topic. +* `update` - (Defaults to 30 minutes) Used when updating the Event Grid Namespace Topic. +* `delete` - (Defaults to 30 minutes) Used when deleting the Event Grid Namespace Topic. + +## Import + +Event Grid Namespace Topics can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_eventgrid_namespace_topic.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.EventGrid/namespaces/eventgrid1/topics/topic1 +``` + +## API Providers + +This resource uses the following Azure API Providers: + +* `Microsoft.EventGrid` - 2025-02-15