Add the jupiterone provider to your project's terraform:
terraform {
required_providers {
jupiterone = {
source = "JupiterOne/jupiterone"
version = "x.x.x" # Replace with desired version
}
}
}
provider "jupiterone" {
# Configuration options
account_id = "xxxxx"
api_key = "xxxx"
region = "us"
}
See the examples directory
- Install Go and
make
- Clone the repository
- Enter the repository directory
- Build the provider with
make build
or invokego install
directly. - Install the provider locally as referenced here
This provider uses Go modules. Please see the Go documentation for the most up to date information about using Go modules.
To add a new dependency github.com/author/dependency
to your Terraform provider:
go get github.com/author/dependency
go mod tidy
If this is your first time developing in go, or developing a terraform provider, it may be wise to do some of the GO Terraform Plugin Framework tutorial. This is what is used to build this provider.
If you wish to work on the provider, you'll first need Go installed on your machine (please check the requirements before proceeding). To compile the provider, run make build
.
make build
# If the above command doesn't work, try the next command in the root directory
go install .
Start your resource development by adding a jupiterone/resource_[j1_entity].go
file. You should take a look at another file, such as the resource_user_group.go
to get an idea of what you need in this file, but we will be going into depth on some of the file contents below.
This type is the base type of the terraform resource you are creating. The functions defined in the rest of the file are added to the interface of this type and enable all further functionality.
You will almost always have the version
and qlient
fields in this type. They are initialized in the base provider and added to an instance of your type in the Configure
method.
type J1EntityResource struct {
version string
qlient graphql.Client
}
This is the type that represents the terraform resource's state. Generally this is the go equivalent of your graphql resource. You can see that there are json and tfsdk field name definitions. I am not yet sure what the json fields are used for, but the tfsdk fields are used to map the terraform fields to this go type.
This J1EntityModel is using example fields from the UserGroupModel
, so you have to modify the fields to represent the entity that you are working with. You will probably have an Id
, but may not a Name
, Description
, etc.
// J1EntityModel is the terraform HCL representation of a user group.
type J1EntityModel struct {
Id types.String `json:"id,omitempty" tfsdk:"id"`
Name types.String `json:"groupName,omitempty" tfsdk:"name"`
Description types.String `json:"groupDescription,omitempty" tfsdk:"description"`
Permissions []string `json:"groupAbacPermission,omitempty" tfsdk:"permissions"`
QueryPolicy []map[string][]string `json:"groupQueryPolicy,omitempty" tfsdk:"query_policy"`
}
This function is what is used to make the provider aware of your new resource. This will be added to the Resources
function in the provider.go
file.
func NewJ1EntityResource() resource.Resource {
return &J1EntityResource{}
}
This function is simply used to define your TypeName for the terraform resource. This is the name that will be used when creating resources in your terraform.
func (*J1EntityResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_j1_entity"
}
In terraform the name will be used like this:
resource "jupiterone_j1_entity" "j1_entity_1" {...}
This function is used to define the schema and documentation for the terraform people will write to build your resource. The terraform go provider lib will use this schema to parse the consumers terraform, validate it, and map it to the J1EntityResource go type.
What you need in this function is totally dependent on what your entity structure looks like. Take a look at some of the other resource.go files to get an idea of what you may need here.
This function is used to add the version and qlient to the J1EntityResource. The qlient is then used to make http calls to the J1 graphql api. Your Configure
method should look much the same as below.
func (r *J1EntityResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}
p, ok := req.ProviderData.(*JupiterOneProvider)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected JupiterOneProvider, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.version = p.version
r.qlient = p.Qlient
}
This function is used to create the actual resource. It parses out the J1EntityModel from terraform plan and you work with that data how ever you need, which is mainly just calling the graphql api to create your resource.
Follow this general structure for your create function and look at other resource.go files to get an idea of what you may need here.
Note that if the resource you are creating does not return an id
from the API, you will need to assign a unique id to the data.Id
field. This is used by terraform to track the resource. The user_group_membership
resource is one where this is necessary and can be used as an example.
func (r *J1EntityResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data J1EntityModel
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Entity specific work goes here, such as calling a gql endpoint
// Check other files for specifics
...
if err != nil {
resp.Diagnostics.AddError("failed to create j1 entity", err.Error())
return
}
data.Id = types.StringValue(gqlResponse.Id)
tflog.Trace(ctx, "Created j1 entity",
map[string]interface{}{"id": data.Id})
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
This function is used to delete your terraform resource. It will generally just call a gql endpoint to do that work.
// Delete implements resource.Resource
func (r *J1EntityResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data J1EntityModel
// Read Terraform ste into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// This is an example call to gql endpoint to delete an entity. You swap with your implementation
if _, err := client.DeleteJ1Entity(ctx, r.qlient, data.Id.ValueString()); err != nil {
resp.Diagnostics.AddError("failed to j1 entity", err.Error())
}
}
This function is used to read your entity from the jupiterone api. This helps terraform know if there have been changes made in the jupiterone application that need to be overwritten with update intervention.
func (r *J1EntityResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data J1EntityModel
// Read Terraform state into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Grab your entity from jupiterone gql api and then map it to the J1EntityModel
// See other resource.go files for examples
entity, err := client.GetJ1Entity(ctx, r.qlient, data.Id.ValueString())
if err != nil {
if strings.Contains(err.Error(), "not found") {
resp.State.RemoveResource(ctx)
} else {
resp.Diagnostics.AddError("failed to get entity", err.Error())
}
return
}
data.Name = types.StringValue(entity.IamGetGroup.GroupName)
data.Description = types.StringValue(entity.IamGetGroup.GroupDescription)
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
This function simply tells terraform which property to target for the import state. You will generally just copy this function over.
func (*J1EntityResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
This function is used to update an entity when terraform finds differences between terraform config and the state of the entity in jupiterone. Your contents will be much like your create function, only you should be calling the update action on the gql api.
// Update implements resource.Resource
func (r *J1EntityResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data J1EntityModel
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Entity specific work goes here, such as calling a gql endpoint
// Check other files for specifics
...
if err != nil {
resp.Diagnostics.AddError("failed to update j1 entity", err.Error())
return
}
tflog.Trace(ctx, "Updated j1 entity",
map[string]interface{}{"id": data.Id})
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
The GraphQL client methods are generated using the khan/genqlient library. The primary advantages are:
- Compile time query checking
- Generated full types for all API calls
node
andyarn
are installed
You should either update an existing .graphql
file in the /jupiterone/internal/client
directory, or create a new one.
If you create a new one, be sure to add the file to the genqlient.yaml file operations
section.
You should always generate the gql client from the production api so that you do not include any in-progress work from dev. Set these environment variables before running the next commands.
export JUPITERONE_ACCOUNT=:your_account_id
export JUPITERONE_API_KEY=:your_api_key
export JUPITERONE_REGION=us
These commands will generate several files:
- introspection_result.json
- jupiterone/internal/client/schema.graphql
- jupiterone/internal/client/generated.go <-- Only generated file that gets committed to the repository
scripts/get_current_schema.bash
make generate-client
In order to test the provider, you can simply run make testacc
. Pre-recorded
API responses (cassettes) are read in from
jupiterone/cassettes/*.yaml files and returned. When
tests are modified, the cassettes need to be re-recorded.
Note: Recording cassettes creates/updates/destroys real resources. Never run this on a production JupiterOne organization.
In order to record cassettes you need to have JUPITERONE_API_KEY
and JUPITERONE_ACCOUNT_ID
for your testing organization in your environment.
To re-record all cassettes:
export JUPITERONE_ACCOUNT_ID=your-account-id
export JUPITERONE_API_KEY=xxxxxx
export JUPITERONE_REGION=us
make cassettes
If you only need to re-record a subset of your tests, delete the related
cassette file and run the tests as usual. This takes advantage of go-vcr
s
default ModeRecordOnce
functionality.
export JUPITERONE_ACCOUNT_ID=your-account-id
export JUPITERONE_API_KEY=xxxxxx
export JUPITERONE_REGION=us
rm jupiterone/cassettes/:some-test.yaml
make testacc
To log the HTTP request and response contents, set the TF_LOG
level to DEBUG
or lower:
export TF_LOG=DEBUG
make testacc
In order to check changes you made locally to the provider, you can use the binary you just compiled by adding the following
to your ~/.terraformrc
file. This is valid for Terraform 0.14+. Please see
Terraform's documentation
for more details.
provider_installation {
# Use /home/$USER/go/bin as an overridden package directory
# for the jupiterone provider. This disables the version and checksum
# verifications for this provider and forces Terraform to look for the
# jupiterone provider plugin in the given directory.
# Replace $USER with your username. On Mac and Linux systems this can be found
# through running "echo $USER" in your terminal.
dev_overrides {
"JupiterOne/jupiterone" = "/Users/$USER/go/bin"
}
# For all other providers, install them directly from their origin provider
# registries as normal. If you omit this, Terraform will _only_ use
# the dev_overrides block, and so no other providers will be available.
direct {}
}
For information about writing acceptance tests, see the main Terraform contributing guide.
This repository contains a GitHub Action configured to automatically build and
publish assets for release when a tag is pushed that matches the pattern v*
(ie. v0.1.0
).
A Goreleaser configuration is provided that produces build artifacts matching the layout required to publish the provider in the Terraform Registry.
Releases will appear as drafts. Once marked as published on the GitHub Releases page, they will become available via the Terraform Registry.
To generate new provider documentation run make docs