diff --git a/internal/services/nginx/nginx_configuration_resource.go b/internal/services/nginx/nginx_configuration_resource.go new file mode 100644 index 000000000000..b3126e0a0804 --- /dev/null +++ b/internal/services/nginx/nginx_configuration_resource.go @@ -0,0 +1,318 @@ +package nginx + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/resource-manager/nginx/2022-08-01/nginxconfiguration" + "github.com/hashicorp/go-azure-sdk/resource-manager/nginx/2022-08-01/nginxdeployment" + "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" +) + +const defaultConfigurationName = "default" + +type ConfigFile struct { + Content string `tfschema:"content"` + VirtualPath string `tfschema:"virtual_path"` +} + +type ProtectedFile struct { + Content string `tfschema:"content"` + VirtualPath string `tfschema:"virtual_path"` +} + +type ConfigurationModel struct { + Name string `tfschema:"name"` // always default + NginxDeploymentId string `tfschema:"nginx_deployment_id"` + ConfigFile []ConfigFile `tfschema:"config_file"` + ProtectedFile []ProtectedFile `tfschema:"protected_file"` + PackageData string `tfschema:"package_data"` + RootFile string `tfschema:"root_file"` +} + +// ToSDKModel used in both Create and Update +func (c ConfigurationModel) ToSDKModel() nginxconfiguration.NginxConfiguration { + req := nginxconfiguration.NginxConfiguration{ + Name: pointer.FromString(c.Name), + Properties: &nginxconfiguration.NginxConfigurationProperties{ + RootFile: pointer.FromString(c.RootFile), + }, + } + + var files []nginxconfiguration.NginxConfigurationFile + for _, file := range c.ConfigFile { + files = append(files, nginxconfiguration.NginxConfigurationFile{ + Content: pointer.FromString(file.Content), + VirtualPath: pointer.FromString(file.VirtualPath), + }) + } + req.Properties.Files = &files + + if len(c.ProtectedFile) > 0 { + var protectedFiles []nginxconfiguration.NginxConfigurationFile + for _, file := range c.ProtectedFile { + protectedFiles = append(protectedFiles, nginxconfiguration.NginxConfigurationFile{ + Content: pointer.FromString(file.Content), + VirtualPath: pointer.FromString(file.VirtualPath), + }) + } + req.Properties.ProtectedFiles = &protectedFiles + } + + if c.PackageData != "" { + req.Properties.Package = &nginxconfiguration.NginxConfigurationPackage{ + Data: pointer.FromString(c.PackageData), + } + } + + return req +} + +type ConfigurationResource struct{} + +var _ sdk.Resource = (*ConfigurationResource)(nil) + +func (m ConfigurationResource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "nginx_deployment_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "config_file": { + Type: pluginsdk.TypeList, + Required: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "content": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsBase64, + }, + + "virtual_path": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + }, + }, + + "protected_file": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "content": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsBase64, + }, + + "virtual_path": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + }, + }, + + "package_data": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "root_file": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + } +} + +func (m ConfigurationResource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + // name of nginx configuration set to a fix value `default` by service team. + "name": { + Type: pluginsdk.TypeString, + Computed: true, + }, + } +} + +func (m ConfigurationResource) ModelObject() interface{} { + return &ConfigurationModel{} +} + +func (m ConfigurationResource) ResourceType() string { + return "azurerm_nginx_configuration" +} + +func (m ConfigurationResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, meta sdk.ResourceMetaData) error { + client := meta.Client.Nginx.NginxConfiguration + + var model ConfigurationModel + if err := meta.Decode(&model); err != nil { + return err + } + + model.Name = defaultConfigurationName + deployID, err := nginxdeployment.ParseNginxDeploymentID(model.NginxDeploymentId) + if err != nil { + return err + } + + subscriptionID := meta.Client.Account.SubscriptionId + id := nginxconfiguration.NewConfigurationID(subscriptionID, deployID.ResourceGroupName, deployID.DeploymentName, model.Name) + // get/list, get will cause internal server error if default configuration not exists + // todo remove set retry to 1 + client.Client.RetryAttempts = 1 + existing, err := client.ConfigurationsGet(ctx, id) + if !response.WasNotFound(existing.HttpResponse) && !response.WasStatusCode(existing.HttpResponse, http.StatusInternalServerError) { + if err != nil { + return fmt.Errorf("retreiving %s: %v", id, err) + } + return meta.ResourceRequiresImport(m.ResourceType(), id) + } + + req := model.ToSDKModel() + + future, err := client.ConfigurationsCreateOrUpdate(ctx, id, req) + if err != nil { + return fmt.Errorf("creating %s: %v", id, err) + } + + if err := future.Poller.PollUntilDone(); err != nil { + return fmt.Errorf("waiting for creation of %s: %v", id, err) + } + + meta.SetID(id) + return nil + }, + } +} + +func (m ConfigurationResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, meta sdk.ResourceMetaData) error { + id, err := nginxconfiguration.ParseConfigurationID(meta.ResourceData.Id()) + if err != nil { + return err + } + + client := meta.Client.Nginx.NginxConfiguration + result, err := client.ConfigurationsGet(ctx, *id) + if err != nil { + return err + } + + if result.Model == nil { + return fmt.Errorf("retrieving %s got nil model", id) + } + + var output ConfigurationModel + output.Name = pointer.ToString(result.Model.Name) + deployID := nginxdeployment.NewNginxDeploymentID(id.SubscriptionId, id.ResourceGroupName, id.DeploymentName) + output.NginxDeploymentId = deployID.ID() + + if prop := result.Model.Properties; prop != nil { + output.RootFile = pointer.ToString(prop.RootFile) + + if prop.Package != nil && prop.Package.Data != nil { + output.PackageData = pointer.ToString(prop.Package.Data) + } + + if files := prop.Files; files != nil { + for _, file := range *files { + output.ConfigFile = append(output.ConfigFile, ConfigFile{ + Content: pointer.ToString(file.Content), + VirtualPath: pointer.ToString(file.VirtualPath), + }) + } + } + + if files := prop.ProtectedFiles; files != nil { + for _, file := range *files { + output.ProtectedFile = append(output.ProtectedFile, ProtectedFile{ + Content: pointer.ToString(file.Content), + VirtualPath: pointer.ToString(file.VirtualPath), + }) + } + } + } + + return meta.Encode(&output) + }, + } +} + +func (m ConfigurationResource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 10 * time.Minute, + Func: func(ctx context.Context, meta sdk.ResourceMetaData) (err error) { + client := meta.Client.Nginx.NginxConfiguration + id, err := nginxconfiguration.ParseConfigurationID(meta.ResourceData.Id()) + if err != nil { + return err + } + + var model ConfigurationModel + if err = meta.Decode(&model); err != nil { + return fmt.Errorf("decoding err: %+v", err) + } + + upd := model.ToSDKModel() + result, err := client.ConfigurationsCreateOrUpdate(ctx, *id, upd) + if err != nil { + return fmt.Errorf("updating %s: %v", id, err) + } + if err := result.Poller.PollUntilDone(); err != nil { + return fmt.Errorf("waiting update %s: %v", *id, err) + } + + return nil + }, + } +} + +func (m ConfigurationResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 10 * time.Minute, + Func: func(ctx context.Context, meta sdk.ResourceMetaData) error { + id, err := nginxconfiguration.ParseConfigurationID(meta.ResourceData.Id()) + if err != nil { + return err + } + + meta.Logger.Infof("deleting %s", id) + client := meta.Client.Nginx.NginxConfiguration + result, err := client.ConfigurationsDelete(ctx, *id) + if err != nil { + return fmt.Errorf("deleting %s: %v", id, err) + } + if err := result.Poller.PollUntilDone(); err != nil { + return fmt.Errorf("waiting deleting %s: %v", *id, err) + } + return nil + }, + } +} + +func (m ConfigurationResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return nginxconfiguration.ValidateConfigurationID +} diff --git a/internal/services/nginx/nginx_configuration_resource_test.go b/internal/services/nginx/nginx_configuration_resource_test.go new file mode 100644 index 000000000000..5a473f575b80 --- /dev/null +++ b/internal/services/nginx/nginx_configuration_resource_test.go @@ -0,0 +1,176 @@ +package nginx_test + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform-provider-azurerm/internal/services/nginx" + + "github.com/hashicorp/go-azure-sdk/resource-manager/nginx/2022-08-01/nginxconfiguration" + "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" + "github.com/hashicorp/terraform-provider-azurerm/utils" +) + +type ConfigurationResource struct{} + +func (a ConfigurationResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := nginxconfiguration.ParseConfigurationID(state.ID) + if err != nil { + return nil, err + } + resp, err := client.Nginx.NginxConfiguration.ConfigurationsGet(ctx, *id) + if err != nil { + return nil, fmt.Errorf("retrieving Configuration %s: %+v", id, err) + } + return utils.Bool(resp.Model != nil), nil +} + +func TestAccConfiguration_basic(t *testing.T) { + data := acceptance.BuildTestData(t, nginx.ConfigurationResource{}.ResourceType(), "test") + r := ConfigurationResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + }) +} + +func TestAccConfiguration_update(t *testing.T) { + data := acceptance.BuildTestData(t, nginx.ConfigurationResource{}.ResourceType(), "test") + r := ConfigurationResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.update(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func (a ConfigurationResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` + + +%s + +resource "azurerm_nginx_configuration" "test" { + nginx_deployment_id = azurerm_nginx_deployment.test.id + root_file = "/etc/nginx/nginx.conf" + + config_file { + content = "aHR0cCB7DQogICAgc2VydmVyIHsNCiAgICAgICAgbGlzdGVuIDgwOw0KICAgICAgICBsb2NhdGlvbiAvIHsNCiAgICAgICAgICAgIGRlZmF1bHRfdHlwZSB0ZXh0L2h0bWw7DQogICAgICAgICAgICByZXR1cm4gMjAwICc8IWRvY3R5cGUgaHRtbD48aHRtbCBsYW5nPSJlbiI+PGhlYWQ+PC9oZWFkPjxib2R5Pg0KICAgICAgICAgICAgICAgIDxkaXY+dGhpcyBvbmUgd2lsbCBiZSB1cGRhdGVkPC9kaXY+DQogICAgICAgICAgICAgICAgPGRpdj5hdCAxMDozOCBhbTwvZGl2Pg0KICAgICAgICAgICAgPC9ib2R5PjwvaHRtbD4nOw0KICAgICAgICB9DQogICAgICAgIGluY2x1ZGUgc2l0ZS8qLmNvbmY7DQogICAgfQ0KfQ==" + virtual_path = "/etc/nginx/nginx.conf" + } +} +`, a.template(data)) +} + +func (a ConfigurationResource) update(data acceptance.TestData) string { + return fmt.Sprintf(` + + +%s + +resource "azurerm_nginx_configuration" "test" { + nginx_deployment_id = azurerm_nginx_deployment.test.id + root_file = "/etc/nginx/nginx.conf" + + config_file { + content = "aHR0cCB7DQogICAgc2VydmVyIHsNCiAgICAgICAgbGlzdGVuIDgwOw0KICAgICAgICBsb2NhdGlvbiAvIHsNCiAgICAgICAgICAgIGRlZmF1bHRfdHlwZSB0ZXh0L2h0bWw7DQogICAgICAgICAgICByZXR1cm4gMjAwICc8IWRvY3R5cGUgaHRtbD48aHRtbCBsYW5nPSJlbiI+PGhlYWQ+PC9oZWFkPjxib2R5Pg0KICAgICAgICAgICAgICAgIDxkaXY+dGhpcyBvbmUgd2lsbCBiZSB1cGRhdGVkPC9kaXY+DQogICAgICAgICAgICAgICAgPGRpdj5hdCAxMDozOCBhbTwvZGl2Pg0KICAgICAgICAgICAgPC9ib2R5PjwvaHRtbD4nOw0KICAgICAgICB9DQogICAgICAgIGluY2x1ZGUgc2l0ZS8qLmNvbmY7DQogICAgfQ0KfQ==" + virtual_path = "/etc/nginx/nginx.conf" + } + + config_file { + content = "DQogICAgICAgIGxvY2F0aW9uIC9iYmIgew0KICAgICAgICAgICAgZGVmYXVsdF90eXBlIHRleHQvaHRtbDsNCiAgICAgICAgICAgIHJldHVybiAyMDAgJzwhZG9jdHlwZSBodG1sPjxodG1sIGxhbmc9ImVuIj48aGVhZD48L2hlYWQ+PGJvZHk+DQogICAgICAgICAgICAgICAgPGRpdj50aGlzIG9uZSB3aWxsIGJlIHVwZGF0ZWQ8L2Rpdj4NCiAgICAgICAgICAgICAgICA8ZGl2PmF0IDEwOjM4IGFtPC9kaXY+DQogICAgICAgICAgICA8L2JvZHk+PC9odG1sPic7DQogICAgICAgIH0NCg==" + virtual_path = "/etc/nginx/site/b.conf" + } +} +`, a.template(data)) +} + +func (a ConfigurationResource) template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-auto-%[1]d" + location = "%[2]s" +} + +resource "azurerm_public_ip" "test" { + name = "acctest%[1]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + allocation_method = "Static" + sku = "Standard" + + tags = { + environment = "Production" + } +} + +resource "azurerm_virtual_network" "test" { + name = "acctestvirtnet%[1]d" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_subnet" "test" { + name = "accsubnet%[1]d" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.2.0/24"] + delegation { + name = "delegation" + + service_delegation { + name = "NGINX.NGINXPLUS/nginxDeployments" + actions = [ + "Microsoft.Network/virtualNetworks/subnets/join/action", + ] + } + } +} + +resource "azurerm_nginx_deployment" "test" { + name = "acctest-%[1]d" + resource_group_name = azurerm_resource_group.test.name + sku = "publicpreview_Monthly_gmz7xq9ge3py" + location = azurerm_resource_group.test.location + + //message: "Conflict managed resource group name: tenant: -91a, subscription xxx, resource group example." + managed_resource_group = "accmr%[1]d" + diagnose_support_enabled = true + + frontend_public { + ip_address = [azurerm_public_ip.test.id] + } + + network_interface { + subnet_id = azurerm_subnet.test.id + } + tags = { + foo = "bar" + } +} +`, data.RandomInteger, data.Locations.Primary) +} diff --git a/internal/services/nginx/registration.go b/internal/services/nginx/registration.go index 679d81f493c2..9df323286bb5 100644 --- a/internal/services/nginx/registration.go +++ b/internal/services/nginx/registration.go @@ -33,5 +33,6 @@ func (r Registration) DataSources() []sdk.DataSource { func (r Registration) Resources() []sdk.Resource { return []sdk.Resource{ DeploymentResource{}, + ConfigurationResource{}, } } diff --git a/website/docs/r/nginx_configuration.html.markdown b/website/docs/r/nginx_configuration.html.markdown new file mode 100644 index 000000000000..0ca94703b5ad --- /dev/null +++ b/website/docs/r/nginx_configuration.html.markdown @@ -0,0 +1,79 @@ +--- +subcategory: "Nginx" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_nginx_configuration" +description: |- + Manages a Nginx Configuration. +--- + +# azurerm_nginx_configuration + +Manages a Nginx Configuration. + +## Example Usage + +```hcl +resource "azurerm_nginx_configuration" "test" { + nginx_deployment_id = azurerm_nginx_deployment.test.id + root_file = "/etc/nginx/nginx.conf" + + config_file { + content = "aHR0cCB7DQogICAgc2VydmVyIHsNCiAgICAgICAgbGlzdGVuIDgwOw0KICAgICAgICBsb2NhdGlvbiAvIHsNCiAgICAgICAgICAgIGRlZmF1bHRfdHlwZSB0ZXh0L2h0bWw7DQogICAgICAgICAgICByZXR1cm4gMjAwICc8IWRvY3R5cGUgaHRtbD48aHRtbCBsYW5nPSJlbiI+PGhlYWQ+PC9oZWFkPjxib2R5Pg0KICAgICAgICAgICAgICAgIDxkaXY+dGhpcyBvbmUgd2lsbCBiZSB1cGRhdGVkPC9kaXY+DQogICAgICAgICAgICAgICAgPGRpdj5hdCAxMDozOCBhbTwvZGl2Pg0KICAgICAgICAgICAgPC9ib2R5PjwvaHRtbD4nOw0KICAgICAgICB9DQogICAgICAgIGluY2x1ZGUgc2l0ZS8qLmNvbmY7DQogICAgfQ0KfQ==" + virtual_path = "/etc/nginx/nginx.conf" + } + + config_file { + content = "DQogICAgICAgIGxvY2F0aW9uIC9iYmIgew0KICAgICAgICAgICAgZGVmYXVsdF90eXBlIHRleHQvaHRtbDsNCiAgICAgICAgICAgIHJldHVybiAyMDAgJzwhZG9jdHlwZSBodG1sPjxodG1sIGxhbmc9ImVuIj48aGVhZD48L2hlYWQ+PGJvZHk+DQogICAgICAgICAgICAgICAgPGRpdj50aGlzIG9uZSB3aWxsIGJlIHVwZGF0ZWQ8L2Rpdj4NCiAgICAgICAgICAgICAgICA8ZGl2PmF0IDEwOjM4IGFtPC9kaXY+DQogICAgICAgICAgICA8L2JvZHk+PC9odG1sPic7DQogICAgICAgIH0NCg==" + virtual_path = "/etc/nginx/site/b.conf" + } +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `config_file` - (Required) One or more `config_file` blocks as defined below. + +* `nginx_deployment_id` - (Required) The ID of the Nginx Deployment. Changing this forces a new Nginx Configuration to be created. + +* `root_file` - (Required) Specify the root file path of this Nginx Configuration. + +--- + +* `package_data` - (Optional) Specify the package data for this configuration. + +* `protected_file` - (Optional) One or more `config_file` blocks as defined below. + +--- + +A `config_file` block supports the following: + +* `content` - (Required) Specify the content of this config file. Content value should be encoded by base64 + +* `virtual_path` - (Required) Specify the path of this config file. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the Nginx Configuration. + +* `name` - The name of this Nginx Configuration. The value of configuration name is a fixed value as `default`. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the Nginx Configuration. +* `read` - (Defaults to 5 minutes) Used when retrieving the Nginx Configuration. +* `update` - (Defaults to 10 minutes) Used when updating the Nginx Configuration. +* `delete` - (Defaults to 10 minutes) Used when deleting the Nginx Configuration. + +## Import + +Nginxs can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_nginx_configuration.example /subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Nginx Configuration.NginxPlus/nginxDeployments/dep1/configurations/default +``` \ No newline at end of file