Author: Xie Shiliang

Terraform is an internationally famous open source resource scheduling tool. According to incomplete statistics, more than 100 cloud vendors and service providers around the world support Terraform. This article starts from the terraform-provider system architecture, explains the Terraform core library, practices the Development of Terraform-Provider, and then unit tests. It describes the whole development process of supporting Terraform in a relatively complete way

This project has been posted on Github (github.com/tencentyun/…) Welcome to Star if you are interested

1. What is Terraform?

Terraform is an open source resource orchestration tool based on Golang that allows users to manage and configure any infrastructure, from public and private cloud infrastructure to external services.

If you do not know what is called resource scheduling, you must know that AWS console, Tencent cloud console, you can manage all your cloud resources on these consoles, Terraform and console function is the same, the essence is to manage your cloud resources, but the console is interface operation. Terraform is implemented through configuration files


When your infrastructure is complex, when you buy a large cloud resource or cloud service from a cloud vendor, when your infrastructure is based on a hybrid cloud… The console interface may not be the best management tool, and Terraform may be a relic

2. How do I use Terraform to manage the infrastructure?

Before we start development, it’s important to understand how users play. This will help us understand our development process and thinking

To put it simply, users can maintain some JSON-formatted. Tf configuration files to add, delete, modify, and query infrastructure resources by adding, deleting, modifying, and querying configurations.

The following document describes the application of Terraform-provider-TencentCloud in The Cloud

Github.com/tencentyun/…

3. Configure the development environment

Terraform supports the plug-in model, and all providers are actually plug-ins, which are distributed as Go binaries. While it is technically possible to write plug-ins in another language, almost all Terraform plug-ins are written in Golang.

This article was developed and tested in the following releases

  • Terraform 0.11.x
  • Go 1.9 (to build the provider plugin)

In order not to make this article too long, please refer to our readme.md on Github for environment-related information, which will not be repeated here, assuming you have your development environment ready

4. The Provider architecture

Following the Go development custom and Github path, I put the development directory in

cd  $GOPATH/src/github.com/tencentyun/terraform-provider-tencentcloudCopy the code

Next, let’s take a look at the TencentCloud plug-in directory to understand the Provider architecture

├─terraform-provider-tencentcloud root directory │ ├─main. Go application import file │ ├─AUTHORS author information │ ├─ changelo.md CHANGELOG │ ├─LICENSE authorization information │ ├─ Debug.tf.example │ ├─ Examples Configuration file Directory │ │ ├─ TencentCloud-eIP EIP example TF file │ │ ├─ TencentCloud-instance CVM example TF file │ │ ├ ─ tencentcloud - NAT NAT gateway example tf file │ │ ├ ─ tencentcloud - VPC VPC sample tf file │ │ └ ─... │ ├─ ├─ Config. go Public Configuration files │ │ ├─ Bass Exercises - Basic_Test ├─ Data_source_tc_availability_zones. Go │ ├─ Data_source_tc_availability_zones. Go │ ├─ Data_source_tc_availability_zones ├─ Data_source_TC_nats. go │ │ ├─ Data_source_tc_vpc. go VPC │ │ ├─ Data_source_tc_vpc. go VPC query │ │ ├─ Data_source_tc_nats_test ├ ─ data_source_tc_vpc_test. Go │ │ ├ ─... Go │ ├─ ├─ Provider_Test. Go │ ├─ Resource_tc_ip. Go Go │ ├─ Bass Exercises - Resource_tc_instance_test. Go │ ├─ Bass Exercises - Resource_tc_instance_test │ ├─ Bass Exercises - Resource_tc_nat_gateway_test. go │ ├─ Bass Exercises - Resource_tc_vpc. go │ ├─ Bass Exercises - Resource_tc_vpc. go │ ├ ─ resource_tc_vpc_test. Go │ │ ├ ─... Go Service │ │ ├─ ├─ Service_vpc. go Service │ │ ├─ Service_vpc. go Service │ ├─... │ │ ├─ Anti-Flag. Go │ ├─ Anti-Flag - Website Web related files │ │ ├─ Anti-Flag - Docs │ │ ├─ ├─ Availability_zones.html. Md │ │ ├─ Nts.html. Markdown │ │ ├─ Availability_zones │ ├─ ├─ Vpc.html. │ ├ down │ │ ├─... │ │ ├─ ├─ Flag School - Instance.html. Markdown │ │ ├─ Flag School - Instance.html ├ ─ nat_gateway. HTML. Markdown │ │ │ │ ├ ─ VPC. HTML. Markdown │ │ │ │ └ ─...Copy the code

