How to handle weird API flow with implicit create step in custom terraform provider

Problem Introduction

I am in a weird situation developing a provider against an API where the CRUDE behavior diverges a bit.

There are two kinds of resources currently relevant, Host and Scope. A host can have many scopes. Scopes are updated with configurations.

This generally fits well into the terraform flow, it has a full CRUDE flow possible - except for one instance.

When a new Host is made, it automatically has a default scope attached to it. It is always there, cannot be deleted etc.

I can’t figure out how to have my provider gracefully handle this, as I would want the tf to treat it like any other resource, but it doesn’t have an explicit CREATE/DELETE, only READ/UPDATE/EXISTS - but every other scope attached to the host would have CREATE/DELETE.

Importing is not an option due to density, requiring an import for every host after creation would render the entire thing pointless.

I originally was going to attempt to split Scopes and Configurations into separate resources so one could be full-filled by the Host (the host providing the Scope ID for a configuration, and then other configurations can get their scope IDs from a scope resource)

However this approach falls apart because the API for both are the same, unless I wanted to add the abstraction of creating an empty scope then applying a configuration against it, which may not be fully supported. It would essentially be two resources controlling one resource which could lead to dramatic conflicts.

Code Example

A paraphrased example of an execution I thought about implementing

resource "host" "test_integrations" {
    name = "test.integrations.domain.com"
    account_hash = "${local.integrationAccountHash}"
    services = [40]

}

resource "configuration" "test_integrations_root_configuration" {
    name = "root"
    parent_host = "${host.test_integrations.id}"
    account_hash = "${local.integrationAccountHash}"
    scope_id = "${host.test_integrations.root_scope_id}"
    hostnames = ["test.integrations.domain.com"]
}

resource "scope" "test_integrations_other" {
    account_hash = "${local.integrationAccountHash}"
    host_hash = "${host.test_integrations.id}"
    path = "/non/root/path"
    name = "Some Other URI Path"
}

resource "configuration" "test_integrations_other_configuration" {
    name = "other"
    parent_host = "${host.test_integrations.id}"
    account_hash = "${local.integrationAccountHash}"
    scope_id = "${host.test_integrations_other.id}"
}

In this example flow, a configuration and scope resource unfortunately are pointing to the same resource which I am worried would cause conflicts or confusion on who is responsible for what and dramatically confuses the create/delete lifecycle (as well as either would require the full payload of the other to successfully talk to the API)

But I can’t figure out how the TF lifecycle would allow for a resource that would only UPDATE/READ/EXISTS if say a flag was given (and how state would handle that)

Finally

An alternative would be to just have a Configuration resource, but then if it was the root configuration it would need to skip create/delete as it is inherently tied to the host

Ideally I’d be able to handle this situation gracefully. I am trying to avoid including the root scope/configuration in the host definition as it would create a split in how they are written and handled.

The documentation for providers implies you can use a resource AS a schema object in a resource, but does not explain how or why. If it works the way I imagine it, it may work to create a resource that is only used to inject into the host perhaps - but I don’t know if that is how it works and if it is how to accomplish it.

Any help in figuring out how one would deal with this would be much appreciated,

I solved this tentatively by finally finding a precedence reference in the AWS provider:

For posterity:

My solution will be to have a secondary resource inheriting the original and overriding create/delete functionality

Example:

func defaultResourceConfiguration() *schema.Resource {
	drc := resourceConfiguration()
	drc.Create = resourceDefaultConfigurationCreate
	drc.Delete = resourceDefaultConfigurationDelete

	return drc
}

func resourceDefaultConfigurationCreate(d *schema.ResourceData, m interface{}) error {
        // double check it exists and return positive
	return resourceConfigurationUpdate(d, m)
}

func resourceDefaultConfigurationDelete(d *schema.ResourceData, m interface{}) error {
	log.Printf("[WARN] Cannot destroy Default Scope Configuration. Terraform will remove this resource from the state file, however resources may remain.")
	return nil
}
1 Like