Extending the Terraform Juju Provider
Notes on my experience extending the Terraform Juju Provider to have more of the features of the Juju library.
Terraform SDKv2
The Terraform Juju provider was written using the Terraform SDKv2. This is not the current version of the Terraform plugin development tooling. Terraform plugins should be written with the new Terraform plugin framework instead of the Terraform SDK.
Hashicorp does provide instructions on how to migrate plugins from the Terraform SDKv2 to the Terraform plugin framework. The Terraform Juju provider will need to be migrated to the new Terraform plugin framework eventually.
Testing Terraform Plugins
Acceptance Tests
Terraform has acceptance tests that make sure the plugin works as expected and delivers the correct infrastructure.
Terraform Plugins must provide acceptance tests that imitate applying configuration through Terraform. Acceptance tests create real infrastructure. The Terraform Juju provider executes Juju commands against a Juju controller, and the Juju will create infrastructure on the associated cloud. I configured a Juju controller locally using LXD on my local machine. When Terraform runs with the Juju provider, the configured infrastructure provisions into LXD containers on my local machine.
HashiCorp runs nightly acceptance tests of providers found in the Terraform Providers GitHub organization to make sure they are working correctly, but only a small handful of providers for Oracle Cloud are in that organization. There are also very few followers for that organization.
\
Implementing Acceptance Tests
Acceptance tests are defined for data sources and resources. They are files named like data_source_name_test.go
or resource_name_test.go
. They can be placed alongside the corresponding files named data_source_name.go
and resource_name.go
in the provider
directory. In all of these cases name
should be replaced with the name of the type of resource.
For example, the new resource I defined was for a machine. This means the files would be named data_source_machine_test.go
, data_source_machine.go
, resource_machine_test.go
, and resource_machine.go
.
The test file defines functions, and each function is an acceptance test. The naming convention for the functions appears to be TestAcc_ResourceName_YourTestName
. You would name a basic acceptance test of the machine resource I mentioned previously as TestAcc_ResourceMachine_Basic
.
The function defintion should take a pointer to a testing.T
struct from Golang's Testing package as its only argument.
The function name and argument would look like:
func TestAcc_ResourceMachine_Basic(t *testing.T) {
// the function body
}
The function body contains the code for configuring Terraform to run the acceptance test and for the checks you would like Terraform to run. Terraform acceptance tests will run Terraform and create infrastructure. This will incur costs on metered platforms like public clouds if you use them to test your Terraform code. Use a local development environment or unmetered platform to test your Terraform code.*
The Terraform SDKv2 libraries have some helper functions you can use in your tests. The Terraform Juju provider, for example, uses the RandomWithPrefix
function from the acctest
Terraform SDKv2 module to generate a random string with a given string attached to the front as a prefix. This function is used in the Terraform Juju provider code to generate unique while identifiable Juju model names for each acceptance test. Using unique model names will keep the tests from colliding and interfering with each other.
The test itself is defined using the Test
function from the resource
module of the Terraform SDKv2. The Test function takes a pointer of the type testing.T
, which is the argument that gets passed into your acceptance test function, and an instance of the type resource.TestCase
.
For my basic testing for the machine resource, I only passed arguments for the PreCheck
, ProviderFactories
, and Steps
members of resource.TestCase
. I did not need other members for this test.
The PreCheck
member defines checks to perform before the test steps are performed.
The ProviderFactories
indicates which Terraform schema.Provider
to use for the tests. In the Terraform Juju provider, this is defined as providerFactories
from the package provider
, which is the name of the package containing the schema.Provider
code for the Terraform Juju provider.
The Steps
member defines an instance of []resource.TestStep
with tests to perform. In the basic test for the machine resource, I defined two test steps. The first step defines a Config member that you can use to configure Terraform. I wrote another function that I modelled after the existing Terraform Juju provider code. The function takes a string for the Juju model name and returns a string literal containing a Terraform plan. The plan defines a simple Juju model resource and a simple Juju machine resource. The first step also defines a number of test checks you can run to verify the resource was created correctly. I used, for example, checks that compare the value of the model
attribute of the machine resource and the name
attribute of the machine resource to make sure they were what I expected.
The second step I defined has Terraform perform an import on the created Terraform resource to verify that the resource imports correctly and the Terraform state matches what the external system (that is, Juju) reports. This step uses Terraform's ImportStateVerify
, ImportState
, and ResourceName
test features.
That should be everything for a basic test of the Terraform resource you define. You can define more complex tests using Golang's testing framework and Terraform's testing capabilities.
Remember, Terraform testing does create real world infrastructure that can cost money if you are using public cloud providers. Since I was testing Juju, I could use LXD containers on my local development machine to simulate machines and infrastructure for applications.
Unit Tests
Terraform has unit tests for providers, but it looks like the Terraform Juju provider does not have any unit tests. I did not have a chance to write unit tests.
Root Resource Was Present But Now Absent
This error often means that Terraform is looking for a resource that was deleted from the Terraform graph during the resource lifecycle.
In my case, the error was occurring because of a client-side issue with the Juju client that my Terraform plugin code was not passing back to the end user correctly. The issue was not an error; rather, it was invalid input to the Juju client. Invalid input is not an exception-causing error. I was not passing in storage volume configuration correctly for the machines I wanted to provision through Juju. I corrected that behavior to either pass in the value the user provides in the Terraform plan or to pass in a default empty string.
Terraform Represents Resources as a Graph
One key thing I learned about how Terraform constructs its state is that Terraform represents resources as a graph. By graph, I mean like a network consisting of individual nodes connected by one or more edges to other nodes. The edges represent dependencies between individual resources. Terraform tries to infer dependencies from the configuration you define in your Terraform plan. You can manually define dependencies as well using Terraform meta directives like depends_on
.