This guide covers adding a new Resource to a Service Package, see adding a New Service Package if the Service Package doesn't exist yet.
At this point in time the AzureRM Provider supports both Typed and Untyped Resources - more information can be found in the High Level Overview.
This guide covers adding a new Typed Resource, which makes uses the Typed SDK within this repository, which requires the following steps:
- Ensure all the dependencies are installed (see Building the Provider).
- Add an SDK Client (if required).
- Define the Resource ID.
- Scaffold an empty/new Resource.
- Register the new Resource.
- Add Acceptance Test(s) for this Resource.
- Run the Acceptance Test(s).
- Add Documentation for this Resource.
- Send the Pull Request.
We'll go through each of those steps in turn, presuming that we're creating a Resource for a Resource Group.
This section covers how to add and configure the SDK Client.
Determining which SDK Client you should be using is a little complicated unfortunately.
The Client for the Service Package can be found in ./internal/services/{name}/client/client.go
- and we can add an instance of the SDK Client we want to use (here resources.GroupsClient
) and configure it (adding credentials etc):
package client
import (
"fmt"
"github.com/hashicorp/go-azure-sdk/resource-manager/resources/2022-09-01/resources"
"github.com/hashicorp/terraform-provider-azurerm/internal/common"
)
type Client struct {
GroupsClient *resources.GroupsClient
}
func NewClient(o *common.ClientOptions) (*Client, error) {
groupsClient, err := resources.NewResourcesClientWithBaseURI(o.Environment.ResourceManager)
if err != nil {
return nil, fmt.Errorf("building Resources Client: %+v", err)
}
o.Configure(groupsClient.Client, o.Authorizer.ResourceManager)
// ...
return &Client{
GroupsClient: groupsClient,
}
}
Things worth noting here:
- The call to
o.Configure
configures the authorization token which should be used for this SDK Client - in most casesResourceManager
is the authorizer you want to use.
At this point, this SDK Client should be usable within the Resource via:
client := metadata.Client.{ServicePackage}.{ClientField}
For example, in this case:
client := metadata.Client.Resource.GroupsClient
Since we're creating a Resource for a Resource Group, which is a part of the Resources API - we'll want to create an empty Go file within the Service Package for Resources, which is located at ./internal/services/resource
.
In this case, this'd be a file called resource_group_example_resource.go
, which we'll start out with the following:
Note: We'd normally name this file
resource_group_resource.go
- but there's an existing Resource for Resource Groups, so we're appendingexample
to the name throughout this guide.
package resource
import "github.com/hashicorp/terraform-provider-azurerm/internal/sdk"
var _ sdk.Resource = ResourceGroupExampleResource{}
type ResourceGroupExampleResource struct {}
Note: Your editor may show a suggestion to implement the methods defined in
sdk.Resource
for theResourceGroupExampleResource
struct - we'd recommend holding off the first time around to explain each of the methods.
In this case the interface sdk.Resource
defines all of the methods required for a Resource which the newly created struct for the Resource Group Resource need to implement, which are:
type Resource interface {
Arguments() map[string]*schema.Schema
Attributes() map[string]*schema.Schema
ModelObject() interface{}
ResourceType() string
Create() ResourceFunc
Read() ResourceFunc
Delete() ResourceFunc
IDValidationFunc() pluginsdk.SchemaValidateFunc
}
To go through these in turn:
Arguments
returns a list of schema fields which are user-specifiable - either Required or Optional.Attributes
returns a list of schema fields which are Computed (read-only).ModelObject
returns a reference to a Go struct which is used as the Model for this Resource (this can also returnnil
if there's no model).ResourceType
returns the name of this resource within the Provider (for exampleazurerm_resource_group_example
).Create
returns a function defining both the Timeout and the Create function (which creates this Resource Group using the Azure API) for this Resource.Read
returns a function defining both the Timeout and the Read function (which retrieves information from the Azure API) for this Resource.Delete
returns a function defining both the Timeout and the Delete function (which deletes this Resource Group using the Azure API) for this Resource.IDValidationFunc
returns a function which validates the Resource ID provided duringterraform import
to ensure it matches what we expect for this Resource.
type ResourceGroupExampleResourceModel struct {
Name string `tfschema:"name"`
Location string `tfschema:"location"`
Tags map[string]string `tfschema:"tags"`
}
func (ResourceGroupExampleResource) Arguments() map[string]*pluginsdk.Schema {
return map[string]*pluginsdk.Schema{
"name": {
Type: pluginsdk.TypeString,
Required: true,
ValidateFunc: validation.StringIsNotEmpty,
},
"location": commonschema.Location(),
"tags": tags.Schema(),
}
}
func (ResourceGroupExampleResource) Attributes() map[string]*pluginsdk.Schema {
return map[string]*pluginsdk.Schema{}
}
func (ResourceGroupExampleResource) ModelObject() interface{} {
return &ResourceGroupExampleResourceModel
}
func (ResourceGroupExampleResource) ResourceType() string {
return "azurerm_resource_group_example"
}
In this case we're using the resource type
azurerm_resource_group_example
as an existing Resource forazurerm_resource_group
exists and the names need to be unique.
These functions define a Resource called azurerm_resource_group_example
, which has two Required arguments (name
and location
) and one Optional argument (tags
). We'll come back to ModelObject
later.
Let's start by implementing the Create function:
func (r ResourceGroupExampleResource) Create() sdk.ResourceFunc {
return sdk.ResourceFunc{
// the Timeout is how long Terraform should wait for this function to run before returning an error
// whilst 30 minutes may initially seem excessive, we set this as a default to account for rate
// limiting - but having this here means that users can override this in their config as necessary
Timeout: 30 * time.Minute,
// the Func returns a function which retrieves the current state of the Resource Group into the state
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.Resource.GroupsClient
// retrieve the Name for this Resource Group from the Terraform Config
// and then create a Resource ID for this Resource Group
// using the Subscription ID & name
subscriptionId := metadata.Client.Account.SubscriptionId
var config ResourceGroupExampleResourceModel
if err := metadata.Decode(&config); err != nil {
return fmt.Errorf("decoding: %+v", err)
}
id := resources.NewResourceGroupID(subscriptionId, config.Name)
// then we want to check for the presence of an existing resource with the resource's ID
// this is because the Azure API uses the `name` as a unique idenfitier and Upserts
// so we don't want to unintentionally adopt this resource by using the same name
existing, err := client.Get(ctx, id)
if err != nil && !response.WasNotFound(existing.HttpResponse) {
return fmt.Errorf("checking for presence of existing %s: %+v", id, err)
}
if !response.WasNotFound(existing.HttpResponse) {
return metadata.ResourceRequiresImport(r.ResourceType(), id)
}
// create the Resource Group
param := resources.Group{
Location: pointer.To(location.Normalize(config.Location)),
Tags: pointer.To(config.Tags),
}
if _, err := client.CreateOrUpdate(ctx, id, param); err != nil {
return fmt.Errorf("creating %s: %+v", id, err)
}
// set the Resource ID, meaning that we track this resource
metadata.SetID(id)
return nil
},
}
}
Let's implement the Update function:
func (r ResourceGroupExampleResource) Update() sdk.ResourceFunc {
return sdk.ResourceFunc{
// the Timeout is how long Terraform should wait for this function to run before returning an error
// whilst 30 minutes may initially seem excessive, we set this as a default to account for rate
// limiting - but having this here means that users can override this in their config as necessary
Timeout: 30 * time.Minute,
// the Func returns a function which retrieves the current state of the Resource Group into the state
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.Resource.GroupsClient
// parse the existing Resource ID from the State
id, err := resources.ParseResourceGroupID(metadata.ResourceData.Get("id").(string))
if err != nil {
return err
}
var config ResourceGroupExampleResourceModel
if err := metadata.Decode(&config); err != nil {
return fmt.Errorf("decoding: %+v", err)
}
// update the Resource Group
// NOTE: for a more complex resource we'd recommend retrieving the existing Resource from the
// API and then conditionally updating it when fields in the config have been updated, which
// can be determined by using `d.HasChanges` - for example:
//
// existing, err := client.Get(ctx, *id)
// if err != nil {
// return fmt.Errorf("retrieving %s: %+v", id, err)
// }
//
// Although the SDK will catch and error in cases where Model is nil we should still check for
// a non-nil Model and nested Properties object to prevent panics
// if existing.Model == nil {
// return fmt.Errorf("retrieving %s: `model` was nil", id)
// }
// if existing.Model.Properties == nil {
// return fmt.Errorf("retrieving %s: `properties` was nil", id)
// }
//
// if d.HasChanges("tags") {
// existing.Model.Properties.Tags = tags.Expand(metadata.ResourceData.Get("tags").(map[string]interface{}))
// }
//
// doing so allows users to take advantage of Terraform's `ignore_changes` functionality.
//
// However since a Resource Group only has one field which is updatable (tags) so in this case we'll only
// enter the update function if `tags` has been updated.
param := resources.Group{
Location: pointer.To(location.Normalize(config.Location)),
Tags: pointer.To(config.Tags),
}
if _, err := client.CreateOrUpdate(ctx, *id, param); err != nil {
return fmt.Errorf("updating %s: %+v", id, err)
}
return nil
},
}
}
Next up, let's implement the Read function - which retrieves the information about the Resource Group from Azure:
func (ResourceGroupExampleResource) Read() sdk.ResourceFunc {
return sdk.ResourceFunc{
// the Timeout is how long Terraform should wait for this function to run before returning an error
// whilst 5 minutes may initially seem excessive, we set this as a default to account for rate
// limiting - but having this here means that users can override this in their config as necessary
Timeout: 5 * time.Minute,
// the Func returns a function which looks up the state of the Resource Group and sets it into the state
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.Resource.GroupsClient
// parse the Resource Group ID from the `id` field
id, err := resources.ParseResourceGroupID(metadata.ResourceData.Id())
if err != nil {
return err
}
// then retrieve the Resource Group by its ID
resp, err := client.Get(ctx, *id)
if err != nil {
// if the Resource Group doesn't exist (e.g. we get a 404 Not Found)
// since this is a Resource (e.g. we created it/it was imported into the state)
// it previously existed - so we must mark this as "gone" for Terraform
if response.WasNotFound(resp.HttpResponse) {
return metadata.MarkAsGone(id)
}
// otherwise it's a genuine error (auth/api error etc) so raise it
// there should be enough context for the user to interpret the error
// or raise a bug report if there's something we should handle
return fmt.Errorf("retrieving %s: %+v", id, err)
}
// at this point we can set information about this Resource Group into the State
// identifier fields such as the name, resource group name to name a few need to be sourced
// from the Resource ID instead of the API response
state := ResourceGroupExampleResourceModel{
Name: id.ResourceGroupName,
}
// the SDK will return a Model as well as a nested Properties object for the resource
// for readability and consistency we assign the Model to a variable and nil check as shown below.
// since the SDK accounts for responses where the Model is nil we do not need to throw an error if
// the Model is nil since this will be caught earlier on. We still nil check to prevent the provider from
// crashing.
if model := resp.Model; model != nil {
// the Location field is a little different - and we have a normalization
// function for this.
// whilst this may seem like a weird thing to call out in an example, because these two fields
// are present on the majority of resources, we hope it explains why they're a little different
// in this case the Location can be returned in various different forms, for example
// "West Europe", "WestEurope" or "westeurope" - as such we normalize these into a
// lower-cased singular word with no spaces (e.g. "westeurope") so this is consistent
// for users
state.Location = location.NormalizeNilable(model.Location)
state.Tags = pointer.From(model.Tags)
if props := model.Properties; props != nil {
// if there are properties to set into state do that here
}
}
return metadata.Encode(&state)
},
}
}
Next we can add the Delete function:
func (ResourceGroupExampleResource) Delete() sdk.ResourceFunc {
return sdk.ResourceFunc{
// the Timeout is how long Terraform should wait for this function to run before returning an error
// whilst 30 minutes may initially seem excessive, it can take a while to delete the nested items
// particularly if we're rate-limited - but users can override this in their config as necessary
Timeout: 30 * time.Minute,
// the Func returns a function which deletes the Resource Group
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.Resource.GroupsClient
id, err := resources.ParseResourceGroupID(metadata.ResourceData.Id())
if err != nil {
return err
}
// trigger the deletion of the Resource Group
// Delete calls that require request options can be populated by the `DefaultDeleteOperationOptions()`
// method in the SDK
if err := client.DeleteThenPoll(ctx, *id, resources.DefaultDeleteOperationOptions()); err != nil {
return fmt.Errorf("deleting %s: %+v", *id, err)
}
return nil
},
}
}
Finally we can add the IDValidationFunc
function:
func (ResourceGroupExampleResource) IDValidationFunc() pluginsdk.SchemaValidateFunc {
return resources.ValidateResourceGroupID
}
At this point the finished Resource should look like (including imports):
package resource
import (
"context"
"fmt"
"time"
"github.com/hashicorp/go-azure-sdk/resource-manager/resources/2022-09-01/resources"
"github.com/hashicorp/go-azure-helpers/resourcemanager/commonschema"
"github.com/hashicorp/go-azure-helpers/resourcemanager/location"
"github.com/hashicorp/go-azure-helpers/resourcemanager/tags"
"github.com/hashicorp/terraform-provider-azurerm/internal/sdk"
"github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk"
)
var _ sdk.Resource = ResourceGroupExampleResource{}
type ResourceGroupExampleResource struct{}
type ResourceGroupExampleResourceModel struct {
Name string `tfschema:"name"`
Location string `tfschema:"location"`
Tags map[string]string `tfschema:"tags"`
}
func (ResourceGroupExampleResource) Arguments() map[string]*pluginsdk.Schema {
return map[string]*pluginsdk.Schema{
"name": {
Type: pluginsdk.TypeString,
Required: true,
ValidateFunc: validation.StringIsNotEmpty,
},
"location": commonschema.Location(),
"tags": commonschema.Tags(),
}
}
func (ResourceGroupExampleResource) Attributes() map[string]*pluginsdk.Schema {
return map[string]*pluginsdk.Schema{}
}
func (ResourceGroupExampleResource) ModelObject() interface{} {
return &ResourceGroupExampleResourceModel
}
func (ResourceGroupExampleResource) ResourceType() string {
return "azurerm_resource_group_example"
}
func (r ResourceGroupExampleResource) Create() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 30 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.Resource.GroupsClient
subscriptionId := metadata.Client.Account.SubscriptionId
var config ResourceGroupExampleResourceModel
if err := metadata.Decode(&config); err != nil {
return fmt.Errorf("decoding: %+v", err)
}
id := resources.NewResourceGroupID(subscriptionId, config.Name)
existing, err := client.Get(ctx, id)
if err != nil && !response.WasNotFound(existing.HttpResponse) {
return fmt.Errorf("checking for presence of existing %s: %+v", id, err)
}
if !response.WasNotFound(existing.HttpResponse) {
return metadata.ResourceRequiresImport(r.ResourceType(), id)
}
param := resources.Group{
Location: pointer.To(location.Normalize(config.Location)),
Tags: pointer.To(config.Tags),
}
if _, err := client.CreateOrUpdate(ctx, id, param); err != nil {
return fmt.Errorf("creating %s: %+v", id, err)
}
metadata.SetID(id)
return nil
},
}
}
func (r ResourceGroupExampleResource) Update() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 30 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.Resource.GroupsClient
id, err := resources.ParseResourceGroupID(metadata.ResourceData.Get("id").(string))
if err != nil {
return err
}
var config ResourceGroupExampleResourceModel
if err := metadata.Decode(&config); err != nil {
return fmt.Errorf("decoding: %+v", err)
}
param := resources.Group{
Location: pointer.To(location.Normalize(config.Location)),
Tags: pointer.To(config.Tags),
}
if _, err := client.CreateOrUpdate(ctx, *id, param); err != nil {
return fmt.Errorf("updating %s: %+v", id, err)
}
return nil
},
}
}
func (ResourceGroupExampleResource) Read() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 5 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.Resource.GroupsClient
id, err := resources.ParseResourceGroupID(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 := ResourceGroupExampleResourceModel{
Name: id.ResourceGroupName,
}
if model := resp.Model; model != nil {
state.Location = location.NormalizeNilable(model.Location)
state.Tags = pointer.From(model.Tags)
if props := model.Properties; props != nil {
// if there are properties to set into state do that here
}
}
return metadata.Encode(&state)
},
}
}
func (ResourceGroupExampleResource) Delete() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 30 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.Resource.GroupsClient
id, err := resources.ParseResourceGroupID(metadata.ResourceData.Id())
if err != nil {
return err
}
if err := client.DeleteThenPoll(ctx, *id, resources.DefaultDeleteOperationOptions()); err != nil {
return fmt.Errorf("deleting %s: %+v", *id, err)
}
return nil
},
}
}
func (ResourceGroupExampleResource) IDValidationFunc() pluginsdk.SchemaValidateFunc {
return resources.ValidateResourceGroupID
}
At this point in time this Resource is now code-complete - there's an optional extension to make this cleaner by using a Typed Model, however this isn't necessary.
Resources are registered within the registration.go
within each Service Package - and should look something like this:
package resource
import "github.com/hashicorp/terraform-provider-azurerm/internal/sdk"
var _ sdk.TypedServiceRegistration = Registration{}
type Registration struct{}
// ...
// Resources returns a list of Resources supported by this Service
func (r Registration) Resources() []sdk.Resource {
return []sdk.Resource{}
}
Note: It's possible that the Service Registration (above) doesn't currently support Typed Resources, in which case you may need to add the following:
var _ sdk.TypedServiceRegistration = Registration{}
type Registration struct {
}
func (Registration) Name() string {
return "Some Service"
}
func (Registration) DataSources() []sdk.DataSource {
return []sdk.DataSource{}
}
func (Registration) Resources() []sdk.Resource {
return []sdk.Resource{}
}
func (Registration) WebsiteCategories() []string {
return []string{
"Some Service",
}
}
In this case you'll also need to add a line to register this Service Registration in the list of Typed Service Registrations.
To register the Resource we need to add an instance of the struct used for the Resource to the list of Resources, for example:
// Resources returns a list of Resources supported by this Service
func (Registration) Resources() []sdk.Resource {
return []sdk.Resource{
ResourceGroupExampleResource{},
}
}
At this point the Resource is registered, as when the Azure Provider builds up a list of supported Resources during initialization, it parses each of the Service Registrations to put together a definitive list of the Resources that we support.
This means that if you Build the Provider, at this point you should be able to apply the following Resource:
provider "azurerm" {
features {}
}
resource "azurerm_resource_group_example" "test" {
name = "example-resources"
location = "West Europe"
}
output "id" {
value = azurerm_resource_group_example.test.id
}
We're going to test the Resource that we've just built by dynamically provisioning a Resource Group using the new azurerm_resource_group_example
Resource.
In Go tests are expected to be in a file name in the format {original_file_name}_test.go
- in our case that'd be resource_group_example_resource_test.go
, into which we'll want to add:
package resource_test
import (
"context"
"fmt"
"testing"
"github.com/hashicorp/terraform-provider-azurerm/internal/acceptance"
"github.com/hashicorp/terraform-provider-azurerm/internal/clients"
"github.com/hashicorp/terraform-provider-azurerm/internal/services/resource/parse"
"github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk"
"github.com/hashicorp/terraform-provider-azurerm/utils"
)
type ResourceGroupExampleTestResource struct{}
func TestAccResourceGroupExample_basic(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_resource_group_example", "test")
testResource := ResourceGroupExampleTestResource{}
data.ResourceTest(t, testResource, []acceptance.TestStep{
data.ApplyStep(testResource.basicConfig, testResource),
data.ImportStep(),
})
}
func TestAccResourceGroupExample_requiresImport(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_resource_group_example", "test")
testResource := ResourceGroupExampleTestResource{}
data.ResourceTest(t, testResource, []acceptance.TestStep{
data.ApplyStep(testResource.basicConfig, testResource),
data.RequiresImportErrorStep(testResource.requiresImportConfig),
})
}
func TestAccResourceGroupExample_complete(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_resource_group_example", "test")
testResource := ResourceGroupExampleTestResource{}
data.ResourceTest(t, testResource, []acceptance.TestStep{
data.ApplyStep(testResource.completeConfig, testResource),
data.ImportStep(),
data.ApplyStep(testResource.basicConfig, testResource),
data.ImportStep(),
data.ApplyStep(testResource.completeConfig, testResource),
data.ImportStep(),
})
}
func (ResourceGroupExampleTestResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) {
id, err := resources.ParseResourceGroupID(state.ID)
if err != nil {
return nil, err
}
resp, err := client.Resource.GroupsClient.Get(ctx, *id)
if err != nil {
return nil, fmt.Errorf("retrieving %s: %+v", *id, err)
}
return pointer.To(resp.Model != nil), nil
}
func (ResourceGroupExampleTestResource) basicConfig(data acceptance.TestData) string {
return fmt.Sprintf(`
provider "azurerm" {
features {}
}
resource "azurerm_resource_group_example" "test" {
name = "acctestRG-%d"
location = "%s"
}
`, data.RandomInteger, data.Locations.Primary)
}
func (r ResourceGroupExampleTestResource) requiresImportConfig(data acceptance.TestData) string {
template := r.basicConfig(data)
return fmt.Sprintf(`
%s
resource "azurerm_resource_group_example" "import" {
name = azurerm_resource_group_example.test.name
location = azurerm_resource_group_example.test.location
}
`, template)
}
func (ResourceGroupExampleTestResource) completeConfig(data acceptance.TestData) string {
return fmt.Sprintf(`
provider "azurerm" {
features {}
}
resource "azurerm_resource_group_example" "test" {
name = "acctestRG-%d"
location = "%s"
tags = {
Hello = "World"
}
}
`, data.RandomInteger, data.Locations.Primary)
}
There's a more detailed breakdown of how this works in the Acceptance Testing reference - but to summarize what's going on here:
- Test Terraform Configurations are defined as methods on the struct
ResourceGroupExampleResource
so that they're easily accessible (this helps to avoid them being unintentionally used in other resources). - The
acceptance.TestData
object contains a number of helpers, including both random integers, strings and the Azure Locations where resources should be provisioned - which are used to ensure when tests are run in parallel that we provision unique resources for testing purposes. - The
ApplyStep
's apply the Terraform Configuration specified and then assert there's no changes after (e.g.terraform apply
and then checking thatterraform plan
shows no changes). - The
ImportStep
takes the Resource ID for the Resource and runsterraform import azurerm_resource_group_example.test {resourceId}
, checking that the fields defined in the state match the fields returned from the Read function. - We append
_test
to the Go package name (e.g.resource_test
) since we need to be able to access both theresource
package and theacceptance
package (which is a circular reference, otherwise).
At this point we should be able to run this test.
Detailed instructions on Running the Tests can be found in this guide - when a Service Principal is configured you can run the test above using:
make acctests SERVICE='resource' TESTARGS='-run=TestAccResourceGroupExample_' TESTTIMEOUT='60m'
Note: We're using the test prefix
TestAccResourceGroupExample_
and not the name of an individual test, but you can do that too by specifying"(TestName1|TestName2)"
etc
Which should output:
==> Checking that code complies with gofmt requirements...
==> Checking that Custom Timeouts are used...
==> Checking that acceptance test packages are used...
TF_ACC=1 go test -v ./internal/services/resource -run=TestAccResourceGroupExample_ -timeout 60m -ldflags="-X=github.com/hashicorp/terraform-provider-azurerm/version.ProviderVersion=acc"
=== RUN TestAccResourceGroupExample_basic
=== PAUSE TestAccResourceGroupExample_basic
=== CONT TestAccResourceGroupExample_basic
--- PASS: TestAccResourceGroupExample_basic (88.15s)
=== RUN TestAccResourceGroupExample_complete
=== PAUSE TestAccResourceGroupExample_complete
=== CONT TestAccResourceGroupExample_complete
--- PASS: TestAccResourceGroupExample_complete (120.23s)
=== RUN TestAccResourceGroupExample_requiresImport
=== PAUSE TestAccResourceGroupExample_requiresImport
=== CONT TestAccResourceGroupExample_requiresImport
--- PASS: TestAccResourceGroupExample_requiresImport (116.15s)
PASS
ok github.com/hashicorp/terraform-provider-azurerm/internal/services/resource 324.753s
At this point in time documentation for each Resource (and Data Source) is written manually, located within the ./website
folder - in this case this will be located at ./website/docs/d/resource_group_example.html.markdown
.
There is a tool within the repository to help scaffold the documentation for a Resource - the documentation for this Resource can be scaffolded via the following command:
$ make scaffold-website BRAND_NAME="Resource Group Example" RESOURCE_NAME="azurerm_resource_group_example" RESOURCE_TYPE="resource" RESOURCE_ID="/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1"
The documentation should look something like below - containing both an example usage and the required, optional and computed fields:
---
subcategory: "Base"
layout: "azurerm"
page_title: "Azure Resource Manager: azurerm_resource_group_example"
description: |-
Manages a Resource Group.
---
# azurerm_resource_group_example
Manages a Resource Group.
## Example Usage
```hcl
resource "azurerm_resource_group_example" "example" {
name = "example"
location = "West Europe"
}
```
## Arguments Reference
The following arguments are supported:
* `location` - (Required) The Azure Region where the Resource Group should exist. Changing this forces a new Resource Group to be created.
* `name` - (Required) The Name which should be used for this Resource Group. Changing this forces a new Resource Group to be created.
---
* `tags` - (Optional) A mapping of tags which should be assigned to the Resource Group.
## Attributes Reference
In addition to the Arguments listed above - the following Attributes are exported:
* `id` - The ID of the Resource Group.
## 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 Resource Group.
* `read` - (Defaults to 5 minutes) Used when retrieving the Resource Group.
* `update` - (Defaults to 30 minutes) Used when updating the Resource Group.
* `delete` - (Defaults to 30 minutes) Used when deleting the Resource Group.
## Import
Resource Groups can be imported using the `resource id`, e.g.
```shell
terraform import azurerm_resource_group_example.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/example
```