Skip to content

Commit

Permalink
Split handwritten / mmv1 howto guides into separate pages (GoogleClou…
Browse files Browse the repository at this point in the history
  • Loading branch information
melinath authored and googlerjk committed Nov 25, 2022
1 parent 26496d5 commit 36b475e
Show file tree
Hide file tree
Showing 14 changed files with 678 additions and 699 deletions.
5 changes: 4 additions & 1 deletion docs/config.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
baseURL = 'https://googlecloudplatform.github.io/magic-modules/'
languageCode = 'en-us'
title = 'Magic Modules'
theme = ["github.com/alex-shpak/hugo-book"]

enableGitInfo = true
disableKinds = ['taxonomy', 'taxonomyTerm']

[module]
[[module.imports]]
path = 'github.com/alex-shpak/hugo-book'

[params]
BookTheme = 'light'
BookLogo = 'magic-modules.svg'
Expand Down
4 changes: 3 additions & 1 deletion docs/content/_index.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
---
title: ""
title: "Home"
weight: 0
type: "docs"
date: 2022-11-14T09:50:49-08:00
---

Expand Down
5 changes: 5 additions & 0 deletions docs/content/docs/how-to/_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: "How To"
weight: 10
isSection: true
---
27 changes: 27 additions & 0 deletions docs/content/docs/how-to/add-handwritten-datasource.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
title: "Add a handwritten datasource"
weight: 22
---

# Add a handwritten datasource

**Note** : only handwritten datasources are currently supported

Datasources are like terraform resources except they don't *create* anything.
They are simply read-only operations that will expose some sort of values needed
for subsequent resource operations. If you're adding a field to an existing
datasource, check the [Resource](#resource) section. Everything there will
be mostly consistent with the type of change you'll need to make. For adding
a new datasource there are 5 steps to doing so.

1. Create a new datasource declaration file and a corresponding test file
1. Add Schema and Read operation implementation
1. Add the datasource to the `provider.go.erb` index
1. Implement a test which will create and resources and read the corresponding
datasource.
1. Add documentation.

For creating a datasource based off an existing resource you can [make use of the
schema directly](https://github.com/GoogleCloudPlatform/magic-modules/blob/1d293f7bfadacaa20580874c8e8634827fb99a14/mmv1/third_party/terraform/data_sources/data_source_cloud_run_service.go).
Otherwise [implementing the schema directly](https://github.com/GoogleCloudPlatform/magic-modules/blob/1d293f7bfadacaa20580874c8e8634827fb99a14/mmv1/third_party/terraform/data_sources/data_source_google_compute_address.go),
similar to normal resource creation, is the desired path.
49 changes: 49 additions & 0 deletions docs/content/docs/how-to/add-handwritten-iam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
title: "Add handwritten IAM resources"
weight: 23
---

# Add handwritten IAM resources

Handwritten IAM support is only recommended for resources that cannot be managed
using [MMv1](/magic-modules/docs/how-to/add-mmv1-iam),
including for handwritten resources, due to the need to manage tests and
documentation by hand. This guidance goes through the motions of adding support
for new handwritten IAM resources, but does not go into the details of the
implementation as any new handwritten IAM resources are expected to be
exceptional.

IAM resources are implemented using an IAM framework, where you implement an
interface for each parent resource supporting `getIamPolicy`/`setIamPolicy` and
the associated IAM resources that target that parent resource- `_member`,
`_binding`, and `_policy`- are created by the framework.

To add support for a new target, create a new file in
`mmv1/third_party/terraform/utils` called `iam_{{resource}}.go`, and implement
the `ResourceIamUpdater`, `newResourceIamUpdaterFunc`, `iamPolicyModifyFunc`,
`resourceIdParserFunc` interfaces from
https://github.com/GoogleCloudPlatform/magic-modules/blob/main/mmv1/third_party/terraform/utils/iam.go.erb
in public types, alongside a public `map[string]*schema.Schema` containing all
fields referenced in the resource.

Once your implementation is complete, add the IAM resources to `provider.go`
inside the `START non-generated IAM resources` block, creating the concrete
resource types using the `ResourceIamMember`, `ResourceIamBinding`, and
`ResourceIamPolicy` functions. For example:

```go
"google_bigtable_instance_iam_binding": ResourceIamBinding(IamBigtableInstanceSchema, NewBigtableInstanceUpdater, BigtableInstanceIdParseFunc),
"google_bigtable_instance_iam_member": ResourceIamMember(IamBigtableInstanceSchema, NewBigtableInstanceUpdater, BigtableInstanceIdParseFunc),
"google_bigtable_instance_iam_policy": ResourceIamPolicy(IamBigtableInstanceSchema, NewBigtableInstanceUpdater, BigtableInstanceIdParseFunc),
```

Following that, write a test for each resource exercising create and update for
both `_policy` and `_binding`, and create for `_member`. No special
accommodations are needed for the IAM test compared to a normal Terraform
resource test.

Documentation for IAM resources is done using single page per target resource,
rather than a distinct page for each IAM resource level. As most of the page is
standard, you can generally copy and edit an existing handwritten page such as
https://github.com/GoogleCloudPlatform/magic-modules/blob/main/mmv1/third_party/terraform/website/docs/r/bigtable_instance_iam.html.markdown
to write the documentation.
216 changes: 216 additions & 0 deletions docs/content/docs/how-to/add-handwritten-test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
---
title: "Add a handwritten test"
weight: 21
---


# Add a handwritten test

For handwritten resources and generated resources that need to test update,
handwritten tests must be added.

Tests are made up of a templated Terraform configuration where unique values
like GCE names are passed in as arguments, and boilerplate to exercise that
configuration.

The test boilerplate effectively does the following:

1. Run `terraform apply` on the configuration, waiting for it to succeed and
recording the results in Terraform state
2. Run `terraform plan`, and fail if Terraform detects any drift
3. Clear the resource from state and run `terraform import` on it
4. Deeply compare the original state from `terraform apply` and the `terraform
import` results, returning an error if any values are not identical
5. Destroy all resources in the configuration using `terraform destroy`,
waiting for the destroy command to succeed
6. Call `GET` on the resource, and fail the test if it is still present

## Simple Tests

Terraform configurations are stored as string constants wrapped in Go functions
like the following:

```go
func testAccComputeFirewall_basic(network, firewall string) string {
return fmt.Sprintf(`
resource "google_compute_network" "foobar" {
name = "%s"
auto_create_subnetworks = false
}
resource "google_compute_firewall" "foobar" {
name = "%s"
description = "Resource created for Terraform acceptance testing"
network = google_compute_network.foobar.name
source_tags = ["foo"]
allow {
protocol = "icmp"
}
}
`, network, firewall)
}
```

For the most part, you can copy and paste a preexisting test case and modify it.
For example, the following test case is a good reference:

```go
func TestAccComputeFirewall_noSource(t *testing.T) {
t.Parallel()

networkName := fmt.Sprintf("tf-test-firewall-%s", randString(t, 10))
firewallName := fmt.Sprintf("tf-test-firewall-%s", randString(t, 10))

vcrTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckComputeFirewallDestroyProducer(t),
Steps: []resource.TestStep{
{
Config: testAccComputeFirewall_noSource(networkName, firewallName),
},
{
ResourceName: "google_compute_firewall.foobar",
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func testAccComputeFirewall_noSource(network, firewall string) string {
return fmt.Sprintf(`
resource "google_compute_network" "foobar" {
name = "%s"
auto_create_subnetworks = false
}
resource "google_compute_firewall" "foobar" {
name = "%s"
description = "Resource created for Terraform acceptance testing"
network = google_compute_network.foobar.name
allow {
protocol = "tcp"
ports = [22]
}
}
`, network, firewall)
}
```

## Update tests

Inside of a test, additional steps can be added in order to transition between
Terraform configurations, updating the stored state as it progresses. This
allows you to exercise update behaviour. This modifies the flow from before:

1. Start with an empty Terraform state
1. For each `Config` and `ImportState` pair:
1. Run `terraform apply` on the configuration, waiting for it to succeed
and recording the results in Terraform state
1. Run `terraform plan`, and fail if Terraform detects any drift
1. Clear the resource from state and run `terraform import` on it
1. Deeply compare the original state from `terraform apply` and the
`terraform import` results, returning an error if any values are not
identical
1. Destroy all resources in the configuration using `terraform destroy`,
waiting for the destroy command to succeed
1. Call `GET` on the resource, and fail the test if it is still present

For example:

```go
func TestAccComputeFirewall_disabled(t *testing.T) {
t.Parallel()

networkName := fmt.Sprintf("tf-test-firewall-%s", randString(t, 10))
firewallName := fmt.Sprintf("tf-test-firewall-%s", randString(t, 10))

vcrTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckComputeFirewallDestroyProducer(t),
Steps: []resource.TestStep{
{
Config: testAccComputeFirewall_disabled(networkName, firewallName),
},
{
ResourceName: "google_compute_firewall.foobar",
ImportState: true,
ImportStateVerify: true,
},
{
Config: testAccComputeFirewall_basic(networkName, firewallName),
},
{
ResourceName: "google_compute_firewall.foobar",
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func testAccComputeFirewall_basic(network, firewall string) string {
return fmt.Sprintf(`
resource "google_compute_network" "foobar" {
name = "%s"
auto_create_subnetworks = false
}
resource "google_compute_firewall" "foobar" {
name = "%s"
description = "Resource created for Terraform acceptance testing"
network = google_compute_network.foobar.name
source_tags = ["foo"]
allow {
protocol = "icmp"
}
}
`, network, firewall)
}

func testAccComputeFirewall_disabled(network, firewall string) string {
return fmt.Sprintf(`
resource "google_compute_network" "foobar" {
name = "%s"
auto_create_subnetworks = false
}
resource "google_compute_firewall" "foobar" {
name = "%s"
description = "Resource created for Terraform acceptance testing"
network = google_compute_network.foobar.name
source_tags = ["foo"]
allow {
protocol = "icmp"
}
disabled = true
}
`, network, firewall)
}
```

## Testing Beta Features

If you worked with a beta feature and had to use beta version guards in a
handwritten resource or set `min_version: beta` in a generated resource, you'll
want to version guard both the test case and configuration by enclosing them in
ERB tags like below. Additionally, if the filename ends in `.go`, rename it to
end in `.go.erb`.

```
<% unless version == 'ga' -%>
// test case + config here
<% end -%>
```

Otherwise, tests using a beta feature are written exactly the same as tests
using a GA one. Normally to use the beta provider, it's necessary to specify
`provider = google-beta`, as Terraform maps any resources prefixed with
`google_` to the `google` provider by default. However, inside the test
framework, the `google-beta` provider has been aliased as the `google` provider
and that is not necessary.

Note: You _may_ use version guards to test different configurations between the
GA and beta provider tests, but it's strongly recommended that you write
different test cases instead, even if they're slightly duplicative.
Loading

0 comments on commit 36b475e

Please sign in to comment.