Free Trial
Engineering

Learn the DNSimple API by Bulding a CLI in Golang

Anthony Eden's profile picture Anthony Eden on

Next week is dotGo 2016, in Paris, France. dotGo is Europe's largest Go conference, bringing together more than 600 developers to hear about the latest and greatest that the Golang community has to offer. In preparation, I'd like to give you a walk through of the basics of the official DNSimple API Go client, providing details on how to connect to the DNSimple API and perform various tasks to automate your domain management from your Go applications.

Getting Started

First, you'll need to have Go installed and set up correctly on your system. Once you've done that, you'll need to create a project in your $GOPATH. For example, I am going to start a project in the directory from my personal GitHub account: cd $GOPATH/src/github.com/aeden && mkdir dnsimple-example && cd dnsimple-example. You will need to change the path to something that works for you.

Once you've done that, you need to install the DNSimple Go library: go get github.com/dnsimple/dnsimple-go/dnsimple.

Note: as you work through this blog post and the accompanying examples, make sure to visit the godoc for the DNSimple API Go client for details about what packages are available to you.

Now that the client library is installed in the $GOPATH, create a file called myapp.go and put the following into that file:

package main

import (
	"fmt"
	"os"

	"github.com/dnsimple/dnsimple-go/dnsimple"
)

func main() {
        oauthToken := ""
	client := dnsimple.NewClient(dnsimple.NewOauthTokenCredentials(oauthToken))
        whoamiResponse, err := client.Identity.Whoami()
        if err != nil {
		fmt.Printf("Whoami() returned error: %v\n", err)
		os.Exit(1)
	}
	fmt.Printf("%+v\n", whoamiResponse.Data.Account)
}

You can now run this code, but authentication will fail:

$ go run myapp.go
Whoami() returned error: GET https://api.dnsimple.com/v2/whoami: 401 Authentication failed
exit status 1

The next step is to properly authenticate with the DNSimple API.

Authenticating

The simplest way to authenticate with the DNSimple API is to create an access token that you can use directly with your account. You may do that by following this guide.

Once you have your access token, you'll need to get it into the client. For this exercise, you will pass the token in as an environment variable, connect to the whoami endpoint and retreive some basic account information. Change the line for setting oauthToken to the following:

oauthToken := os.Getenv("TOKEN")

Now when you run this code you'll need to also ensure that the token you created on the DNSimple site is passed in via the TOKEN environment variable. For example:

$ TOKEN=token go run myapp.go
&{ID:1 Email:john@example.com PlanIdentifier:dnsimple-personal CreatedAt:2010-07-01T07:45:09.416Z UpdatedAt:2016-09-24T20:07:52.562Z}

At this point you have connected to the DNSimple API and have been able to perform an authenticated call. Now you can move on to working with your domains.

Listing Domains

To list the domains in your account, you'll use the Domains.ListDomains method. It requires the account ID as its first argument, so you'll need to get that from the whoami response you previously printed. The ID in the account struct is an int type and the ListDomains method requires a string type, so you'll need to convert the type using Go's built-in strconv.Itoa function.

Here is what the section of code starting after the printing of the account data looks like:

func main() {
	// ...

        fmt.Printf("%+v\n", whoamiResponse.Data.Account)

	accountId := strconv.Itoa(whoamiResponse.Data.Account.ID)

	listDomainsResponse, err := client.Domains.ListDomains(accountId, nil)
	if err != nil {
		fmt.Printf("ListDomains() returned error: %v\n", err)
		os.Exit(1)
	}

	for domain := range listDomainsResponse.Data {
		fmt.Printf("%+v\n", domain)
	}
}

Make sure to also add the import of the strconv package in the import section of the code.

If you run this code you will see that the first line outputs the account and the subsequent lines output information about each domain returned. Note that if you have more than 30 domains then this will only return the first page of domains.

Interlude: Switching to Command Functions

Before we move on, let's clean up the code a bit. First, instead of printing the entire Account struct, change the line to print something a bit simpler and clear: the email address from the account:

       fmt.Printf("Connected as %+v\n\n", whoamiResponse.Data.Account.Email)

Next you can introduce a simple command mechanism so your program can be used to do different things. Each command will be implemented as a function. A builder function is used to return the specific command function. For example, here is the function that builds the list domains function:

