-
Notifications
You must be signed in to change notification settings - Fork 0
NDFC Provider Development workflow
This wiki contains detailed walk through of how to develop a terraform resource for NDFC
Install Terraform Framework Code Generator
https://developer.hashicorp.com/terraform/plugin/code-generation/framework-generator
go install github.com/hashicorp/terraform-plugin-codegen-framework/cmd/tfplugingen-framework@latest
Download and install generator tool
Repo: https://github.com/mdmohan/tfgenerator
Clone the repo and run `go install .`
Binary `$GOPATH/bin/generator`
The diagram below depicts the code structure. This structure shall be strictly enforced on any new code added.

Code generation files are in generator
folder. More details shall follow in the code generation section
Most or all code of the provider functionality is inside internal/provider
folder.
Here are some basic guidelines on the expected content and naming of the files under different modules
-
internal/provider/<name>_resource.go
.
This file shall contain the terraform resource interface - which is used by terraform core rpc

-
internal/provider/ndfc/<ndfc_function>.go
.
Implementation of an ndfc functionality. Eg. vrf, network, interface -
internal/provider/resources/resource_<name>/<name>_resource_gen.go
Code generated bytfplugin-gen
-
internal/provider/resources/resource_<name>/resource_codec_gen.go
Code generated bytfgenerator
Following sections walks through the procedure to create a new resource. An actual ndfc functionality vrf
is used here to explain the work flow.
The code generator generates most of the code that is needed to process the resource/datasource information.
Here are the steps
The details are documented at generator.md
The resource yaml file is a blue print of the resource itself. It provides details on how the resource config (tf) is structured and also on the NDFC payload used for managing the function in NDFC (APIs). A detailed documentation of the different fields used in the resource yaml is available at generator/fields.MD
The NDFC API guide or a network capture from the UI page of the functionality (here vrf) are the places where the information about the attributes are gathered. Using the gathered information, proceed to following steps.
- Create a file
vrf.yaml
in thegenerator/defs
folder
$GOPATH/src/terraform-provider-ndfc/
├─ generator/defs
│ ├─ defs.yaml
│ ├─ vrf.yaml
Add the newly created resource file name (vrf.yaml
) into the list files
in defs.yaml
.
Note that only resource definition files listed in defs.yaml
are picked up during generation.
defs.yaml
content
---
files:
- ndfc.yaml
- networks.yaml
- vrf.yaml >> new entry
Note: If the file is not in defs.yaml
it is ignored by generator
Content of the resource yaml
For field help - See
vrf.yaml
---
resource:
name: vrf
generate_tf_resource: true
attributes:
- model_name: id
id: true
tf_name: id
reference: true
description: ID
type: String
example: fabricname/vrf
- &fname
model_name: fabric
tf_name: fabric_name
reference: true
description: The name of the fabric
type: String
example: CML
- &vrfName
model_name: vrfName
tf_name: vrf_name
type: String
id: false
mandatory: true
description: The name of the VRF
example: VRF1
- &vrfTemplate
model_name: vrfTemplate
tf_name: vrf_template
type: String
description: The name of the VRF template
default_value: Default_VRF_Universal
example: Default_VRF_Universal
- &vrfExtensionTemplate
model_name: vrfExtensionTemplate
tf_name: vrf_extension_template
type: String
description: The name of the VRF extension template
default_value: Default_VRF_Extension_Universal
example: Default_VRF_Extension_Universal
- &vrfId
model_name: vrfId
tf_name: vrf_id
type: Int64
computed: true
min_int: 1
max_int: 16777214
description: VNI ID of VRF
example: 50000
- &vrfVlanId
model_name: vrfVlanId
ndfc_nested: [vrfTemplateConfig]
tf_name: vlan_id
type: Int64
ndfc_type: string
computed: true
min_int: 2
max_int: 4094
description: VLAN ID
example: 1500
NOTE: Make sure installs mentioned in per-requisites are done
From the code base root terraform-resource-ndfc
run following
go generate
Above command will run all the stages of generator and on successful completion following files are generated
terraform-resource-ndfc/
├─ internal/provider/
│ ├─ test_utils_vrf_test.go
│ ├─ resources/
│ │ ├─ resource_vrf/
│ │ │ ├─ vrf_resource_gen.go
│ │ │ ├─ resource_codec_gen.go
internal/provider/resources/resource_vrf/vrf_resource_gen.go
This file contains the schema and the data structs used by terraform to access the resource data. This is generated by hashicorp's tfplugin-gen
tool
Note down following type definition generated in this file
type VrfModel struct {
...terraform types
}
Make sure that this contains all the fields defined in the yaml Above structure is used to receive config from terraform as well as sending the results back to terraform
internal/provider/resources/resource_vrf/resource_codec_gen.go
.
-
This file is generated by
tfgenerator
tool -
Contains the payload definitions in go native types and helper functions.
- As terraform model struct contains custom types, a more accessible go native structure is generated for easier manipulations.
- Json tags used for creating/reading payload using
encoding/json
library
Types:
type NDFCVrfModel struct { //Go types }
Note that go types are easier to operate on and hence in the functions NDFCVrfModel
is used to pass data around.
Following functions can be used to convert the data between VrfModel
and NDFCVrfModel
.
-
func (VrfModel v) GetModelData()
.
ConvertsVrfModel
toNDFCVrfModel
=> Use to retrieve info from TF.This function is used to convert config data coming in at the
Read
,Create
,Update
from TF custom format to native go structure. Alternatively, the data can be directly accessed fromVrfModel
, but conversion APIs are required for each fieldeg: To do a string comparison.
//VrfModel *v if v.Id.ToStringValue() == "test_id" {. // Note the data type conversion needed at every step }
-
fund (VrfModel *v) SetModelData()
.
ConvertsNDFCModelVrf
toVrfModel
=> Use to set info to TF.
To start a resource development, one could either start with developing the backend code that talks to ndfc or the front end that talks to TF.
Start by generating a skeleton code using scafolding feature of tfplugin-gen, or copy paste another resource code and rename the variables
-
Using Scafold tool
Run the tool from project root
tfplugingen-framework scaffold resource --name vrf --output-dir internal/provider --package provider
internal/provider/vrf_resource.go
- generated filepackage provider import ( "context" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" ) var _ resource.Resource = (*vrfResource)(nil) func NewVrfResource() resource.Resource { return &vrfResource{} } type vrfResource struct{} type vrfResourceModel struct { Id types.String `tfsdk:"id"` } func (r *vrfResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_vrf" } func (r *vrfResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Computed: true, }, }, } } func (r *vrfResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data vrfResourceModel // Read Terraform plan data into the model resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } // Create API call logic // Example data value setting data.Id = types.StringValue("example-id") // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func (r *vrfResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var data vrfResourceModel // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } // Read API call logic // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func (r *vrfResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { var data vrfResourceModel // Read Terraform plan data into the model resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } // Update API call logic // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func (r *vrfResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var data vrfResourceModel // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } // Delete API call logic }
Modify the vrfResource structure to
type vrfResource struct{ client *ndfc.NDFC // This is the global ndfc instance that abstracts the NDFC and REST interface - needed to call into NDFC functions }
Also rename the
vrfResourceModel
struct instances in all functions toVrfModel
-
Setting up the front end code
-
Schema and Metadata
Change Schema & Metadata functions as following
func (r *vrfResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = resource_vrf.VrfResourceSchema() } func (r *vrfResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + ndfc.ResourceVrf }
- Config function
Add a config function as below, to setup the client instance.
func (d *vrfResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { // Prevent panic if the provider has not been configured. if req.ProviderData == nil { return } tflog.Info(ctx, "VRF Configure") client, ok := req.ProviderData.(*ndfc.NDFC) if !ok { resp.Diagnostics.AddError( "Unexpected resource Configure Type", fmt.Sprintf("Expected *nd.NDFC, got: %T. Please report this issue to the provider developers.", req.ProviderData), ) return } d.client = client }
- CRUD Functions
Skeletons of all CRUD functions are already generated by scaffold. Modify them to suite the resource being developed. A general scheme that can be followed is to retrieve the config data into Model struct (
VrfModel
) and pass it on to a handler function that abstracts the actual implementation. Conversions to native type (NDFCVrfModel
) may be done if needed.func (r *vrfResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data vrfResourceModel // Read Terraform plan data into the model resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } r.client.RscCreateVrf(ctx, &resp.Diagnostics, &data) // Backend code that creates the resource, retrieve it and fill data if resp.Diagonostics.HasError() { tflog.Error(ctx, "Create Bulk VRF Failed") return } // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) }
In a similar way other functions can be written
-
-
Setting up backend code
Backend code is typically the code that contains the logic to send/retrieve and process information related to NDFC Its preferred that this code is placed in
internal/provider/ndfc
folder.-
Implementing NDFC APIs In this section of code, the actual HTTP apis interacting with NDFC needs to be setup.
Instead of directly calling the HTTP library APIGET/POST/PUT
directly, it is recommended to use the abstraction ininternal/provider/api
package.-
Define a API struct and Inherit
NDFCAPICommon
. -
In the new derived structure, include all data needed for the corresponding resource under development.
-
Implement
GetUrl/PostUrl/PutUrl/DeleteUrl
methods for the new structure.
These methods must include logic to construct the URL required for the corresponding op.internal/provider/api/vrf_api.go
type VrfAPI struct { NDFCAPICommon fabricName string mutex *sync.Mutex PutVrf string Payload string DelList []string } const UrlVrfGetBulk = "/lan-fabric/rest/top-down/v2/fabrics/%s/vrfs" const UrlVrfCreateBulk = "/lan-fabric/rest/top-down/v2/bulk-create/vrfs" const UrlVrfDeleteBulk = "/lan-fabric/rest/top-down/v2/fabrics/%s/bulk-delete/vrfs" const UrlVrfGet = "/lan-fabric/rest/top-down/v2/fabrics/%s/vrfs/%s" const UrlVrfUpdate = "/lan-fabric/rest/top-down/v2/fabrics/%s/vrfs/%s" const UrlVrfAttachmentsGet = "/lan-fabric/rest/top-down/fabrics/%s/vrfs/attachments" func NewVrfAPI(fabricName string, lock *sync.Mutex, client *nd.Client) *VrfAPI { api := new(VrfAPI) api.fabricName = fabricName api.mutex = lock api.NDFCAPI = api api.client = client return api } func (c *VrfAPI) GetLock() *sync.Mutex { log.Printf("GetLock - VrfAPI %v", c.mutex) return c.mutex } func (c *VrfAPI) GetUrl() string { log.Printf("GetUrl - VrfAPI") return fmt.Sprintf(UrlVrfGetBulk, c.fabricName) } func (c *VrfAPI) PostUrl() string { log.Printf("PostUrl - VrfAPI") return UrlVrfCreateBulk } ...
-
Backend code logic File: internal/provider/ndfc/.go.
Implement RscCreate<resource_name>, RscRead<>, RscUpdate<>, RscDelete<> functions in this file. The logic can be broken down into multiple files if required.
eg. internal/provider/ndfc/vrf_single.go
func (c NDFC) RscCreateVrf(ctx context.Context, dg *diag.Diagnostics, vrfData *resource_vrf.VrfModel) *resource_vrf.VrfModel { tflog.Debug(ctx, fmt.Sprintf("RscCreateBulkVrf entry fabirc %s", vrfBulk.FabricName.ValueString())) vrf := vrfData.GetModelData() if vrf == nil { tflog.Error(ctx, "Data conversion from model failed") dg.AddError("Data conversion from model failed", "GetModelData returned empty") return nil } //Check if VRF is present ret := IsVrfPresent(vrfData.VrfName) // Implement this function if ret { dg.AddError("VRF creation error", fmt.Sprintf("VRF %s exists", vrfData.VrfName)) return nil } data, err := json.Marshal(vrf) if err != nil { dg.AddError() return nil } vrfApi := api.NewVrfAPI() res, err := vrfApi.Post(data) if err != nil { tflog.Error(ctx, fmt.Sprintf("Error POST: %s", err.Error())) dg.AddError("post failed") return nil } out := c.RscGetVrf(ctx, dg, vrf.VrfName) return out }
-
-