Skip to content

NDFC Provider Development workflow

Murali Das Mohan edited this page Oct 24, 2024 · 7 revisions

Introduction

This wiki contains detailed walk through of how to develop a terraform resource for NDFC

Prerequisites

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`

Code base description

Code structure

The diagram below depicts the code structure. This structure shall be strictly enforced on any new code added.

Screenshot 2024-10-01 at 8 59 07 AM

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
Screenshot 2024-10-01 at 8 32 51 AM
  • 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 by tfplugin-gen

  • internal/provider/resources/resource_<name>/resource_codec_gen.go
    Code generated by tfgenerator

Start the resource development

Following sections walks through the procedure to create a new resource. An actual ndfc functionality vrf is used here to explain the work flow.

Step 1: Code Generation

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

Writing the resource yaml

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 the generator/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

Using the code generator

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().
    Converts VrfModel to NDFCVrfModel => 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 from VrfModel, but conversion APIs are required for each field

    eg: 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().
    Converts NDFCModelVrf to VrfModel => Use to set info to TF.

Step 2: Develop the resource functionality

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.

Front End - interface with terraform

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 file

    package 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 to VrfModel

  • 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 API GET/POST/PUT directly, it is recommended to use the abstraction in internal/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
      
         
      }