The structure is mainly divided into five parts

  • main.go, plug-in entry
  • Because your plugin is ultimately intended for use by the user, an ideal example would be for the user to pull up the code and run
  • Tencentcloud, the most important directory, which is our plugins directory, is filled with Go files
    • provider.goThis is the root of the plug-in and is used to describe the properties of the plug-in, such as the configured secret key, supported resource list, callback configuration, and so on
    • data_source_*.goSome resources defined for read calls, mainly query interfaces
    • resource_*.goDefine some write call resources, including resource add delete change check interface
    • service_*.goSome common methods by resource category
  • Vendor, the dependent third-party library
  • Website, document, importance, examples

5. Life cycle

Here is the entire Terraform implementation:


  • ① ~ ④ are looking forProvider.tencentcloudThis is when the plug-in loads
  • ⑤ Is to read the user’s configuration file. Through the configuration file, you can obtain which resources belong to and the status of each resource
  • ⑥ Call different functions according to the state of the resource.Create Update DeleteBoth are write operations, andReadOperation, only inUpdate“, as the front operation

What is the Create?

When a new resource configuration is added to the.tf file, Terraform says Create

What is the Update?

Terraform considers an Update when it modifies one or more of the parameters in the.tf file for a resource that has already been created

What is the Delete?

When a resource configuration created in the.tf file is deleted or the terraform destroy command is executed, terraform considers the resource as Delete

What is the Read?

As the name implies, this is a query for a resource, such as the “Read” operation, which is only used as a pre-update operation to check the existence of the resource and Update the property of the resource to the local

Careful you must have noticed
tencentcloud-sdk-gothis
package.
tencentcloud-sdk-goIs a base that we encapsulate that is separate from Terraform
Tencent Cloud APIThis is what the Go SDK does
Tencent Cloud APIOf course, you can also use it without it, directly in your
terraform-providerBut we do not recommend doing so. Using SDK can make your code more elegant, can realize the centralized management of input parameters and HTTP requests, can make your common interface better reuse, reduce code redundancy

6. Define resources

The Terraform website has a guide for Writing Custom Providers starting from main.go.

Being a Terraform provider (developing the Terraform plug-in) is really an abstraction of the upstream API, and the so-called resources are our services, such as cloud hosts, private networks, NAT gateways. The convention is to put each resource in its own plugins directory and name it with a prefix of resource_ or data_source_, for example

tencentcloud/resource_tc_nat_gateway.go

package tencentcloud

import (
    "github.com/hashicorp/terraform/helper/schema"
)

func resourceTencentCloudNatGateway() *schema.Resource {
    return &schema.Resource{
        Create: resourceTencentCloudNatGatewayCreate,
        Read:   resourceTencentCloudNatGatewayRead,
        Update: resourceTencentCloudNatGatewayUpdate,
        Delete: resourceTencentCloudNatGatewayDelete,

        Schema: map[string]*schema.Schema{
            "vpc_id": &schema.Schema{
                Type:     schema.TypeString,
                Required: true,
                ForceNew: true,
            },
            "name": &schema.Schema{
                Type:         schema.TypeString,
                Required:     true,
                ValidateFunc: validateStringLengthInRange(1, 60),
            },
            "max_concurrent": &schema.Schema{
                Type:     schema.TypeInt,
                Required: true,
            },
            "bandwidth": &schema.Schema{
                Type:     schema.TypeInt,
                Required: true,
            },
            "assigned_eip_set": &schema.Schema{
                Type:     schema.TypeSet,
                Required: true,
                Elem: &schema.Schema{
                    Type: schema.TypeString,
                },
                MinItems: 1,
                MaxItems: 10,
            },
        },
    }
}

func resourceTencentCloudNatGatewayCreate(d *schema.ResourceData, meta interface{}) error {
    return nil
}

func resourceTencentCloudNatGatewayRead(d *schema.ResourceData, meta interface{}) error {
    return nil
}