func buildListDomainsFunc(client *dnsimple.Client) func(string) {
	return func(accountId string) {
		listDomainsResponse, err := client.Domains.ListDomains(accountId, nil)
		if err != nil {
			fmt.Printf("ListDomains() returned error: %v\n", err)
			os.Exit(1)
		}

		for _, domain := range listDomainsResponse.Data {
			fmt.Printf("%+v\n", domain)
		}
	}
}

The client is passed in when the function is built and the function returns the anonymous function that implements the command. That anonymous function will always expect an account ID (since we will always need the account ID).

The command name will be passed as the first argument to the program, so the main function needs to be updated to select the command or fallback to a usage message if no matching command is found. First, here's the usage function:

func usage(commandName string) {
	if commandName == "" {
		fmt.Printf("Command is required\n\n")
	} else {
		fmt.Printf("Unknown command: %v\n\n", commandName)
	}
	fmt.Printf("The following commands are supported:\n")
	fmt.Printf("  domains.list\n")
}

And now the updated main function:

func main() {
	oauthToken := os.Getenv("TOKEN")
	client := dnsimple.NewClient(dnsimple.NewOauthTokenCredentials(oauthToken))

	listDomains := buildListDomainsFunc(client)

	commands := map[string]func(string){
		"domains.list": listDomains,
	}

	if len(os.Args) <= 1 {
		usage("")
		return
	}

	commandName := os.Args[1]
	commandFunc := commands[commandName]
	if commandFunc == nil {
		usage(commandName)
	} else {
		fmt.Printf("Connecting to DNSimple API...\n\n")

		whoamiResponse, err := client.Identity.Whoami()
		if err != nil {
			fmt.Printf("Whoami() returned error: %v\n", err)
			os.Exit(1)
		}
		fmt.Printf("Connected as %+v\n\n", whoamiResponse.Data.Account.Email)

		accountId := strconv.Itoa(whoamiResponse.Data.Account.ID)

		commandFunc(accountId)
	}
}

The main function breaks down into the following steps:

  • Build the client
  • Create the command functions
  • Map the command names to their functions
  • Retrieve the command name from the command-line args
  • Retrieve the command function from the command map
  • Get the account ID
  • Execute the command function

To call the program now:

$ TOKEN=token go run myapp.go domains.list

Or, to display the help message:

$ TOKEN=token go run myapp.go

Or

$ TOKEN=token go run myapp.go bogus

Adding a Domain

Next you will implement a command to add a new domain. Since this new command requires at least one additional argument (the domain name), we will need to change the command implementation a bit as well.

First, let's consider how to have commands that can take a variety of arguments passed in when the applicaiton is run. I selected the following format:

domains.create name:example.com

Thus, the first argument is the name of the command and then subsequent arguments are name/value pairs.

Now the command function signature needs to be changed to accept both the account identifier and a map of options.

Here is an example of the buildListDomainsFunc after the change:

func buildListDomainsFunc(client *dnsimple.Client) func(string, map[string]string) {
       return func(accountId string, _options map[string]string) {
              // nothing changes here
       }
}

And here is how it is called now:

commandFunc(accountId, extractOptions(os.Args))

A new extractOptions function pulls the name/value pairs out of the command line args if any are present (make sure to import strings after adding this):

func extractOptions(args []string) map[string]string {
	options := map[string]string{}
	if len(args) > 2 {
		for _, arg := range args[2:] {
			argNameVal := strings.Split(arg, ":")
			options[argNameVal[0]] = argNameVal[1]
		}
	}
	return options
}

Next, you'll need a function to build the command function to create the domain:

func buildCreateDomainFunc(client *dnsimple.Client) func(string, map[string]string) {
	return func(accountId string, options map[string]string) {
		domainAttributes := dnsimple.Domain{Name: options["name"]}
		createDomainResponse, err := client.Domains.CreateDomain(accountId, domainAttributes)
		if err != nil {
			fmt.Printf("CreateDomain() returned error: %v\n", err)
			os.Exit(1)
		}

		fmt.Printf("%+v\n", createDomainResponse.Data)
	}
}

Note that the Domains.CreateDomain function accepts the account ID along with a Domain struct. The Domain struct represents the container of the new domain attributes.

