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

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

--

--

--

Passionate DevOps and Automation Engineer

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

How a Quality Engineer can contribute in a Sprint Planning

ZEIº on DHDL

Apache Spark With DynamoDB Use Cases

No code running club tracking system

<span>Photo by <a href=”https://unsplash.com/@sven_creat?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCo

Stop the Code Wars!

How to Handle your Python packaging in Lambda with Serverless plugins

Flutter + Firebase: Sign in with Apple

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Allan John

Allan John

Passionate DevOps and Automation Engineer

More from Medium

Introduction to drone.io CI/CD platform Part 2

Deploy Your Virtualized Web Application Using Openshift Virtualization, Ansible, And MetalLB For A…

How to Learn Kubernetes: Prerequisites, Paths, and Resources

Create And Deploy A MEAN Stack As A Helm Chart