Add custom resource code

Add custom resource code #

This document covers how to add “custom code” to MMv1 resources. Custom code can be used to add arbitrary logic to a resource while still generating most of the code; it allows for a balance between maintainability and supporting real-worlds APIs that deviate from what MMv1 can support. Custom code should only be added if the desired behavior can’t be achieved otherwise.

Most custom code attributes are strings that contain a path to a template file relative to the mmv1 directory. For example:

custom_code: !ruby/object:Provider::Terraform::CustomCode
  # References mmv1/templates/terraform/custom_delete/resource_name_custom_delete.go.erb
  custom_delete: templates/terraform/custom_delete/resource_name_custom_delete.go.erb

By convention, the template files are stored in a directory matching the type of custom code, and the name of the file includes the resource (and, if relevant, field) impacted by the custom code. Like handwritten resource and test code, custom code is written as ruby templates which render go code.

When in doubt about the behavior of custom code, write the custom code, generate the providers, and inspect what changed in the providers using git diff.

The following sections describe types of custom code in more detail.

Add reusable variables and functions #

custom_code: !ruby/object:Provider::Terraform::CustomCode
  constants: templates/terraform/constants/PRODUCT_RESOURCE.go.erb

Use custom_code.constants to inject top-level code in a resource file. This is useful for anything that should be referenced from other parts of the resource, such as:

  • Constants
  • Regexes compiled at build time
  • Functions, such as diff suppress functions
  • Methods

Modify the API request or response #

API requests and responses can be modified in the following order:

  1. Modify the API request value for a specific field
  2. Modify the API request data for an entire resource
  3. Modify the API response data for an entire resource
  4. Modify the API response value for a specific field

These are described in more detail in the following sections.

Modify the API request value for a specific field #

- !ruby/object:Api::Type::String
  name: 'FIELD'
  custom_expand: 'templates/terraform/custom_expand/PRODUCT_RESOURCE_FIELD.go.erb'

Set custom_expand on a field to inject code that modifies the value to send to the API for that field. Custom expanders run before any encoder or update_encoder. The referenced file must include the function signature for the expander. For example:

func expand<%= prefix -%><%= titlelize_property(property) -%>(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) {
  if v == nil {
    return nil, nil
  }

  return base64.StdEncoding.EncodeToString([]byte(v.(string))), nil
}

The parameters the function receives are:

  • v: The value for the field
  • d: Terraform resource data. Use d.Get("field_name") to get a field’s current value.
  • config: Config object. Can be used to make API calls.

The function returns a final value that will be sent to the API.

Modify the API request data for an entire resource #

custom_code: !ruby/object:Provider::Terraform::CustomCode
  encoder: templates/terraform/encoder/PRODUCT_RESOURCE.go.erb
  update_encoder: templates/terraform/update_encoder/PRODUCT_RESOURCE.go.erb

Use custom_code.encoder to inject code that modifies the data that will be sent in the API request. This is useful if the API expects the data to be in a significantly different structure than Terraform does - for example, if the API expects the entire object to be nested under a key, or a particular field must never be sent to the API. The encoder will run after any custom_expand code.

The encoder code will be wrapped in a function like:

func resourceProductResourceEncoder(d *schema.ResourceData, meta interface{}, obj map[string]interface{}) (map[string]interface{}, error) {
  // Your code will be injected here.
}

The parameters the function receives are:

  • d: Terraform resource data. Use d.Get("field_name") to get a field’s current value.
  • meta: Can be cast to a Config object (which can make API calls) using meta.(*transport_tpg.Config)
  • obj: The data that will be sent to the API.

The function returns data that will be sent to the API and an optional error.

If the Create and Update methods for the resource need different logic, set custom_code.update_encoder to override the logic for update only. It is otherwise the same as custom_code.encoder.

Modify the API response data for an entire resource #

custom_code: !ruby/object:Provider::Terraform::CustomCode
  decoder: templates/terraform/decoder/PRODUCT_RESOURCE.go.erb

Use custom_code.decoder to inject code that modifies the data that will be sent in the API request. This is useful if the API expects the data to be in a significantly different structure than Terraform does - for example, if the API returns the entire object nested under a key, or uses a different name for a field in the response than in the request. The decoder will run before any custom_flatten code.

The decoder code will be wrapped in a function like:

func resourceProductResourceDecoder(d *schema.ResourceData, meta interface{}, res map[string]interface{}) (map[string]interface{}, error) {
    // Your code will be injected here.
}