func resourceTencentCloudNatGatewayUpdate(d *schema.ResourceData, meta interface{}) error {
    return nil
}

func resourceTencentCloudNatGatewayDelete(d *schema.ResourceData, meta interface{}) error {
    return nil
}Copy the code

This essentially returns a schema.Resource structure in which we define Resource parameters and CRUD operations

  • Create
  • Read
  • Update
  • Delete
  • Schema

Schema is a nested array of type MAP [string]*schema.Schema. This is a very important array. In Terraform, you also understand that these are attributes of a resource

In our example, all the attributes of a NAT gateway (which we can see in the NAT gateway cloud API)

Each attribute, its value, is a structure containing several attributes, all of which revolve around the resource attribute value, as described below

Type schema.ValueType

Defines the data type, optional value, and corresponding data type of the value of this property

  • TypeBool – bool
  • TypeInt – int
  • TypeFloat – float64
  • TypeString – string
  • TypeList – []interface{}
  • TypeMap – mapstringinterface{}
  • TypeSet – *schema.Set

Required bool

The default value is false. When this parameter is set to true, users need to set this parameter when adding, deleting, or modifying resources

Optional bool

Both Optional and Required are mutually exclusive. You cannot configure both Required and Optional, that is, an attribute (parameter) is either Required or Optional

ForceNew bool

If the value is set to true, when the value of a resource attribute changes, the modification action is not triggered. Instead, the resource is deleted and a new resource is created.

Modify = Delete + Create

This is a very useful property, many of our cloud resources do not support modification of many properties, such as

  • The subnet specified during the creation of a CVM instance cannot be modified after it is created
  • The VPC specified during the creation of a NAT gateway cannot be modified after being created

This limitation can be achieved with front-end technology on the console, and Terraform can also achieve this limitation, but ForceNew implements more advanced usage and gives users more choice,

One interesting thing is if all of the properties of a cloud resource are
RequiredAnd attributes are combined to have uniqueness, such as routing policy of routing table, DNAT rule, KeyPair… So if you change a property, you’re essentially deleting an old resource and creating a new one and you can add all of the properties
ForceNewSet to
trueAnd then you don’t have to implement it
UpdateFunction, because no matter which attribute the user modifies, it goes
Delete
CreateIs not going to go to
UpdateBut the effect is the same, the user is unaware

ValidateFunc SchemaValidateFunc

Property value extended validation function to verify IP validity example:

func validateIp(v interface{}, k string) (ws []string, errors []error) {
    value := v.(string)
    ip := net.ParseIP(value)
    if ip == nil {
        errors = append(errors, fmt.Errorf("%q must contain a valid IP", k)) 
    }   
    return
}Copy the code

MinItems, MaxItemsint

When Type is TypeSet or TypeList, you can assign values to MinItems and MaxItems to limit the minimum and maximum number of attribute value elements. In the above code, we limit the number of associated EIP of the NAT gateway to 1 to 10

CRUD operations

These four operations, Create Read Update Delete, refer to the four functions that we’re going to focus on.

In the “Life Cycle” section, we learned that Terraform calls these four functions to determine whether new resources need to be created, updated, or destroyed, based on the schema and state of the resource

7. The CRUD implementation

Knowing the user behavior, Terraform execution flow, and resource management logic, now is the time to implement these features

Because there is a lot to cover here, I continue to use NAT gateway as an example to detail the implementation of a resource CURD

Before we begin, we need to introduce more packages that we will use later

import ( "encoding/json" "errors" "fmt" "log" "github.com/hashicorp/terraform/helper/schema" "github.com/zqfan/tencentcloud-sdk-go/common" vpc "github.com/zqfan/tencentcloud-sdk-go/services/vpc/unversioned" ) / /... func resourceTencentCloudNatGatewayCreate(d *schema.ResourceData, meta interface{}) error { return nil } func resourceTencentCloudNatGatewayRead(d *schema.ResourceData, meta interface{}) error { return nil } func resourceTencentCloudNatGatewayUpdate(d *schema.ResourceData, meta interface{}) error { return nil } func resourceTencentCloudNatGatewayDelete(d *schema.ResourceData, meta interface{}) error { return nil }Copy the code

If Terraform returns nil, the operation succeeds. If it returns nil, the operation succeeds. If it returns nil, the operation succeeds

Parameter d of type *schema.ResourceData and parameter meta of type interface{} are used as inputs.

That’s the key to our lesson!

Parameter D is the most commonly used parameter in our development process. Its data type is an object, which contains unusual methods. Here we introduce several commonly used methods

func (*ResourceData) Get

func (d *ResourceData) Get(key string) interface{}Copy the code

The data used to get the given Key, and returns nil if the given Key does not exist

Data Set through the Set method, as well as user-configured parameters, can be obtained through this method

In general, when we do Create resources, we use it a lot

func (*ResourceData) GetOk

func (d *ResourceData) GetOk(key string) (interface{}, bool)Copy the code

Check whether the given Key is set to a value other than zero. This is usually used when we get the value of an Optional property

func (*ResourceData) SetId

func (d *ResourceData) SetId(v string)Copy the code

Terraform manages resources around ids. Each resource has a unique ID, and an ID represents a resource. Therefore, when creating a resource, you need to call this method to write the resource ID. nat-79r5e43i

At this point, do you have a question? What if our resource doesn’t have a unique ID?

For resources that do not have unique ids, such as adding, deleting, modifying and querying routing policies and security group rules, we need to construct our own ids.

You can use a parameter as an ID; Multiple parameters can also be combined; You can also implement an algorithm to generate ids yourself.

The premise is that it has to be unique, and then when we use ID, we reverse solve it, and that indirectly achieves the unique ID that we need

func (*ResourceData) Id

func (d *ResourceData) Id() stringCopy the code

Get the current resource ID, that is, the value written by the SetId method. For example, when we Read Update Delete, we need to use the ID to map to the corresponding resource, so as to complete the reading, modification, and deletion of a resource

func (*ResourceData) Set

func (d *ResourceData) Set(key string, value interface{}) errorCopy the code

After setting a value for a Key, you can use the Get method to obtain the value, which is generally used for the Read operation. After the data is Read from the server, the resource properties are Set locally for subsequent resource management operations

func (*ResourceData) HasChange

func (d *ResourceData) HasChange(key string) boolCopy the code

Imagine how our program knows when a user changes his configuration file (that is, changes the properties of a resource).

This is where HasChange is needed to check whether a given Key has changed. A very useful and frequently used method is to monitor the user’s configuration file during an Update operation and trigger the change when it has changed

func (*ResourceData) GetChange

func (d *ResourceData) GetChange(key string) (interface{}, interface{})Copy the code

The method is that when we know that data has changed using HasChange, we can use this method to retrieve the data before and after the change, namely old data and new data

For example, if a user changes the elastic IP address associated with the NAT gateway, the user needs to unbind the deleted elastic IP address from the server and bind the added elastic IP address to the NAT gateway by comparing the old and new data

func (*ResourceData) Partial

func (d *ResourceData) Partial(on bool)Copy the code

In general, many of our resource attributes can be modified, such as the NAT gateway in our example. The NAT gateway name, maximum number of concurrent connections max_concurrent, bandwidth upper limit bandwidth, and associated elastic IP address assigned_eip_SET are all supported.

For users, this is NAT gateway property value, but for us developers, involves the backend interface is not the same, at this time, if the user modified the multiple attribute values, according to the way of the implementation of document flow, if performed in front of the modification is successful, the execution fails, if to exit the program, to the user an error, is not reasonable, Because actually our back end has already changed some of those property values.

In this case, the data on the server is inconsistent with the local data of the user, and serious problems may occur in subsequent operations

The default value of this method is false. If you have set SetPartial to enable partial modification, even if an error occurs, you can Update an attribute that has already been modified. It also synchronizes the status locally so that the next time the program executes, it will not be considered updated

In three words, “non-transactional.”

func (*ResourceData) SetPartial

func (d *ResourceData) SetPartial(k string)Copy the code

This method is used in conjunction with the Partial method, which allows logic to modify Partial properties

7.1 Creating a Resource

This section describes how to create a NAT gateway

