Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Terraform testing framework: Dynamically set object properties from variables when testing modules #34534

Closed
SanderBlom opened this issue Jan 17, 2024 · 5 comments · Fixed by #34699
Labels
enhancement new new issue not yet triaged terraform test

Comments

@SanderBlom
Copy link

SanderBlom commented Jan 17, 2024

Terraform Version

Terraform v1.6.6
on linux_amd64

Use Cases

Background
My team and I are working on a Terraform module (https://github.com/blinqas/station) that facilitates the setup of projects and workspaces in Terraform Cloud, in conjunction with managing associated GitHub repositories (holds TF code for each workspace) and Azure resources associated with each workload.

Example use case:
If someone needs to deploy a new application in azure, the platform team can just call the module and it will create:

  • Everything we need in TFC
  • Github repo and link this to the workspace
  • Resource group in Azure
  • Managed identity with owner at the resource group level and potential other permissions provided by the platform team.

The application team can then create add their own Terraform resources in the new repository to deploy the infrastructure needed for their application. This makes it much faster to deploy new resources and the platform team has full control over the permissions given to each project.

Current issue
One of the core components of our module is the tfe block that creates a new workspace for each new workload. This is where we define essential attributes like organization, project name, workspace name, TF/ENV variables, etc. In the current implementation, these details are provided as variables to the module.

Example

module "web_app_1" {
  source              = "git::https://github.com/blinqas/station.git?ref=trunk"
  environment_name    = "prod"
  resource_group_name = "web-app-1"
  tags                = local.tags.common

  tfe = {
    organization_name     = "myTFCorg"
    project_name          = "Station"
    workspace_name        = "web-application-1"
    workspace_description = "Web application for x managed by y"
    vcs_repo = {
      identifier     = github_repository.repos["web-application-1-infrastructure"].full_name
      branch         = "trunk"
      oauth_token_id = var.vcs_repo_oauth_token_id
    }
  }
}

To increase flexibility and avoid hard-coding sensitive information (like the TFC org name) when writing the tests, I am exploring the possibility of dynamically setting the organization_name within the tfe block, ideally through an environment variable. However, I've encountered a limitation: it seems that the Terraform testing framework does not currently allow for referencing a variable within the variable block. This limitation is a significant hurdle, as it restricts our ability to write flexible and reusable tests.

A solution to this is to set the whole tfe block as an environment variable but this is kinda pointless as the test will be much less readable and we would have to wrap the testing in a bash/python script...

Current test for the TFE block

provider "tfe" {}
provider "azurerm" {
  features {}
}
provider "azuread" {

}

variables {
  tfe = {
    project_name                         = "tests_tfe"
    organization_name                    = "blinq-lab-tfc-org"
    workspace_name                       = "tfe_test"
    workspace_description                = "Workspace description"
    create_federated_identity_credential = true # Configures Federated Credentials on the workload identity for plan and apply phases.

    module_outputs_to_workspace_var = {
      applications             = true
      groups                   = true
      role_definitions         = true
      user_assigned_identities = true
      resource_groups          = true
    }

    workspace_env_vars = {
      tfe_test_env_var_1 = {
        value       = "test_env_var"
        category    = "env"
        description = "Test non sensitive env var"
      }

      tfe_test_env_var_2 = {
        value       = "test_env_var"
        category    = "env"
        description = "Test sensitive env var"
        sensitive   = true
      }
    }

    workspace_vars = {
      tfe_test_var_1 = {
        value       = "test"
        category    = "terraform"
        description = "Test workspace var from station tests"
        hcl         = false
        sensitive   = false
      },
      tfe_test_var_2 = {
        value       = "tfe_test_var_2_test_value"
        category    = "terraform"
        description = "Test workspace var from station tests. This should be sensitive"
        hcl         = false
        sensitive   = true
      },
      tfe_test_var_3 = {
        value       = "{\"key\": \"value\", \"another_key\": \"another_value\"}"
        category    = "terraform"
        description = "Test workspace var from station tests. Testing hcl format"
        hcl         = true
        sensitive   = false
      }
    }
  }
}

run "setup_create_tfc_test_project" {
  variables {
    tfc_project_name = "tests_tfe"
  }
  module {
    source = "./tests/setup-tfe-project"
  }
}


run "tfe_create_workspace" {

  module {
    source = "./"
  }

  assert {
    condition     = module.station-tfe.workspace.name == "tfe_test"
    error_message = "The workspace name does NOT match the input"
  }

  assert {
    condition     = module.station-tfe.workspace.description == "Workspace description"
    error_message = "The workspace description does NOT match the input"
  }
}

run "tfe_workspace_varaibles" {
  module {
    source = "./"
  }

  # Assertions for tfe_test_env_var_1
  assert {
    condition     = module.station-tfe.workspace_variables.tfe_test_env_var_1.value == "test_env_var"
    error_message = "The workspace_env_vars.tfe_test_env_var_1 had not the expected value"
  }
  assert {
    condition     = module.station-tfe.workspace_variables.tfe_test_env_var_1.sensitive == false
    error_message = "The workspace_env_vars.tfe_test_env_var_1 was set as sensitive"
  }
  assert {
    condition     = module.station-tfe.workspace_variables.tfe_test_env_var_1.category == "env"
    error_message = "The workspace_env_vars.tfe_test_env_var_1 was NOT set as type env"
  }

  # Assertions for tfe_test_env_var_2
  assert {
    condition     = module.station-tfe.workspace_variables.tfe_test_env_var_2.value == ""
    error_message = "We could read the variable and this should not work when it's marked as sensitive"
  }
  assert {
    condition     = module.station-tfe.workspace_variables.tfe_test_env_var_2.sensitive == true
    error_message = "The workspace_env_vars.tfe_test_env_var_2 was not set as sensitive"
  }
  assert {
    condition     = module.station-tfe.workspace_variables.tfe_test_env_var_2.category == "env"
    error_message = "The workspace_env_vars.tfe_test_env_var_2 was not of type env"
  }

  # Assertions for tfe_test_var_1
  assert {
    condition     = module.station-tfe.workspace_variables.tfe_test_var_1.value == "test"
    error_message = "The workspace_vars.tfe_test_var_1 had not the expected value"
  }
  assert {
    condition     = module.station-tfe.workspace_variables.tfe_test_var_1.sensitive == false
    error_message = "The workspace_vars.tfe_test_var_1 was set as sensitive"
  }

  # Assertions for tfe_test_var_2
  assert {
    condition     = module.station-tfe.workspace_variables.tfe_test_var_2.value == ""
    error_message = "The workspace_vars.tfe_test_var_2 had not the expected value when sensitive"
  }
  assert {
    condition     = module.station-tfe.workspace_variables.tfe_test_var_2.sensitive == true
    error_message = "The workspace_vars.tfe_test_var_2 was not set as sensitive"
  }
  assert {
    condition     = module.station-tfe.workspace_variables.tfe_test_var_2.hcl == false
    error_message = "The workspace_vars.tfe_test_var_2 was set as hcl, but expected hcl == false"
  }

  # Assertions for tfe_test_var_3
  assert {
    condition     = module.station-tfe.workspace_variables.tfe_test_var_3.value == "{\"key\": \"value\", \"another_key\": \"another_value\"}"
    error_message = "The workspace_vars.tfe_test_var_3 had not the expected value"
  }
  assert {
    condition     = module.station-tfe.workspace_variables.tfe_test_var_3.sensitive == false
    error_message = "The workspace_vars.tfe_test_var_3 was set as sensitive"
  }
  assert {
    condition     = module.station-tfe.workspace_variables.tfe_test_var_3.hcl == true
    error_message = "The workspace_vars.tfe_test_var_3 was NOT set as hcl"
  }
}

Refactoring the module would be our last resort because of how time-consuming it would be to update it for all of our clients.
Since the module also is open source and other people can contribute, it would be a hassle for others who want to perform tests against they're own TFC org. They would then have to update all the test files with their org name, test and revert the changes before making a PR.

I have already been in contact with Omar Ismail at Hasicorp regarding this issue and he suggested that we created an issue on this.

Attempted Solutions

It is not possible to reference a variable inside a variable. I have also tried to reference a local block where I pass in the value from a variable to the local block.

variables {
    tfe = {
      project_name                         = "tests_tfe"
      organization_name               = var.TFC_org_name
      workspace_name                  = "web_app_n"
      workspace_description         = "Web application n managed by x"
  }
}

Proposal

It would be very helpful for us and other who have structured their module to use blocks to be able to update parts of the block using variables like so:

provider "tfe" {}
provider "azurerm" {
  features {}
}
provider "azuread" {

}


variables {
    tfe = {
      project_name                    = "tests_tfe"
      organization_name               = var.TFC_org_name #This is currently not possible
      workspace_name                  = "web_app_n"
      workspace_description           = "Web application n managed by x"
  }
}

run "setup_create_tfc_test_project" {
  variables {
    tfc_project_name = "tests_tfe"
  }
  module {
    source = "./tests/setup-tfe-project"
  }
}


run "tfe_create_workspace" {

  module {
    source = "./"
  }

  assert {
    condition     = module.station-tfe.workspace.name == "web_app_n"
    error_message = "The workspace name does NOT match the input"
  }
}

References

No response

@crw
Copy link
Contributor

crw commented Jan 17, 2024

Thanks for this feature request! If you are viewing this issue and would like to indicate your interest, please use the 👍 reaction on the issue description to upvote this issue. We also welcome additional use case descriptions. Thanks again!

@bew
Copy link

bew commented Jan 18, 2024

Where do you expect var.TFC_org_name to be set?
In a tfvars file? (cf #34538 that is on this topic)
In a (local) variable on top of the tftest file?

@SanderBlom
Copy link
Author

SanderBlom commented Jan 18, 2024

Where do you expect var.TFC_org_name to be set? In a tfvars file? (cf #34538 that is on this topic) In a (local) variable on top of the tftest file?

Where we define the variables is not that important for me as long as this can be possible somehow. One solution can be to create a variable file like variables.tftest.hcl? And the we can set the value in either in the file (default value), .tfvar file or as an environment variable using TF_VAR_* or TF_TEST_VAR_* if they want to differentiate somehow?

@DanielMSchmidt
Copy link
Contributor

While looking into this I found a workaround that I though might be worth sharing:

  1. You can use the variables block in each test to overwrite existing variables
  2. Auto-loaded var files as well as CLI arguments go into the same scope as the global variables, therefore one can not reference them in the global section.
  3. You can reference both within the variables section in each run, where you can do
run "tfe_create_workspace" {
  variables {
    tfe = merge(var.tfe, { organization_name : var.org_name })
  }
  module {
    source = "./"
  }
  #...
}

It's probably worth fixing this anyways by making an extra scope / precedence level for externally configured vars, but this workaround works today on Terraform 1.6.0 and above.

Copy link
Contributor

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues.
If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 25, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement new new issue not yet triaged terraform test
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants