Building a Terraform Provider-Part-II — Auth and Configure Provider

Allan John
5 min readJun 3, 2022
Images from Unsplash

Introduction

This is a continuation of the Part I, where we created the Provider function and created a RestAPI client to work with the provider.

Authentication

So now we need to define the functions to authenticate against the RestAPI servers and get the Bearer Tokens. So lets modify the file client/auth.go

Custom Client

package clientimport (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
)
// Get Bearer Token for CustomAPI
func (c *Client) GetCustomClientToken() (*CustomAuthResponse, error) {
// Check Credentials provided are empty
if c.CustomAuth.Username == "" || c.CustomAuth.Password == "" {
return nil, fmt.Errorf("define CustomAPI username and password")
}
// Convert Credentials Struct to JSON
creds, err := json.Marshal(c.CustomAuth)
if err != nil {
return nil, err
}

// Prepare Client for Getting the Token
url := fmt.Sprintf("%s/login", c.CustomHostURL)
method := "POST"
payload := strings.NewReader(string(creds))

// Initialize the Request client
req, err := http.NewRequest(method, url, payload)
if err != nil {
return nil, err
}

// Add Headers for the request
req.Header.Add("Content-Type", "application/json")
req.Host = "custom-api-restapi.testing.com"

// Make the request
res, err := c.CustomHTTPClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()

// Read the Body from response
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}

// Check status code
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("status: %d, body: %s", res.StatusCode, body)
}

// Prepare the response as a struct
ctAr := CustomAuthResponse{}

// Convert Json resposne to struct
err = json.Unmarshal(body, &ctAr)
if err != nil {
return nil, err
}

// Return the Struct
return &ctAr, nil
}

This function is actually a simple one, to make a POST request with credentials and get Bearer Token as response. Just its the GoLang way :)

Point to note is req.Host. When adding a separate HostName in the headers, I did a mistake by adding the host in req.Header.Host. This is totally wrong and Go ignores this Header. To properly add the Host Header, it should be set as req.Host. Otherwise the hostname provided in the URL will be used. This is fine, but if the hostname in the URL and the Host in the Header should be different, then this should be noted.

Azure Client

So, Lets add same Auth stuff for Azure Client as well. Probably there might be a better way to do this, but I preferred to do this way, since I want to make it as simplistic as possible and not too complicated that the next guy reading cannot understand

Modifying the same file and adding the function

// Get Bearer Token for Azure API
func (c *Client) GetAzureToken() (*AzAuthResponse, error) {
// Check Credentials provided are empty
if c.AzAuth.ClientID == "" || c.AzAuth.ClientSecret == "" {
return nil, fmt.Errorf("define client_id and client_secret")
}

// Prepare Client for getting the token
url := c.AzHostURL
method := "POST"
payload := strings.NewReader(fmt.Sprintf("grant_type=client_credentials&client_id=%s&client_secret=%s&resource=https://management.azure.com/", c.AzAuth.ClientID, c.AzAuth.ClientSecret))

// Initialize the Request client
req, err := http.NewRequest(method, url, payload)
if err != nil {
return nil, err
}
//requestDump, err := httputil.DumpRequest(req, true)
//if err != nil {
// return nil, err
//}

// Adding proper Headers
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

// Make Request
res, err := c.AzHTTPClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()

// Read the Body from response
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}

// Check status code
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("status: %d, body: %s", res.StatusCode, body)
}

// Prepare the response as a struct

azAr := AzAuthResponse{}

// Convert Json response to struct
err = json.Unmarshal(body, &azAr)
if err != nil {
return nil, err
}

// Return the Struct
return &azAr, nil
}

Nothing much here, same as the previous function, just difference of urls and payload. Something interesting which I found is the httputilDumpRequest function. This is really nice to have when there is trouble when debugging, to get the whole request that is being sent. I commented it out after the code was working and in future if I need to debug, I just uncomment it and return the output and see what’s going on.

Now our Auth functions are ready. So when a new Client is created, the authentication is done and we will have the Bearer Tokens

Configure Provider

Time to configure our provider. Modify the file records/provider.go and add the following function

// providerConfigure - Configure Provider
func providerConfigure(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) {

// Get credentials and prepare it for provider
username := d.Get("username").(string)
password := d.Get("password").(string)

clientId := d.Get("client_id").(string)
clientSecret := d.Get("client_secret").(string)
tenantId := d.Get("tenant_id").(string)
subId := d.Get("subscription_id").(string)

// Prepare URL variable for Customclient
var custom_client_url *string

hVal, ok := d.GetOk("custom_client_url")
if ok {
tempHost := hVal.(string)
custom_client_url= &tempHost
}

// Warning or errors can be collected in a slice type
var diags diag.Diagnostics

// If all values are provided then create client
if (username != "") && (password != "") && (clientId != "") && (clientSecret != "") && (tenantId != "") && (subId != "") {
c, err := client.NewClient(custom_client_url, &username, &password, &clientId, &clientSecret, &tenantId, &subId)
if err != nil {
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: "Unable to create RestApi Client",
Detail: fmt.Sprintf("Something wrong with Provider to create client. Error: %s", err),
})

return nil, diags
}

return c, diags
}

// if values are missing, then create client and return the response
c, err := client.NewClient(nil, nil, nil, nil, nil, nil, nil)
if err != nil {
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: "Unable to create RestApi Client",
Detail: fmt.Sprintf("Unable to authenticate user for authenticated RestApi client: %s", err),
})
return nil, diags
}
return c, diags
}

So when the function runs, the provider is configured, by creating a new Client. First, we check if all variables are okay and not empty. Then we pass this information to the function that creates the new client. The newClient function will create a client which will be used for all the resources we are going to use.

Don’t forget to add the packages to import in the top of the file

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"terraform-provider-rest/client"
)

Now we have laid the foundation for the provider with all the necessary stuff. lets start with the heavy stuff, Resource Creation. This will be described more in Part — III, since its a bit lengthy process.

Hope you enjoyed!, If you liked it please continue with Part-III

--

--