func resourceTencentCloudNatGatewayCreate(d *schema.ResourceData, Meta interface error {{}) / / create the request object args: = VPC. NewCreateNatGatewayRequest () / / to the object attribute assignment, to note here, Because args.vpcid = common.stringptr (d.set ("vpc_id").(string)) args.natName = common.stringptr (d.set ("name").(string)) // Because If v, ok := d.GetOk("max_concurrent"); if v, ok := d.GetOk("max_concurrent"); ok { args.MaxConcurrent = common.IntPtr(v.(int)) } if v, ok := d.GetOk("bandwidth"); Ok {args.bandwidth = common.intptr (v.(int))} // assigned_eip_set Eips := d.set (* schema.set).list () args.eip_set = Common.stringptrs (expandStringList(eips)) client := meta.(*TencentCloudClient) conn := client.vpcconn response, err := conn.CreateNatGateway(args) b, _ := json.Marshal(response) log.Printf("[DEBUG] conn.CreateNatGateway response: %s", b) if _, ok := err.(*common.APIError); ok { return fmt.Errorf("conn.CreateNatGateway error: % v ", err)} / / because the create NAT gateway is asynchronous, here, we only got a BillId, so you need to use the polling logic if _, err: = client. PollingVpcBillResult (response. BillId); err ! Printf("[DEBUG] conn.createnatGateway NatGatewayId: = nil {return err} // Log the NAT gateway ID for debugging. %s", * response.natgatewayID) // call SetId to write resource ID D.setid (* response.natgatewayId) return nil}Copy the code

PollingVpcBillResult PollingVpcBillResult PollingVpcBillResult PollingVpcBillResult PollingVpcBillResult PollingVpcBillResult PollingVpcBillResult PollingVpcBillResult PollingVpcBillResult PollingVpcBillResult PollingVpcBillResult Some resource deletions are also asynchronous.

The server only returns a task ID, at which point we need to poll the task on the client side, until the result is returned, we can know the true state of the resource!

This method is located in service_vpc.go and is used as a method of the *TencentCloudClient object. The core of this method is the Terraform official resource library.

func (client *TencentCloudClient) PollingVpcBillResult(billId *string) (status bool, err error) { queryReq := vpc.NewQueryNatGatewayProductionStatusRequest() queryReq.BillId = billId status = false // Err = resource.Retry(3*time.Minute, func() *resource.RetryError {queryResp, err := client.vpcConn.QueryNatGatewayProductionStatus(queryReq) b, _ := json.Marshal(queryResp) log.Printf("[DEBUG] client.vpcConn.QueryNatGatewayProductionStatus response: %s", b) if _, ok := err.(*common.APIError); Ok {// Return NonRetryableError, resource will exit retry, And returns the error message return resource. NonRetryableError (FMT) Errorf (" client. VpcConn. QueryNatGatewayProductionStatus error: %v", err))} If * queryresp.data. Status == vpc.BillStatusSuccess {return nil} // returns a RetryableError, Return resource.RetryableError(fmt.Errorf("billId %v, not ready, status: %v", billId, *queryResp.Data.Status)) }) return }Copy the code

7.2 Reading Resources

At the end of the Create code, we see SetId, and the Read operation, we’re looking for the resource based on its ID, and then we call Set to write back locally

func resourceTencentCloudNatGatewayRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*TencentCloudClient).vpcConn descReq := vpc.NewDescribeNatGatewayRequest() descReq.NatId = common.StringPtr(d.Id()) descResp, err := conn.DescribeNatGateway(descReq) b, _ := json.Marshal(descResp) log.Printf("[DEBUG] conn.DescribeNatGateway response: %s", b) if _, ok := err.(*common.APIError); Conn.describenatgateway error: %v", err)} ok {return fmt.Errorf(" conn.describenatgateway error: %v", err)} What does SetId("") mean? if *descResp.TotalCount == 0 || len(descResp.Data) == 0 { d.SetId("") return nil } else if err ! = nil { return err } nat := descResp.Data[0] d.Set("name", *nat.NatName) d.Set("max_concurrent", *nat.MaxConcurrent) d.Set("bandwidth", *nat.Bandwidth) d.Set("assigned_eip_set", nat.EipSet) return nil }Copy the code

We left a question at line 15, which is something that a lot of developers don’t understand when they first develop Terraform!

When there is no data from the server query, we are not direct error, but the ID is empty, and returns nil, do so because the purpose of our cloud resource management behavior, not only in Terraform, and console, may also have other tools based on cloud API, if not because of your code bugs that cause the failure of the query and the data is not found, That is, when other tools delete the resource causing the resource to be found

  • returnnilSo it doesn’t quit, so it doesn’t think it’s an error
  • When the ID is null, we want to change the state of the resource. As mentioned earlier, Terraform manages the resource entirely based on THE ID. When the ID is null, Terraform does not find the resource ID, and it assumes that it is a new resource, which is what we expect

7.3 Modifying Resources

In the lifecycle section, Terraform actually calls Read before the Update operation. Why?

Because Terraform determines the state of a resource based on the local Terraform. tfState file, which records the status of all configurations (i.e. resources), Terraform reads data from the server before performing an Update. Compare the latest data with the local data to obtain the latest resource status

func resourceTencentCloudNatGatewayUpdate(d *schema.ResourceData, Meta Interface {}) error {client := meta.(*TencentCloudClient) conn := client.vpcconn / / logo have modify attributeUpdate: = false updateReq: = VPC. NewModifyNatGatewayRequest () updateReq. VpcId = Common.stringptr (d.gate ("vpc_id").(string)) updatereq.natid = common.stringptr (d.id ()) // Change the NAT gateway name if d.HasChange("name") { d.SetPartial("name") var name string if v, ok := d.GetOk("name"); ok { name = v.(string) } else { return fmt.Errorf("cann't change name to empty string") } updateReq.NatName = Common.stringptr (name) attributeUpdate = true} // Modify the upper limit of bandwidth if d.haschange ("bandwidth") {d.setpartial ("bandwidth") var bandwidth int if v, ok := d.GetOk("bandwidth"); ok { bandwidth = v.(int) } else { return fmt.Errorf("cann't change bandwidth to empty string") } updateReq.Bandwidth = Common.intptr (bandwidth) attributeUpdate = true} // Change the name and bandwidth upper limit to the same interface. If attributeUpdate {updateResp, err := conn.ModifyNatGateway(updateReq) b, _ := json.Marshal(updateResp) log.Printf("[DEBUG] conn.ModifyNatGateway response: %s", b) if _, ok := err.(*common.APIError); ok { return fmt.Errorf("conn.ModifyNatGateway error: %v", err)}} // Set the maximum number of concurrent connections. If d.haschange ("max_concurrent") {d.setpartial ("max_concurrent") old_mc, new_mc := d.GetChange("max_concurrent") old_max_concurrent := old_mc.(int) new_max_concurrent := new_mc.(int) if new_max_concurrent <= old_max_concurrent { return fmt.Errorf("max_concurrent only supports upgrade") } upgradeReq := vpc.NewUpgradeNatGatewayRequest() upgradeReq.VpcId = updateReq.VpcId upgradeReq.NatId = updateReq.NatId upgradeReq.MaxConcurrent = common.IntPtr(new_max_concurrent) upgradeResp, err := conn.UpgradeNatGateway(upgradeReq) b, _ := json.Marshal(upgradeResp) log.Printf("[DEBUG] conn.UpgradeNatGateway response: %s", b) if _, ok := err.(*common.APIError); ok { return fmt.Errorf("conn.UpgradeNatGateway error: %v", err) } if _, err := client.PollingVpcBillResult(upgradeResp.BillId); err ! = nil {return err}} // Modify the associated elastic EIP. This logic is slightly more complex because 'assigned_eip_set' is an array. Unbind array elements removed by the user; Call the binding interface, If d.haschange ("assigned_eip_set") {o, n := d.GetChange("assigned_eip_set") os := o.(*schema.Set) ns := n.(*schema.Set) old_eip_set := os.List() new_eip_set :=  ns.List() if len(old_eip_set) > 0 && len(new_eip_set) > 0 { // Unassign old EIP unassignIps := os.Difference(ns) if unassignIps.Len() ! = 0 { unbindReq := vpc.NewEipUnBindNatGatewayRequest() unbindReq.VpcId = updateReq.VpcId unbindReq.NatId = updateReq.NatId unbindReq.AssignedEipSet = common.StringPtrs(expandStringList(unassignIps.List())) unbindResp, err := conn.EipUnBindNatGateway(unbindReq) b, _ := json.Marshal(unbindResp) log.Printf("[DEBUG] conn.EipUnBindNatGateway response: %s", b) if _, ok := err.(*common.APIError); ok { return fmt.Errorf("conn.EipUnBindNatGateway error: %v", err) } if _, err := client.PollingVpcTaskResult(unbindResp.TaskId); err ! = nil { return err } } // Assign new EIP assignIps := ns.Difference(os) if assignIps.Len() ! = 0 { bindReq := vpc.NewEipBindNatGatewayRequest() bindReq.VpcId = updateReq.VpcId bindReq.NatId = updateReq.NatId bindReq.AssignedEipSet = common.StringPtrs(expandStringList(assignIps.List())) bindResp, err := conn.EipBindNatGateway(bindReq) b, _ := json.Marshal(bindResp) log.Printf("[DEBUG] conn.EipBindNatGateway response: %s", b) if _, ok := err.(*common.APIError); ok { return fmt.Errorf("conn.EipBindNatGateway error: %v", err) } if _, err := client.PollingVpcTaskResult(bindResp.TaskId); err ! = nil {return err}} else {return errEipUnassigned} d.setpartial ("assigned_eip_set")} // Disable allow partial attribute modification d.Partial(false) return nil }Copy the code

The main idea is summarized as follows:

  1. callPartialMethods openAllow partial attribute modificationfunction
  2. callHasChangeMethod to check for changes,
  3. callSetPartialMethod adds the property to the collection of partial property modifications
  4. callGetChangeMethod to retrieve old and new data (or directlyGetLatest data)
  5. Commit changes
  6. callPartialMethods closeAllow partial attribute modificationfunction

7.4 Deleting a Resource

To delete a resource, you can delete the resource from the server based on the resource ID

func resourceTencentCloudNatGatewayDelete(d *schema.ResourceData, meta interface{}) error {

    client := meta.(*TencentCloudClient)

    deleteReq := vpc.NewDeleteNatGatewayRequest()
    deleteReq.VpcId = common.StringPtr(d.Get("vpc_id").(string))
    deleteReq.NatId = common.StringPtr(d.Id())

    deleteResp, err := client.vpcConn.DeleteNatGateway(deleteReq)
    b, _ := json.Marshal(deleteResp)
    log.Printf("[DEBUG] client.vpcConn.DeleteNatGateway response: %s", b)
    if _, ok := err.(*common.APIError); ok {
        return fmt.Errorf("[ERROR] client.vpcConn.DeleteNatGateway error: %v", err)
    }

    _, err = client.PollingVpcTaskResult(deleteResp.TaskId)
    return err
}Copy the code

Sample is one of the most simple delete operations, in practice, if your resource to delete is asynchronous, or delete operations, also depends on other resources to delete, such as when delete a private network resources, if there are other resources in the network, such as the subnet, VPN, etc., call delete interface, complains, lead to delete failed! Terraform deletes resources in a sequential order. If the dependencies are deleted, Terraform deletes resources in a sequential order. If the dependencies are deleted, Terraform deletes resources in a sequential order. You can delete the topmost dependent resource

At this point, a basic resource management program is written! Finally, you need to configure the resource management function into provider.go’s ResourcesMap to be used

8. Write unit test cases

When it comes to testing, you can write your own TF files and compile plug-ins

go build -o terraform-provider-tencentcloudCopy the code

Then test your program

terrform plan
terrform applyCopy the code

We strongly recommend that you write your own unit test cases and test your application. In the previous chapter on Provider Architecture, you will see many *_test.go as our unit test cases

Unit test cases are also essential to becoming an official Terraform provider

Let’s start with the flow chart of Terraform’s unit test system


package tencentcloud import ( "encoding/json" "fmt" "log" "testing" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" "github.com/zqfan/tencentcloud-sdk-go/common" vpc "github.com/zqfan/tencentcloud-sdk-go/services/vpc/unversioned" ) func TestAccTencentCloudNatGateway_basic(t *testing.T)  { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: TestAccProviders, / / configure resources destruction results check function CheckDestroy: testAccCheckNatGatewayDestroy, / / configuration testing step Steps: []resource.TestStep{{// configure the Config: testAccNatGatewayConfig, // configure the Check function Check: Resource.Com poseTestCheckFunc (ID / / verification resources testAccCheckTencentCloudDataSourceID (" tencentcloud_nat_gateway. My_nat "), // Verify that the resource attributes match, Affirmation is to create a successful resource. TestCheckResourceAttr (" tencentcloud_nat_gateway. My_nat ", "name", "terraform_test"), resource.TestCheckResourceAttr("tencentcloud_nat_gateway.my_nat", "max_concurrent", "3000000"), resource.TestCheckResourceAttr("tencentcloud_nat_gateway.my_nat", "bandwidth", "500"), resource.TestCheckResourceAttr("tencentcloud_nat_gateway.my_nat", "assigned_eip_set.#", "2"), ), }, {/ / configuration configuration content Config: testAccNatGatewayConfigUpdate, Check: Resource.Com poseTestCheckFunc (testAccCheckTencentCloudDataSourceID (" tencentcloud_nat_gateway. My_nat "), / / verify the modified attribute values, If I can get a match, Must be modified successful resource. TestCheckResourceAttr (" tencentcloud_nat_gateway. My_nat ", "name", "new_name"), resource.TestCheckResourceAttr("tencentcloud_nat_gateway.my_nat", "max_concurrent", "10000000"), resource.TestCheckResourceAttr("tencentcloud_nat_gateway.my_nat", "bandwidth", "1000"), resource.TestCheckResourceAttr("tencentcloud_nat_gateway.my_nat", "assigned_eip_set.#", "2"), ), }, }, }} // testAccProviders create test providers according to Config, and then destroy them after testing. Is according to the ID resource exists func testAccCheckNatGatewayDestroy (s * terraform State) error {conn: = Testaccprovider.meta ().(*TencentCloudClient).vpcconn terraform.tfstate for _, rs := range s.RootModule().Resources { if rs.Type ! = "tencentcloud_nat_gateway" { continue } descReq := vpc.NewDescribeNatGatewayRequest() descReq.NatId = common.StringPtr(rs.Primary.ID) descResp, err := conn.DescribeNatGateway(descReq) b, _ := json.Marshal(descResp) log.Printf("[DEBUG] conn.DescribeNatGateway response: %s", b) if _, ok := err.(*common.APIError); ok { return fmt.Errorf("conn.DescribeNatGateway error: %v", err) } else if *descResp.TotalCount ! = 0 {return ftt. Errorf("NAT Gateway still exists.")}} return nil Const testAccNatGatewayConfig = 'resource "tencentCloud_vpc" "main" {name = "terraform test" Cidr_block = "10.6.0.0/16"} resource "tencentCloud_eip" "eip_dev_dnat" {name = "terraform_test"} resource "tencentcloud_eip" "eip_test_dnat" { name = "terraform_test" } resource "tencentcloud_nat_gateway" "my_nat" { vpc_id = "${tencentcloud_vpc.main.id}" name = "terraform_test" max_concurrent = 3000000 bandwidth = 500 assigned_eip_set = [ Eip_dev_dnat. public_ip}, "${tencentcloud_ip. Eip_test_dnat. public_ip}", "${tencentcloud_ip. You must have noticed, Isn't that tf and debug modified files as yao const testAccNatGatewayConfigUpdate = ` resource "tencentcloud_vpc" main "{" name =" terraform Test "cidr_block = "10.6.0.0/16"} resource" tencentCloud_eip ""eip_dev_dnat" {name = "terraform_test"} resource "tencentcloud_eip" "eip_test_dnat" { name = "terraform_test" } resource "tencentcloud_eip" "new_eip" { name = "terraform_test" } resource "tencentcloud_nat_gateway" "my_nat" { vpc_id = "${tencentcloud_vpc.main.id}" name = "new_name" max_concurrent = 10000000 bandwidth = 1000 assigned_eip_set = [ "${tencentcloud_eip.eip_dev_dnat.public_ip}",  "${tencentcloud_eip.new_eip.public_ip}", ] } `Copy the code

To begin testing

export TF_ACC=true
cd tencentcloud
go test -i; go test -test.run TestAccTencentCloudNatGateway_basic -vCopy the code

With testAccProviders, you can write more complex scenarios for the same resource manager, add Steps, and Create Update Delete. Or break it up into multiple test cases, which can be more comprehensive!