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.go
This 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 ondata_source_*.go
Some resources defined for read calls, mainly query interfacesresource_*.go
Define some write call resources, including resource add delete change check interfaceservice_*.go
Some 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 for
Provider
.tencentcloud
This 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
Delete
Both are write operations, andRead
Operation, 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-provider
But 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
Required
And 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
ForceNew
Set to
true
And then you don’t have to implement it
Update
Function, because no matter which attribute the user modifies, it goes
Delete
–
Create
Is not going to go to
Update
But 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
- return
nil
So 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:
- call
Partial
Methods openAllow partial attribute modificationfunction - call
HasChange
Method to check for changes, - call
SetPartial
Method adds the property to the collection of partial property modifications - call
GetChange
Method to retrieve old and new data (or directlyGet
Latest data) - Commit changes
- call
Partial
Methods 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!