The parameters the function receives are:

  • d: Terraform resource data. Use d.Get("field_name") to get a field’s current value.
  • meta: Can be cast to a Config object (which can make API calls) using meta.(*transport_tpg.Config)
  • res: The data (“response”) returned by the API.

The function returns data that will be set in Terraform state and an optional error.

Modify the API response value for a specific field #

- !ruby/object:Api::Type::String
  name: 'FIELD'
  custom_flatten: 'templates/terraform/custom_flatten/PRODUCT_RESOURCE_FIELD.go.erb'

Set custom_flatten on a field to inject code that modifies the value returned by the API prior to storing it in Terraform state. Custom flatteners run after any decoder. The referenced file must include the function signature for the flattener. For example:

func flatten<%= prefix -%><%= titlelize_property(property) -%>(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} {
  if v == nil {
    return "0"
  }
  return v
}

The parameters the function receives are:

  • v: The value for the field
  • d: Terraform resource data. Use d.Get("field_name") to get a field’s current value.
  • config: Config object. Can be used to make API calls.

The function returns a final value that will be stored in Terraform state for the field, which will be compared with the user’s configuration to determine if there is a diff.

Inject code before / after CRUD operations and Import #

custom_code: !ruby/object:Provider::Terraform::CustomCode
  pre_create: templates/terraform/pre_create/PRODUCT_RESOURCE.go.erb
  post_create: templates/terraform/post_create/PRODUCT_RESOURCE.go.erb

  pre_read: templates/terraform/pre_read/PRODUCT_RESOURCE.go.erb

  pre_update: templates/terraform/pre_update/PRODUCT_RESOURCE.go.erb
  post_update: templates/terraform/post_update/PRODUCT_RESOURCE.go.erb

  pre_delete: templates/terraform/pre_delete/PRODUCT_RESOURCE.go.erb
  post_delete: templates/terraform/post_delete/PRODUCT_RESOURCE.go.erb

  post_import: templates/terraform/post_import/PRODUCT_RESOURCE.go.erb

CRUD operations can be modified with pre/post hooks. This code will be injected directly into the relevant CRUD method as close as possible to the related API call and will have access to any variables that are present when it runs. pre_create and pre_update run after any encoder. Some example use cases:

  • Use post_create to set an update-only field after create finishes.
  • Use pre_delete to detach a disk before deleting it.
  • Use post_import to parse attributes from the import ID and call d.Set("field") so that the resource can be read from the API.

Custom create error handling #

custom_code: !ruby/object:Provider::Terraform::CustomCode 
  post_create_failure: templates/terraform/post_create_failure/PRODUCT_RESOURCE.go.erb

Use custom_code.post_create_failure to inject code that runs if a Create request to the API returns an error.

The post_create_failure code will be wrapped in a function like:

func resourceProductResourcePostCreateFailure(d *schema.ResourceData, meta interface{}) {
    // Your code will be injected here.
}

The parameters the function receives are:

  • d: Terraform resource data. Use d.Get("field_name") to get a field’s current value.
  • meta: Can be cast to a Config object (which can make API calls) using meta.(*transport_tpg.Config)

Replace entire CRUD methods #

custom_code: !ruby/object:Provider::Terraform::CustomCode
  custom_create: templates/terraform/custom_create/PRODUCT_RESOURCE.go.erb
  custom_update: templates/terraform/custom_update/PRODUCT_RESOURCE.go.erb
  custom_delete: templates/terraform/custom_delete/PRODUCT_RESOURCE.go.erb
  custom_import: templates/terraform/custom_import/PRODUCT_RESOURCE.go.erb

Custom methods replace the entire contents of the Create, Update, Delete, or Import methods. For example:

func resourceProductResourceImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
    // Your code will be injected here.
}

Custom methods are similar to handwritten code and should be avoided if possible. If you have to replace two or more methods, the resource should be handwritten instead.

Add extra fields to a resource #

Use custom_code.extra_schema_entry to add additional fields to a resource. Do not use extra_schema_entry unless there is no other option. The extra fields are injected at the end of the resource’s Schema field. They should be formatted as entries in the map. For example:

"foo": &schema.Schema{ ... },

Any fields added in this way will need to be have documentation manually added using the top-level docs field:

docs: !ruby/object:Provider::Terraform::Docs
  optional_properties: |
    * `FIELD_NAME` - (Optional, [Beta](https://terraform.io/docs/providers/google/guides/provider_versions.html)) FIELD_DESCRIPTION    

See Add documentation (Handwritten) for more information about what to include in the field documentation.

What’s next? #