In the main function, create the new command function:

listDomains := buildListDomainsFunc(client)
createDomain := buildCreateDomainFunc(client)

commands := map[string]func(string, map[string]string){
	"domains.list":   listDomains,
	"domains.create": createDomain,
}

And make sure to add the additional line to the usage function:

fmt.Printf("  domains.create name:example.com\n")

At this point your code should look something like the code in this gist. You can run it as follows:

$ TOKEN=token go run myapp.go domains.create name:some-new-domain.com

Adding Records

The next command is for adding a record. The implementation is similar in that you will need to build and register the command function. First, add a buildCreateRecordFunc function:

func buildCreateRecordFunc(client *dnsimple.Client) func(string, map[string]string) {
	return func(accountId string, options map[string]string) {
		domainId := options["domain"]

		recordAttributes := dnsimple.ZoneRecord{Name: options["name"], Type: options["type"], Content: options["content"]}
		if options["ttl"] != "" {
			ttl, err := strconv.Atoi(options["ttl"])
			if err != nil {
				fmt.Printf("Problem parsing TTL: %v\n", err)
				os.Exit(1)
			}
			recordAttributes.TTL = ttl
		}

		createRecordResponse, err := client.Zones.CreateRecord(accountId, domainId, recordAttributes)
		if err != nil {
			fmt.Printf("CreateRecord() returned error: %v\n", err)
			os.Exit(1)
		}

		fmt.Printf("%+v\n", createRecordResponse.Data)
	}
}

This function expects the domain name to be passed as domain and requires the type and content parameters. If you do not provide a value for name, then the record is created on the naked domain. Finally, if the TTL is present this function converts it to an int as that was it required by the dnsimple.ZoneRecord struct.

Take a look at this gist to see how the code should look at this point.

Here is an example of how to use this new command:

$ TOKEN=token go run myapp.go records.create domain:some-domain.com content:1.2.3.5 ttl:600 type:A

Registering a Domain

As the final example demonstrating how to use the DNSimple API from Golang, let's make it possible to register a domain name. This is done with the Registrar.RegisterDomain function.

func buildRegisterDomainFunc(client *dnsimple.Client) func(string, map[string]string) {
	return func(accountId string, options map[string]string) {
		registerRequest := &dnsimple.DomainRegisterRequest{}
		if options["registrant_id"] != "" {
			registrantId, err := strconv.Atoi(options["registrant_id"])
			if err != nil {
				fmt.Printf("Problem parsing registrant ID: %v\n", err)
				os.Exit(1)
			}
			registerRequest.RegistrantID = registrantId
		}

		registerDomainResponse, err := client.Registrar.RegisterDomain(accountId, options["name"], registerRequest)
		if err != nil {
			fmt.Printf("RegisterDomain() returned error: %v\n", err)
			os.Exit(1)
		}

		fmt.Printf("%+v\n", registerDomainResponse.Data)
	}
}

The Registrar.RegisterDomain function requires three arguments: the account ID, the domain name, and a RegistrarRequest struct. Inside the RegistrarRequest struct instance you must specify the registrant ID as an integer. Thus, this function converts the registrant ID from a string to an integer and sets the value in the registrarRequest before calling Registrar.RegisterDomain.

Here is a gist of the code at this point.

To use this command:

$ TOKEN=token go run myapp.go domains.register name:example.com registrant_id:1

Conclusion

This blog post just scratches the surface of what you can do with the DNSimple API. The examples above are oriented towards building a CLI tool, but our client works great even if you want to integrate domain or record management into your application.

If you're interested in finding out more, take a look at the API v2 documentation as well as the godocs for the DNSimple Go API client. You'll find a wealth of information about how to use the API and what endpoints are available. If you have any questions along the way, make sure to drop us a line, we're always happy to help!

Share on Twitter and Facebook

Anthony Eden's profile picture

Anthony Eden

I break things so Simone continues to have plenty to do. I occasionally have useful ideas, like building a domain and DNS provider that doesn't suck.

We think domain management should be easy.
That's why we continue building DNSimple.

Try us free for 30 days
4.5 stars

4.3 out of 5 stars.

Based on Trustpilot.com and G2.com reviews.