Repository: hashicorp/go-tfe
Branch: main
Commit: 5fb6f0f94644
Files: 358
Total size: 2.5 MB
Directory structure:
gitextract_9p7rzvxe/
├── .copywrite.hcl
├── .github/
│ ├── CODEOWNERS
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ ├── config.yml
│ │ └── feature_request.md
│ ├── actions/
│ │ ├── lint-go-tfe/
│ │ │ └── action.yml
│ │ └── test-go-tfe/
│ │ └── action.yml
│ ├── dependabot.yml
│ ├── pull_request_template.md
│ └── workflows/
│ ├── changelog.yml
│ ├── ci.yml
│ ├── codeql-analysis.yml
│ ├── create-jira-issue.yml
│ ├── jira-pr-transition.yml
│ ├── merged-pr.yml
│ └── nightly-tfe-ci.yml
├── .gitignore
├── .golangci.yml
├── CHANGELOG.md
├── LICENSE
├── META.d/
│ ├── _summary.yaml
│ └── data.yaml
├── Makefile
├── README.md
├── admin_opa_version.go
├── admin_opa_version_integration_test.go
├── admin_organization.go
├── admin_organization_integration_test.go
├── admin_run.go
├── admin_run_integration_test.go
├── admin_run_test.go
├── admin_sentinel_version.go
├── admin_sentinel_version_integration_test.go
├── admin_setting.go
├── admin_setting_cost_estimation.go
├── admin_setting_cost_estimation_integration_test.go
├── admin_setting_customization.go
├── admin_setting_customization_integration_test.go
├── admin_setting_general.go
├── admin_setting_general_integration_test.go
├── admin_setting_oidc.go
├── admin_setting_oidc_integration_test.go
├── admin_setting_saml.go
├── admin_setting_saml_integration_test.go
├── admin_setting_scim.go
├── admin_setting_scim_groups.go
├── admin_setting_scim_groups_integration_test.go
├── admin_setting_scim_integration_test.go
├── admin_setting_scim_token.go
├── admin_setting_scim_token_integration_test.go
├── admin_setting_smtp.go
├── admin_setting_smtp_integration_test.go
├── admin_setting_twilio.go
├── admin_setting_twilio_integration_test.go
├── admin_terraform_version.go
├── admin_terraform_version_integration_test.go
├── admin_user.go
├── admin_user_integration_test.go
├── admin_workspace.go
├── admin_workspace_integration_test.go
├── agent.go
├── agent_integration_test.go
├── agent_pool.go
├── agent_pool_integration_test.go
├── agent_token.go
├── agent_token_integration_test.go
├── apply.go
├── apply_integration_test.go
├── audit_trail.go
├── audit_trail_integration_test.go
├── aws_oidc_configuration.go
├── aws_oidc_configuration_integration_test.go
├── azure_oidc_configuration.go
├── azure_oidc_configuration_integration_test.go
├── comment.go
├── comment_integration_test.go
├── configuration_version.go
├── configuration_version_integration_test.go
├── const.go
├── cost_estimate.go
├── cost_estimate_integration_test.go
├── data_retention_policy.go
├── docs/
│ ├── CONTRIBUTING.md
│ ├── RELEASES.md
│ └── TESTS.md
├── entitlement_helper_test.go
├── errors.go
├── example_test.go
├── examples/
│ ├── backing_data/
│ │ └── main.go
│ ├── configuration_versions/
│ │ └── main.go
│ ├── organizations/
│ │ └── main.go
│ ├── projects/
│ │ └── main.go
│ ├── registry_modules/
│ │ └── main.go
│ ├── run_errors/
│ │ ├── README.md
│ │ ├── main.go
│ │ └── terraform/
│ │ └── main.tf
│ ├── state_versions/
│ │ ├── main.go
│ │ └── state.json
│ ├── users/
│ │ └── main.go
│ └── workspaces/
│ └── main.go
├── gcp_oidc_configuration.go
├── gcp_oidc_configuration_integration_test.go
├── generate_mocks.sh
├── github_app_installation.go
├── github_app_installation_integration_test.go
├── go.mod
├── go.sum
├── gpg_key.go
├── gpg_key_integration_test.go
├── helper_test.go
├── hyok_configuration.go
├── hyok_configuration_integration_test.go
├── hyok_customer_key_version.go
├── hyok_customer_key_version_integration_test.go
├── hyok_encrypted_data_key.go
├── hyok_encrypted_data_key_integration_test.go
├── internal_run_task.go
├── internal_workspace_run_task.go
├── ip_ranges.go
├── ip_ranges_integration_test.go
├── logreader.go
├── logreader_integration_test.go
├── mocks/
│ ├── admin_opa_version_mocks.go
│ ├── admin_organization_mocks.go
│ ├── admin_run_mocks.go
│ ├── admin_sentinel_version_mocks.go
│ ├── admin_setting_cost_estimation_mocks.go
│ ├── admin_setting_customization_mocks.go
│ ├── admin_setting_general_mocks.go
│ ├── admin_setting_mocks.go
│ ├── admin_setting_oidc_mocks.go
│ ├── admin_setting_saml_mocks.go
│ ├── admin_setting_scim_groups_mocks.go
│ ├── admin_setting_scim_mocks.go
│ ├── admin_setting_scim_token_mocks.go
│ ├── admin_setting_smtp_mocks.go
│ ├── admin_setting_twilio_mocks.go
│ ├── admin_terraform_version_mocks.go
│ ├── admin_user_mocks.go
│ ├── admin_workspace_mocks.go
│ ├── agent_pool_mocks.go
│ ├── agent_token_mocks.go
│ ├── agents.go
│ ├── apply_mocks.go
│ ├── audit_trail_mocks.go
│ ├── comment_mocks.go
│ ├── configuration_version_mocks.go
│ ├── cost_estimate_mocks.go
│ ├── github_app_installation_mocks.go
│ ├── gpg_key_mocks.go
│ ├── ip_ranges_mocks.go
│ ├── logreader_mocks.go
│ ├── notification_configuration_mocks.go
│ ├── oauth_client_mocks.go
│ ├── oauth_token_mocks.go
│ ├── organization_membership_mocks.go
│ ├── organization_mocks.go
│ ├── organization_token_mocks.go
│ ├── plan_export_mocks.go
│ ├── plan_mocks.go
│ ├── policy_check_mocks.go
│ ├── policy_evaluation.go
│ ├── policy_mocks.go
│ ├── policy_set_mocks.go
│ ├── policy_set_parameter_mocks.go
│ ├── policy_set_version_mocks.go
│ ├── project_mocks.go
│ ├── query_runs_mocks.go
│ ├── registry_module_mocks.go
│ ├── registry_no_code_module_mocks.go
│ ├── registry_provider_mocks.go
│ ├── registry_provider_platform_mocks.go
│ ├── registry_provider_version_mocks.go
│ ├── run_events_mocks.go
│ ├── run_mocks.go
│ ├── run_tasks_mocks.go
│ ├── run_trigger_mocks.go
│ ├── ssh_key_mocks.go
│ ├── state_version_mocks.go
│ ├── state_version_output_mocks.go
│ ├── tag_mocks.go
│ ├── task_result_mocks.go
│ ├── task_stages_mocks.go
│ ├── team_access_mocks.go
│ ├── team_member_mocks.go
│ ├── team_mocks.go
│ ├── team_project_access_mocks.go
│ ├── team_token_mocks.go
│ ├── test_run_mocks.go
│ ├── test_variables_mocks.go
│ ├── user_mocks.go
│ ├── user_token_mocks.go
│ ├── variable_mocks.go
│ ├── variable_set_mocks.go
│ ├── variable_set_variable_mocks.go
│ ├── workspace_mocks.go
│ ├── workspace_resources.go
│ └── workspace_run_tasks_mocks.go
├── notification_configuration.go
├── notification_configuration_integration_test.go
├── oauth_client.go
├── oauth_client_integration_test.go
├── oauth_token.go
├── oauth_token_integration_test.go
├── organization.go
├── organization_audit_configuration.go
├── organization_audit_configuration_integration_test.go
├── organization_integration_test.go
├── organization_membership.go
├── organization_membership_integration_test.go
├── organization_tags.go
├── organization_tags_integration_test.go
├── organization_token.go
├── organization_token_integration_test.go
├── plan.go
├── plan_export.go
├── plan_export_integration_test.go
├── plan_integration_test.go
├── policy.go
├── policy_check.go
├── policy_check_integration_test.go
├── policy_evaluation.go
├── policy_evaluation_beta_test.go
├── policy_integration_test.go
├── policy_set.go
├── policy_set_integration_test.go
├── policy_set_parameter.go
├── policy_set_parameter_integration_test.go
├── policy_set_version.go
├── policy_set_version_integration_test.go
├── project.go
├── projects_integration_test.go
├── query_runs.go
├── query_runs_integration_test.go
├── registry_module.go
├── registry_module_integration_test.go
├── registry_module_test.go
├── registry_no_code_module.go
├── registry_no_code_module_integration_test.go
├── registry_provider.go
├── registry_provider_integration_test.go
├── registry_provider_platform.go
├── registry_provider_platform_integration_test.go
├── registry_provider_version.go
├── registry_provider_version_integration_test.go
├── request.go
├── request_hooks.go
├── request_hooks_test.go
├── request_test.go
├── reserved_tag_key.go
├── reserved_tag_key_integration_test.go
├── run.go
├── run_event.go
├── run_event_integration_test.go
├── run_integration_test.go
├── run_task.go
├── run_task_integration_test.go
├── run_task_request.go
├── run_tasks_integration.go
├── run_tasks_integration_test.go
├── run_trigger.go
├── run_trigger_integration_test.go
├── scripts/
│ ├── generate_resource/
│ │ ├── go.mod
│ │ ├── go.sum
│ │ ├── main.go
│ │ └── templates.go
│ ├── gofmtcheck.sh
│ ├── hyok-testing.sh
│ ├── rebase-fork.sh
│ └── setup-test-envvars.sh
├── ssh_key.go
├── ssh_key_integration_test.go
├── stack.go
├── stack_configuration.go
├── stack_configuration_integration_test.go
├── stack_configuration_summary.go
├── stack_configuration_summary_integration_test.go
├── stack_deployment.go
├── stack_deployment_groups.go
├── stack_deployment_groups_integration_test.go
├── stack_deployment_groups_summary.go
├── stack_deployment_groups_summary_integration_test.go
├── stack_deployment_integration_test.go
├── stack_deployment_runs.go
├── stack_deployment_runs_integration_test.go
├── stack_deployment_steps.go
├── stack_deployment_steps_integration_test.go
├── stack_diagnostic.go
├── stack_diagnostic_integration_test.go
├── stack_integration_test.go
├── stack_state.go
├── stack_state_integration_test.go
├── state_version.go
├── state_version_integration_test.go
├── state_version_output.go
├── state_version_output_integration_test.go
├── subscription_updater_test.go
├── tag.go
├── task_result.go
├── task_stages.go
├── task_stages_integration_beta_test.go
├── task_stages_integration_test.go
├── team.go
├── team_access.go
├── team_access_integration_test.go
├── team_integration_test.go
├── team_member.go
├── team_member_integration_test.go
├── team_project_access.go
├── team_project_access_integration_test.go
├── team_token.go
├── team_token_integration_test.go
├── test-fixtures/
│ ├── archive-dir/
│ │ ├── bar.txt
│ │ ├── exe
│ │ ├── foo.txt
│ │ └── sub/
│ │ └── zip.txt
│ ├── config-version/
│ │ └── main.tf
│ ├── config-version-with-test/
│ │ ├── main.tf
│ │ └── main.tftest.hcl
│ ├── json-state/
│ │ └── state.json
│ ├── json-state-outputs/
│ │ └── everything.json
│ ├── policy-set-version/
│ │ ├── enforce-mandatory-tags.sentinel
│ │ └── sentinel.hcl
│ ├── stack-source/
│ │ ├── .terraform-version
│ │ ├── components.tfstack.hcl
│ │ ├── deployments.tfdeploy.hcl
│ │ ├── nulls/
│ │ │ └── main.tf
│ │ └── pet/
│ │ └── main.tf
│ └── state-version/
│ └── terraform.tfstate
├── test_config.go
├── test_run.go
├── test_run_integration_test.go
├── test_variables.go
├── test_variables_integration_test.go
├── tfe.go
├── tfe_integration_test.go
├── tfe_test.go
├── type_helpers.go
├── user.go
├── user_integration_test.go
├── user_token.go
├── user_token_integration_test.go
├── validations.go
├── validations_test.go
├── variable.go
├── variable_integration_test.go
├── variable_set.go
├── variable_set_test.go
├── variable_set_variable.go
├── variable_set_variable_test.go
├── vault_oidc_configuration.go
├── vault_oidc_configuration_integration_test.go
├── workspace.go
├── workspace_integration_test.go
├── workspace_resources.go
├── workspace_resources_integration_test.go
├── workspace_run_task.go
└── workspace_run_task_integration_test.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .copywrite.hcl
================================================
schema_version = 1
project {
license = "MPL-2.0"
copyright_year = 2018
# (OPTIONAL) A list of globs that should not have copyright/license headers.
# Supports doublestar glob patterns for more flexibility in defining which
# files or folders should be ignored
header_ignore = [
# "vendors/**",
# "**autogen**",
]
}
================================================
FILE: .github/CODEOWNERS
================================================
* @hashicorp/tf-core-cloud
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Let us know about an unexpected error, a crash, or an incorrect behavior.
labels: bug
---
#### go-tfe version
```plaintext
...
```
## Description
## Testing plan
```plaintext
...
```
#### Expected Behavior
#### Actual Behavior
#### Additional Context
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
blank_issues_enabled: false
contact_links:
- name: HCP Terraform and Terraform Enterprise Troubleshooting and Feature Requests
url: https://support.hashicorp.com/hc/en-us/requests/new
about: For issues and feature requests concerning the HCP Terraform and Terraform Enterprise platform itself, please submit a HashiCorp support request or email tf-cloud@hashicorp.support
- name: Terraform Language or Workflow Questions
url: https://discuss.hashicorp.com
about: Please ask Terraform language or workflow related questions through the HashiCorp Discuss forum
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest a new feature or other enhancement.
labels: feature-request
---
#### Use-cases
#### Attempted Solutions
#### Proposal
================================================
FILE: .github/actions/lint-go-tfe/action.yml
================================================
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
name: Lint
description: Lints go-tfe
runs:
using: composite
steps:
- name: Set up Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version-file: go.mod
cache: true
- run: make fmtcheck
shell: bash
- name: Install golangci-lint
run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/6008b81b81c690c046ffc3fd5bce896da715d5fd/install.sh | sh -s -- -b $(go env GOPATH)/bin $GOLANGCILINT_VERSION
shell: bash
env:
GOLANGCILINT_VERSION: v2.11.3
- run: make lint
shell: bash
- name: Ensure generate_mocks.sh ends in a newline
run: test "" = "$(tail -c1 "generate_mocks.sh")"
shell: bash
- name: Install mockgen
shell: bash
run: go install go.uber.org/mock/mockgen@v0.4.0
- name: Get dependencies
shell: bash
run: go mod download
- name: Generate mocks
shell: bash
run: ./generate_mocks.sh
- name: verify go.mod and go.sum are consistent
shell: bash
run : go mod tidy
- name: Ensure mocks are generated
shell: bash
run: git diff --exit-code
================================================
FILE: .github/actions/test-go-tfe/action.yml
================================================
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
name: Test
description: Tests go-tfe within a matrix
inputs:
matrix_index:
description: Index of the matrix strategy runner
required: true
matrix_total:
description: Total number of matrix strategy runners
required: true
address:
description: Address of the HCP Terraform instance to test against
required: true
admin_configuration_token:
description: HCP Terraform Admin API Configuration role token
required: true
admin_provision_licenses_token:
description: HCP Terraform Admin API Provision Licenses role token
required: true
admin_security_maintenance_token:
description: HCP Terraform Admin API Security Maintenance role token
required: true
admin_site_admin_token:
description: HCP Terraform Admin API Site Admin role token
required: true
admin_subscription_token:
description: HCP Terraform Admin API Subscription role token
required: true
admin_support_token:
description: HCP Terraform Admin API Support role token
required: true
admin_version_maintenance_token:
description: HCP Terraform Admin API Version Maintenance role token
required: true
token:
description: HCP Terraform token
required: true
oauth-client-github-token:
description: The GitHub token used for testing OAuth scenarios for VCS workspaces
required: false
enterprise:
description: Test enterprise features (`address` must be running in ON_PREM mode)
required: false
datadog-workflow-token:
description: Datadog API key for test optimization
required: false
skip-statement:
description: Skip tests with this statement substring in their name
required: false
runs:
using: composite
steps:
- name: Set up Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version-file: go.mod
cache: true
cache-dependency-path: go.sum
- name: Sync dependencies
shell: bash
run: |
go mod download
go mod tidy
- name: Install gotestsum
shell: bash
run: go install gotest.tools/gotestsum@c4a0df2e75a225d979a444342dd3db752b53619f # v1.13.0
- name: Download artifact
id: download-artifact
uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe # v3.1.4
with:
workflow_conclusion: success
name: junit-test-summary
if_no_artifact_found: warn
branch: main
- name: Split integration tests
id: test_split
uses: hashicorp-forge/go-test-split-action@796beedbdb3d1bea14cad2d3057bab5c5cf15fe5 # v1.0.2
with:
index: ${{ inputs.matrix_index }}
total: ${{ inputs.matrix_total }}
junit-summary: ./ci-summary.xml
- name: Configure Datadog Test Optimization
uses: datadog/test-visibility-github-action@v2
with:
languages: go
api_key: ${{ inputs.datadog-workflow-token }}
site: datadoghq.com
- name: Run integration tests
shell: bash
env:
TFE_ADDRESS: ${{ inputs.address }}
TFE_TOKEN: ${{ inputs.token }}
TFE_ADMIN_CONFIGURATION_TOKEN: ${{ inputs.admin_configuration_token }}
TFE_ADMIN_PROVISION_LICENSES_TOKEN: ${{ inputs.admin_provision_licenses_token }}
TFE_ADMIN_SECURITY_MAINTENANCE_TOKEN: ${{ inputs.admin_security_maintenance_token }}
TFE_ADMIN_SITE_ADMIN_TOKEN: ${{ inputs.admin_site_admin_token }}
TFE_ADMIN_SUBSCRIPTION_TOKEN: ${{ inputs.admin_subscription_token }}
TFE_ADMIN_SUPPORT_TOKEN: ${{ inputs.admin_support_token }}
TFE_ADMIN_VERSION_MAINTENANCE_TOKEN: ${{ inputs.admin_version_maintenance_token }}
TFC_RUN_TASK_URL: "http://testing-mocks.tfe:22180/runtasks/pass"
GITHUB_POLICY_SET_IDENTIFIER: "svc-team-tf-core-cloud/test-policy-set"
GITHUB_REGISTRY_MODULE_IDENTIFIER: "svc-team-tf-core-cloud/terraform-random-module"
GITHUB_REGISTRY_NO_CODE_MODULE_IDENTIFIER: "hashicorp/terraform-random-no-code-module"
OAUTH_CLIENT_GITHUB_TOKEN: "${{ inputs.oauth-client-github-token }}"
SKIP_HYOK_INTEGRATION_TESTS: "${{ inputs.skip-hyok-integration-tests }}"
HYOK_ORGANIZATION_NAME: "${{ inputs.hyok-organization-name }}"
HYOK_WORKSPACE_NAME: "${{ inputs.hyok-workspace-name }}"
HYOK_POOL_ID: "${{ inputs.hyok-pool-id }}"
HYOK_PLAN_ID: "${{ inputs.hyok-plan-id }}"
HYOK_STATE_VERSION_ID: "${{ inputs.hyok-state-version-id }}"
HYOK_CUSTOMER_KEY_VERSION_ID: "${{ inputs.hyok-customer-key-version-id }}"
HYOK_ENCRYPTED_DATA_KEY_ID: "${{ inputs.hyok-encrypted-data-key-id }}"
GO111MODULE: "on"
ENABLE_TFE: ${{ inputs.enterprise }}
run: |
gotestsum --junitfile summary.xml --format short-verbose --rerun-fails=3 --packages=./ -- -parallel=2 -timeout=59m -coverprofile cover.out -run "${{ steps.test_split.outputs.run }}" ${{ inputs.skip-statement }}
- name: Upload test artifacts
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: junit-test-summary-${{ matrix.index }}
path: |
summary.xml
cover.out
retention-days: 1
================================================
FILE: .github/dependabot.yml
================================================
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
version: 2
updates:
- package-ecosystem: gomod
directory: "/"
schedule:
interval: daily
================================================
FILE: .github/pull_request_template.md
================================================
## Description
## Testing plan
## External links
## Output from tests
Including output from tests may require access to a TFE instance. Ignore this section if you have no environment to test against.
```
$ TFE_ADDRESS="https://example" TFE_TOKEN="example" go test ./... -v -run TestFunctionsAffectedByChange
...
```
## Rollback Plan
## Changes to Security Controls
================================================
FILE: .github/workflows/changelog.yml
================================================
# This workflow makes sure contributors don't forget to add a changelog entry or explicitly opt-out of it.
name: Changelog
on:
pull_request_target:
types:
- opened
- ready_for_review
- reopened
- synchronize
- labeled
- unlabeled
# This workflow runs for not-yet-reviewed external contributions and so it
# intentionally has no write access and only limited read access to the
# repository.
permissions:
contents: read
pull-requests: write
jobs:
check-changelog-entry:
name: "Check Changelog Entry"
runs-on: ubuntu-latest
concurrency:
group: changelog-${{ github.head_ref }}
cancel-in-progress: true
steps:
- name: "Changed files"
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: changelog
with:
filters: |
changelog:
- 'CHANGELOG.md'
- name: "Check for changelog entry"
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
async function createOrUpdateChangelogComment(commentDetails, deleteComment) {
const commentStart = "## Changelog Warning"
const body = commentStart + "\n\n" + commentDetails;
const { number: issue_number } = context.issue;
const { owner, repo } = context.repo;
// List all comments
const allComments = (await github.rest.issues.listComments({
issue_number,
owner,
repo,
})).data;
const existingComment = allComments.find(c => c.body.startsWith(commentStart));
const comment_id = existingComment?.id;
if (deleteComment) {
if (existingComment) {
await github.rest.issues.deleteComment({
owner,
repo,
comment_id,
});
}
return;
}
core.setFailed(commentDetails);
if (existingComment) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id,
body,
});
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body,
});
}
}
const changelogChangesPresent = ${{steps.changelog.outputs.changelog}};
const prLabels = await github.rest.issues.listLabelsOnIssue({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo
});
const noChangelogNeededLabel = prLabels.data.find(label => label.name === 'no-changelog-needed');
if (noChangelogNeededLabel) {
if (changelogChangesPresent) {
await createOrUpdateChangelogComment("Please remove either the 'no-changelog-needed' label or the changelog entry from this PR.");
return;
}
await createOrUpdateChangelogComment("", true);
return;
}
if (!changelogChangesPresent) {
await createOrUpdateChangelogComment("Please add a changelog entry to `CHANGELOG.md` for this change. If you believe this change does not need a changelog entry, please add the 'no-changelog-needed' label.");
return;
}
// Nothing to complain about, so delete any existing comment
await createOrUpdateChangelogComment("", true);
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches: [main]
pull_request:
concurrency:
group: ${{ github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: ./.github/actions/lint-go-tfe
tests:
name: Test
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# If you adjust these parameters, also adjust the jrm input files on the "Merge reports" step below
total: [8]
index: [0, 1, 2, 3, 4, 5, 6, 7]
steps:
- name: terraform-cloud-outputs
id: tflocal
uses: hashicorp-forge/terraform-cloud-action/outputs@5583d5f554d268ac91b3c37fd0a5e9da2c78c017 # v1.1.0
with:
token: ${{ secrets.TF_WORKFLOW_TFLOCAL_CLOUD_TFC_TOKEN }}
organization: hashicorp-v2
workspace: tflocal-go-tfe
- name: Checkout code
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: ./.github/actions/test-go-tfe
with:
matrix_index: ${{ matrix.index }}
matrix_total: ${{ matrix.total }}
address: ${{ fromJSON(steps.tflocal.outputs.workspace-outputs-json).tfe_address }}
token: ${{ fromJSON(steps.tflocal.outputs.workspace-outputs-json).tfe_token }}
admin_configuration_token: ${{ fromJSON(steps.tflocal.outputs.workspace-outputs-json).tfe_admin_token_by_role.configuration }}
admin_provision_licenses_token: ${{ fromJSON(steps.tflocal.outputs.workspace-outputs-json).tfe_admin_token_by_role.provision-licenses }}
admin_security_maintenance_token: ${{ fromJSON(steps.tflocal.outputs.workspace-outputs-json).tfe_admin_token_by_role.security-maintenance }}
admin_site_admin_token: ${{ fromJSON(steps.tflocal.outputs.workspace-outputs-json).tfe_admin_token_by_role.site-admin }}
admin_subscription_token: ${{ fromJSON(steps.tflocal.outputs.workspace-outputs-json).tfe_admin_token_by_role.subscription }}
admin_support_token: ${{ fromJSON(steps.tflocal.outputs.workspace-outputs-json).tfe_admin_token_by_role.support }}
admin_version_maintenance_token: ${{ fromJSON(steps.tflocal.outputs.workspace-outputs-json).tfe_admin_token_by_role.version-maintenance }}
oauth-client-github-token: ${{ secrets.OAUTH_CLIENT_GITHUB_TOKEN }}
datadog-workflow-token: ${{ secrets.TF_WORKFLOW_DATADOG_API_KEY }}
tests-combine-summaries:
name: Combine Test Reports
needs: [tests]
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: 20
- name: Set up Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: "^1.22"
cache: true
- name: Download artifacts
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.18.0
- name: Install junit-report-merger
run: npm install -g junit-report-merger
- name: Install gocovmerge
run: go install github.com/wadey/gocovmerge@latest
# Note -- we're intentionally including this in the same job as the running of the tests themselves. This is to
# future proof for when Datadog supports tracing of Go tests rather than just uploading coverage results.
# Ref: https://docs.datadoghq.com/continuous_integration/setup_tests/
- name: prepare datadog-ci
run: |
curl -L --fail "https://github.com/DataDog/datadog-ci/releases/latest/download/datadog-ci_linux-x64" --output "/usr/local/bin/datadog-ci"
chmod +x /usr/local/bin/datadog-ci
- name: Merge coverage reports
run: |
gocovmerge junit-test-summary-{0..7}/cover.out > merged-coverage.out
- name: Merge junit reports
run: jrm ./ci-summary.xml "junit-test-summary-{0..7}/*.xml"
- name: Upload test artifacts
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: junit-test-summary
path: ./ci-summary.xml
- name: upload coverage
env:
DATADOG_API_KEY: "${{ secrets.TF_WORKFLOW_DATADOG_API_KEY }}"
DD_ENV: ci
run: |
coverage=$(go tool cover -func merged-coverage.out | tail -n 1 | awk '{print $3}' | tr -d -c 0-9.)
datadog-ci junit upload --service "$GITHUB_REPOSITORY" --report-measures=test.code_coverage.lines_pct:$coverage ./ci-summary.xml
tests-summarize:
name: Summarize Tests
needs: [tests]
runs-on: ubuntu-latest
if: ${{ always() }}
steps:
- name: Check tests Status
run: |
if [ "${{ needs.tests.result }}" = "success" ]; then
exit 0
fi
exit 1
================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
schedule:
- cron: '15 2 * * 2'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@9a866ed4524fc3422c3af1e446dab8efa3503411 # codeql-bundle-20230418
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@9a866ed4524fc3422c3af1e446dab8efa3503411 # codeql-bundle-20230418
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@9a866ed4524fc3422c3af1e446dab8efa3503411 # codeql-bundle-20230418
================================================
FILE: .github/workflows/create-jira-issue.yml
================================================
name: Jira Issue Sync
on:
issues:
types: [opened, closed, deleted, reopened]
issue_comment:
types: [created]
jobs:
call-workflow:
uses: hashicorp/terraform-provider-tfe/.github/workflows/jira-issue-sync.yml@main
with:
project: TF
issue-extra-fields: |
{ "customfield_10091": ["TF-Core-Cloud"],
"components": [{ "name": "Go-TFE" }],
"customfield_10008": "${{ contains(github.event.issue.labels.*.name, 'bug') && 'TF-9179' || 'TF-7490' }}"
}
secrets: inherit
================================================
FILE: .github/workflows/jira-pr-transition.yml
================================================
name: Jira PR Transition
on:
pull_request:
types: [opened, closed, reopened, converted_to_draft, ready_for_review]
jobs:
call-workflow:
uses: hashicorp/terraform-provider-tfe/.github/workflows/jira-pr-transition.yml@main
secrets: inherit
================================================
FILE: .github/workflows/merged-pr.yml
================================================
name: Merged Pull Request
permissions:
pull-requests: write
# only trigger on pull request closed events
on:
pull_request_target:
types: [ closed ]
jobs:
merge_job:
# this job will only run if the PR has been merged
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: "Reminder to the contributor that merged this PR: if your changes have added important functionality or fixed a relevant bug, open a follow-up PR to update CHANGELOG.md with a note on your changes."
})
================================================
FILE: .github/workflows/nightly-tfe-ci.yml
================================================
name: Nightly TFE Tests
on:
workflow_dispatch:
schedule:
# Monday-Friday at 7:30AM UTC (90 minutes after infrastructure rebuild)
- cron: '30 7 * * 1-5'
jobs:
instance:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: terraform-cloud/apply
uses: hashicorp-forge/terraform-cloud-action/apply@5583d5f554d268ac91b3c37fd0a5e9da2c78c017 # v1.1.0
with:
organization: hashicorp-v2
workspace: tflocal-go-tfe-nightly
token: ${{ secrets.TF_WORKFLOW_TFLOCAL_CLOUD_TFC_TOKEN }}
wait: true
tests:
needs: instance
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
total: [ 1 ]
index: [ 0 ]
steps:
- name: terraform-cloud/outputs
id: tflocal
uses: hashicorp-forge/terraform-cloud-action/outputs@5583d5f554d268ac91b3c37fd0a5e9da2c78c017 # v1.1.0
with:
token: ${{ secrets.TF_WORKFLOW_TFLOCAL_CLOUD_TFC_TOKEN }}
organization: hashicorp-v2
workspace: tflocal-go-tfe-nightly
- name: Checkout code
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: ./.github/actions/test-go-tfe
with:
matrix_index: ${{ matrix.index }}
matrix_total: ${{ matrix.total }}
address: ${{ fromJSON(steps.tflocal.outputs.workspace-outputs-json).tfe_address }}
token: ${{ fromJSON(steps.tflocal.outputs.workspace-outputs-json).tfe_token }}
oauth-client-github-token: ${{ secrets.OAUTH_CLIENT_GITHUB_TOKEN }}
enterprise: "1"
tests-summarize:
needs: [ tests ]
runs-on: ubuntu-latest
if: ${{ always() }}
steps:
- name: Check tests Status
run: |
if [ "${{ needs.tests.result }}" = "success" ]; then
exit 0
fi
exit 1
slack-notify:
needs: [ tests-summarize ]
if: always() && (needs.tests-summarize.result == 'failure')
runs-on: ubuntu-latest
steps:
- name: Send slack notification on failure
uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844 # v1.23.0
with:
payload: |
{
"text": ":x::moon::sob: Nightly TFE tests *FAILED* on ${{ github.repository }}",
"attachments": [
{
"color": "#C41E3A",
"blocks": [
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Workflow:*\n${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
]
}
]
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
cleanup:
runs-on: ubuntu-latest
needs: ["tests-summarize"]
if: "${{ always() }}"
steps:
- name: terraform-cloud/destroy
uses: hashicorp-forge/terraform-cloud-action/destroy@5583d5f554d268ac91b3c37fd0a5e9da2c78c017 # v1.1.0
with:
token: ${{ secrets.TF_WORKFLOW_TFLOCAL_CLOUD_TFC_TOKEN }}
organization: hashicorp-v2
workspace: tflocal-go-tfe-nightly
================================================
FILE: .gitignore
================================================
.DS_Store
# Commonplace IDE output that should never be committed
.out
.vscode/*.log
.vscode/settings.json
.idea/
.envrc
.rgignore
================================================
FILE: .golangci.yml
================================================
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
version: "2"
run:
timeout: 5m
linters:
# This set of linters are enabled by default: deadcode, errcheck, gosimple, govet, ineffasign, staticcheck, struccheck, typecheck, unused, varcheck
enable:
# List of all linters: https://golangci-lint.run/usage/linters/
- whitespace #https://github.com/ultraware/whitespace
# - noctx #https://github.com/sonatard/noctx
- nilerr #https://github.com/gostaticanalysis/nilerr
- nestif #https://github.com/nakabonne/nestif
- copyloopvar #https://github.com/karamaru-alpha/copyloopvar
- bodyclose #https://github.com/timakin/bodyclose
- goconst #https://github.com/jgautheron/goconst
- errcheck #https://github.com/kisielk/errcheck
- staticcheck # stylecheck/gosimple checks are part of staticcheck in v2
- revive #golint is deprecated and golangci-lint recommends to use revive instead https://github.com/mgechev/revive
#other deprecated lint libraries: maligned, scopelint, interfacer
- gocritic #https://github.com/go-critic/go-critic
- unparam #https://github.com/mvdan/unparam
- misspell #https://github.com/client9/misspell
exclusions:
rules:
- path: tfe_integration_test.go
linters:
- errcheck # Many calls in this test are known to return an error and not checked
- linters:
- staticcheck
text: "Ascii" # Permanently part of the public interface unless we break the API
- path: _test\.go
linters:
- errcheck
- goconst
- unused
- unparam
settings:
errcheck:
# https://github.com/kisielk/errcheck#excluding-functions
check-type-assertions: true
check-blank: true
goconst:
min-len: 20
min-occurrences: 5
ignore-calls: false
gocritic:
enabled-tags:
- diagnostic
- opinionated
- performance
disabled-checks:
- unnamedResult
- hugeParam
- singleCaseSwitch
- ifElseChain
revive:
severity: warning
rules:
- name: indent-error-flow #Prevents redundant else statements
severity: warning
- name: useless-break
severity: warning
================================================
FILE: CHANGELOG.md
================================================
# Unreleased
## Bug Fixes
* Improve API error handling to decode both JSON:API error objects and regular JSON errors arrays by @uk1288 [#1304](https://github.com/hashicorp/go-tfe/pull/1304)
## Enhancements
* Adds the `ProviderType` field to `AdminSAMLSetting` and `AdminSAMLSettingsUpdateOptions` to support the `provider-type` SAML setting.
* Adds `SCIMSettings` / `AdminSCIMSetting` to support managing site-level SCIM settings by @skj-skj [#1307](https://github.com/hashicorp/go-tfe/pull/1307)
* Adds BETA support for delegating policy overrides on teams by @jbonhag [#1301](https://github.com/hashicorp/go-tfe/pull/1301)
* Adds `AdminSCIMTokens` to support managing site-level SCIM tokens by @skj-skj [#1310](https://github.com/hashicorp/go-tfe/pull/1310)
* Add support for trigger patterns and working directories to stacks by @aaabdelgany [#1305](https://github.com/hashicorp/go-tfe/pull/1305)
* Adds `PolicyUpdatePatterns` to `PolicySet`, `PolicySetCreateOptions`, and `PolicySetUpdateOptions` to support `policy-update-patterns` by @nithishravindra [#1306](https://github.com/hashicorp/go-tfe/pull/1306/)
* Adds `AdminSCIMGroups` to support fetching SCIM groups provisioned in Terraform Enterprise via an IdP by @skj-skj [#1314](https://github.com/hashicorp/go-tfe/pull/1314)
# v1.103.0
## Enhancements
* Adds the `ProjectExclusions` field to the `PolicySet` struct to support project level exclusions of policy sets by @subhro-acharjee-1 [#1290](https://github.com/hashicorp/go-tfe/pull/1290)
# v1.102.0
## Enhancements
* Adds the `Size` field to `StateVersion` by @shaunakone [#1280](https://github.com/hashicorp/go-tfe/pull/1280)
* Upgrade go version from `1.24` to `1.25` by @uk1288 [#1297](https://github.com/hashicorp/go-tfe/pull/1297)
# v1.101.0
## Enhancements
* Adds the `SpeculativeEnabled` field to the `StackCreateOptions` and `StackUpdateOptions` structs by @arunatibm [1279](https://github.com/hashicorp/go-tfe/pull/1279)
* Adds `Name` and `Provider` fields to `RegistryModuleCreateWithVCSConnectionOptions` to support explicit module naming for monorepos with non-standard repository names, by @jillirami [#1277](https://github.com/hashicorp/go-tfe/pull/1277)
# v1.100.0
## Enhancements
* Adds `ReadWithOptions` method to `RunTriggers` to support including related resources when reading a run trigger by @Maed223 [#1275](https://github.com/hashicorp/go-tfe/pull/1275)
# v1.99.0
## Enhancements
* Adds `ProjectRemoteState` field to `Workspace` to support state sharing at the project level, by @hs26gill [#1248](https://github.com/hashicorp/go-tfe/pull/1248)
* Adds 'Migration' field to `StackCreateOptions` and `CreationSource` to stack struct to provide insights re: stack creation [#1266](https://github.com/hashicorp/go-tfe/pull/1266)
# v1.98.0
## Enhancements
* Adds `UserTokensEnabled` field to `Organization` to support enabling/disabling user tokens for an organization by @JarrettSpiker [#1225](https://github.com/hashicorp/go-tfe/pull/1225)
* Adds `DeploymentRunStatus` and `DeploymentStepStatus` types by @Maed223 [#1261](https://github.com/hashicorp/go-tfe/pull/1261)
## Bug Fixes
* Resolve differences between given and actual status composition in `StackConfigurationStatus` and `DeploymentGroupStatus` by @Maed223 [#1261](https://github.com/hashicorp/go-tfe/pull/1261)
# v1.97.0
## Enhancements
* Add variable set support for stacks with `ApplyToStacks`, `RemoveFromStacks`, and `UpdateStacks` API methods by @nithishravindra [#1251](https://github.com/hashicorp/go-tfe/pull/1251)
# v1.96.0
## Enhancements
* QueryRun API is now generally available in HCP Terraform (not available in Terraform Enterprise), by @sowju-hashicorp [#1245](https://github.com/hashicorp/go-tfe/pull/1245)
* Remove org settings validation in RegistryModulesCreateMonorepo tests, by @jillirami ([#1236](https://github.com/hashicorp/go-tfe/pull/1236))
* Add `RemoteTFENumericVersion()` to the `Client` interface, which exposes the `X-TFE-Current-Version` header set by a remote TFE instance by @skj-skj [#1246](https://github.com/hashicorp/go-tfe/pull/1246)
# v1.95.0
## Enhancements
* Add `Sort` option to `Agents` and `AgentPools`, by @twitnithegirl [#1229](https://github.com/hashicorp/go-tfe/pull/1229)
* Add serialization for `StacksEnabled` field, `CanEnableStacks` & `CanCreateProject` permissions on Organization Read by @a-anurag27 [#1230](https://github.com/hashicorp/go-tfe/pull/1230)
* Adds new stacks resources `StackDeployments`, `StackDiagnostics`, and `StackStates`, by @ctrombley [#1226](https://github.com/hashicorp/go-tfe/pull/1226)
* Adds new `Diagnostics` methods to `StackConfigurations`, and `StackDeploymentSteps`, by @ctrombley [#1226](https://github.com/hashicorp/go-tfe/pull/1226)
* Adds new `Artifacts` method to `StackDeploymentSteps`, by @ctrombley [#1226](https://github.com/hashicorp/go-tfe/pull/1226)
* Add serialization for `StacksEnabled` field, `CanEnableStacks` & `CanCreateProject` permissions on Organization Read by @a-anurag27 [#1230](https://github.com/hashicorp/go-tfe/pull/1230)
# v1.94.0
## Enhancements
* Add BETA support for Action invocations via `CreateRunOptions` by @mutahhir [#1206](https://github.com/hashicorp/go-tfe/pull/1206)
# v1.93.0
## Enhancements
* Exports the StackConfiguration UploadTarGzip receiver function [#1219](https://github.com/hashicorp/go-tfe/pull/1219)
* Updates BETA stacks resource schemas to match latest API spec by @ctrombley [#1220](https://github.com/hashicorp/go-tfe/pull/1220)
* Adds support for Hold Your Own Key by @helenjw , @iuri-slywitch-hashicorp and @dominic-retli-hashi [#1201](https://github.com/hashicorp/go-tfe/pull/1201)
## Deprecations
* The beta `StackDeployments`, `StackPlan`, `StackPlanOperation`, and `StackSource` resources have been removed, by @sahar-azizighannad [#1205](https://github.com/hashicorp/go-tfe/pull/1205)
# v1.92.0
## Enhancements
* Adds BETA support for performing Registry Module test runs on custom Agents by @hashimooon [#1209](httpsLhttps://github.com/hashicorp/go-tfe/pull/1209)
* Adds support for managing agent pools on `Stacks` by @maed223 [#1214](https://github.com/hashicorp/go-tfe/pull/1214)
## Bug Fixes
* Fixes arch validation on Terraform, OPA, and Sentinel Tool Versions when providing top level `url` and `sha` with multiple `archs` by @kelsi-hoyle [#1212](https://github.com/hashicorp/go-tfe/pull/1212)
# v1.91.1
## Bug Fixes
* Fixes timestamp attribute mapping for deployment runs to use correct API field names (`created-at`/`updated-at` instead of `started-at`/`completed-at`) by @shwetamurali [#1199](https://github.com/hashicorp/go-tfe/pull/1199)
## Enhancements
* Adds support for listing `StackConfigurationSummaries` by @hwatkins05-hashicorp [#1204](https://github.com/hashicorp/go-tfe/pull/1204)
# v1.91.0
## Enhancements
* Adds `Logs` method to `QueryRuns`, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users by @brandonc [#1186](https://github.com/hashicorp/go-tfe/pull/1186)
* Adds `Source` field to `Workspace` by @jpadrianoGo [#1124](https://github.com/hashicorp/go-tfe/pull/1124)
* Adds `CreatedBy` relation to `AgentToken` by @jpadrianoGo [#1149](https://github.com/hashicorp/go-tfe/pull/1149)
* Adds `CreatedAt` field to `AgentPool` by @jpadrianoGo [#1150](https://github.com/hashicorp/go-tfe/pull/1150)
* Adds `CanceledAt`, `RunEvents`, `TriggerReason` field to `Run` by @jpadrianoGo [#1161](https://github.com/hashicorp/go-tfe/pull/1161)
* Adds `AllowedProjects` and `ExcludedWorkspaces` to `AgentPool` by @tylerwolf [#1185](https://github.com/hashicorp/go-tfe/pull/1185)
* Adds `DefaultExecutionMode`, `DefaultAgentPool`, and `SettingOverwrites` to `Project` by @tylerwolf [#1185](https://github.com/hashicorp/go-tfe/pull/1185)
* Adds `IconUrl`, `InstallationType`, and `InstallationURL` to githubAppInstallation, by @jpadrianoGo [#1191](https://github.com/hashicorp/go-tfe/pull/1191)
* Adds support for `Workspace` VCSRepoOptions source_directory and tag_prefix, by @jillrami [#1194] (https://github.com/hashicorp/go-tfe/pull/1194)
# v1.90.0
## Bug Fixes
* Fixes `IngressAttributes` field decoding in `PolicySetVersion` by @rageshganeshkumar [#1164](https://github.com/hashicorp/go-tfe/pull/1164)
* Fixes issue [1061](https://github.com/hashicorp/go-tfe/issues/1061), validation accepts all RunStatus including `"cost_estimated"` by @KenCox-Hashicorp [#1171](https://github.com/hashicorp/go-tfe/pull/1171)
## Enhancements
* Add support for querying and filtering private registry modules based on a search query, `provider`, `registry_name` and `organization_name`, by @gautambaghel [#1179](https://github.com/hashicorp/go-tfe/pull/1179)
* Adds support for `RegistryModule` VCS source_directory and tag_prefix options, by @jillrami [#1154] (https://github.com/hashicorp/go-tfe/pull/1154)
* Adds endpoint for reruning a stack deployment by @hwatkins05-hashicorp/@Maed223 [#1176](https://github.com/hashicorp/go-tfe/pull/1176)
* Adds `ReadByName` for `StackDeploymentGroup` by @Maed223 [#1181](https://github.com/hashicorp/go-tfe/pull/1181)
* Adds BETA support for listing `QueryRuns`, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users by @brandonc [#1177](https://github.com/hashicorp/go-tfe/pull/1177)
* Adds BETA support for `CreateAndUpload` for `StackConfiguration`, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users by @Maed223 [#1184](https://github.com/hashicorp/go-tfe/pull/1184)
# v1.89.0
## Enhancements
* Add the `Links` attribute to the `StackDeploymentStep` struct to support the deployment step GET endpoint by @shwetamurali [#1167](https://github.com/h)
* Adds endpoint for cancelling `StackDeploymentRun` by @Maed223 [#1172](https://github.com/hashicorp/go-tfe/pull/1172)
* Adds `CreatedAt` and `UpdatedAt` fields to `StackConfiguration` by @Maed223 [#1168](https://github.com/hashicorp/go-tfe/pull/1168)
* Adds endpoint for advancing `StackDeploymentStep` by @hwatkins05-hashicorp [#1166](https://github.com/hashicorp/go-tfe/pull/1166)
# v1.88.0
## Enhancements
* Adds BETA support for reading, testing and updating Organization Audit Configuration by @glennsarti-hashi [#1151](https://github.com/hashicorp/go-tfe/pull/1151)
* Adds `Completed` status to `StackConfiguration` by @hwatkins05-hashicorp [#1163](https://github.com/hashicorp/go-tfe/pull/1163)
# v1.87.0
## Bug Fixes
* Fixes incorrect primary key usage on `StackDeploymentRun`, `StackDeploymentGroup`, and `StackDeploymentStep`, by @Maed223 [#1156](https://github.com/hashicorp/go-tfe/pull/1156)
## Enhancements
* Updates endpoint for updating stack configuration from `actions/update-configuration` to `fetch-latest-from-vcs` by @hwatkins05-hashicorp [#1157](https://github.com/hashicorp/go-tfe/pull/1157)
# v1.86.0
## Enhancements
* Adds option for `Includes` for `StackDeploymentRuns` Read/List, by @Maed223 [#1152](https://github.com/hashicorp/go-tfe/pull/1152)
# v1.85.0
## Bug Fixes
* Fix the registry URL path used for `ReadTerraformRegistryModule` by @paladin-devops [#1141](https://github.com/hashicorp/go-tfe/pull/1141)
## Enhancements
* Adds support for managing reserved tag keys within a TFE organization, by @ctrombley [#1145](https://github.com/hashicorp/go-tfe/pull/1145)
# v1.84.0
## Enhancements
* Adds BETA support for listing `StackConfigurationList`, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users by @shwetamurali [#1138](https://github.com/hashicorp/go-tfe/pull/1138)
* Adds BETA support for approving all plans for a stack deployment run, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users by @ctrombley [#1136](https://github.com/hashicorp/go-tfe/pull/1136)
* Adds BETA support for listing and reading `StackDeploymentSteps`, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users by @ctrombley [#1133](https://github.com/hashicorp/go-tfe/pull/1133)
* Adds BETA support for approving all plans in `StackDeploymentGroups`, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users by @hwatkins05-hashicorp [#1137](https://github.com/hashicorp/go-tfe/pull/1137)
## Bug Fixes
* Fix the registry URL path used for `ReadTerraformRegistryModule` by @paladin-devops [#1141](https://github.com/hashicorp/go-tfe/pull/1141)
# v1.83.0
## Enhancements
* Adds BETA support for listing `StackDeploymentRuns`, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users by @shwetamurali [#1134](https://github.com/hashicorp/go-tfe/pull/1134)
* Add support for HCP Terraform `/api/v2/workspaces/{external_id}/all-vars` API endpoint to fetch the list of all variables available to a workspace (include inherited variables from varsets) by @debrin-hc [#1105](https://github.com/hashicorp/go-tfe/pull/1105)
* Adds BETA support for listing `StackDeploymentGroups`, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users by @hwatkins05-hashicorp [#1128](https://github.com/hashicorp/go-tfe/pull/1128)
* Adds BETA support for removing/adding VCS backing for a Stack, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users by @Maed223 [#1131](https://github.com/hashicorp/go-tfe/pull/1131)
* Adds BETA support for reading a `StackDeploymentGroup`, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users by @hwatkins05-hashicorp [#1132](https://github.com/hashicorp-go-tfe/pull/1132)
## Enhancements
* Adds `PrivateRunTasks` field to Entitlements by @glennsarti [#944](https://github.com/hashicorp/go-tfe/pull/944)
* Adds `AgentPool` relationship to options when creating and updating Run Tasks by @glennsarti [#944](https://github.com/hashicorp/go-tfe/pull/944)
# v1.82.0
## Enhancements
* Adds BETA support for speculative runs with `Stacks` resources and removes VCS repo validity check on `Stack` creation, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users by @hwatkins05-hashicorp [#1119](https://github.com/hashicorp/go-tfe/pull/1119)
* Adds support for listing team tokens, by @mkam [#1109](https://github.com/hashicorp/go-tfe/pull/1109)
## Bug Fixes
* Fixes hard coded public terraform registry URL for ReadTerraformRegistryModule, by @paladin-devops [#1126](https://github.com/hashicorp/go-tfe/pull/1126)
# v1.81.0
## Enhancements
* Adds `IngressAttributes` field to `PolicySetVersion` by @jpadrianoGo [#1092](https://github.com/hashicorp/go-tfe/pull/1092)
* Adds `ConfirmedBy` field to `Run` by @jpadrianoGo [#1110](https://github.com/hashicorp/go-tfe/pull/1110)
# v1.80.0
## Enhancements
* Adds BETA support for `PolicyPaths` to the `Runs` interface, by sebasslash [#1104](https://github.com/hashicorp/go-tfe/pull/1104)
# v1.79.0
## BREAKING CHANGES
* Updates team token `Description` to be a pointer, allowing for both nil descriptions and empty string descriptions. Team token descriptions and the ability to create multiple team tokens is in BETA, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users, by @mkam [#1088](https://github.com/hashicorp/go-tfe/pull/1088)
## Enhancements
* Adds `AgentPool` field to the OAuthClientUpdateOptions struct, which is used to associate a VCS Provider with an AgentPool for PrivateVCS support by @jpogran [#1075](https://github.com/hashicorp/go-tfe/pull/1075)
* Add BETA support for use of OPA and Sentinel with Linux arm64 agents, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users @natalie-todd [#1090](https://github.com/hashicorp/go-tfe/pull/1090)
# v1.78.0
## Enhancements
* Adds `Links` field to `EffectiveTagBindings` to check whether an effective tag binding is inherited, by @sebasslash [#1087](https://github.com/hashicorp/go-tfe/pull/1087)
# v1.77.0
## Enhancements
* Remove `DefaultProject` from `OrganizationUpdateOptions` to prevent updating an organization's default project, by @netramali [#1078](https://github.com/hashicorp/go-tfe/pull/1078)
* Adds support for creating multiple team tokens by adding `Description` to `TeamTokenCreateOptions`. This provides BETA support, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users, by @mkam [#1083](https://github.com/hashicorp/go-tfe/pull/1083)
* Adds support for reading and deleting team tokens by ID, by @mkam [#1083](https://github.com/hashicorp/go-tfe/pull/1083)
## BREAKING CHANGES
In the last release, Runs interface method `ListForOrganization` included pagination fields `TotalCount` and `TotalPages`, but these are being removed as this feature approaches general availablity by @brandonc [#1074](https://github.com/hashicorp/go-tfe/pull/1074)
# v1.76.0
## Enhancements
* Adds `DefaultProject` to `OrganizationUpdateOptions` to support updating an organization's default project. This provides BETA support, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users, by @mkam [#1056](https://github.com/hashicorp/go-tfe/pull/1056)
* Adds `ReadTerraformRegistryModule` to support reading a registry module from Terraform Registry's proxied endpoints by @paladin-devops [#1057](https://github.com/hashicorp/go-tfe/pull/1057)
* Adds a new method `ListForOrganization` to list Runs in an organization by @arybolovlev [#1059](https://github.com/hashicorp/go-tfe/pull/1059)
## Bug fixes
* Adds `ToolVersionArchitecture` to `AdminTerraformVersionUpdateOptions` and `AdminTerraformVersion`. This provides BETA support, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users by @kelsi-hoyle [#1047](https://github.com/hashicorp/go-tfe/pull/1047)
# v1.75.0
## Enhancements
* Adds `EffectiveTagBindings` relation to projects and workspaces, allowing the relation to be included when listing projects or workspaces by @sebasslash [#1043](https://github.com/hashicorp/go-tfe/pull/1043)
# v1.74.1
## Enhancements
* Add parallelism to create options for TF Test Runs by @dsa0x [1037](https://github.com/hashicorp/go-tfe/pull/1025)
# v1.74.0
## Enhancements
* Add BETA support for adding custom project permission for variable sets `ProjectVariableSetsPermission` by @netramali [21879](https://github.com/hashicorp/atlas/pull/21879)
# v1.73.1
## Bug fixes
* Includes a critical security update in an upstream depdendency `hashicorp/go-slug` @NodyHub [#1025](https://github.com/hashicorp/go-tfe/pull/1025)
* Fix bug in BETA support for Linux arm64 agents, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users @natalie-todd [#1022](https://github.com/hashicorp/go-tfe/pull/1022)
# v1.73.0
## Enhancements
* Add support for team notification configurations @notchairmk [#1016](https://github.com/hashicorp/go-tfe/pull/1016)
# v1.72.0
## Enhancements
* Add support for project level auto destroy settings @simonxmh [#1011](https://github.com/hashicorp/go-tfe/pull/1011)
* Add BETA support for Linux arm64 agents, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users @natalie-todd [#1022](https://github.com/hashicorp/go-tfe/pull/1022)
* Adds support to delete all tag bindings on either a project or workspace by @sebasslash [#1023](https://github.com/hashicorp/go-tfe/pull/1023)
# v1.71.0
## Enhancements
* Add support for listing effective tag bindings for a workspace or project by @brandonc [#996](https://github.com/hashicorp/go-tfe/pull/996)
* Add support for listing no-code modules by @paladin-devops [#1003](https://github.com/hashicorp/go-tfe/pull/1003)
# v1.70.0
## Enhancements
* Actually adds support for adding/updating key/value tags, which was not unintentionally removed from the last release by @brandonc [#987](https://github.com/hashicorp/go-tfe/pull/987)
# v1.69.0
## Enhancements
* Adds BETA support for a variable set `Parent` relation, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users by @jbonhag [#992](https://github.com/hashicorp/go-tfe/pull/992)
* Add support for adding/updating key/value tags by @brandonc [#991](https://github.com/hashicorp/go-tfe/pull/991)
* Add support for reading a registry module by its unique identifier by @dsa0x [#988](https://github.com/hashicorp/go-tfe/pull/988)
* Add support for enabling Stacks on an organization by @brandonc [#987](https://github.com/hashicorp/go-tfe/pull/987)
* Add support for filtering by key/value tags by @brandonc [#987](https://github.com/hashicorp/go-tfe/pull/987)
* Adds `SpeculativePlanManagementEnabled` field to `Organization` by @lilincmu [#983](https://github.com/hashicorp/go-tfe/pull/983)
# v1.68.0
## Enhancements
* Add support for reading a no-code module's variables by @paladin-devops [#979](https://github.com/hashicorp/go-tfe/pull/979)
* Add Waypoint entitlements (the `waypoint-actions` and `waypoint-templates-and-addons` attributes) to `Entitlements` by @ignatius-j [#984](https://github.com/hashicorp/go-tfe/pull/984)
# v1.67.1
## Bug Fixes
* Fixes a bug in `NewRequest` that did not allow query parameters to be specified in the first parameter, which broke several methods: `RegistryModules ReadVersion`, `VariableSets UpdateWorkspaces`, and `Workspaces Readme` by @brandonc [#982](https://github.com/hashicorp/go-tfe/pull/982)
# v1.67.0
## Enhancements
* `Workspaces`: The `Unlock` method now returns a `ErrWorkspaceLockedStateVersionStillPending` error if the latest state version upload is still pending within the platform. This is a retryable error. by @brandonc [#978](https://github.com/hashicorp/go-tfe/pull/978)
# v1.66.0
## Enhancements
* Adds `billable-rum-count` attribute to `StateVersion` by @shoekstra [#974](https://github.com/hashicorp/go-tfe/pull/974)
## Bug Fixes
* Fixed the incorrect error "workspace already unlocked" being returned when attempting to unlock a workspace that was locked by a Team or different User @ctrombley / @lucasmelin [#975](https://github.com/hashicorp/go-tfe/pull/975)
# v1.65.0
## Enhancements
* Adds support for deleting `Stacks` that still have deployments through `ForceDelete` by @hashimoon [#969](https://github.com/hashicorp/go-tfe/pull/969)
## Bug Fixes
* Fixed `RegistryNoCodeModules` method `UpgradeWorkspace` to return a `WorkspaceUpgrade` type. This resulted in a BREAKING CHANGE, yet the previous type was not properly decoded nor reflective of the actual API result by @paladin-devops [#955](https://github.com/hashicorp/go-tfe/pull/955)
# v1.64.2
## Enhancements
* Adds support for including no-code permissions to the `OrganizationPermissions` struct [#967](https://github.com/hashicorp/go-tfe/pull/967)
# v1.64.1
## Bug Fixes
* Fixes BETA feature regression in `Stacks` associated with decoding `StackVCSRepo` data by @brandonc [#964](https://github.com/hashicorp/go-tfe/pull/964)
# v1.64.0
* Adds support for creating different organization token types by @glennsarti [#943](https://github.com/hashicorp/go-tfe/pull/943)
* Adds more BETA support for `Stacks` resources, which is is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users by @DanielMSchmidt [#963](https://github.com/hashicorp/go-tfe/pull/963)
# v1.63.0
## Enhancements
* Adds more BETA support for `Stacks` resources, which is is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users by @brandonc [#957](https://github.com/hashicorp/go-tfe/pull/957) and @DanielMSchmidt [#960](https://github.com/hashicorp/go-tfe/pull/960)
# v1.62.0
## Bug Fixes
* Fixed `RegistryNoCodeModules` methods `CreateWorkspace` and `UpdateWorkspace` to return a `Workspace` type. This resulted in a BREAKING CHANGE, yet the previous type was not properly decoded nor reflective of the actual API result by @paladin-devops [#954](https://github.com/hashicorp/go-tfe/pull/954)
## Enhancements
* Adds `AllowMemberTokenManagement` permission to `Team` by @juliannatetreault [#922](https://github.com/hashicorp/go-tfe/pull/922)
* Adds `PrivateRunTasks` field to Entitlements by @glennsarti [#944](https://github.com/hashicorp/go-tfe/pull/944)
* Adds `AgentPool` relationship to options when creating and updating Run Tasks by @glennsarti [#944](https://github.com/hashicorp/go-tfe/pull/944)
# v1.61.0
## Enhancements
* Adds support for creating no-code workspaces by @paladin-devops [#927](https://github.com/hashicorp/go-tfe/pull/927)
* Adds support for upgrading no-code workspaces by @paladin-devops [#935](https://github.com/hashicorp/go-tfe/pull/935)
# v1.60.0
## Enhancements
* Adds more BETA support for `Stacks` resources, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users by @brandonc. [#934](https://github.com/hashicorp/go-tfe/pull/934)
# v1.59.0
## Features
* Adds support for the Run Tasks Integration API by @karvounis-form3 [#929](https://github.com/hashicorp/go-tfe/pull/929)
# v1.58.0
## Enhancements
* Adds BETA support for `Stacks` resources, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users by @brandonc. [#920](https://github.com/hashicorp/go-tfe/pull/920)
* Adds support for managing resources that have HCP IDs by @roncodingenthusiast. [#924](https://github.com/hashicorp/go-tfe/pull/924)
# v1.57.0
## Enhancements
* Adds the `IsUnified` field to `Project`, `Organization` and `Team` by @roncodingenthusiast [#915](https://github.com/hashicorp/go-tfe/pull/915)
* Adds Workspace auto-destroy notification types to `NotificationTriggerType` by @notchairmk [#918](https://github.com/hashicorp/go-tfe/pull/918)
* Adds `CreatedAfter` and `CreatedBefore` Date Time filters to `AdminRunsListOptions` by @maed223 [#916](https://github.com/hashicorp/go-tfe/pull/916)
# v1.56.0
## Enhancements
* Adds `ManageAgentPools` permission to team `OrganizationAccess` by @emlanctot [#901](https://github.com/hashicorp/go-tfe/pull/901)
# v1.55.0
## Enhancements
* Adds the `CurrentRunStatus` filter to allow filtering workspaces by their current run status by @arybolovlev [#899](https://github.com/hashicorp/go-tfe/pull/899)
# v1.54.0
## Enhancements
* Adds the `AutoDestroyActivityDuration` field to `Workspace` by @notchairmk [#902](https://github.com/hashicorp/go-tfe/pull/902)
## Deprecations
* The `IsSiteAdmin` field on User has been deprecated. Use the `IsAdmin` field instead [#900](https://github.com/hashicorp/go-tfe/pull/900)
# v1.53.0
## Enhancements
* Adds `ManageTeams`, `ManageOrganizationAccess`, and `AccessSecretTeams` permissions to team `OrganizationAccess` by @juliannatetreault [#874](https://github.com/hashicorp/go-tfe/pull/874)
* Mocks are now generated using the go.uber.org/mock package [#897](https://github.com/hashicorp/go-tfe/pull/897)
# v1.52.0
## Enhancements
* Add `EnforcementLevel` to `Policy` create and update options. This will replace the deprecated `[]Enforce` method for specifying enforcement level. @JarrettSpiker [#895](https://github.com/hashicorp/go-tfe/pull/895)
## Deprecations
* The `Enforce` fields on `Policy`, `PolicyCreateOptions`, and `PolicyUpdateOptions` have been deprecated. Use the `EnforcementLevel` instead. @JarrettSpiker [#895](https://github.com/hashicorp/go-tfe/pull/895)
# v1.51.0
## Enhancements
* Adds `Teams` field to `OrganizationMembershipCreateOptions` to allow users to be added to teams at the same time they are invited to an organization. by @JarrettSpiker [#886](https://github.com/hashicorp/go-tfe/pull/886)
* `IsCloud()` returns true when TFP-AppName is "HCP Terraform" by @sebasslash [#891](https://github.com/hashicorp/go-tfe/pull/891)
* `OrganizationScoped` attribute for `OAuthClient` is now generally available by @netramali [#873](https://github.com/hashicorp/go-tfe/pull/873)
# v1.50.0
## Enhancements
* Adds Bitbucket Data Center as a new `ServiceProviderType` and ensures similar validation as Bitbucket Server by @zainq11 [#879](https://github.com/hashicorp/go-tfe/pull/879)
* Add `GlobalRunTasks` field to `Entitlements`. by @glennsarti [#865](https://github.com/hashicorp/go-tfe/pull/865)
* Add `Global` field to `RunTask`. by @glennsarti [#865](https://github.com/hashicorp/go-tfe/pull/865)
* Add `Stages` field to `WorkspaceRunTask`. by @glennsarti [#865](https://github.com/hashicorp/go-tfe/pull/865)
* Changing BETA `OrganizationScoped` attribute of `OAuthClient` to be a pointer for bug fix by @netramali [884](https://github.com/hashicorp/go-tfe/pull/884)
* Adds `Query` parameter to `VariableSetListOptions` to allow searching variable sets by name, by @JarrettSpiker[#877](https://github.com/hashicorp/go-tfe/pull/877)
## Deprecations
* The `Stage` field has been deprecated on `WorkspaceRunTask`. Instead, use `Stages`. by @glennsarti [#865](https://github.com/hashicorp/go-tfe/pull/865)
# v1.49.0
## Enhancements
* Adds `post_apply` to list of possible `stages` for Run Tasks by @glennsarti [#878](https://github.com/hashicorp/go-tfe/pull/878)
# v1.48.0
## Features
* For Terraform Enterprise users who have data retention policies defined on Organizations or Workspaces: A new DataRetentionPolicyChoice relation has been added to reflect that [data retention policies are polymorphic](https://developer.hashicorp.com/terraform/enterprise/api-docs/data-retention-policies#data-retention-policy-types). Organizations and workspaces may be related to a `DataRetentionPolicyDeleteOlder` or `DataRetentionPolicyDontDelete` record through the `DataRetentionPolicyChoice` struct. Data retention policies can be read using `ReadDataRetentionPolicyChoice`, and set or updated (including changing their type) using `SetDataRetentionPolicyDeleteOlder` or `SetDataRetentionPolicyDontDelete` by @JarrettSpiker [#652](https://github.com/hashicorp/go-tfe/pull/844)
## Deprecations
* The `DataRetentionPolicy` type, and the `DataRetentionPolicy` relationship on `Organization` and `Workspace`s have been deprecated. The `DataRetentionPolicy` type is equivalent to the new `DataRetentionPolicyDeleteOlder`. The Data retention policy relationships on `Organization` and `Workspace`s are now [polymorphic](https://developer.hashicorp.com/terraform/enterprise/api-docs/data-retention-policies#data-retention-policy-types), and are represented by the `DataRetentionPolicyChoice` relationship. The existing `DataRetentionPolicy` relationship will continue to be populated when reading an `Organization` or `Workspace`, but it may be removed in a future release. @JarrettSpiker [#652](https://github.com/hashicorp/go-tfe/pull/844)
* The `SetDataRetentionPolicy` function on `Organizations` and `Workspaces` is now deprecated in favour of `SetDataRetentionPolicyDeleteOlder` or `SetDataRetentionPolicyDontDelete`. `SetDataRetentionPolicy` will only update the data retention policy when communicating with TFE versions v202311 and v202312. @JarrettSpiker [#652](https://github.com/hashicorp/go-tfe/pull/844)
* The `ReadDataRetentionPolicy` function on `Organizations` and `Workspaces` is now deprecated in favour of `ReadDataRetentionPolicyChoice`. `ReadDataRetentionPolicyChoice` may return the different multiple data retention policy types added in TFE 202401-1. `SetDataRetentionPolicy` will only update the data retention policy when communicating with TFE versions v202311 and v202312. @JarrettSpiker [#652](https://github.com/hashicorp/go-tfe/pull/844)
## Enhancements
* Adds `Variables` relationship field to `Workspace` by @arybolovlev [#872](https://github.com/hashicorp/go-tfe/pull/872)
# v1.47.1
## Bug fixes
* Change the error message for `ErrWorkspaceStillProcessing` to be the same error message returned by the API by @uturunku1 [#864](https://github.com/hashicorp/go-tfe/pull/864)
# v1.47.0
## Enhancements
* Adds BETA `description` attribute to `Project` by @netramali [#861](https://github.com/hashicorp/go-tfe/pull/861)
* Adds `Read` method to `TestVariables` by @aaabdelgany [#851](https://github.com/hashicorp/go-tfe/pull/851)
# v1.46.0
## Enhancements
* Adds `Query` field to `Project` and `Team` list options, to allow projects and teams to be searched by name by @JarrettSpiker [#849](https://github.com/hashicorp/go-tfe/pull/849)
* Adds `AgenPool` relation to `OAuthClient` create options to support for Private VCS by enabling creation of OAuth Client when AgentPoolID is set (as an optional param) @roleesinhaHC [#841](https://github.com/hashicorp/go-tfe/pull/841)
* Add `Sort` field to workspace list options @Maed223 [#859](https://github.com/hashicorp/go-tfe/pull/859)
# v1.45.0
## Enhancements
* Updates go-tfe client to export the instance name using `AppName()` @sebasslash [#848](https://github.com/hashicorp/go-tfe/pull/848)
* Add `DeleteByName` API endpoint to `RegistryModule` @laurenolivia [#847](https://github.com/hashicorp/go-tfe/pull/847)
* Update deprecated `RegistryModule` endpoints `DeleteProvider` and `DeleteVersion` with new API calls @laurenolivia [#847](https://github.com/hashicorp/go-tfe/pull/847)
# v1.44.0
## Enhancements
* Updates `Workspaces` to include an `AutoDestroyAt` attribute on create and update by @notchairmk and @ctrombley [#786](https://github.com/hashicorp/go-tfe/pull/786)
* Adds `AgentsEnabled` and `PolicyToolVersion` attributes to `PolicySet` by @mrinalirao [#752](https://github.com/hashicorp/go-tfe/pull/752)
# v1.43.0
## Features
* Adds `AggregatedCommitStatusEnabled` field to `Organization` by @mjyocca [#829](https://github.com/hashicorp/go-tfe/pull/829)
## Enhancements
* Adds `GlobalProviderSharing` field to `AdminOrganization` by @alex-ikse [#837](https://github.com/hashicorp/go-tfe/pull/837)
# v1.42.0
## Deprecations
* The `Sourceable` field has been deprecated on `RunTrigger`. Instead, use `SourceableChoice` to locate the non-empty field representing the actual sourceable value by @brandonc [#816](https://github.com/hashicorp/go-tfe/pull/816)
## Features
* Added `AdminOPAVersion` and `AdminSentinelVersion` Terraform Enterprise admin endpoints by @mrinalirao [#758](https://github.com/hashicorp/go-tfe/pull/758)
## Enhancements
* Adds `LockedBy` relationship field to `Workspace` by @brandonc [#816](https://github.com/hashicorp/go-tfe/pull/816)
* Adds `CreatedBy` relationship field to `TeamToken`, `UserToken`, and `OrganizationToken` by @brandonc [#816](https://github.com/hashicorp/go-tfe/pull/816)
* Added `Sentinel` field to `PolicyResult` by @stefan-kiss. [Issue#790](https://github.com/hashicorp/go-tfe/issues/790)
# v1.41.0
## Enhancements
* Allow managing workspace and organization data retention policies by @mwudka [#801](https://github.com/hashicorp/go-tfe/pull/817)
# v1.40.0
## Bug Fixes
* Removed unused field `AgentPoolID` from the Workspace model. (Callers should be using the `AgentPool` relation instead) by @brandonc [#815](https://github.com/hashicorp/go-tfe/pull/815)
## Enhancements
* Add organization scope field for oauth clients by @Netra2104 [#812](https://github.com/hashicorp/go-tfe/pull/812)
* Added BETA support for including `projects` relationship to oauth_client on create by @Netra2104 [#806](https://github.com/hashicorp/go-tfe/pull/806)
* Added BETA method `AddProjects` and `RemoveProjects` for attaching/detaching oauth_client to projects by Netra2104 [#806](https://github.com/hashicorp/go-tfe/pull/806)
* Adds a missing interface `WorkspaceResources` and the `List` method by @stefan-kiss [Issue#754](https://github.com/hashicorp/go-tfe/issues/754)
# v1.39.2
## Bug Fixes
* Fixes a dependency build failure for 32 bit linux architectures by @brandonc [#814](https://github.com/hashicorp/go-tfe/pull/814)
# v1.39.1
## Bug Fixes
* Fixes an issue where the request body is not preserved during certain retry scenarios by @sebasslash [#813](https://github.com/hashicorp/go-tfe/pull/813)
# v1.39.0
## Features
* New WorkspaceSettingOverwritesOptions field for allowing workspaces to defer some settings to a default from their organization or project by @SwiftEngineer [#762](https://github.com/hashicorp/go-tfe/pull/762)
* Added support for setting a default execution mode and agent pool at the organization level by @SwiftEngineer [#762](https://github.com/hashicorp/go-tfe/pull/762)
* Added validation when configuring registry module publishing by @hashimoon [#804](https://github.com/hashicorp/go-tfe/pull/804)
* Removed BETA labels for StateVersion Upload method, ConfigurationVersion `provisional` field, and `save-plan` runs by @brandonc [#800](https://github.com/hashicorp/go-tfe/pull/800)
* Allow soft deleting, restoring, and permanently deleting StateVersion and ConfigurationVersion backing data by @mwudka [#801](https://github.com/hashicorp/go-tfe/pull/801)
* Added the `AutoApplyRunTrigger` attribute to Workspaces by @nfagerlund [#798](https://github.com/hashicorp/go-tfe/pull/798)
* Removed BETA labels for `priority` attribute in variable sets by @Netra2104 [#796](https://github.com/hashicorp/go-tfe/pull/796)
# v1.38.0
## Features
* Added BETA support for including `priority` attribute to variable_set on create and update by @Netra2104 [#778](https://github.com/hashicorp/go-tfe/pull/778)
# v1.37.0
## Features
* Add the tags attribute to VCSRepo to be used with registry modules by @hashimoon [#793](https://github.com/hashicorp/go-tfe/pull/793)
# v1.36.0
## Features
* Added BETA support for private module registry test variables by @aaabdelgany [#787](https://github.com/hashicorp/go-tfe/pull/787)
## Bug Fixes
* Fix incorrect attribute type for `RegistryModule.VCSRepo.Tags` by @hashimoon [#789](https://github.com/hashicorp/go-tfe/pull/789)
* Fix nil dereference panic within `StateVersions` `upload` after not handling certain state version create errors by @brandonc [#792](https://github.com/hashicorp/go-tfe/pull/792)
# v1.35.0
## Features
* Added BETA support for private module registry tests by @hashimoon [#781](https://github.com/hashicorp/go-tfe/pull/781)
## Enhancements
* Removed beta flags for `PolicySetProjects` and `PolicySetWorkspaceExclusions` by @Netra2104 [#770](https://github.com/hashicorp/go-tfe/pull/770)
# v1.34.0
## Features
* Added support for the new Terraform Test Runs API by @liamcervante [#755](https://github.com/hashicorp/go-tfe/pull/755)
## Bug Fixes
* "project" was being rejected as an invalid `Include` option when listing workspaces by @brandonc [#765](https://github.com/hashicorp/go-tfe/pull/765)
# v1.33.0
## Enhancements
* Removed beta tags for TeamProjectAccess by @rberecka [#756](https://github.com/hashicorp/go-tfe/pull/756)
* Added BETA support for including `workspaceExclusions` relationship to policy_set on create by @Netra2104 [#757](https://github.com/hashicorp/go-tfe/pull/757)
* Added BETA method `AddWorkspaceExclusions` and `RemoveWorkspaceExclusions` for attaching/detaching workspace-exclusions to a policy-set by @hs26gill [#761](https://github.com/hashicorp/go-tfe/pull/761)
# v1.32.1
## Dependency Update
* Updated go-slug dependency to v0.12.1
# v1.32.0
## Enhancements
* Added BETA support for adding and updating custom permissions to `TeamProjectAccesses`. A `TeamProjectAccessType` of `"custom"` can set various permissions applied at
the project level to the project itself (`TeamProjectAccessProjectPermissionsOptions`) and all of the workspaces in a project (`TeamProjectAccessWorkspacePermissionsOptions`) by @rberecka [#745](https://github.com/hashicorp/go-tfe/pull/745)
* Added BETA field `Provisional` to `ConfigurationVersions` by @brandonc [#746](https://github.com/hashicorp/go-tfe/pull/746)
# v1.31.0
## Enhancements
* Added BETA support for including `projects` relationship and `projects-count` attribute to policy_set on create by @hs26gill [#737](https://github.com/hashicorp/go-tfe/pull/737)
* Added BETA method `AddProjects` and `RemoveProjects` for attaching/detaching policy set to projects by @Netra2104 [#735](https://github.com/hashicorp/go-tfe/pull/735)
# v1.30.0
## Enhancements
* Adds `SignatureSigningMethod` and `SignatureDigestMethod` fields in `AdminSAMLSetting` struct by @karvounis-form3 [#731](https://github.com/hashicorp/go-tfe/pull/731)
* Adds `Certificate`, `PrivateKey`, `TeamManagementEnabled`, `AuthnRequestsSigned`, `WantAssertionsSigned`, `SignatureSigningMethod`, `SignatureDigestMethod` fields in `AdminSAMLSettingsUpdateOptions` struct by @karvounis-form3 [#731](https://github.com/hashicorp/go-tfe/pull/731)
# v1.29.0
## Enhancements
* Adds `RunPreApplyCompleted` run status by @uk1288 [#727](https://github.com/hashicorp/go-tfe/pull/727)
* Added BETA support for saved plan runs, by @nfagerlund [#724](https://github.com/hashicorp/go-tfe/pull/724)
* New `SavePlan` fields in `Run` and `RunCreateOptions`
* New `RunPlannedAndSaved` `RunStatus` value
* New `PlannedAndSavedAt` field in `RunStatusTimestamps`
* New `RunOperationSavePlan` constant for run list filters
# v1.28.0
## Enhancements
* Update `Workspaces` to include associated `project` resource by @glennsarti [#714](https://github.com/hashicorp/go-tfe/pull/714)
* Adds BETA method `Upload` method to `StateVersions` and support for pending state versions by @brandonc [#717](https://github.com/hashicorp/go-tfe/pull/717)
* Adds support for the query parameter `q` to search `Organization Tags` by name by @sharathrnair87 [#720](https://github.com/hashicorp/go-tfe/pull/720)
* Added ContextWithResponseHeaderHook support to `IPRanges` by @brandonc [#717](https://github.com/hashicorp/go-tfe/pull/717)
## Bug Fixes
* `ConfigurationVersions`, `PolicySetVersions`, and `RegistryModules` `Upload` methods were sending API credentials to the specified upload URL, which was unnecessary by @brandonc [#717](https://github.com/hashicorp/go-tfe/pull/717)
# v1.27.0
## Enhancements
* Adds `RunPreApplyRunning` and `RunQueuingApply` run statuses by @uk1288 [#712](https://github.com/hashicorp/go-tfe/pull/712)
## Bug Fixes
* AgentPool `Update` is not able to remove all allowed workspaces from an agent pool. That operation is now handled by a separate `UpdateAllowedWorkspaces` method using `AgentPoolAllowedWorkspacesUpdateOptions` by @hs26gill [#701](https://github.com/hashicorp/go-tfe/pull/701)
# v1.26.0
## Enhancements
* Adds BETA fields `ResourceImports` count to both `Plan` and `Apply` types as well as `AllowConfigGeneration` to the `Run` struct type. These fields are not generally available and are subject to change in a future release.
# v1.25.1
## Bug Fixes
* Workspace safe delete conflict error when workspace is locked has been restored
to the original message using the error `ErrWorkspaceLockedCannotDelete` instead of
`ErrWorkspaceLocked`
# v1.25.0
## Enhancements
* Workspace safe delete 409 conflict errors associated with resources still being managed or being processed (indicating that you should try again later) are now the named errors `ErrWorkspaceStillProcessing` and `ErrWorkspaceNotSafeToDelete` by @brandonc [#703](https://github.com/hashicorp/go-tfe/pull/703)
# v1.24.0
## Enhancements
* Adds support for a new variable field `version-id` by @arybolovlev [#697](https://github.com/hashicorp/go-tfe/pull/697)
* Adds `ExpiredAt` field to `OrganizationToken`, `TeamToken`, and `UserToken`. This enhancement will be available in TFE release, v202305-1. @JuliannaTetreault [#672](https://github.com/hashicorp/go-tfe/pull/672)
* Adds `ContextWithResponseHeaderHook` context for use with the ClientRequest Do method that allows callers to define a callback which receives raw http Response headers. @apparentlymart [#689](https://github.com/hashicorp/go-tfe/pull/689)
# v1.23.0
## Features
* `ApplyToProjects` and `RemoveFromProjects` to `VariableSets` endpoints now generally available.
* `ListForProject` to `VariableSets` endpoints now generally available.
## Enhancements
* Adds `OrganizationScoped` and `AllowedWorkspaces` fields for creating workspace scoped agent pools and adds `AllowedWorkspacesName` for filtering agents pools associated with a given workspace by @hs26gill [#682](https://github.com/hashicorp/go-tfe/pull/682/files)
## Bug Fixes
# v1.22.0
## Beta API Changes
* The beta `no_code` field in `RegistryModuleCreateOptions` has been changed from `bool` to `*bool` and will be removed in a future version because a new, preferred method for managing no-code registry modules has been added in this release.
## Features
* Add beta endpoints `Create`, `Read`, `Update`, and `Delete` to manage no-code provisioning for a `RegistryModule`. This allows users to enable no-code provisioning for a registry module, and to configure the provisioning settings for that module version. This also allows users to disable no-code provisioning for a module version. @dsa0x [#669](https://github.com/hashicorp/go-tfe/pull/669)
# v1.21.0
## Features
* Add beta endpoints `ApplyToProjects` and `RemoveFromProjects` to `VariableSets`. Applying a variable set to a project will apply that variable set to all current and future workspaces in that project.
* Add beta endpoint `ListForProject` to `VariableSets` to list all variable sets applied to a project.
* Add endpoint `RunEvents` which lists events for a specific run by @glennsarti [#680](https://github.com/hashicorp/go-tfe/pull/680)
## Bug Fixes
* `VariableSets.Read` did not honor the Include values due to a syntax error in the struct tag of `VariableSetReadOptions` by @sgap [#678](https://github.com/hashicorp/go-tfe/pull/678)
## Enhancements
* Adds `ProjectID` filter to allow filtering of workspaces of a given project in an organization by @hs26gill [#671](https://github.com/hashicorp/go-tfe/pull/671)
* Adds `Name` filter to allow filtering of projects by @hs26gill [#668](https://github.com/hashicorp/go-tfe/pull/668/files)
* Adds `ManageMembership` permission to team `OrganizationAccess` by @JarrettSpiker [#652](https://github.com/hashicorp/go-tfe/pull/652)
* Adds `RotateKey` and `TrimKey` Admin endpoints by @mpminardi [#666](https://github.com/hashicorp/go-tfe/pull/666)
* Adds `Permissions` to `User` by @jeevanragula [#674](https://github.com/hashicorp/go-tfe/pull/674)
* Adds `IsEnterprise` and `IsCloud` boolean methods to the client by @sebasslash [#675](https://github.com/hashicorp/go-tfe/pull/675)
# v1.20.0
## Enhancements
* Update team project access to include additional project roles by @joekarl [#642](https://github.com/hashicorp/go-tfe/pull/642)
# v1.19.0
## Enhancements
* Removed Beta tags from `Project` features by @hs26gill [#637](https://github.com/hashicorp/go-tfe/pull/637)
* Add `Filter` and `Sort` fields to `AdminWorkspaceListOptions` to allow filtering and sorting of workspaces by @laurenolivia [#641](https://github.com/hashicorp/go-tfe/pull/641)
* Add support for `List` and `Read` Github app installation APIs by @roleesinhaHC [#655](https://github.com/hashicorp/go-tfe/pull/655)
* Add `GHAInstallationID` field to `VCSRepoOptions` and `VCSRepo` structs by @roleesinhaHC [#655](https://github.com/hashicorp/go-tfe/pull/655)
# v1.18.0
## Enhancements
* Adds `BaseURL` and `BaseRegistryURL` methods to `Client` to expose its configuration by @brandonc [#638](https://github.com/hashicorp/go-tfe/pull/638)
* Adds `ReadWorkspaces` and `ReadProjects` permissions to `Organizations` by @JuliannaTetreault [#614](https://github.com/hashicorp/go-tfe/pull/614)
# v1.17.0
## Enhancements
* Add Beta endpoint `TeamProjectAccesses` to manage Project Access for Teams by @hs26gill [#599](https://github.com/hashicorp/go-tfe/pull/599)
* Updates api doc links from terraform.io to developer.hashicorp domain by @uk1288 [#629](https://github.com/hashicorp/go-tfe/pull/629)
* Adds `UploadTarGzip()` method to `RegistryModules` and `ConfigurationVersions` interface by @sebasslash [#623](https://github.com/hashicorp/go-tfe/pull/623)
* Adds `ManageProjects` field to `OrganizationAccess` struct by @hs26gill [#633](https://github.com/hashicorp/go-tfe/pull/633)
* Adds agent-count to `AgentPools` endpoint. @evilensky [#611](https://github.com/hashicorp/go-tfe/pull/611)
* Adds `Links` to `Workspace`, (currently contains "self" and "self-html" paths) @brandonc [#622](https://github.com/hashicorp/go-tfe/pull/622)
# v1.16.0
## Bug Fixes
* Project names were being incorrectly validated as ID's @brandonc [#608](https://github.com/hashicorp/go-tfe/pull/608)
## Enhancements
* Adds `List()` method to `GPGKeys` interface by @sebasslash [#602](https://github.com/hashicorp/go-tfe/pull/602)
* Adds `ProviderBinaryUploaded` field to `RegistryPlatforms` struct by @sebasslash [#602](https://github.com/hashicorp/go-tfe/pull/602)
# v1.15.0
## Enhancements
* Add Beta `Projects` endpoint. The API is in not yet available to all users @hs26gill [#564](https://github.com/hashicorp/go-tfe/pull/564)
# v1.14.0
## Enhancements
* Adds Beta parameter `Overridable` for OPA `policy set` update API (`PolicySetUpdateOptions`) @mrinalirao [#594](https://github.com/hashicorp/go-tfe/pull/594)
* Adds new task stage status values representing `canceled`, `errored`, `unreachable` @mrinalirao [#594](https://github.com/hashicorp/go-tfe/pull/594)
# v1.13.0
## Bug Fixes
* Fixes `AuditTrail` pagination parameters (`CurrentPage`, `PreviousPage`, `NextPage`, `TotalPages`, `TotalCount`), which were not deserialized after reading from the List endpoint by @brandonc [#586](https://github.com/hashicorp/go-tfe/pull/586)
## Enhancements
* Add OPA support to the Policy Set APIs by @mrinalirao [#575](https://github.com/hashicorp/go-tfe/pull/575)
* Add OPA support to the Policy APIs by @mrinalirao [#579](https://github.com/hashicorp/go-tfe/pull/579)
* Add support for enabling no-code provisioning in an existing or new `RegistryModule` by @miguelhrocha [#562](https://github.com/hashicorp/go-tfe/pull/562)
* Add Policy Evaluation and Policy Set Outcome APIs by @mrinalirao [#583](https://github.com/hashicorp/go-tfe/pull/583)
* Add OPA support to Task Stage APIs by @mrinalirao [#584](https://github.com/hashicorp/go-tfe/pull/584)
# v1.12.0
## Enhancements
* Add `search[wildcard-name]` to `WorkspaceListOptions` by @laurenolivia [#569](https://github.com/hashicorp/go-tfe/pull/569)
* Add `NotificationTriggerAssessmentCheckFailed` notification trigger type by @rexredinger [#549](https://github.com/hashicorp/go-tfe/pull/549)
* Add `RemoteTFEVersion()` to the `Client` interface, which exposes the `X-TFE-Version` header set by a remote TFE instance by @sebasslash [#563](https://github.com/hashicorp/go-tfe/pull/563)
* Validate the module version as a version instead of an ID [#409](https://github.com/hashicorp/go-tfe/pull/409)
* Add `AllowForceDeleteWorkspaces` setting to `Organizations` by @JarrettSpiker [#539](https://github.com/hashicorp/go-tfe/pull/539)
* Add `SafeDelete` and `SafeDeleteID` APIs to `Workspaces` by @JarrettSpiker [#539](https://github.com/hashicorp/go-tfe/pull/539)
* Add `ForceExecute()` to `Runs` to allow force executing a run by @annawinkler [#570](https://github.com/hashicorp/go-tfe/pull/570)
* Pre-plan and Pre-Apply Run Tasks are now generally available (beta comments removed) by @glennsarti [#555](https://github.com/hashicorp/go-tfe/pull/555)
# v1.11.0
## Enhancements
* Add `Query` and `Status` fields to `OrganizationMembershipListOptions` to allow filtering memberships by status or username by @sebasslash [#550](https://github.com/hashicorp/go-tfe/pull/550)
* Add `ListForWorkspace` method to `VariableSets` interface to enable fetching variable sets associated with a workspace by @tstapler [#552](https://github.com/hashicorp/go-tfe/pull/552)
* Add `NotificationTriggerAssessmentDrifted` and `NotificationTriggerAssessmentFailed` notification trigger types by @lawliet89 [#542](https://github.com/hashicorp/go-tfe/pull/542)
## Bug Fixes
* Fix marshalling of run variables in `RunCreateOptions`. The `Variables` field type in `Run` struct has changed from `[]*RunVariable` to `[]*RunVariableAttr` by @Uk1288 [#531](https://github.com/hashicorp/go-tfe/pull/531)
# v1.10.0
## Enhancements
* Add `Query` param field to `OrganizationListOptions` to allow searching based on name or email by @laurenolivia [#529](https://github.com/hashicorp/go-tfe/pull/529)
* Add optional `AssessmentsEnforced` to organizations and `AssessmentsEnabled` to workspaces for managing the workspace and organization health assessment (drift detection) setting by @rexredinger [#462](https://github.com/hashicorp/go-tfe/pull/462)
## Bug Fixes
* Fixes null value returned in variable set relationship in `VariableSetVariable` by @sebasslash [#521](https://github.com/hashicorp/go-tfe/pull/521)
# v1.9.0
## Enhancements
* `RunListOptions` is generally available, and rename field (Name -> User) by @mjyocca [#472](https://github.com/hashicorp/go-tfe/pull/472)
* [Beta] Adds optional `JsonState` field to `StateVersionCreateOptions` by @megan07 [#514](https://github.com/hashicorp/go-tfe/pull/514)
## Bug Fixes
* Fixed invalid memory address error when using `TaskResults` field by @glennsarti [#517](https://github.com/hashicorp/go-tfe/pull/517)
# v1.8.0
## Enhancements
* Adds support for reading and listing Agents by @laurenolivia [#456](https://github.com/hashicorp/go-tfe/pull/456)
* It was previously logged that we added an `Include` param field to `PolicySetListOptions` to allow policy list to include related resource data such as workspaces, policies, newest_version, or current_version by @Uk1288 [#497](https://github.com/hashicorp/go-tfe/pull/497) in 1.7.0, but this was a mistake and the field is added in v1.8.0
# v1.7.0
## Enhancements
* Adds new run creation attributes: `allow-empty-apply`, `terraform-version`, `plan-only` by @sebasslash [#482](https://github.com/hashicorp/go-tfe/pull/447)
* Adds additional Task Stage and Run Statuses for Pre-plan run tasks by @glennsarti [#469](https://github.com/hashicorp/go-tfe/pull/469)
* Adds `stage` field to the create and update methods for Workspace Run Tasks by @glennsarti [#469](https://github.com/hashicorp/go-tfe/pull/469)
* Adds `ResourcesProcessed`, `StateVersion`, `TerraformVersion`, `Modules`, `Providers`, and `Resources` fields to the State Version struct by @laurenolivia [#484](https://github.com/hashicorp/go-tfe/pull/484)
* Add `Include` param field to `PolicySetListOptions` to allow policy list to include related resource data such as workspaces, policies, newest_version, or current_version by @Uk1288 [#497](https://github.com/hashicorp/go-tfe/pull/497)
* Allow FileTriggersEnabled to be set to false when Git tags are present by @mjyocca @hashimoon [#468] (https://github.com/hashicorp/go-tfe/pull/468)
# v1.6.0
## Enhancements
* Remove beta messaging for Run Tasks by @glennsarti [#447](https://github.com/hashicorp/go-tfe/pull/447)
* Adds `Description` field to the `RunTask` object by @glennsarti [#447](https://github.com/hashicorp/go-tfe/pull/447)
* Add `Name` field to `OAuthClient` by @barrettclark [#466](https://github.com/hashicorp/go-tfe/pull/466)
* Add support for creating both public and private `RegistryModule` with no VCS connection by @Uk1288 [#460](https://github.com/hashicorp/go-tfe/pull/460)
* Add `ConfigurationSourceAdo` configuration source option by @mjyocca [#467](https://github.com/hashicorp/go-tfe/pull/467)
* [beta] state version outputs may now include a detailed-type attribute in a future API release by @brandonc [#479](https://github.com/hashicorp/go-tfe/pull/429)
# v1.5.0
## Enhancements
* [beta] Add support for triggering Workspace runs through matching Git tags [#434](https://github.com/hashicorp/go-tfe/pull/434)
* Add `Query` param field to `AgentPoolListOptions` to allow searching based on agent pool name, by @JarrettSpiker [#417](https://github.com/hashicorp/go-tfe/pull/417)
* Add organization scope and allowed workspaces field for scope agents by @Netra2104 [#453](https://github.com/hashicorp/go-tfe/pull/453)
* Adds `Namespace` and `RegistryName` fields to `RegistryModuleID` to allow reading of Public Registry Modules by @Uk1288 [#464](https://github.com/hashicorp/go-tfe/pull/464)
## Bug fixes
* Fixed JSON mapping for Configuration Versions failing to properly set the `speculative` property [#459](https://github.com/hashicorp/go-tfe/pull/459)
# v1.4.0
## Enhancements
* Adds `RetryServerErrors` field to the `Config` object by @sebasslash [#439](https://github.com/hashicorp/go-tfe/pull/439)
* Adds support for the GPG Keys API by @sebasslash [#429](https://github.com/hashicorp/go-tfe/pull/429)
* Adds support for new `WorkspaceLimit` Admin setting for organizations [#425](https://github.com/hashicorp/go-tfe/pull/425)
* Adds support for new `ExcludeTags` workspace list filter field by @Uk1288 [#438](https://github.com/hashicorp/go-tfe/pull/438)
* [beta] Adds additional filter fields to `RunListOptions` by @mjyocca [#424](https://github.com/hashicorp/go-tfe/pull/424)
* [beta] Renames the optional StateVersion field `ExtState` to `JSONStateOutputs` and changes the purpose and type by @annawinkler [#444](https://github.com/hashicorp/go-tfe/pull/444) and @brandoncroft [#452](https://github.com/hashicorp/go-tfe/pull/452)
# v1.3.0
## Enhancements
* Adds support for Microsoft Teams notification configuration by @JarrettSpiker [#398](https://github.com/hashicorp/go-tfe/pull/389)
* Add support for Audit Trail API by @sebasslash [#407](https://github.com/hashicorp/go-tfe/pull/407)
* Adds Private Registry Provider, Provider Version, and Provider Platform APIs support by @joekarl and @annawinkler [#313](https://github.com/hashicorp/go-tfe/pull/313)
* Adds List Registry Modules endpoint by @chroju [#385](https://github.com/hashicorp/go-tfe/pull/385)
* Adds `WebhookURL` field to `VCSRepo` struct by @kgns [#413](https://github.com/hashicorp/go-tfe/pull/413)
* Adds `Category` field to `VariableUpdateOptions` struct by @jtyr [#397](https://github.com/hashicorp/go-tfe/pull/397)
* Adds `TriggerPatterns` to `Workspace` by @matejrisek [#400](https://github.com/hashicorp/go-tfe/pull/400)
* [beta] Adds `ExtState` field to `StateVersionCreateOptions` by @brandonc [#416](https://github.com/hashicorp/go-tfe/pull/416)
# v1.2.0
## Enhancements
* Adds support for reading current state version outputs to StateVersionOutputs, which can be useful for reading outputs when users don't have the necessary permissions to read the entire state by @brandonc [#370](https://github.com/hashicorp/go-tfe/pull/370)
* Adds Variable Set methods for `ApplyToWorkspaces` and `RemoveFromWorkspaces` by @byronwolfman [#375](https://github.com/hashicorp/go-tfe/pull/375)
* Adds `Names` query param field to `TeamListOptions` by @sebasslash [#393](https://github.com/hashicorp/go-tfe/pull/393)
* Adds `Emails` query param field to `OrganizationMembershipListOptions` by @sebasslash [#393](https://github.com/hashicorp/go-tfe/pull/393)
* Adds Run Tasks API support by @glennsarti [#381](https://github.com/hashicorp/go-tfe/pull/381), [#382](https://github.com/hashicorp/go-tfe/pull/382) and [#383](https://github.com/hashicorp/go-tfe/pull/383)
## Bug fixes
* Fixes ignored comment when performing apply, discard, cancel, and force-cancel run actions [#388](https://github.com/hashicorp/go-tfe/pull/388)
# v1.1.0
## Enhancements
* Add Variable Set API support by @rexredinger [#305](https://github.com/hashicorp/go-tfe/pull/305)
* Add Comments API support by @alex-ikse [#355](https://github.com/hashicorp/go-tfe/pull/355)
* Add beta support for SSOTeamID to `Team`, `TeamCreateOptions`, `TeamUpdateOptions` by @xlgmokha [#364](https://github.com/hashicorp/go-tfe/pull/364)
# v1.0.0
## Breaking Changes
* Renamed methods named Generate to Create for `AgentTokens`, `OrganizationTokens`, `TeamTokens`, `UserTokens` by @sebasslash [#327](https://github.com/hashicorp/go-tfe/pull/327)
* Methods that express an action on a relationship have been prefixed with a verb, e.g `Current()` is now `ReadCurrent()` by @sebasslash [#327](https://github.com/hashicorp/go-tfe/pull/327)
* All list option structs are now pointers @uturunku1 [#309](https://github.com/hashicorp/go-tfe/pull/309)
* All errors have been refactored into constants in `errors.go` @uturunku1 [#310](https://github.com/hashicorp/go-tfe/pull/310)
* The `ID` field in Create/Update option structs has been renamed to `Type` in accordance with the JSON:API spec by @omarismail, @uturunku1 [#190](https://github.com/hashicorp/go-tfe/pull/190), [#323](https://github.com/hashicorp/go-tfe/pull/323), [#332](https://github.com/hashicorp/go-tfe/pull/332)
* Nested URL params (consisting of an organization, module and provider name) used to identify a `RegistryModule` have been refactored into a struct `RegistryModuleID` by @sebasslash [#337](https://github.com/hashicorp/go-tfe/pull/337)
## Enhancements
* Added missing include fields for `AdminRuns`, `AgentPools`, `ConfigurationVersions`, `OAuthClients`, `Organizations`, `PolicyChecks`, `PolicySets`, `Policies` and `RunTriggers` by @uturunku1 [#339](https://github.com/hashicorp/go-tfe/pull/339)
* Cleanup documentation and improve consistency by @uturunku1 [#331](https://github.com/hashicorp/go-tfe/pull/331)
* Add more linters to our CI pipeline by @sebasslash [#326](https://github.com/hashicorp/go-tfe/pull/326)
* Resolve `TFE_HOSTNAME` as fallback for `TFE_ADDRESS` by @sebasslash [#340](https://github.com/hashicorp/go-tfe/pull/326)
* Adds a `fetching` status to `RunStatus` and adds the `Archive` method to the ConfigurationVersions interface by @mpminardi [#338](https://github.com/hashicorp/go-tfe/pull/338)
* Added a `Download` method to the `ConfigurationVersions` interface by @tylerwolf [#358](https://github.com/hashicorp/go-tfe/pull/358)
* API Coverage documentation by @laurenolivia [#334](https://github.com/hashicorp/go-tfe/pull/334)
## Bug Fixes
* Fixed invalid memory address error when `AdminSMTPSettingsUpdateOptions.Auth` field is empty and accessed by @uturunku1 [#335](https://github.com/hashicorp/go-tfe/pull/335)
================================================
FILE: LICENSE
================================================
Copyright (c) 2018 HashiCorp, Inc.
Mozilla Public License, version 2.0
1. Definitions
1.1. “Contributor”
means each individual or legal entity that creates, contributes to the
creation of, or owns Covered Software.
1.2. “Contributor Version”
means the combination of the Contributions of others (if any) used by a
Contributor and that particular Contributor’s Contribution.
1.3. “Contribution”
means Covered Software of a particular Contributor.
1.4. “Covered Software”
means Source Code Form to which the initial Contributor has attached the
notice in Exhibit A, the Executable Form of such Source Code Form, and
Modifications of such Source Code Form, in each case including portions
thereof.
1.5. “Incompatible With Secondary Licenses”
means
a. that the initial Contributor has attached the notice described in
Exhibit B to the Covered Software; or
b. that the Covered Software was made available under the terms of version
1.1 or earlier of the License, but not also under the terms of a
Secondary License.
1.6. “Executable Form”
means any form of the work other than Source Code Form.
1.7. “Larger Work”
means a work that combines Covered Software with other material, in a separate
file or files, that is not Covered Software.
1.8. “License”
means this document.
1.9. “Licensable”
means having the right to grant, to the maximum extent possible, whether at the
time of the initial grant or subsequently, any and all of the rights conveyed by
this License.
1.10. “Modifications”
means any of the following:
a. any file in Source Code Form that results from an addition to, deletion
from, or modification of the contents of Covered Software; or
b. any new file in Source Code Form that contains any Covered Software.
1.11. “Patent Claims” of a Contributor
means any patent claim(s), including without limitation, method, process,
and apparatus claims, in any patent Licensable by such Contributor that
would be infringed, but for the grant of the License, by the making,
using, selling, offering for sale, having made, import, or transfer of
either its Contributions or its Contributor Version.
1.12. “Secondary License”
means either the GNU General Public License, Version 2.0, the GNU Lesser
General Public License, Version 2.1, the GNU Affero General Public
License, Version 3.0, or any later versions of those licenses.
1.13. “Source Code Form”
means the form of the work preferred for making modifications.
1.14. “You” (or “Your”)
means an individual or a legal entity exercising rights under this
License. For legal entities, “You” includes any entity that controls, is
controlled by, or is under common control with You. For purposes of this
definition, “control” means (a) the power, direct or indirect, to cause
the direction or management of such entity, whether by contract or
otherwise, or (b) ownership of more than fifty percent (50%) of the
outstanding shares or beneficial ownership of such entity.
2. License Grants and Conditions
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
a. under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or as
part of a Larger Work; and
b. under Patent Claims of such Contributor to make, use, sell, offer for
sale, have made, import, and otherwise transfer either its Contributions
or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution become
effective for each Contribution on the date the Contributor first distributes
such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under this
License. No additional rights or licenses will be implied from the distribution
or licensing of Covered Software under this License. Notwithstanding Section
2.1(b) above, no patent license is granted by a Contributor:
a. for any code that a Contributor has removed from Covered Software; or
b. for infringements caused by: (i) Your and any other third party’s
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
c. under Patent Claims infringed by Covered Software in the absence of its
Contributions.
This License does not grant any rights in the trademarks, service marks, or
logos of any Contributor (except as may be necessary to comply with the
notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this License
(see Section 10.2) or under the terms of a Secondary License (if permitted
under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its Contributions
are its original creation(s) or it has sufficient rights to grant the
rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under applicable
copyright doctrines of fair use, fair dealing, or other equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
Section 2.1.
3. Responsibilities
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under the
terms of this License. You must inform recipients that the Source Code Form
of the Covered Software is governed by the terms of this License, and how
they can obtain a copy of this License. You may not attempt to alter or
restrict the recipients’ rights in the Source Code Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
a. such Covered Software must also be made available in Source Code Form,
as described in Section 3.1, and You must inform recipients of the
Executable Form how they can obtain a copy of such Source Code Form by
reasonable means in a timely manner, at a charge no more than the cost
of distribution to the recipient; and
b. You may distribute such Executable Form under the terms of this License,
or sublicense it under different terms, provided that the license for
the Executable Form does not attempt to limit or alter the recipients’
rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for the
Covered Software. If the Larger Work is a combination of Covered Software
with a work governed by one or more Secondary Licenses, and the Covered
Software is not Incompatible With Secondary Licenses, this License permits
You to additionally distribute such Covered Software under the terms of
such Secondary License(s), so that the recipient of the Larger Work may, at
their option, further distribute the Covered Software under the terms of
either this License or such Secondary License(s).
3.4. Notices
You may not remove or alter the substance of any license notices (including
copyright notices, patent notices, disclaimers of warranty, or limitations
of liability) contained within the Source Code Form of the Covered
Software, except that You may alter any license notices to the extent
required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on behalf
of any Contributor. You must make it absolutely clear that any such
warranty, support, indemnity, or liability obligation is offered by You
alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
If it is impossible for You to comply with any of the terms of this License
with respect to some or all of the Covered Software due to statute, judicial
order, or regulation then You must: (a) comply with the terms of this License
to the maximum extent possible; and (b) describe the limitations and the code
they affect. Such description must be placed in a text file included with all
distributions of the Covered Software under this License. Except to the
extent prohibited by statute or regulation, such description must be
sufficiently detailed for a recipient of ordinary skill to be able to
understand it.
5. Termination
5.1. The rights granted under this License will terminate automatically if You
fail to comply with any of its terms. However, if You become compliant,
then the rights granted under this License from a particular Contributor
are reinstated (a) provisionally, unless and until such Contributor
explicitly and finally terminates Your grants, and (b) on an ongoing basis,
if such Contributor fails to notify You of the non-compliance by some
reasonable means prior to 60 days after You have come back into compliance.
Moreover, Your grants from a particular Contributor are reinstated on an
ongoing basis if such Contributor notifies You of the non-compliance by
some reasonable means, this is the first time You have received notice of
non-compliance with this License from such Contributor, and You become
compliant prior to 30 days after Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions, counter-claims,
and cross-claims) alleging that a Contributor Version directly or
indirectly infringes any patent, then the rights granted to You by any and
all Contributors for the Covered Software under Section 2.1 of this License
shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
license agreements (excluding distributors and resellers) which have been
validly granted by You or Your distributors under this License prior to
termination shall survive termination.
6. Disclaimer of Warranty
Covered Software is provided under this License on an “as is” basis, without
warranty of any kind, either expressed, implied, or statutory, including,
without limitation, warranties that the Covered Software is free of defects,
merchantable, fit for a particular purpose or non-infringing. The entire
risk as to the quality and performance of the Covered Software is with You.
Should any Covered Software prove defective in any respect, You (not any
Contributor) assume the cost of any necessary servicing, repair, or
correction. This disclaimer of warranty constitutes an essential part of this
License. No use of any Covered Software is authorized under this License
except under this disclaimer.
7. Limitation of Liability
Under no circumstances and under no legal theory, whether tort (including
negligence), contract, or otherwise, shall any Contributor, or anyone who
distributes Covered Software as permitted above, be liable to You for any
direct, indirect, special, incidental, or consequential damages of any
character including, without limitation, damages for lost profits, loss of
goodwill, work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses, even if such party shall have been
informed of the possibility of such damages. This limitation of liability
shall not apply to liability for death or personal injury resulting from such
party’s negligence to the extent applicable law prohibits such limitation.
Some jurisdictions do not allow the exclusion or limitation of incidental or
consequential damages, so this exclusion and limitation may not apply to You.
8. Litigation
Any litigation relating to this License may be brought only in the courts of
a jurisdiction where the defendant maintains its principal place of business
and such litigation shall be governed by laws of that jurisdiction, without
reference to its conflict-of-law provisions. Nothing in this Section shall
prevent a party’s ability to bring cross-claims or counter-claims.
9. Miscellaneous
This License represents the complete agreement concerning the subject matter
hereof. If any provision of this License is held to be unenforceable, such
provision shall be reformed only to the extent necessary to make it
enforceable. Any law or regulation which provides that the language of a
contract shall be construed against the drafter shall not be used to construe
this License against a Contributor.
10. Versions of the License
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version of
the License under which You originally received the Covered Software, or
under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a modified
version of this License if you rename the license and remove any
references to the name of the license steward (except to note that such
modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular file, then
You may include the notice in a location (such as a LICENSE file in a relevant
directory) where a recipient would be likely to look for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - “Incompatible With Secondary Licenses” Notice
This Source Code Form is “Incompatible
With Secondary Licenses”, as defined by
the Mozilla Public License, v. 2.0.
================================================
FILE: META.d/_summary.yaml
================================================
# Copyright IBM Corp. 2018, 2025
# SPDX-License-Identifier: MPL-2.0
---
schema: 1.1
partition: tfc
category: library
summary:
owner: team-tf-core-cloud
description: HCP Terraform and Terraform Enterprise API Client/SDK in Golang
visibility: external
================================================
FILE: META.d/data.yaml
================================================
# Copyright IBM Corp. 2018, 2025
# SPDX-License-Identifier: MPL-2.0
data_summary:
gdpr:
exempt: true
last_reviewed: 2024-11-04
================================================
FILE: Makefile
================================================
.PHONY: vet fmt lint test mocks envvars generate
# Make target to generate resource scaffolding for specified RESOURCE
generate: check-resource
@cd ./scripts/generate_resource; \
go mod tidy; \
go run . $(RESOURCE) ;
vet:
go vet
fmt:
gofmt -s -l -w .
fmtcheck:
./scripts/gofmtcheck.sh
lint:
golangci-lint run .
test:
go test ./... $(TESTARGS) -timeout=30m
# Make target to generate mocks for specified FILENAME
mocks: check-filename
@echo "mockgen -source=$(FILENAME) -destination=mocks/$(subst .go,_mocks.go,$(FILENAME)) -package=mocks" >> generate_mocks.sh
./generate_mocks.sh
envvars:
./scripts/setup-test-envvars.sh
check-filename:
ifndef FILENAME
$(error Missing FILENAME param. Example usage: FILENAME=example_resource.go make mocks)
endif
check-resource:
ifndef RESOURCE
$(error Missing RESOURCE param. Example usage: RESOURCE=foo_bar make generate)
endif
================================================
FILE: README.md
================================================
HCP Terraform and Terraform Enterprise Go Client
==============================
[](https://github.com/hashicorp/go-tfe/actions/workflows/ci.yml)
[](https://github.com/hashicorp/go-tfe/blob/main/LICENSE)
[](https://godoc.org/github.com/hashicorp/go-tfe)
[](https://goreportcard.com/report/github.com/hashicorp/go-tfe)
[](https://github.com/hashicorp/go-tfe/issues)
The official Go API client for [HCP Terraform and Terraform Enterprise](https://www.hashicorp.com/products/terraform).
This client supports the [HCP Terraform V2 API](https://developer.hashicorp.com/terraform/cloud-docs/api-docs).
As Terraform Enterprise is a self-hosted distribution of HCP Terraform, this
client supports both HCP Terraform and Terraform Enterprise use cases. In all package
documentation and API, the platform will always be stated as 'Terraform
Enterprise' - but a feature will be explicitly noted as only supported in one or
the other, if applicable (rare).
## Version Information
Almost always, minor version changes will indicate backwards-compatible features and enhancements. Occasionally, function signature changes that reflect a bug fix may appear as a minor version change. Patch version changes will be used for bug fixes, performance improvements, and otherwise unimpactful changes.
## Example Usage
Construct a new TFE client, then use the various endpoints on the client to
access different parts of the Terraform Enterprise API. The following example lists
all organizations.
### (Recommended Approach) Using custom config to provide configuration details to the API client
```go
import (
"context"
"log"
"github.com/hashicorp/go-tfe"
)
config := &tfe.Config{
Address: "https://tfe.local",
Token: "insert-your-token-here",
RetryServerErrors: true,
}
client, err := tfe.NewClient(config)
if err != nil {
log.Fatal(err)
}
orgs, err := client.Organizations.List(context.Background(), nil)
if err != nil {
log.Fatal(err)
}
```
### Using the default config with env vars
The default configuration makes use of the `TFE_ADDRESS` and `TFE_TOKEN` environment variables.
1. `TFE_ADDRESS` - URL of a HCP Terraform or Terraform Enterprise instance. Example: `https://tfe.local`
1. `TFE_TOKEN` - An [API token](https://developer.hashicorp.com/terraform/cloud-docs/users-teams-organizations/api-tokens) for the HCP Terraform or Terraform Enterprise instance.
**Note:** Alternatively, you can set `TFE_HOSTNAME` which serves as a fallback for `TFE_ADDRESS`. It will only be used if `TFE_ADDRESS` is not set and will resolve the host to an `https` scheme. Example: `tfe.local` => resolves to `https://tfe.local`
The environment variables are used as a fallback to configure TFE client if the Address or Token values are not provided as in the cases below:
#### Using the default configuration
```go
import (
"context"
"log"
"github.com/hashicorp/go-tfe"
)
// Passing nil to tfe.NewClient method will also use the default configuration
client, err := tfe.NewClient(tfe.DefaultConfig())
if err != nil {
log.Fatal(err)
}
orgs, err := client.Organizations.List(context.Background(), nil)
if err != nil {
log.Fatal(err)
}
```
#### When Address or Token has no value
```go
import (
"context"
"log"
"github.com/hashicorp/go-tfe"
)
config := &tfe.Config{
Address: "",
Token: "",
}
client, err := tfe.NewClient(config)
if err != nil {
log.Fatal(err)
}
orgs, err := client.Organizations.List(context.Background(), nil)
if err != nil {
log.Fatal(err)
}
```
## Documentation
For complete usage of the API client, see the [full package docs](https://pkg.go.dev/github.com/hashicorp/go-tfe).
## Examples
See the [examples directory](https://github.com/hashicorp/go-tfe/tree/main/examples).
## Running tests
See [TESTS.md](docs/TESTS.md).
## Issues and Contributing
See [CONTRIBUTING.md](docs/CONTRIBUTING.md)
## Releases
See [RELEASES.md](docs/RELEASES.md)
================================================
FILE: admin_opa_version.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"reflect"
"time"
)
// Compile-time proof of interface implementation.
var _ AdminOPAVersions = (*adminOPAVersions)(nil)
// AdminOPAVersions describes all the admin OPA versions related methods that
// the Terraform Enterprise API supports.
// Note that admin OPA versions are only available in Terraform Enterprise.
//
// TFE API docs: https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/opa-versions
type AdminOPAVersions interface {
// List all the OPA versions.
List(ctx context.Context, options *AdminOPAVersionsListOptions) (*AdminOPAVersionsList, error)
// Read a OPA version by its ID.
Read(ctx context.Context, id string) (*AdminOPAVersion, error)
// Create a OPA version.
Create(ctx context.Context, options AdminOPAVersionCreateOptions) (*AdminOPAVersion, error)
// Update a OPA version.
Update(ctx context.Context, id string, options AdminOPAVersionUpdateOptions) (*AdminOPAVersion, error)
// Delete a OPA version
Delete(ctx context.Context, id string) error
}
// adminOPAVersions implements AdminOPAVersions.
type adminOPAVersions struct {
client *Client
}
// AdminOPAVersion represents a OPA Version
type AdminOPAVersion struct {
ID string `jsonapi:"primary,opa-versions"`
Version string `jsonapi:"attr,version"`
URL string `jsonapi:"attr,url,omitempty"`
SHA string `jsonapi:"attr,sha,omitempty"`
Deprecated bool `jsonapi:"attr,deprecated"`
DeprecatedReason *string `jsonapi:"attr,deprecated-reason,omitempty"`
Official bool `jsonapi:"attr,official"`
Enabled bool `jsonapi:"attr,enabled"`
Beta bool `jsonapi:"attr,beta"`
Usage int `jsonapi:"attr,usage"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Archs []*ToolVersionArchitecture `jsonapi:"attr,archs,omitempty"`
}
// AdminOPAVersionsListOptions represents the options for listing
// OPA versions.
type AdminOPAVersionsListOptions struct {
ListOptions
// Optional: A query string to find an exact version
Filter string `url:"filter[version],omitempty"`
// Optional: A search query string to find all versions that match version substring
Search string `url:"search[version],omitempty"`
}
// AdminOPAVersionCreateOptions for creating an OPA version.
type AdminOPAVersionCreateOptions struct {
Type string `jsonapi:"primary,opa-versions"`
Version string `jsonapi:"attr,version"` // Required
URL string `jsonapi:"attr,url,omitempty"` // Required w/ SHA unless Archs are provided
SHA string `jsonapi:"attr,sha,omitempty"` // Required w/ URL unless Archs are provided
Official *bool `jsonapi:"attr,official,omitempty"`
Deprecated *bool `jsonapi:"attr,deprecated,omitempty"`
DeprecatedReason *string `jsonapi:"attr,deprecated-reason,omitempty"`
Enabled *bool `jsonapi:"attr,enabled,omitempty"`
Beta *bool `jsonapi:"attr,beta,omitempty"`
Archs []*ToolVersionArchitecture `jsonapi:"attr,archs,omitempty"` // Required unless URL and SHA are provided
}
// AdminOPAVersionUpdateOptions for updating OPA version.
type AdminOPAVersionUpdateOptions struct {
Type string `jsonapi:"primary,opa-versions"`
Version *string `jsonapi:"attr,version,omitempty"`
URL *string `jsonapi:"attr,url,omitempty"`
SHA *string `jsonapi:"attr,sha,omitempty"`
Official *bool `jsonapi:"attr,official,omitempty"`
Deprecated *bool `jsonapi:"attr,deprecated,omitempty"`
DeprecatedReason *string `jsonapi:"attr,deprecated-reason,omitempty"`
Enabled *bool `jsonapi:"attr,enabled,omitempty"`
Beta *bool `jsonapi:"attr,beta,omitempty"`
Archs []*ToolVersionArchitecture `jsonapi:"attr,archs,omitempty"`
}
// AdminOPAVersionsList represents a list of OPA versions.
type AdminOPAVersionsList struct {
*Pagination
Items []*AdminOPAVersion
}
// List all the OPA versions.
func (a *adminOPAVersions) List(ctx context.Context, options *AdminOPAVersionsListOptions) (*AdminOPAVersionsList, error) {
req, err := a.client.NewRequest("GET", "admin/opa-versions", options)
if err != nil {
return nil, err
}
ol := &AdminOPAVersionsList{}
err = req.Do(ctx, ol)
if err != nil {
return nil, err
}
return ol, nil
}
// Read a OPA version by its ID.
func (a *adminOPAVersions) Read(ctx context.Context, id string) (*AdminOPAVersion, error) {
if !validStringID(&id) {
return nil, ErrInvalidOPAVersionID
}
u := fmt.Sprintf("admin/opa-versions/%s", url.PathEscape(id))
req, err := a.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
ov := &AdminOPAVersion{}
err = req.Do(ctx, ov)
if err != nil {
return nil, err
}
return ov, nil
}
// Create a new OPA version.
func (a *adminOPAVersions) Create(ctx context.Context, options AdminOPAVersionCreateOptions) (*AdminOPAVersion, error) {
if err := options.valid(); err != nil {
return nil, err
}
req, err := a.client.NewRequest("POST", "admin/opa-versions", &options)
if err != nil {
return nil, err
}
ov := &AdminOPAVersion{}
err = req.Do(ctx, ov)
if err != nil {
return nil, err
}
return ov, nil
}
// Update an existing OPA version.
func (a *adminOPAVersions) Update(ctx context.Context, id string, options AdminOPAVersionUpdateOptions) (*AdminOPAVersion, error) {
if !validStringID(&id) {
return nil, ErrInvalidOPAVersionID
}
u := fmt.Sprintf("admin/opa-versions/%s", url.PathEscape(id))
req, err := a.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
ov := &AdminOPAVersion{}
err = req.Do(ctx, ov)
if err != nil {
return nil, err
}
return ov, nil
}
// Delete a OPA version.
func (a *adminOPAVersions) Delete(ctx context.Context, id string) error {
if !validStringID(&id) {
return ErrInvalidOPAVersionID
}
u := fmt.Sprintf("admin/opa-versions/%s", url.PathEscape(id))
req, err := a.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o AdminOPAVersionCreateOptions) valid() error {
if (reflect.DeepEqual(o, AdminOPAVersionCreateOptions{})) {
return ErrRequiredOPAVerCreateOps
}
if o.Version == "" {
return ErrRequiredVersion
}
if !o.validArch() {
return ErrRequiredArchsOrURLAndSha
}
return nil
}
func (o AdminOPAVersionCreateOptions) validArch() bool {
if o.Archs == nil && o.URL != "" && o.SHA != "" {
return true
}
for _, a := range o.Archs {
if !validArch(a) {
return false
}
}
return true
}
================================================
FILE: admin_opa_version_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAdminOPAVersions_List(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
t.Run("without list options", func(t *testing.T) {
oList, err := client.Admin.OPAVersions.List(ctx, nil)
require.NoError(t, err)
assert.NotEmpty(t, oList.Items)
})
t.Run("with list options", func(t *testing.T) {
oList, err := client.Admin.OPAVersions.List(ctx, &AdminOPAVersionsListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
// Out of range page number, so the items should be empty
assert.Empty(t, oList.Items)
assert.Equal(t, 999, oList.CurrentPage)
oList, err = client.Admin.OPAVersions.List(ctx, &AdminOPAVersionsListOptions{
ListOptions: ListOptions{
PageNumber: 1,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Equal(t, 1, oList.CurrentPage)
for _, item := range oList.Items {
assert.NotNil(t, item.ID)
assert.NotEmpty(t, item.Version)
assert.NotEmpty(t, item.URL)
assert.NotEmpty(t, item.SHA)
assert.NotNil(t, item.Official)
assert.NotNil(t, item.Deprecated)
if item.Deprecated {
assert.NotNil(t, item.DeprecatedReason)
} else {
assert.Nil(t, item.DeprecatedReason)
}
assert.NotNil(t, item.Enabled)
assert.NotNil(t, item.Beta)
assert.NotNil(t, item.Usage)
assert.NotNil(t, item.CreatedAt)
assert.NotEmpty(t, item.Archs)
}
})
t.Run("with filter query string", func(t *testing.T) {
oList, err := client.Admin.OPAVersions.List(ctx, &AdminOPAVersionsListOptions{
Filter: "0.59.0",
})
require.NoError(t, err)
assert.Equal(t, 1, len(oList.Items))
// Query for a OPA version that does not exist
oList, err = client.Admin.OPAVersions.List(ctx, &AdminOPAVersionsListOptions{
Filter: "1000.1000.42",
})
require.NoError(t, err)
assert.Empty(t, oList.Items)
})
t.Run("with search version query string", func(t *testing.T) {
searchVersion := "0.59.0"
oList, err := client.Admin.OPAVersions.List(ctx, &AdminOPAVersionsListOptions{
Search: searchVersion,
})
require.NoError(t, err)
assert.NotEmpty(t, oList.Items)
t.Run("ensure each version matches substring", func(t *testing.T) {
for _, item := range oList.Items {
assert.Equal(t, true, strings.Contains(item.Version, searchVersion))
}
})
})
}
func TestAdminOPAVersions_CreateDelete(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
version := createAdminOPAVersion()
url := "https://www.hashicorp.com"
amd64Sha := *String(genSha(t))
t.Run("with valid options including top level url & sha and archs", func(t *testing.T) {
opts := AdminOPAVersionCreateOptions{
Version: version,
Deprecated: Bool(true),
DeprecatedReason: String("Test Reason"),
Official: Bool(false),
Enabled: Bool(false),
Beta: Bool(false),
URL: url,
SHA: amd64Sha,
Archs: []*ToolVersionArchitecture{
{
URL: url,
Sha: amd64Sha,
OS: linux,
Arch: amd64,
},
{
URL: url,
Sha: *String(genSha(t)),
OS: linux,
Arch: arm64,
}},
}
ov, err := client.Admin.OPAVersions.Create(ctx, opts)
require.NoError(t, err)
defer func() {
deleteErr := client.Admin.OPAVersions.Delete(ctx, ov.ID)
require.NoError(t, deleteErr)
}()
assert.Equal(t, opts.Version, ov.Version)
assert.Equal(t, *opts.Official, ov.Official)
assert.Equal(t, *opts.Deprecated, ov.Deprecated)
assert.Equal(t, *opts.DeprecatedReason, *ov.DeprecatedReason)
assert.Equal(t, *opts.Enabled, ov.Enabled)
assert.Equal(t, *opts.Beta, ov.Beta)
assert.Equal(t, len(opts.Archs), len(ov.Archs))
assert.Equal(t, opts.URL, ov.URL)
assert.Equal(t, opts.SHA, ov.SHA)
for i, arch := range opts.Archs {
assert.Equal(t, arch.URL, ov.Archs[i].URL)
assert.Equal(t, arch.Sha, ov.Archs[i].Sha)
assert.Equal(t, arch.OS, ov.Archs[i].OS)
assert.Equal(t, arch.Arch, ov.Archs[i].Arch)
}
})
t.Run("with valid options including archs", func(t *testing.T) {
version = createAdminOPAVersion()
opts := AdminOPAVersionCreateOptions{
Version: version,
Deprecated: Bool(true),
DeprecatedReason: String("Test Reason"),
Official: Bool(false),
Enabled: Bool(false),
Beta: Bool(false),
Archs: []*ToolVersionArchitecture{
{
URL: "https://www.hashicorp.com",
Sha: *String(genSha(t)),
OS: linux,
Arch: amd64,
},
{
URL: "https://www.hashicorp.com",
Sha: *String(genSha(t)),
OS: linux,
Arch: arm64,
}},
}
ov, err := client.Admin.OPAVersions.Create(ctx, opts)
require.NoError(t, err)
defer func() {
deleteErr := client.Admin.OPAVersions.Delete(ctx, ov.ID)
require.NoError(t, deleteErr)
}()
assert.Equal(t, opts.Version, ov.Version)
assert.Equal(t, *opts.Official, ov.Official)
assert.Equal(t, *opts.Deprecated, ov.Deprecated)
assert.Equal(t, *opts.DeprecatedReason, *ov.DeprecatedReason)
assert.Equal(t, *opts.Enabled, ov.Enabled)
assert.Equal(t, *opts.Beta, ov.Beta)
assert.Equal(t, len(opts.Archs), len(ov.Archs))
for i, arch := range opts.Archs {
assert.Equal(t, arch.URL, ov.Archs[i].URL)
assert.Equal(t, arch.Sha, ov.Archs[i].Sha)
assert.Equal(t, arch.OS, ov.Archs[i].OS)
assert.Equal(t, arch.Arch, ov.Archs[i].Arch)
}
})
t.Run("with valid options including, url, and sha", func(t *testing.T) {
opts := AdminOPAVersionCreateOptions{
Version: version,
URL: url,
SHA: genSha(t),
Deprecated: Bool(true),
DeprecatedReason: String("Test Reason"),
Official: Bool(false),
Enabled: Bool(false),
Beta: Bool(false),
}
ov, err := client.Admin.OPAVersions.Create(ctx, opts)
require.NoError(t, err)
defer func() {
deleteErr := client.Admin.OPAVersions.Delete(ctx, ov.ID)
require.NoError(t, deleteErr)
}()
assert.Equal(t, opts.Version, ov.Version)
assert.Equal(t, opts.URL, ov.URL)
assert.Equal(t, opts.SHA, ov.SHA)
assert.Equal(t, *opts.Official, ov.Official)
assert.Equal(t, *opts.Deprecated, ov.Deprecated)
assert.Equal(t, *opts.DeprecatedReason, *ov.DeprecatedReason)
assert.Equal(t, *opts.Enabled, ov.Enabled)
assert.Equal(t, *opts.Beta, ov.Beta)
assert.Equal(t, 1, len(ov.Archs))
assert.Equal(t, opts.URL, ov.Archs[0].URL)
assert.Equal(t, opts.SHA, ov.Archs[0].Sha)
assert.Equal(t, linux, ov.Archs[0].OS)
assert.Equal(t, amd64, ov.Archs[0].Arch)
})
t.Run("with only required options including tool version url and sha", func(t *testing.T) {
version = createAdminOPAVersion()
opts := AdminOPAVersionCreateOptions{
Version: version,
URL: "https://www.hashicorp.com",
SHA: genSha(t),
}
ov, err := client.Admin.OPAVersions.Create(ctx, opts)
require.NoError(t, err)
defer func() {
deleteErr := client.Admin.OPAVersions.Delete(ctx, ov.ID)
require.NoError(t, deleteErr)
}()
assert.Equal(t, opts.Version, ov.Version)
assert.Equal(t, opts.URL, ov.URL)
assert.Equal(t, opts.SHA, ov.SHA)
assert.Equal(t, false, ov.Official)
assert.Equal(t, false, ov.Deprecated)
assert.Nil(t, ov.DeprecatedReason)
assert.Equal(t, true, ov.Enabled)
assert.Equal(t, false, ov.Beta)
assert.Equal(t, 1, len(ov.Archs))
assert.Equal(t, opts.URL, ov.Archs[0].URL)
assert.Equal(t, opts.SHA, ov.Archs[0].Sha)
assert.Equal(t, linux, ov.Archs[0].OS)
assert.Equal(t, amd64, ov.Archs[0].Arch)
})
t.Run("with only required options including archs", func(t *testing.T) {
version = createAdminOPAVersion()
opts := AdminOPAVersionCreateOptions{
Version: version,
Archs: []*ToolVersionArchitecture{
{
URL: url,
Sha: amd64Sha,
OS: linux,
Arch: amd64,
},
{
URL: url,
Sha: *String(genSha(t)),
OS: linux,
Arch: arm64,
}},
}
ov, err := client.Admin.OPAVersions.Create(ctx, opts)
require.NoError(t, err)
defer func() {
deleteErr := client.Admin.OPAVersions.Delete(ctx, ov.ID)
require.NoError(t, deleteErr)
}()
assert.Equal(t, opts.Version, ov.Version)
assert.Equal(t, false, ov.Official)
assert.Equal(t, false, ov.Deprecated)
assert.Nil(t, ov.DeprecatedReason)
assert.Equal(t, true, ov.Enabled)
assert.Equal(t, false, ov.Beta)
assert.Equal(t, len(opts.Archs), len(ov.Archs))
for i, arch := range opts.Archs {
assert.Equal(t, arch.URL, ov.Archs[i].URL)
assert.Equal(t, arch.Sha, ov.Archs[i].Sha)
assert.Equal(t, arch.OS, ov.Archs[i].OS)
assert.Equal(t, arch.Arch, ov.Archs[i].Arch)
}
})
t.Run("with empty options", func(t *testing.T) {
_, err := client.Admin.OPAVersions.Create(ctx, AdminOPAVersionCreateOptions{})
require.Equal(t, err, ErrRequiredOPAVerCreateOps)
})
}
func TestAdminOPAVersions_ReadUpdate(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
t.Run("reads and updates", func(t *testing.T) {
version := createAdminOPAVersion()
sha := String(genSha(t))
opts := AdminOPAVersionCreateOptions{
Version: version,
URL: "https://www.hashicorp.com",
SHA: genSha(t),
Official: Bool(false),
Deprecated: Bool(true),
DeprecatedReason: String("Test Reason"),
Enabled: Bool(false),
Beta: Bool(false),
Archs: []*ToolVersionArchitecture{{
URL: "https://www.hashicorp.com",
Sha: *sha,
OS: linux,
Arch: amd64,
}},
}
ov, err := client.Admin.OPAVersions.Create(ctx, opts)
require.NoError(t, err)
id := ov.ID
defer func() {
deleteErr := client.Admin.OPAVersions.Delete(ctx, id)
require.NoError(t, deleteErr)
}()
ov, err = client.Admin.OPAVersions.Read(ctx, id)
require.NoError(t, err)
assert.Equal(t, opts.Version, ov.Version)
assert.Equal(t, opts.Archs[0].URL, ov.URL)
assert.Equal(t, opts.Archs[0].Sha, ov.SHA)
assert.Equal(t, *opts.Official, ov.Official)
assert.Equal(t, *opts.Deprecated, ov.Deprecated)
assert.Equal(t, *opts.DeprecatedReason, *ov.DeprecatedReason)
assert.Equal(t, *opts.Enabled, ov.Enabled)
assert.Equal(t, *opts.Beta, ov.Beta)
assert.Equal(t, len(opts.Archs), len(ov.Archs))
for i, arch := range opts.Archs {
assert.Equal(t, arch.URL, ov.Archs[i].URL)
assert.Equal(t, arch.Sha, ov.Archs[i].Sha)
assert.Equal(t, arch.OS, ov.Archs[i].OS)
assert.Equal(t, arch.Arch, ov.Archs[i].Arch)
}
updateVersion := createAdminOPAVersion()
updateURL := "https://app.terraform.io/"
updateOpts := AdminOPAVersionUpdateOptions{
Version: String(updateVersion),
URL: String(updateURL),
Deprecated: Bool(false),
}
ov, err = client.Admin.OPAVersions.Update(ctx, id, updateOpts)
require.NoError(t, err)
assert.Equal(t, updateVersion, ov.Version)
assert.Equal(t, updateURL, ov.URL)
assert.Equal(t, opts.SHA, ov.SHA)
assert.Equal(t, *opts.Official, ov.Official)
assert.Equal(t, *updateOpts.Deprecated, ov.Deprecated)
assert.Equal(t, *opts.Enabled, ov.Enabled)
assert.Equal(t, *opts.Beta, ov.Beta)
assert.Equal(t, len(opts.Archs), len(ov.Archs))
assert.Equal(t, *updateOpts.URL, ov.Archs[0].URL)
assert.Equal(t, opts.Archs[0].Sha, ov.Archs[0].Sha)
assert.Equal(t, opts.Archs[0].OS, ov.Archs[0].OS)
assert.Equal(t, opts.Archs[0].Arch, ov.Archs[0].Arch)
})
t.Run("update with Archs", func(t *testing.T) {
version := genSafeRandomTerraformVersion()
sha := String(genSha(t))
opts := AdminOPAVersionCreateOptions{
Version: *String(version),
Official: Bool(false),
Deprecated: Bool(true),
DeprecatedReason: String("Test Reason"),
Enabled: Bool(false),
Beta: Bool(false),
Archs: []*ToolVersionArchitecture{{
URL: "https://www.hashicorp.com",
Sha: *sha,
OS: linux,
Arch: amd64,
}},
}
ov, err := client.Admin.OPAVersions.Create(ctx, opts)
require.NoError(t, err)
id := ov.ID
defer func() {
deleteErr := client.Admin.OPAVersions.Delete(ctx, id)
require.NoError(t, deleteErr)
}()
updateArchOpts := AdminOPAVersionUpdateOptions{
Archs: []*ToolVersionArchitecture{{
URL: "https://www.hashicorp.com",
Sha: *sha,
OS: linux,
Arch: arm64,
}},
}
ov, err = client.Admin.OPAVersions.Update(ctx, id, updateArchOpts)
require.NoError(t, err)
assert.Equal(t, opts.Version, ov.Version)
assert.Equal(t, "", ov.URL)
assert.Equal(t, "", ov.SHA)
assert.Equal(t, *opts.Official, ov.Official)
assert.Equal(t, *opts.Deprecated, ov.Deprecated)
assert.Equal(t, *opts.Enabled, ov.Enabled)
assert.Equal(t, *opts.Beta, ov.Beta)
assert.Equal(t, len(ov.Archs), 1)
assert.Equal(t, updateArchOpts.Archs[0].URL, ov.Archs[0].URL)
assert.Equal(t, updateArchOpts.Archs[0].Sha, ov.Archs[0].Sha)
assert.Equal(t, updateArchOpts.Archs[0].OS, ov.Archs[0].OS)
assert.Equal(t, updateArchOpts.Archs[0].Arch, ov.Archs[0].Arch)
})
t.Run("with non-existent OPA version", func(t *testing.T) {
randomID := "random-id"
_, err := client.Admin.OPAVersions.Read(ctx, randomID)
require.Error(t, err)
})
}
================================================
FILE: admin_organization.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ AdminOrganizations = (*adminOrganizations)(nil)
// AdminOrganizations describes all of the admin organization related methods that the Terraform
// Enterprise API supports. Note that admin settings are only available in Terraform Enterprise.
//
// TFE API docs: https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/organizations
type AdminOrganizations interface {
// List all the organizations visible to the current user.
List(ctx context.Context, options *AdminOrganizationListOptions) (*AdminOrganizationList, error)
// Read attributes of an existing organization via admin API.
Read(ctx context.Context, organization string) (*AdminOrganization, error)
// Update attributes of an existing organization via admin API.
Update(ctx context.Context, organization string, options AdminOrganizationUpdateOptions) (*AdminOrganization, error)
// Delete an organization by its name via admin API
Delete(ctx context.Context, organization string) error
// ListModuleConsumers lists specific organizations in the Terraform Enterprise installation that have permission to use an organization's modules.
ListModuleConsumers(ctx context.Context, organization string, options *AdminOrganizationListModuleConsumersOptions) (*AdminOrganizationList, error)
// UpdateModuleConsumers specifies a list of organizations that can use modules from the sharing organization's private registry. Setting a list of module consumers will turn off global module sharing for an organization.
UpdateModuleConsumers(ctx context.Context, organization string, consumerOrganizations []string) error
}
// adminOrganizations implements AdminOrganizations.
type adminOrganizations struct {
client *Client
}
// AdminOrganization represents a Terraform Enterprise organization returned from the Admin API.
type AdminOrganization struct {
Name string `jsonapi:"primary,organizations"`
AccessBetaTools bool `jsonapi:"attr,access-beta-tools"`
ExternalID string `jsonapi:"attr,external-id"`
GlobalModuleSharing *bool `jsonapi:"attr,global-module-sharing"`
GlobalProviderSharing *bool `jsonapi:"attr,global-provider-sharing"`
IsDisabled bool `jsonapi:"attr,is-disabled"`
NotificationEmail string `jsonapi:"attr,notification-email"`
SsoEnabled bool `jsonapi:"attr,sso-enabled"`
TerraformBuildWorkerApplyTimeout string `jsonapi:"attr,terraform-build-worker-apply-timeout"`
TerraformBuildWorkerPlanTimeout string `jsonapi:"attr,terraform-build-worker-plan-timeout"`
ApplyTimeout string `jsonapi:"attr,apply-timeout"`
PlanTimeout string `jsonapi:"attr,plan-timeout"`
TerraformWorkerSudoEnabled bool `jsonapi:"attr,terraform-worker-sudo-enabled"`
WorkspaceLimit *int `jsonapi:"attr,workspace-limit"`
// Relations
Owners []*User `jsonapi:"relation,owners"`
}
// AdminOrganizationUpdateOptions represents the admin options for updating an organization.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/organizations#request-body
type AdminOrganizationUpdateOptions struct {
AccessBetaTools *bool `jsonapi:"attr,access-beta-tools,omitempty"`
GlobalModuleSharing *bool `jsonapi:"attr,global-module-sharing,omitempty"`
GlobalProviderSharing *bool `jsonapi:"attr,global-provider-sharing,omitempty"`
IsDisabled *bool `jsonapi:"attr,is-disabled,omitempty"`
TerraformBuildWorkerApplyTimeout *string `jsonapi:"attr,terraform-build-worker-apply-timeout,omitempty"`
TerraformBuildWorkerPlanTimeout *string `jsonapi:"attr,terraform-build-worker-plan-timeout,omitempty"`
ApplyTimeout *string `jsonapi:"attr,apply-timeout,omitempty"`
PlanTimeout *string `jsonapi:"attr,plan-timeout,omitempty"`
TerraformWorkerSudoEnabled bool `jsonapi:"attr,terraform-worker-sudo-enabled,omitempty"`
WorkspaceLimit *int `jsonapi:"attr,workspace-limit,omitempty"`
}
// AdminOrganizationList represents a list of organizations via Admin API.
type AdminOrganizationList struct {
*Pagination
Items []*AdminOrganization
}
// AdminOrgIncludeOpt represents the available options for include query params.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/organizations#available-related-resources
type AdminOrgIncludeOpt string
const AdminOrgOwners AdminOrgIncludeOpt = "owners"
// AdminOrganizationListOptions represents the options for listing organizations via Admin API.
type AdminOrganizationListOptions struct {
ListOptions
// Optional: A query string used to filter organizations.
// Any organizations with a name or notification email partially matching this value will be returned.
Query string `url:"q,omitempty"`
// Optional: A list of relations to include. See available resources
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/organizations#available-related-resources
Include []AdminOrgIncludeOpt `url:"include,omitempty"`
}
// AdminOrganizationListModuleConsumersOptions represents the options for listing organization module consumers through the Admin API
type AdminOrganizationListModuleConsumersOptions struct {
ListOptions
}
type AdminOrganizationID struct {
ID string `jsonapi:"primary,organizations"`
}
// List all the organizations visible to the current user.
func (s *adminOrganizations) List(ctx context.Context, options *AdminOrganizationListOptions) (*AdminOrganizationList, error) {
if err := options.valid(); err != nil {
return nil, err
}
u := "admin/organizations"
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
orgl := &AdminOrganizationList{}
err = req.Do(ctx, orgl)
if err != nil {
return nil, err
}
return orgl, nil
}
// ListModuleConsumers lists specific organizations in the Terraform Enterprise installation that have permission to use an organization's modules.
func (s *adminOrganizations) ListModuleConsumers(ctx context.Context, organization string, options *AdminOrganizationListModuleConsumersOptions) (*AdminOrganizationList, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf("admin/organizations/%s/relationships/module-consumers", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
orgl := &AdminOrganizationList{}
err = req.Do(ctx, orgl)
if err != nil {
return nil, err
}
return orgl, nil
}
// Read an organization by its name.
func (s *adminOrganizations) Read(ctx context.Context, organization string) (*AdminOrganization, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf("admin/organizations/%s", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
org := &AdminOrganization{}
err = req.Do(ctx, org)
if err != nil {
return nil, err
}
return org, nil
}
// Update an organization by its name.
func (s *adminOrganizations) Update(ctx context.Context, organization string, options AdminOrganizationUpdateOptions) (*AdminOrganization, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf("admin/organizations/%s", url.PathEscape(organization))
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
org := &AdminOrganization{}
err = req.Do(ctx, org)
if err != nil {
return nil, err
}
return org, nil
}
// UpdateModuleConsumers updates an organization to specify a list of organizations that can use modules from the sharing organization's private registry.
func (s *adminOrganizations) UpdateModuleConsumers(ctx context.Context, organization string, consumerOrganizationIDs []string) error {
if !validStringID(&organization) {
return ErrInvalidOrg
}
u := fmt.Sprintf("admin/organizations/%s/relationships/module-consumers", url.PathEscape(organization))
var organizations []*AdminOrganizationID
for _, id := range consumerOrganizationIDs {
if !validStringID(&id) {
return ErrInvalidOrg
}
organizations = append(organizations, &AdminOrganizationID{ID: id})
}
req, err := s.client.NewRequest("PATCH", u, organizations)
if err != nil {
return err
}
err = req.Do(ctx, nil)
if err != nil {
return err
}
return nil
}
// Delete an organization by its name.
func (s *adminOrganizations) Delete(ctx context.Context, organization string) error {
if !validStringID(&organization) {
return ErrInvalidOrg
}
u := fmt.Sprintf("admin/organizations/%s", url.PathEscape(organization))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o *AdminOrganizationListOptions) valid() error {
return nil
}
================================================
FILE: admin_organization_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAdminOrganizations_List(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
org, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
t.Run("with no list options", func(t *testing.T) {
adminOrgList, err := client.Admin.Organizations.List(ctx, nil)
require.NoError(t, err)
// Given that org creation occurs on every test, the ordering is not
// guaranteed. It may be that the `org` created in this test does not appear
// in this list, so we want to test that the items are filled.
assert.NotEmpty(t, adminOrgList.Items)
})
t.Run("with list options", func(t *testing.T) {
// creating second org so that the query can only find the main org
_, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
adminOrgList, err := client.Admin.Organizations.List(ctx, &AdminOrganizationListOptions{
Query: org.Name,
})
require.NoError(t, err)
assert.Equal(t, true, adminOrgItemsContainsName(adminOrgList.Items, org.Name))
assert.Equal(t, 1, adminOrgList.CurrentPage)
assert.Equal(t, 1, adminOrgList.TotalCount)
})
t.Run("with list options and org name that doesn't exist", func(t *testing.T) {
randomName := "random-org-name"
adminOrgList, err := client.Admin.Organizations.List(ctx, &AdminOrganizationListOptions{
Query: randomName,
})
require.NoError(t, err)
assert.Equal(t, false, adminOrgItemsContainsName(adminOrgList.Items, org.Name))
assert.Equal(t, 1, adminOrgList.CurrentPage)
assert.Equal(t, 0, adminOrgList.TotalCount)
})
t.Run("with owners included", func(t *testing.T) {
adminOrgList, err := client.Admin.Organizations.List(ctx, &AdminOrganizationListOptions{
Include: []AdminOrgIncludeOpt{AdminOrgOwners},
})
require.NoError(t, err)
require.NotEmpty(t, adminOrgList.Items)
assert.NotNil(t, adminOrgList.Items[0].Owners)
assert.NotEmpty(t, adminOrgList.Items[0].Owners[0].Email)
})
}
func TestAdminOrganizations_Read(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
t.Run("it fails to read an organization with an invalid id", func(t *testing.T) {
adminOrg, err := client.Admin.Organizations.Read(ctx, "")
require.Error(t, err)
assert.EqualError(t, err, ErrInvalidOrg.Error())
assert.Nil(t, adminOrg)
})
t.Run("it returns ErrResourceNotFound for an organization that doesn't exist", func(t *testing.T) {
orgName := fmt.Sprintf("non-existing-%s", randomString(t))
adminOrg, err := client.Admin.Organizations.Read(ctx, orgName)
require.Error(t, err)
assert.EqualError(t, err, ErrResourceNotFound.Error())
assert.Nil(t, adminOrg)
})
t.Run("it reads an organization successfully", func(t *testing.T) {
org, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
adminOrg, err := client.Admin.Organizations.Read(ctx, org.Name)
require.NoError(t, err)
require.NotNilf(t, adminOrg, "Organization is not nil")
assert.Equal(t, adminOrg.Name, org.Name)
// attributes part of an AdminOrganization response that are not null
assert.NotNilf(t, adminOrg.AccessBetaTools, "AccessBetaTools is not nil")
assert.NotNilf(t, adminOrg.ExternalID, "ExternalID is not nil")
assert.NotNilf(t, adminOrg.IsDisabled, "IsDisabled is not nil")
assert.NotNilf(t, adminOrg.NotificationEmail, "NotificationEmail is not nil")
assert.NotNilf(t, adminOrg.SsoEnabled, "SsoEnabled is not nil")
assert.NotNilf(t, adminOrg.TerraformWorkerSudoEnabled, "TerraformWorkerSudoEnabledis not nil")
assert.Nilf(t, adminOrg.WorkspaceLimit, "WorkspaceLimit is nil")
})
}
func TestAdminOrganizations_Delete(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
t.Run("it fails to delete an organization with an invalid id", func(t *testing.T) {
err := client.Admin.Organizations.Delete(ctx, "")
require.Error(t, err)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("it returns ErrResourceNotFound during an attempt to delete an organization that doesn't exist", func(t *testing.T) {
orgName := fmt.Sprintf("non-existing-%s", randomString(t))
err := client.Admin.Organizations.Delete(ctx, orgName)
require.Error(t, err)
assert.EqualError(t, err, ErrResourceNotFound.Error())
})
t.Run("it deletes an organization successfully", func(t *testing.T) {
originalOrg, _ := createOrganization(t, client)
adminOrg, err := client.Admin.Organizations.Read(ctx, originalOrg.Name)
require.NoError(t, err)
require.NotNil(t, adminOrg)
assert.Equal(t, adminOrg.Name, originalOrg.Name)
err = client.Admin.Organizations.Delete(ctx, adminOrg.Name)
require.NoError(t, err)
// Cannot find deleted org
_, err = client.Admin.Organizations.Read(ctx, originalOrg.Name)
assert.Error(t, err)
assert.EqualError(t, err, ErrResourceNotFound.Error())
})
}
func TestAdminOrganizations_ModuleConsumers(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
t.Run("returns error if invalid org string is used", func(t *testing.T) {
org1, org1TestCleanup := createOrganization(t, client)
defer org1TestCleanup()
err := client.Admin.Organizations.UpdateModuleConsumers(ctx, org1.Name, []string{"1Hello!"})
assert.Error(t, err, "Organization 1Hello! not found")
})
t.Run("can list and update module consumers", func(t *testing.T) {
org1, org1TestCleanup := createOrganization(t, client)
defer org1TestCleanup()
org2, org2TestCleanup := createOrganization(t, client)
defer org2TestCleanup()
err := client.Admin.Organizations.UpdateModuleConsumers(ctx, org1.Name, []string{org2.Name})
require.NoError(t, err)
adminModuleConsumerList, err := client.Admin.Organizations.ListModuleConsumers(ctx, org1.Name, nil)
require.NoError(t, err)
assert.Equal(t, len(adminModuleConsumerList.Items), 1)
assert.Equal(t, adminModuleConsumerList.Items[0].Name, org2.Name)
org3, org3TestCleanup := createOrganization(t, client)
defer org3TestCleanup()
err = client.Admin.Organizations.UpdateModuleConsumers(ctx, org1.Name, []string{org3.Name})
require.NoError(t, err)
adminModuleConsumerList, err = client.Admin.Organizations.ListModuleConsumers(ctx, org1.Name, nil)
require.NoError(t, err)
assert.Equal(t, len(adminModuleConsumerList.Items), 1)
assert.Equal(t, adminModuleConsumerList.Items[0].Name, org3.Name)
})
}
func TestAdminOrganizations_Update(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
t.Run("it fails to update an organization with an invalid id", func(t *testing.T) {
_, err := client.Admin.Organizations.Update(ctx, "", AdminOrganizationUpdateOptions{})
require.Error(t, err)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("it returns ErrResourceNotFound for during an update on an organization that doesn't exist", func(t *testing.T) {
orgName := fmt.Sprintf("non-existing-%s", randomString(t))
_, err := client.Admin.Organizations.Update(ctx, orgName, AdminOrganizationUpdateOptions{})
require.Error(t, err)
assert.EqualError(t, err, ErrResourceNotFound.Error())
})
t.Run("fetches and updates organization", func(t *testing.T) {
org, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
adminOrg, err := client.Admin.Organizations.Read(ctx, org.Name)
require.NoError(t, err)
require.NotNilf(t, adminOrg, "Org returned as nil")
accessBetaTools := true
globalModuleSharing := false
globalProviderSharing := false
isDisabled := false
applyTimeout := "24h"
planTimeout := "24h"
terraformWorkerSudoEnabled := true
opts := AdminOrganizationUpdateOptions{
AccessBetaTools: &accessBetaTools,
GlobalModuleSharing: &globalModuleSharing,
GlobalProviderSharing: &globalProviderSharing,
IsDisabled: &isDisabled,
TerraformBuildWorkerApplyTimeout: &applyTimeout,
TerraformBuildWorkerPlanTimeout: &planTimeout,
ApplyTimeout: &applyTimeout,
PlanTimeout: &planTimeout,
TerraformWorkerSudoEnabled: terraformWorkerSudoEnabled,
}
adminOrg, err = client.Admin.Organizations.Update(ctx, org.Name, opts)
require.NotNilf(t, adminOrg, "Org returned as nil when it shouldn't be.")
require.NoError(t, err)
assert.Equal(t, accessBetaTools, adminOrg.AccessBetaTools)
assert.Equal(t, adminOrg.GlobalModuleSharing, &globalModuleSharing)
assert.Equal(t, adminOrg.GlobalProviderSharing, &globalProviderSharing)
assert.Equal(t, isDisabled, adminOrg.IsDisabled)
assert.Equal(t, applyTimeout, adminOrg.TerraformBuildWorkerApplyTimeout)
assert.Equal(t, planTimeout, adminOrg.TerraformBuildWorkerPlanTimeout)
assert.Equal(t, applyTimeout, adminOrg.ApplyTimeout)
assert.Equal(t, planTimeout, adminOrg.PlanTimeout)
assert.Equal(t, terraformWorkerSudoEnabled, adminOrg.TerraformWorkerSudoEnabled)
assert.Nil(t, adminOrg.WorkspaceLimit, "default workspace limit should be nil")
isDisabled = true
globalModuleSharing = true
globalProviderSharing = true
workspaceLimit := 42
opts = AdminOrganizationUpdateOptions{
GlobalModuleSharing: &globalModuleSharing,
GlobalProviderSharing: &globalProviderSharing,
IsDisabled: &isDisabled,
WorkspaceLimit: &workspaceLimit,
}
adminOrg, err = client.Admin.Organizations.Update(ctx, org.Name, opts)
require.NoError(t, err)
require.NotNilf(t, adminOrg, "Org returned as nil when it shouldn't be.")
assert.Equal(t, adminOrg.GlobalModuleSharing, &globalModuleSharing)
assert.Equal(t, adminOrg.GlobalProviderSharing, &globalProviderSharing)
assert.Equal(t, adminOrg.IsDisabled, isDisabled)
assert.Equal(t, &workspaceLimit, adminOrg.WorkspaceLimit)
globalModuleSharing = false
globalProviderSharing = false
isDisabled = false
workspaceLimit = 0
opts = AdminOrganizationUpdateOptions{
GlobalModuleSharing: &globalModuleSharing,
GlobalProviderSharing: &globalProviderSharing,
IsDisabled: &isDisabled,
WorkspaceLimit: &workspaceLimit,
}
adminOrg, err = client.Admin.Organizations.Update(ctx, org.Name, opts)
require.NoError(t, err)
require.NotNilf(t, adminOrg, "Org returned as nil when it shouldn't be.")
assert.Equal(t, &globalModuleSharing, adminOrg.GlobalModuleSharing)
assert.Equal(t, &globalProviderSharing, adminOrg.GlobalProviderSharing)
assert.Equal(t, adminOrg.IsDisabled, isDisabled)
assert.Equal(t, &workspaceLimit, adminOrg.WorkspaceLimit)
})
}
func adminOrgItemsContainsName(items []*AdminOrganization, name string) bool {
hasName := false
for _, item := range items {
if item.Name == name {
hasName = true
break
}
}
return hasName
}
================================================
FILE: admin_run.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"strings"
"time"
)
// Compile-time proof of interface implementation.
var _ AdminRuns = (*adminRuns)(nil)
// AdminRuns describes all the admin run related methods that the Terraform
// Enterprise API supports.
// It contains endpoints to help site administrators manage their runs.
//
// TFE API docs: https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/runs
type AdminRuns interface {
// List all the runs of the given installation.
List(ctx context.Context, options *AdminRunsListOptions) (*AdminRunsList, error)
// Force-cancel a run by its ID.
ForceCancel(ctx context.Context, runID string, options AdminRunForceCancelOptions) error
}
// AdminRun represents AdminRuns interface.
type AdminRun struct {
ID string `jsonapi:"primary,runs"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
HasChanges bool `jsonapi:"attr,has-changes"`
Status RunStatus `jsonapi:"attr,status"`
StatusTimestamps *RunStatusTimestamps `jsonapi:"attr,status-timestamps"`
// Relations
Workspace *AdminWorkspace `jsonapi:"relation,workspace"`
Organization *AdminOrganization `jsonapi:"relation,workspace.organization"`
}
// AdminRunsList represents a list of runs.
type AdminRunsList struct {
*Pagination
Items []*AdminRun
}
// AdminRunIncludeOpt represents the available options for include query params.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/runs#available-related-resources
type AdminRunIncludeOpt string
const (
AdminRunWorkspace AdminRunIncludeOpt = "workspace"
AdminRunWorkspaceOrg AdminRunIncludeOpt = "workspace.organization"
AdminRunWorkspaceOrgOwners AdminRunIncludeOpt = "workspace.organization.owners"
)
// AdminRunsListOptions represents the options for listing runs.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/runs#query-parameters
type AdminRunsListOptions struct {
ListOptions
RunStatus string `url:"filter[status],omitempty"`
CreatedBefore string `url:"filter[to],omitempty"`
CreatedAfter string `url:"filter[from],omitempty"`
Query string `url:"q,omitempty"`
// Optional: A list of relations to include. See available resources
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/runs#available-related-resources
Include []AdminRunIncludeOpt `url:"include,omitempty"`
}
// adminRuns implements the AdminRuns interface.
type adminRuns struct {
client *Client
}
// List all the runs of the terraform enterprise installation.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/runs#list-all-runs
func (s *adminRuns) List(ctx context.Context, options *AdminRunsListOptions) (*AdminRunsList, error) {
if err := options.valid(); err != nil {
return nil, err
}
u := "admin/runs"
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
rl := &AdminRunsList{}
err = req.Do(ctx, rl)
if err != nil {
return nil, err
}
return rl, nil
}
// AdminRunForceCancelOptions represents the options for force-canceling a run.
type AdminRunForceCancelOptions struct {
// An optional comment explaining the reason for the force-cancel.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/runs#request-body
Comment *string `json:"comment,omitempty"`
}
// ForceCancel is used to forcefully cancel a run by its ID.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/runs#force-a-run-into-the-quot-cancelled-quot-state
func (s *adminRuns) ForceCancel(ctx context.Context, runID string, options AdminRunForceCancelOptions) error {
if !validStringID(&runID) {
return ErrInvalidRunID
}
u := fmt.Sprintf("admin/runs/%s/actions/force-cancel", url.PathEscape(runID))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o *AdminRunsListOptions) valid() error {
if o == nil { // nothing to validate
return nil
}
if err := validateAdminRunDateRanges(o.CreatedBefore, o.CreatedAfter); err != nil {
return err
}
if err := validateAdminRunFilterParams(o.RunStatus); err != nil {
return err
}
return nil
}
func validateAdminRunDateRanges(before, after string) error {
if validString(&before) {
_, err := time.Parse(time.RFC3339, before)
if err != nil {
return fmt.Errorf("invalid date format for CreatedBefore: '%s', must be in RFC3339 format", before)
}
}
if validString(&after) {
_, err := time.Parse(time.RFC3339, after)
if err != nil {
return fmt.Errorf("invalid date format for CreatedAfter: '%s', must be in RFC3339 format", after)
}
}
return nil
}
func validateAdminRunFilterParams(runStatus string) error {
// For the platform, an invalid filter value is a semantically understood query that returns an empty set, no error, no warning. But for go-tfe, an invalid value is good enough reason to error prior to a network call to the platform:
if validString(&runStatus) {
sanitizedRunstatus := strings.TrimSpace(runStatus)
runStatuses := strings.Split(sanitizedRunstatus, ",")
// iterate over our statuses, and ensure it is valid.
for _, status := range runStatuses {
switch status {
case string(RunApplied),
string(RunApplyQueued),
string(RunApplying),
string(RunCanceled),
string(RunConfirmed),
string(RunCostEstimated),
string(RunCostEstimating),
string(RunDiscarded),
string(RunErrored),
string(RunFetching),
string(RunFetchingCompleted),
string(RunPending),
string(RunPlanned),
string(RunPlannedAndFinished),
string(RunPlannedAndSaved),
string(RunPlanning),
string(RunPlanQueued),
string(RunPolicyChecked),
string(RunPolicyChecking),
string(RunPolicyOverride),
string(RunPolicySoftFailed),
string(RunPostPlanAwaitingDecision),
string(RunPostPlanCompleted),
string(RunPostPlanRunning),
string(RunPreApplyRunning),
string(RunPreApplyCompleted),
string(RunPrePlanCompleted),
string(RunPrePlanRunning),
string(RunQueuing),
string(RunQueuingApply),
"":
// do nothing
default:
return fmt.Errorf(`invalid value "%s" for run status`, status)
}
}
}
return nil
}
================================================
FILE: admin_run_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"encoding/json"
"fmt"
"testing"
"time"
retryablehttp "github.com/hashicorp/go-retryablehttp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAdminRuns_List_RunDependent(t *testing.T) {
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
org, orgCleanup := createOrganization(t, client)
defer orgCleanup()
wTest, wTestCleanup := createWorkspace(t, client, org)
defer wTestCleanup()
rTest1, rTestCleanup1 := createRun(t, client, wTest)
defer rTestCleanup1()
rTest2, rTestCleanup2 := createRun(t, client, wTest)
defer rTestCleanup2()
t.Run("without list options", func(t *testing.T) {
rl, err := client.Admin.Runs.List(ctx, nil)
require.NoError(t, err)
require.NotEmpty(t, rl.Items)
assert.Equal(t, adminRunItemsContainsID(rl.Items, rTest1.ID), true)
assert.Equal(t, adminRunItemsContainsID(rl.Items, rTest2.ID), true)
})
t.Run("with list options", func(t *testing.T) {
rl, err := client.Admin.Runs.List(ctx, &AdminRunsListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
// Out of range page number, so the items should be empty
assert.Empty(t, rl.Items)
assert.Equal(t, 999, rl.CurrentPage)
rl, err = client.Admin.Runs.List(ctx, &AdminRunsListOptions{
ListOptions: ListOptions{
PageNumber: 1,
PageSize: 100,
},
})
require.NoError(t, err)
require.NotEmpty(t, rl.Items)
assert.Equal(t, 1, rl.CurrentPage)
assert.Equal(t, adminRunItemsContainsID(rl.Items, rTest1.ID), true)
assert.Equal(t, adminRunItemsContainsID(rl.Items, rTest2.ID), true)
})
t.Run("with workspace included", func(t *testing.T) {
rl, err := client.Admin.Runs.List(ctx, &AdminRunsListOptions{
Include: []AdminRunIncludeOpt{AdminRunWorkspace},
})
require.NoError(t, err)
require.NotEmpty(t, rl.Items)
require.NotNil(t, rl.Items[0].Workspace)
assert.NotEmpty(t, rl.Items[0].Workspace.Name)
})
t.Run("with workspace.organization included", func(t *testing.T) {
rl, err := client.Admin.Runs.List(ctx, &AdminRunsListOptions{
Include: []AdminRunIncludeOpt{AdminRunWorkspaceOrg},
})
require.NoError(t, err)
require.NotEmpty(t, rl.Items)
require.NotNil(t, rl.Items[0].Workspace)
require.NotNil(t, rl.Items[0].Workspace.Organization)
assert.NotEmpty(t, rl.Items[0].Workspace.Organization.Name)
})
t.Run("with invalid Include option", func(t *testing.T) {
_, err := client.Admin.Runs.List(ctx, &AdminRunsListOptions{
Include: []AdminRunIncludeOpt{"workpsace"},
})
assert.Equal(t, ErrInvalidIncludeValue, err)
})
t.Run("with RunStatus.pending filter", func(t *testing.T) {
r1, err := client.Runs.Read(ctx, rTest1.ID)
require.NoError(t, err)
r2, err := client.Runs.Read(ctx, rTest2.ID)
require.NoError(t, err)
// There should be pending Runs
rl, err := client.Admin.Runs.List(ctx, &AdminRunsListOptions{
RunStatus: string(RunPending),
})
require.NoError(t, err)
require.NotEmpty(t, rl.Items)
assert.Equal(t, adminRunItemsContainsID(rl.Items, r1.ID), false)
assert.Equal(t, adminRunItemsContainsID(rl.Items, r2.ID), true)
})
t.Run("with RunStatus.applied filter", func(t *testing.T) {
// There should be no applied Runs
rl, err := client.Admin.Runs.List(ctx, &AdminRunsListOptions{
RunStatus: string(RunApplied),
})
require.NoError(t, err)
assert.Empty(t, rl.Items)
})
t.Run("with query", func(t *testing.T) {
rl, err := client.Admin.Runs.List(ctx, &AdminRunsListOptions{
Query: rTest1.ID,
})
require.NoError(t, err)
require.NotEmpty(t, rl.Items)
assert.Equal(t, adminRunItemsContainsID(rl.Items, rTest1.ID), true)
assert.Equal(t, adminRunItemsContainsID(rl.Items, rTest2.ID), false)
rl, err = client.Admin.Runs.List(ctx, &AdminRunsListOptions{
Query: rTest2.ID,
})
require.NoError(t, err)
require.NotEmpty(t, rl.Items)
assert.Equal(t, adminRunItemsContainsID(rl.Items, rTest1.ID), false)
assert.Equal(t, adminRunItemsContainsID(rl.Items, rTest2.ID), true)
})
}
func TestAdminRuns_ForceCancel_RunDependent(t *testing.T) {
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
org, orgCleanup := createOrganization(t, client)
defer orgCleanup()
wTest, wTestCleanup := createWorkspace(t, client, org)
defer wTestCleanup()
// We need to create 2 runs here.
// The first run will automatically be planned
// so that one cannot be cancelled.
rTest1, rCleanup1 := createRun(t, client, wTest)
defer rCleanup1()
// The second one will be pending until the first one is
// confirmed or discarded, so we can cancel that one.
rTest2, rCleanup2 := createRun(t, client, wTest)
defer rCleanup2()
assert.Equal(t, true, rTest1.Actions.IsCancelable)
assert.Equal(t, true, rTest1.Permissions.CanForceCancel)
assert.Equal(t, true, rTest2.Actions.IsCancelable)
assert.Equal(t, true, rTest2.Permissions.CanForceCancel)
t.Run("when the run does not exist", func(t *testing.T) {
err := client.Admin.Runs.ForceCancel(ctx, "nonexisting", AdminRunForceCancelOptions{})
assert.Equal(t, ErrResourceNotFound, err)
})
t.Run("with invalid run ID", func(t *testing.T) {
err := client.Admin.Runs.ForceCancel(ctx, badIdentifier, AdminRunForceCancelOptions{})
assert.EqualError(t, err, ErrInvalidRunID.Error())
})
t.Run("with can force cancel", func(t *testing.T) {
rTestPlanning, err := client.Runs.Read(ctx, rTest1.ID)
require.NoError(t, err)
ctxPollRunStatus, cancelPollPlanned := context.WithTimeout(ctx, 2*time.Minute)
defer cancelPollPlanned()
pollRunStatus(t, client, ctxPollRunStatus, rTestPlanning, []RunStatus{RunPlanning, RunPlanned, RunCostEstimated})
require.NotNil(t, rTestPlanning.Actions)
require.NotNil(t, rTestPlanning.Permissions)
assert.Equal(t, true, rTestPlanning.Actions.IsCancelable)
assert.Equal(t, true, rTestPlanning.Permissions.CanForceCancel)
rTestPending, err := client.Runs.Read(ctx, rTest2.ID)
require.NoError(t, err)
pollRunStatus(t, client, ctxPollRunStatus, rTest2, []RunStatus{RunPending})
require.NotNil(t, rTestPlanning.Actions)
require.NotNil(t, rTestPlanning.Permissions)
assert.Equal(t, true, rTestPending.Actions.IsCancelable)
assert.Equal(t, true, rTestPending.Permissions.CanForceCancel)
comment1 := "Misclick"
err = client.Admin.Runs.ForceCancel(ctx, rTestPending.ID, AdminRunForceCancelOptions{
Comment: String(comment1),
})
require.NoError(t, err)
rTestPendingResult, err := client.Runs.Read(ctx, rTestPending.ID)
require.NoError(t, err)
assert.Equal(t, RunCanceled, rTestPendingResult.Status)
comment2 := "Another misclick"
err = client.Admin.Runs.ForceCancel(ctx, rTestPlanning.ID, AdminRunForceCancelOptions{
Comment: String(comment2),
})
require.NoError(t, err)
rTestPlanningResult, err := client.Runs.Read(ctx, rTestPlanning.ID)
require.NoError(t, err)
assert.Equal(t, RunCanceled, rTestPlanningResult.Status)
})
}
func TestAdminRuns_ListFilterByDates_RunDependent(t *testing.T) {
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
org, orgCleanup := createOrganization(t, client)
defer orgCleanup()
wTest, wTestCleanup := createWorkspace(t, client, org)
defer wTestCleanup()
timestamp1 := time.Now().Format(time.RFC3339)
// Sleeping helps ensure that the timestamps on client and server don't
// need to be exactly in sync
time.Sleep(2 * time.Second)
rTest1, rCleanup1 := createRun(t, client, wTest)
defer rCleanup1()
rTest2, rCleanup2 := createRun(t, client, wTest)
defer rCleanup2()
time.Sleep(2 * time.Second)
timestamp2 := time.Now().Format(time.RFC3339)
_, rCleanup3 := createRun(t, client, wTest)
defer rCleanup3()
time.Sleep(2 * time.Second)
timestamp3 := time.Now().Format(time.RFC3339)
t.Run("has valid date ranges", func(t *testing.T) {
rl, err := client.Admin.Runs.List(ctx, &AdminRunsListOptions{
CreatedAfter: timestamp1,
CreatedBefore: timestamp2,
})
require.NoError(t, err)
assert.Equal(t, 2, len(rl.Items))
assert.Equal(t, adminRunItemsContainsID(rl.Items, rTest1.ID), true)
assert.Equal(t, adminRunItemsContainsID(rl.Items, rTest2.ID), true)
})
t.Run("has no items when CreatedAfter and CreatedBefore datetimes has no overlap", func(t *testing.T) {
rl, err := client.Admin.Runs.List(ctx, &AdminRunsListOptions{
CreatedAfter: timestamp3,
CreatedBefore: timestamp2,
})
require.NoError(t, err)
assert.Equal(t, 0, len(rl.Items))
})
t.Run("errors with invalid input for CreatedAfter", func(t *testing.T) {
_, err := client.Admin.Runs.List(ctx, &AdminRunsListOptions{
CreatedAfter: "invalid",
})
assert.Error(t, err)
})
}
func TestAdminRuns_AdminRunsListOptions_valid(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
t.Run("has valid status", func(t *testing.T) {
opts := AdminRunsListOptions{
RunStatus: string(RunPending),
}
err := opts.valid()
require.NoError(t, err)
})
t.Run("has invalid status", func(t *testing.T) {
opts := AdminRunsListOptions{
RunStatus: "random_status",
}
err := opts.valid()
assert.Error(t, err)
})
t.Run("has invalid status, even with a valid one", func(t *testing.T) {
statuses := fmt.Sprintf("%s,%s", string(RunPending), "random_status")
opts := AdminRunsListOptions{
RunStatus: statuses,
}
err := opts.valid()
assert.Error(t, err)
})
t.Run("has trailing comma and trailing space", func(t *testing.T) {
opts := AdminRunsListOptions{
RunStatus: "pending, ",
}
err := opts.valid()
require.NoError(t, err)
})
}
func TestAdminRun_ForceCancel_Marshal(t *testing.T) {
t.Parallel()
opts := AdminRunForceCancelOptions{
Comment: String("cancel comment"),
}
reqBody, err := serializeRequestBody(&opts)
require.NoError(t, err)
req, err := retryablehttp.NewRequest("POST", "url", reqBody)
require.NoError(t, err)
bodyBytes, err := req.BodyBytes()
require.NoError(t, err)
expectedBody := `{"comment":"cancel comment"}`
assert.Equal(t, expectedBody, string(bodyBytes))
}
func TestAdminRun_Unmarshal(t *testing.T) {
t.Parallel()
data := map[string]interface{}{
"data": map[string]interface{}{
"type": "runs",
"id": "run-VCsNJXa59eUza53R",
"attributes": map[string]interface{}{
"created-at": "2018-03-02T23:42:06.651Z",
"has-changes": true,
"status": RunApplied,
"status-timestamps": map[string]string{
"plan-queued-at": "2020-03-16T23:15:59+00:00",
},
},
},
}
byteData, err := json.Marshal(data)
if err != nil {
t.Fatal(err)
}
planQueuedParsedTime, err := time.Parse(time.RFC3339, "2020-03-16T23:15:59+00:00")
require.NoError(t, err)
adminRun := &AdminRun{}
responseBody := bytes.NewReader(byteData)
err = unmarshalResponse(responseBody, adminRun)
require.NoError(t, err)
assert.Equal(t, adminRun.ID, "run-VCsNJXa59eUza53R")
assert.Equal(t, adminRun.HasChanges, true)
assert.Equal(t, adminRun.Status, RunApplied)
assert.Equal(t, adminRun.StatusTimestamps.PlanQueuedAt, planQueuedParsedTime)
}
func adminRunItemsContainsID(items []*AdminRun, id string) bool {
hasID := false
for _, item := range items {
if item.ID == id {
hasID = true
break
}
}
return hasID
}
================================================
FILE: admin_run_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func Test_validateAdminRunFilterParams(t *testing.T) {
t.Parallel()
// All RunStatus values - keep this in sync with run.go
validRunStatuses := []string{
"applied",
"applying",
"apply_queued",
"canceled",
"confirmed",
"cost_estimated",
"cost_estimating",
"discarded",
"errored",
"fetching",
"fetching_completed",
"pending",
"planned",
"planned_and_finished",
"planned_and_saved",
"planning",
"plan_queued",
"policy_checked",
"policy_checking",
"policy_override",
"policy_soft_failed",
"post_plan_awaiting_decision",
"post_plan_completed",
"post_plan_running",
"pre_apply_running",
"pre_apply_completed",
"pre_plan_completed",
"pre_plan_running",
"queuing",
"queuing_apply",
}
for _, v := range validRunStatuses {
t.Run(v, func(t *testing.T) {
require.NoError(t, validateAdminRunFilterParams(v), fmt.Sprintf("'%s' should be valid", v))
})
}
// empty string is allowed
require.NoError(t, validateAdminRunFilterParams(""), "empty string should be valid")
// comma-separated list, all valid
require.NoError(t, validateAdminRunFilterParams("applied,planned,canceled"), "'applied,planned,canceled' should be valid)")
// invalid values
require.Error(t, validateAdminRunFilterParams("cost_estimate"), "invalid value: cost_estimate")
// comma-separated list, some invalid
require.Error(t, validateAdminRunFilterParams("applied,not-planned,canceled"), "'applied,not-planned,canceled' should be invalid)")
}
================================================
FILE: admin_sentinel_version.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"reflect"
"time"
)
// Compile-time proof of interface implementation.
var _ AdminSentinelVersions = (*adminSentinelVersions)(nil)
// AdminSentinelVersions describes all the admin Sentinel versions related methods that
// the Terraform Enterprise API supports.
// Note that admin Sentinel versions are only available in Terraform Enterprise.
//
// TFE API docs: https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/sentinel-versions
type AdminSentinelVersions interface {
// List all the Sentinel versions.
List(ctx context.Context, options *AdminSentinelVersionsListOptions) (*AdminSentinelVersionsList, error)
// Read a Sentinel version by its ID.
Read(ctx context.Context, id string) (*AdminSentinelVersion, error)
// Create a Sentinel version.
Create(ctx context.Context, options AdminSentinelVersionCreateOptions) (*AdminSentinelVersion, error)
// Update a Sentinel version.
Update(ctx context.Context, id string, options AdminSentinelVersionUpdateOptions) (*AdminSentinelVersion, error)
// Delete a Sentinel version
Delete(ctx context.Context, id string) error
}
// adminSentinelVersions implements AdminSentinelVersions.
type adminSentinelVersions struct {
client *Client
}
// AdminSentinelVersion represents a Sentinel Version
type AdminSentinelVersion struct {
ID string `jsonapi:"primary,sentinel-versions"`
Version string `jsonapi:"attr,version"`
URL string `jsonapi:"attr,url,omitempty"`
SHA string `jsonapi:"attr,sha,omitempty"`
Deprecated bool `jsonapi:"attr,deprecated"`
DeprecatedReason *string `jsonapi:"attr,deprecated-reason,omitempty"`
Official bool `jsonapi:"attr,official"`
Enabled bool `jsonapi:"attr,enabled"`
Beta bool `jsonapi:"attr,beta"`
Usage int `jsonapi:"attr,usage"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Archs []*ToolVersionArchitecture `jsonapi:"attr,archs,omitempty"`
}
// AdminSentinelVersionsListOptions represents the options for listing
// Sentinel versions.
type AdminSentinelVersionsListOptions struct {
ListOptions
// Optional: A query string to find an exact version
Filter string `url:"filter[version],omitempty"`
// Optional: A search query string to find all versions that match version substring
Search string `url:"search[version],omitempty"`
}
// AdminSentinelVersionCreateOptions for creating an Sentinel version.
type AdminSentinelVersionCreateOptions struct {
Type string `jsonapi:"primary,sentinel-versions"`
Version string `jsonapi:"attr,version"` // Required
URL string `jsonapi:"attr,url,omitempty"` // Required w/ SHA unless Archs are provided
SHA string `jsonapi:"attr,sha,omitempty"` // Required w/ URL unless Archs are provided
Official *bool `jsonapi:"attr,official,omitempty"`
Deprecated *bool `jsonapi:"attr,deprecated,omitempty"`
DeprecatedReason *string `jsonapi:"attr,deprecated-reason,omitempty"`
Enabled *bool `jsonapi:"attr,enabled,omitempty"`
Beta *bool `jsonapi:"attr,beta,omitempty"`
Archs []*ToolVersionArchitecture `jsonapi:"attr,archs,omitempty"` // Required unless URL and SHA are provided
}
// AdminSentinelVersionUpdateOptions for updating Sentinel version.
type AdminSentinelVersionUpdateOptions struct {
Type string `jsonapi:"primary,sentinel-versions"`
Version *string `jsonapi:"attr,version,omitempty"`
URL *string `jsonapi:"attr,url,omitempty"`
SHA *string `jsonapi:"attr,sha,omitempty"`
Official *bool `jsonapi:"attr,official,omitempty"`
Deprecated *bool `jsonapi:"attr,deprecated,omitempty"`
DeprecatedReason *string `jsonapi:"attr,deprecated-reason,omitempty"`
Enabled *bool `jsonapi:"attr,enabled,omitempty"`
Beta *bool `jsonapi:"attr,beta,omitempty"`
Archs []*ToolVersionArchitecture `jsonapi:"attr,archs,omitempty"`
}
// AdminSentinelVersionsList represents a list of Sentinel versions.
type AdminSentinelVersionsList struct {
*Pagination
Items []*AdminSentinelVersion
}
// List all the Sentinel versions.
func (a *adminSentinelVersions) List(ctx context.Context, options *AdminSentinelVersionsListOptions) (*AdminSentinelVersionsList, error) {
req, err := a.client.NewRequest("GET", "admin/sentinel-versions", options)
if err != nil {
return nil, err
}
sl := &AdminSentinelVersionsList{}
err = req.Do(ctx, sl)
if err != nil {
return nil, err
}
return sl, nil
}
// Read a Sentinel version by its ID.
func (a *adminSentinelVersions) Read(ctx context.Context, id string) (*AdminSentinelVersion, error) {
if !validStringID(&id) {
return nil, ErrInvalidSentinelVersionID
}
u := fmt.Sprintf("admin/sentinel-versions/%s", url.PathEscape(id))
req, err := a.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
sv := &AdminSentinelVersion{}
err = req.Do(ctx, sv)
if err != nil {
return nil, err
}
return sv, nil
}
// Create a new Sentinel version.
func (a *adminSentinelVersions) Create(ctx context.Context, options AdminSentinelVersionCreateOptions) (*AdminSentinelVersion, error) {
if err := options.valid(); err != nil {
return nil, err
}
req, err := a.client.NewRequest("POST", "admin/sentinel-versions", &options)
if err != nil {
return nil, err
}
sv := &AdminSentinelVersion{}
err = req.Do(ctx, sv)
if err != nil {
return nil, err
}
return sv, nil
}
// Update an existing Sentinel version.
func (a *adminSentinelVersions) Update(ctx context.Context, id string, options AdminSentinelVersionUpdateOptions) (*AdminSentinelVersion, error) {
if !validStringID(&id) {
return nil, ErrInvalidSentinelVersionID
}
u := fmt.Sprintf("admin/sentinel-versions/%s", url.PathEscape(id))
req, err := a.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
sv := &AdminSentinelVersion{}
err = req.Do(ctx, sv)
if err != nil {
return nil, err
}
return sv, nil
}
// Delete a Sentinel version.
func (a *adminSentinelVersions) Delete(ctx context.Context, id string) error {
if !validStringID(&id) {
return ErrInvalidSentinelVersionID
}
u := fmt.Sprintf("admin/sentinel-versions/%s", url.PathEscape(id))
req, err := a.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o AdminSentinelVersionCreateOptions) valid() error {
if (reflect.DeepEqual(o, AdminSentinelVersionCreateOptions{})) {
return ErrRequiredSentinelVerCreateOps
}
if o.Version == "" {
return ErrRequiredVersion
}
if !o.validArch() {
return ErrRequiredArchsOrURLAndSha
}
return nil
}
func (o AdminSentinelVersionCreateOptions) validArch() bool {
if o.Archs == nil && o.URL != "" && o.SHA != "" {
return true
}
for _, a := range o.Archs {
if !validArch(a) {
return false
}
}
return true
}
================================================
FILE: admin_sentinel_version_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAdminSentinelVersions_List(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
t.Run("without list options", func(t *testing.T) {
sList, err := client.Admin.SentinelVersions.List(ctx, nil)
require.NoError(t, err)
assert.NotEmpty(t, sList.Items)
})
t.Run("with list options", func(t *testing.T) {
sList, err := client.Admin.SentinelVersions.List(ctx, &AdminSentinelVersionsListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
// Out of range page number, so the items should be empty
assert.Empty(t, sList.Items)
assert.Equal(t, 999, sList.CurrentPage)
sList, err = client.Admin.SentinelVersions.List(ctx, &AdminSentinelVersionsListOptions{
ListOptions: ListOptions{
PageNumber: 1,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Equal(t, 1, sList.CurrentPage)
for _, item := range sList.Items {
assert.NotNil(t, item.ID)
assert.NotEmpty(t, item.Version)
assert.NotEmpty(t, item.URL)
assert.NotEmpty(t, item.SHA)
assert.NotNil(t, item.Official)
assert.NotNil(t, item.Deprecated)
if item.Deprecated {
assert.NotNil(t, item.DeprecatedReason)
} else {
assert.Nil(t, item.DeprecatedReason)
}
assert.NotNil(t, item.Enabled)
assert.NotNil(t, item.Beta)
assert.NotNil(t, item.Usage)
assert.NotNil(t, item.CreatedAt)
assert.NotNil(t, item.Archs)
}
})
t.Run("with filter query string", func(t *testing.T) {
sList, err := client.Admin.SentinelVersions.List(ctx, &AdminSentinelVersionsListOptions{
Filter: "0.22.1",
})
require.NoError(t, err)
assert.Equal(t, 1, len(sList.Items))
// Query for a Sentinel version that does not exist
sList, err = client.Admin.SentinelVersions.List(ctx, &AdminSentinelVersionsListOptions{
Filter: "1000.1000.42",
})
require.NoError(t, err)
assert.Empty(t, sList.Items)
})
t.Run("with search version query string", func(t *testing.T) {
searchVersion := "0.22.1"
sList, err := client.Admin.SentinelVersions.List(ctx, &AdminSentinelVersionsListOptions{
Search: searchVersion,
})
require.NoError(t, err)
assert.NotEmpty(t, sList.Items)
t.Run("ensure each version matches substring", func(t *testing.T) {
for _, item := range sList.Items {
assert.Equal(t, true, strings.Contains(item.Version, searchVersion))
}
})
})
}
func TestAdminSentinelVersions_CreateDelete(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
version := createAdminSentinelVersion()
url := "https://www.hashicorp.com"
amd64Sha := String(genSha(t))
t.Run("with valid options including top level url & sha and archs", func(t *testing.T) {
opts := AdminSentinelVersionCreateOptions{
Version: version,
Deprecated: Bool(true),
DeprecatedReason: String("Test Reason"),
Official: Bool(false),
Enabled: Bool(false),
Beta: Bool(false),
URL: url,
SHA: *amd64Sha,
Archs: []*ToolVersionArchitecture{
{
URL: url,
Sha: *amd64Sha,
OS: linux,
Arch: amd64,
},
{
URL: url,
Sha: *String(genSha(t)),
OS: linux,
Arch: arm64,
}},
}
sv, err := client.Admin.SentinelVersions.Create(ctx, opts)
require.NoError(t, err)
defer func() {
deleteErr := client.Admin.SentinelVersions.Delete(ctx, sv.ID)
require.NoError(t, deleteErr)
}()
assert.Equal(t, opts.Version, sv.Version)
assert.Equal(t, *opts.Official, sv.Official)
assert.Equal(t, *opts.Deprecated, sv.Deprecated)
assert.Equal(t, *opts.DeprecatedReason, *sv.DeprecatedReason)
assert.Equal(t, *opts.Enabled, sv.Enabled)
assert.Equal(t, *opts.Beta, sv.Beta)
assert.Equal(t, opts.URL, sv.URL)
assert.Equal(t, opts.SHA, sv.SHA)
assert.Equal(t, len(opts.Archs), len(sv.Archs))
for i, arch := range opts.Archs {
assert.Equal(t, arch.URL, sv.Archs[i].URL)
assert.Equal(t, arch.Sha, sv.Archs[i].Sha)
assert.Equal(t, arch.OS, sv.Archs[i].OS)
assert.Equal(t, arch.Arch, sv.Archs[i].Arch)
}
})
t.Run("with valid options including archs", func(t *testing.T) {
opts := AdminSentinelVersionCreateOptions{
Version: version,
Deprecated: Bool(true),
DeprecatedReason: String("Test Reason"),
Official: Bool(false),
Enabled: Bool(false),
Beta: Bool(false),
Archs: []*ToolVersionArchitecture{
{
URL: url,
Sha: *amd64Sha,
OS: linux,
Arch: amd64,
},
{
URL: url,
Sha: *String(genSha(t)),
OS: linux,
Arch: arm64,
}},
}
sv, err := client.Admin.SentinelVersions.Create(ctx, opts)
require.NoError(t, err)
defer func() {
deleteErr := client.Admin.SentinelVersions.Delete(ctx, sv.ID)
require.NoError(t, deleteErr)
}()
assert.Equal(t, opts.Version, sv.Version)
assert.Equal(t, *opts.Official, sv.Official)
assert.Equal(t, *opts.Deprecated, sv.Deprecated)
assert.Equal(t, *opts.DeprecatedReason, *sv.DeprecatedReason)
assert.Equal(t, *opts.Enabled, sv.Enabled)
assert.Equal(t, *opts.Beta, sv.Beta)
assert.Equal(t, len(opts.Archs), len(sv.Archs))
for i, arch := range opts.Archs {
assert.Equal(t, arch.URL, sv.Archs[i].URL)
assert.Equal(t, arch.Sha, sv.Archs[i].Sha)
assert.Equal(t, arch.OS, sv.Archs[i].OS)
assert.Equal(t, arch.Arch, sv.Archs[i].Arch)
}
})
t.Run("with valid options including url, and sha", func(t *testing.T) {
opts := AdminSentinelVersionCreateOptions{
Version: version,
URL: url,
SHA: *amd64Sha,
Deprecated: Bool(true),
DeprecatedReason: String("Test Reason"),
Official: Bool(false),
Enabled: Bool(false),
Beta: Bool(false),
}
sv, err := client.Admin.SentinelVersions.Create(ctx, opts)
require.NoError(t, err)
defer func() {
deleteErr := client.Admin.SentinelVersions.Delete(ctx, sv.ID)
require.NoError(t, deleteErr)
}()
assert.Equal(t, opts.Version, sv.Version)
assert.Equal(t, opts.URL, sv.URL)
assert.Equal(t, opts.SHA, sv.SHA)
assert.Equal(t, *opts.Official, sv.Official)
assert.Equal(t, *opts.Deprecated, sv.Deprecated)
assert.Equal(t, *opts.DeprecatedReason, *sv.DeprecatedReason)
assert.Equal(t, *opts.Enabled, sv.Enabled)
assert.Equal(t, *opts.Beta, sv.Beta)
assert.Equal(t, 1, len(sv.Archs))
assert.Equal(t, opts.URL, sv.Archs[0].URL)
assert.Equal(t, opts.SHA, sv.Archs[0].Sha)
assert.Equal(t, linux, sv.Archs[0].OS)
assert.Equal(t, amd64, sv.Archs[0].Arch)
})
t.Run("with only required options including tool version url and sha", func(t *testing.T) {
version = createAdminSentinelVersion()
opts := AdminSentinelVersionCreateOptions{
Version: version,
URL: url,
SHA: *amd64Sha,
}
sv, err := client.Admin.SentinelVersions.Create(ctx, opts)
require.NoError(t, err)
defer func() {
deleteErr := client.Admin.SentinelVersions.Delete(ctx, sv.ID)
require.NoError(t, deleteErr)
}()
assert.Equal(t, opts.Version, sv.Version)
assert.Equal(t, opts.URL, sv.URL)
assert.Equal(t, opts.SHA, sv.SHA)
assert.Equal(t, false, sv.Official)
assert.Equal(t, false, sv.Deprecated)
assert.Nil(t, sv.DeprecatedReason)
assert.Equal(t, true, sv.Enabled)
assert.Equal(t, false, sv.Beta)
assert.Equal(t, 1, len(sv.Archs))
for i, arch := range opts.Archs {
assert.Equal(t, arch.URL, sv.Archs[i].URL)
assert.Equal(t, arch.Sha, sv.Archs[i].Sha)
assert.Equal(t, arch.OS, sv.Archs[i].OS)
assert.Equal(t, arch.Arch, sv.Archs[i].Arch)
}
})
t.Run("with only required options including archs", func(t *testing.T) {
version = createAdminSentinelVersion()
opts := AdminSentinelVersionCreateOptions{
Version: version,
Archs: []*ToolVersionArchitecture{
{
URL: url,
Sha: *amd64Sha,
OS: linux,
Arch: amd64,
},
{
URL: "https://www.hashicorp.com",
Sha: *String(genSha(t)),
OS: linux,
Arch: arm64,
}},
}
sv, err := client.Admin.SentinelVersions.Create(ctx, opts)
require.NoError(t, err)
defer func() {
deleteErr := client.Admin.SentinelVersions.Delete(ctx, sv.ID)
require.NoError(t, deleteErr)
}()
assert.Equal(t, opts.Version, sv.Version)
assert.Equal(t, false, sv.Official)
assert.Equal(t, false, sv.Deprecated)
assert.Nil(t, sv.DeprecatedReason)
assert.Equal(t, true, sv.Enabled)
assert.Equal(t, false, sv.Beta)
assert.Equal(t, len(opts.Archs), len(sv.Archs))
for i, arch := range opts.Archs {
assert.Equal(t, arch.URL, sv.Archs[i].URL)
assert.Equal(t, arch.Sha, sv.Archs[i].Sha)
assert.Equal(t, arch.OS, sv.Archs[i].OS)
assert.Equal(t, arch.Arch, sv.Archs[i].Arch)
}
})
t.Run("with empty options", func(t *testing.T) {
_, err := client.Admin.SentinelVersions.Create(ctx, AdminSentinelVersionCreateOptions{})
require.Equal(t, err, ErrRequiredSentinelVerCreateOps)
})
}
func TestAdminSentinelVersions_ReadUpdate(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
t.Run("reads and updates", func(t *testing.T) {
version := createAdminSentinelVersion()
sha := String(genSha(t))
opts := AdminSentinelVersionCreateOptions{
Version: version,
URL: "https://www.hashicorp.com",
SHA: genSha(t),
Official: Bool(false),
Deprecated: Bool(true),
DeprecatedReason: String("Test Reason"),
Enabled: Bool(false),
Beta: Bool(false),
Archs: []*ToolVersionArchitecture{{
URL: "https://www.hashicorp.com",
Sha: *sha,
OS: linux,
Arch: amd64,
}},
}
sv, err := client.Admin.SentinelVersions.Create(ctx, opts)
require.NoError(t, err)
id := sv.ID
defer func() {
deleteErr := client.Admin.SentinelVersions.Delete(ctx, id)
require.NoError(t, deleteErr)
}()
sv, err = client.Admin.SentinelVersions.Read(ctx, id)
require.NoError(t, err)
assert.Equal(t, opts.Version, sv.Version)
assert.Equal(t, opts.Archs[0].URL, sv.URL)
assert.Equal(t, opts.Archs[0].Sha, sv.SHA)
assert.Equal(t, *opts.Official, sv.Official)
assert.Equal(t, *opts.Deprecated, sv.Deprecated)
assert.Equal(t, *opts.DeprecatedReason, *sv.DeprecatedReason)
assert.Equal(t, *opts.Enabled, sv.Enabled)
assert.Equal(t, *opts.Beta, sv.Beta)
assert.Equal(t, len(opts.Archs), len(sv.Archs))
for i, arch := range opts.Archs {
assert.Equal(t, arch.URL, sv.Archs[i].URL)
assert.Equal(t, arch.Sha, sv.Archs[i].Sha)
assert.Equal(t, arch.OS, sv.Archs[i].OS)
assert.Equal(t, arch.Arch, sv.Archs[i].Arch)
}
updateVersion := createAdminSentinelVersion()
updateURL := "https://app.terraform.io/"
updateOpts := AdminSentinelVersionUpdateOptions{
Version: String(updateVersion),
URL: String(updateURL),
Deprecated: Bool(false),
}
sv, err = client.Admin.SentinelVersions.Update(ctx, id, updateOpts)
require.NoError(t, err)
assert.Equal(t, updateVersion, sv.Version)
assert.Equal(t, updateURL, sv.URL)
assert.Equal(t, opts.SHA, sv.SHA)
assert.Equal(t, *opts.Official, sv.Official)
assert.Equal(t, *updateOpts.Deprecated, sv.Deprecated)
assert.Equal(t, *opts.Enabled, sv.Enabled)
assert.Equal(t, *opts.Beta, sv.Beta)
assert.Equal(t, len(opts.Archs), len(sv.Archs))
assert.Equal(t, updateURL, sv.Archs[0].URL)
assert.Equal(t, opts.SHA, sv.Archs[0].Sha)
assert.Equal(t, opts.Archs[0].OS, sv.Archs[0].OS)
assert.Equal(t, opts.Archs[0].Arch, sv.Archs[0].Arch)
})
t.Run("update with Archs", func(t *testing.T) {
version := createAdminSentinelVersion()
sha := String(genSha(t))
opts := AdminSentinelVersionCreateOptions{
Version: *String(version),
URL: *String("https://www.hashicorp.com"),
SHA: *String(genSha(t)),
Official: Bool(false),
Deprecated: Bool(true),
DeprecatedReason: String("Test Reason"),
Enabled: Bool(false),
Beta: Bool(false),
Archs: []*ToolVersionArchitecture{{
URL: "https://www.hashicorp.com",
Sha: *sha,
OS: linux,
Arch: amd64,
}},
}
sv, err := client.Admin.SentinelVersions.Create(ctx, opts)
require.NoError(t, err)
id := sv.ID
defer func() {
deleteErr := client.Admin.SentinelVersions.Delete(ctx, id)
require.NoError(t, deleteErr)
}()
updateArchOpts := AdminSentinelVersionUpdateOptions{
Archs: []*ToolVersionArchitecture{{
URL: "https://www.hashicorp.com",
Sha: *sha,
OS: linux,
Arch: arm64,
}},
}
sv, err = client.Admin.SentinelVersions.Update(ctx, id, updateArchOpts)
require.NoError(t, err)
assert.Equal(t, opts.Version, sv.Version)
assert.Equal(t, "", sv.URL)
assert.Equal(t, "", sv.SHA)
assert.Equal(t, *opts.Official, sv.Official)
assert.Equal(t, *opts.Deprecated, sv.Deprecated)
assert.Equal(t, *opts.Enabled, sv.Enabled)
assert.Equal(t, *opts.Beta, sv.Beta)
assert.Equal(t, len(sv.Archs), 1)
assert.Equal(t, updateArchOpts.Archs[0].URL, sv.Archs[0].URL)
assert.Equal(t, updateArchOpts.Archs[0].Sha, sv.Archs[0].Sha)
assert.Equal(t, updateArchOpts.Archs[0].OS, sv.Archs[0].OS)
assert.Equal(t, updateArchOpts.Archs[0].Arch, sv.Archs[0].Arch)
})
t.Run("with non-existent Sentinel version", func(t *testing.T) {
randomID := "random-id"
_, err := client.Admin.SentinelVersions.Read(ctx, randomID)
require.Error(t, err)
})
}
================================================
FILE: admin_setting.go
================================================
// Copyright IBM Corp. 2018, 2026
// SPDX-License-Identifier: MPL-2.0
package tfe
// SCIMResource groups the SCIM related resources together.
// This struct should be constructed with keyed fields only or obtained via the client
// to prevent breakages when new fields are added.
type SCIMResource struct {
SCIMSettings
Tokens AdminSCIMTokens
Groups AdminSCIMGroups
}
// AdminSettings describes all the admin settings related methods that the Terraform Enterprise API supports.
// Note that admin settings are only available in Terraform Enterprise.
//
// TFE API docs: https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/settings
type AdminSettings struct {
General GeneralSettings
SAML SAMLSettings
CostEstimation CostEstimationSettings
SMTP SMTPSettings
Twilio TwilioSettings
Customization CustomizationSettings
OIDC OIDCSettings
SCIM *SCIMResource
}
func newAdminSettings(client *Client) *AdminSettings {
return &AdminSettings{
General: &adminGeneralSettings{client: client},
SAML: &adminSAMLSettings{client: client},
CostEstimation: &adminCostEstimationSettings{client: client},
SMTP: &adminSMTPSettings{client: client},
Twilio: &adminTwilioSettings{client: client},
Customization: &adminCustomizationSettings{client: client},
OIDC: &adminOIDCSettings{client: client},
SCIM: &SCIMResource{
SCIMSettings: &adminSCIMSettings{client: client},
Tokens: &adminSCIMTokens{client: client},
Groups: &adminSCIMGroups{client: client},
},
}
}
================================================
FILE: admin_setting_cost_estimation.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
)
// Compile-time proof of interface implementation.
var _ CostEstimationSettings = (*adminCostEstimationSettings)(nil)
// CostEstimationSettings describes all the cost estimation admin settings for the Admin Setting API.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/settings
type CostEstimationSettings interface {
// Read returns the cost estimation settings.
Read(ctx context.Context) (*AdminCostEstimationSetting, error)
// Update updates the cost estimation settings.
Update(ctx context.Context, options AdminCostEstimationSettingOptions) (*AdminCostEstimationSetting, error)
}
type adminCostEstimationSettings struct {
client *Client
}
// AdminCostEstimationSetting represents the admin cost estimation settings.
type AdminCostEstimationSetting struct {
ID string `jsonapi:"primary,cost-estimation-settings"`
Enabled bool `jsonapi:"attr,enabled"`
AWSAccessKeyID string `jsonapi:"attr,aws-access-key-id"`
AWSAccessKey string `jsonapi:"attr,aws-secret-key"`
AWSEnabled bool `jsonapi:"attr,aws-enabled"`
AWSInstanceProfileEnabled bool `jsonapi:"attr,aws-instance-profile-enabled"`
GCPCredentials string `jsonapi:"attr,gcp-credentials"`
GCPEnabled bool `jsonapi:"attr,gcp-enabled"`
AzureEnabled bool `jsonapi:"attr,azure-enabled"`
AzureClientID string `jsonapi:"attr,azure-client-id"`
AzureClientSecret string `jsonapi:"attr,azure-client-secret"`
AzureSubscriptionID string `jsonapi:"attr,azure-subscription-id"`
AzureTenantID string `jsonapi:"attr,azure-tenant-id"`
}
// AdminCostEstimationSettingOptions represents the admin options for updating
// the cost estimation settings.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/settings#request-body-1
type AdminCostEstimationSettingOptions struct {
Enabled *bool `jsonapi:"attr,enabled,omitempty"`
AWSAccessKeyID *string `jsonapi:"attr,aws-access-key-id,omitempty"`
AWSAccessKey *string `jsonapi:"attr,aws-secret-key,omitempty"`
GCPCredentials *string `jsonapi:"attr,gcp-credentials,omitempty"`
AzureClientID *string `jsonapi:"attr,azure-client-id,omitempty"`
AzureClientSecret *string `jsonapi:"attr,azure-client-secret,omitempty"`
AzureSubscriptionID *string `jsonapi:"attr,azure-subscription-id,omitempty"`
AzureTenantID *string `jsonapi:"attr,azure-tenant-id,omitempty"`
}
// Read returns the cost estimation settings.
func (a *adminCostEstimationSettings) Read(ctx context.Context) (*AdminCostEstimationSetting, error) {
req, err := a.client.NewRequest("GET", "admin/cost-estimation-settings", nil)
if err != nil {
return nil, err
}
ace := &AdminCostEstimationSetting{}
err = req.Do(ctx, ace)
if err != nil {
return nil, err
}
return ace, nil
}
// Update updates the cost-estimation settings.
func (a *adminCostEstimationSettings) Update(ctx context.Context, options AdminCostEstimationSettingOptions) (*AdminCostEstimationSetting, error) {
req, err := a.client.NewRequest("PATCH", "admin/cost-estimation-settings", &options)
if err != nil {
return nil, err
}
ace := &AdminCostEstimationSetting{}
err = req.Do(ctx, ace)
if err != nil {
return nil, err
}
return ace, nil
}
================================================
FILE: admin_setting_cost_estimation_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAdminSettings_CostEstimation_Read(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
costEstimationSettings, err := client.Admin.Settings.CostEstimation.Read(ctx)
require.NoError(t, err)
assert.Equal(t, "cost-estimation", costEstimationSettings.ID)
assert.NotNil(t, costEstimationSettings.Enabled)
}
func TestAdminSettings_CostEstimation_Update(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
_, err := client.Admin.Settings.CostEstimation.Read(ctx)
require.NoError(t, err)
costEnabled := false
costEstimationSettings, err := client.Admin.Settings.CostEstimation.Update(ctx, AdminCostEstimationSettingOptions{
Enabled: Bool(costEnabled),
})
require.NoError(t, err)
assert.Equal(t, costEnabled, costEstimationSettings.Enabled)
}
================================================
FILE: admin_setting_customization.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
)
// Compile-time proof of interface implementation.
var _ CustomizationSettings = (*adminCustomizationSettings)(nil)
// CustomizationSettings describes all the Customization admin settings.
type CustomizationSettings interface {
// Read returns the customization settings.
Read(ctx context.Context) (*AdminCustomizationSetting, error)
// Update updates the customization settings.
Update(ctx context.Context, options AdminCustomizationSettingsUpdateOptions) (*AdminCustomizationSetting, error)
}
type adminCustomizationSettings struct {
client *Client
}
// AdminCustomizationSetting represents the Customization settings in Terraform Enterprise for the Admin Settings API.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/settings
type AdminCustomizationSetting struct {
ID string `jsonapi:"primary,customization-settings"`
SupportEmail string `jsonapi:"attr,support-email-address"`
LoginHelp string `jsonapi:"attr,login-help"`
Footer string `jsonapi:"attr,footer"`
Error string `jsonapi:"attr,error"`
NewUser string `jsonapi:"attr,new-user"`
}
// Read returns the Customization settings.
func (a *adminCustomizationSettings) Read(ctx context.Context) (*AdminCustomizationSetting, error) {
req, err := a.client.NewRequest("GET", "admin/customization-settings", nil)
if err != nil {
return nil, err
}
cs := &AdminCustomizationSetting{}
err = req.Do(ctx, cs)
if err != nil {
return nil, err
}
return cs, nil
}
// AdminCustomizationSettingsUpdateOptions represents the admin options for updating
// Customization settings.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/settings#request-body-6
type AdminCustomizationSettingsUpdateOptions struct {
SupportEmail *string `jsonapi:"attr,support-email-address,omitempty"`
LoginHelp *string `jsonapi:"attr,login-help,omitempty"`
Footer *string `jsonapi:"attr,footer,omitempty"`
Error *string `jsonapi:"attr,error,omitempty"`
NewUser *string `jsonapi:"attr,new-user,omitempty"`
}
// Update updates the customization settings.
func (a *adminCustomizationSettings) Update(ctx context.Context, options AdminCustomizationSettingsUpdateOptions) (*AdminCustomizationSetting, error) {
req, err := a.client.NewRequest("PATCH", "admin/customization-settings", &options)
if err != nil {
return nil, err
}
cs := &AdminCustomizationSetting{}
err = req.Do(ctx, cs)
if err != nil {
return nil, err
}
return cs, nil
}
================================================
FILE: admin_setting_customization_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAdminSettings_Customization_Read(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
customizationSettings, err := client.Admin.Settings.Customization.Read(ctx)
require.NoError(t, err)
assert.Equal(t, "customization", customizationSettings.ID)
assert.NotNil(t, customizationSettings.SupportEmail)
assert.NotNil(t, customizationSettings.LoginHelp)
assert.NotNil(t, customizationSettings.Footer)
assert.NotNil(t, customizationSettings.Error)
assert.NotNil(t, customizationSettings.NewUser)
}
func TestAdminSettings_Customization_Update(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
email := "test@example.com"
loginHelp := "
Login Help
"
footer := "
Custom Footer Content
"
customError := "Custom Error Instructions"
newUser := "New user? Click Here"
customizationSettings, err := client.Admin.Settings.Customization.Update(ctx, AdminCustomizationSettingsUpdateOptions{
SupportEmail: String(email),
LoginHelp: String(loginHelp),
Footer: String(footer),
Error: String(customError),
NewUser: String(newUser),
})
require.NoError(t, err)
assert.Equal(t, email, customizationSettings.SupportEmail)
assert.Equal(t, loginHelp, customizationSettings.LoginHelp)
assert.Equal(t, footer, customizationSettings.Footer)
assert.Equal(t, customError, customizationSettings.Error)
assert.Equal(t, newUser, customizationSettings.NewUser)
}
================================================
FILE: admin_setting_general.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
)
// Compile-time proof of interface implementation.
var _ GeneralSettings = (*adminGeneralSettings)(nil)
// GeneralSettings describes the general admin settings for the Admin Setting API.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/settings
type GeneralSettings interface {
// Read returns the general settings
Read(ctx context.Context) (*AdminGeneralSetting, error)
// Update updates general settings.
Update(ctx context.Context, options AdminGeneralSettingsUpdateOptions) (*AdminGeneralSetting, error)
}
type adminGeneralSettings struct {
client *Client
}
// AdminGeneralSetting represents a the general settings in Terraform Enterprise.
type AdminGeneralSetting struct {
ID string `jsonapi:"primary,general-settings"`
LimitUserOrganizationCreation bool `jsonapi:"attr,limit-user-organization-creation"`
APIRateLimitingEnabled bool `jsonapi:"attr,api-rate-limiting-enabled"`
APIRateLimit int `jsonapi:"attr,api-rate-limit"`
SendPassingStatusesEnabled bool `jsonapi:"attr,send-passing-statuses-for-untriggered-speculative-plans"`
AllowSpeculativePlansOnPR bool `jsonapi:"attr,allow-speculative-plans-on-pull-requests-from-forks"`
RequireTwoFactorForAdmin bool `jsonapi:"attr,require-two-factor-for-admins"`
FairRunQueuingEnabled bool `jsonapi:"attr,fair-run-queuing-enabled"`
LimitOrgsPerUser bool `jsonapi:"attr,limit-organizations-per-user"`
DefaultOrgsPerUserCeiling int `jsonapi:"attr,default-organizations-per-user-ceiling"`
LimitWorkspacesPerOrg bool `jsonapi:"attr,limit-workspaces-per-organization"`
DefaultWorkspacesPerOrgCeiling int `jsonapi:"attr,default-workspaces-per-organization-ceiling"`
TerraformBuildWorkerApplyTimeout string `jsonapi:"attr,terraform-build-worker-apply-timeout"`
TerraformBuildWorkerPlanTimeout string `jsonapi:"attr,terraform-build-worker-plan-timeout"`
ApplyTimeout string `jsonapi:"attr,apply-timeout"`
PlanTimeout string `jsonapi:"attr,plan-timeout"`
DefaultRemoteStateAccess bool `jsonapi:"attr,default-remote-state-access"`
}
// AdminGeneralSettingsUpdateOptions represents the admin options for updating
// general settings.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/settings#request-body
type AdminGeneralSettingsUpdateOptions struct {
LimitUserOrgCreation *bool `jsonapi:"attr,limit-user-organization-creation,omitempty"`
APIRateLimitingEnabled *bool `jsonapi:"attr,api-rate-limiting-enabled,omitempty"`
APIRateLimit *int `jsonapi:"attr,api-rate-limit,omitempty"`
SendPassingStatusUntriggeredPlans *bool `jsonapi:"attr,send-passing-statuses-for-untriggered-speculative-plans,omitempty"`
AllowSpeculativePlansOnPR *bool `jsonapi:"attr,allow-speculative-plans-on-pull-requests-from-forks,omitempty"`
DefaultRemoteStateAccess *bool `jsonapi:"attr,default-remote-state-access,omitempty"`
ApplyTimeout *string `jsonapi:"attr,apply-timeout"`
PlanTimeout *string `jsonapi:"attr,plan-timeout"`
}
// Read returns the general settings.
func (a *adminGeneralSettings) Read(ctx context.Context) (*AdminGeneralSetting, error) {
req, err := a.client.NewRequest("GET", "admin/general-settings", nil)
if err != nil {
return nil, err
}
ags := &AdminGeneralSetting{}
err = req.Do(ctx, ags)
if err != nil {
return nil, err
}
return ags, nil
}
// Update updates the general settings.
func (a *adminGeneralSettings) Update(ctx context.Context, options AdminGeneralSettingsUpdateOptions) (*AdminGeneralSetting, error) {
req, err := a.client.NewRequest("PATCH", "admin/general-settings", &options)
if err != nil {
return nil, err
}
ags := &AdminGeneralSetting{}
err = req.Do(ctx, ags)
if err != nil {
return nil, err
}
return ags, nil
}
================================================
FILE: admin_setting_general_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAdminSettings_General_Read(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
generalSettings, err := client.Admin.Settings.General.Read(ctx)
require.NoError(t, err)
assert.Equal(t, "general", generalSettings.ID)
assert.NotNil(t, generalSettings.LimitUserOrganizationCreation)
assert.NotNil(t, generalSettings.APIRateLimitingEnabled)
assert.NotNil(t, generalSettings.APIRateLimit)
assert.NotNil(t, generalSettings.SendPassingStatusesEnabled)
assert.NotNil(t, generalSettings.AllowSpeculativePlansOnPR)
assert.NotNil(t, generalSettings.RequireTwoFactorForAdmin)
assert.NotNil(t, generalSettings.FairRunQueuingEnabled)
assert.NotNil(t, generalSettings.LimitOrgsPerUser)
assert.NotNil(t, generalSettings.DefaultOrgsPerUserCeiling)
assert.NotNil(t, generalSettings.LimitWorkspacesPerOrg)
assert.NotNil(t, generalSettings.DefaultWorkspacesPerOrgCeiling)
assert.NotNil(t, generalSettings.TerraformBuildWorkerApplyTimeout)
assert.NotNil(t, generalSettings.TerraformBuildWorkerPlanTimeout)
assert.NotNil(t, generalSettings.ApplyTimeout)
assert.NotNil(t, generalSettings.PlanTimeout)
assert.NotNil(t, generalSettings.DefaultRemoteStateAccess)
}
func TestAdminSettings_General_Update(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
generalSettings, err := client.Admin.Settings.General.Read(ctx)
require.NoError(t, err)
origLimitOrgCreation := generalSettings.LimitUserOrganizationCreation
origAPIRateLimitEnabled := generalSettings.APIRateLimitingEnabled
origAPIRateLimit := generalSettings.APIRateLimit
origDefaultRemoteState := generalSettings.DefaultRemoteStateAccess
origApplyTimeout := generalSettings.ApplyTimeout
origPlanTimeout := generalSettings.PlanTimeout
limitOrgCreation := true
apiRateLimitEnabled := true
apiRateLimit := 50
defaultRemoteStateAccess := false
applyTimeout := "2h"
planTimeout := "30m"
generalSettings, err = client.Admin.Settings.General.Update(ctx, AdminGeneralSettingsUpdateOptions{
LimitUserOrgCreation: Bool(limitOrgCreation),
APIRateLimitingEnabled: Bool(apiRateLimitEnabled),
APIRateLimit: Int(apiRateLimit),
DefaultRemoteStateAccess: Bool(defaultRemoteStateAccess),
ApplyTimeout: &applyTimeout,
PlanTimeout: &planTimeout,
})
require.NoError(t, err)
assert.Equal(t, limitOrgCreation, generalSettings.LimitUserOrganizationCreation)
assert.Equal(t, apiRateLimitEnabled, generalSettings.APIRateLimitingEnabled)
assert.Equal(t, apiRateLimit, generalSettings.APIRateLimit)
assert.Equal(t, defaultRemoteStateAccess, generalSettings.DefaultRemoteStateAccess)
assert.Equal(t, applyTimeout, generalSettings.ApplyTimeout)
assert.Equal(t, planTimeout, generalSettings.PlanTimeout)
// Undo Updates, revert back to original
generalSettings, err = client.Admin.Settings.General.Update(ctx, AdminGeneralSettingsUpdateOptions{
LimitUserOrgCreation: Bool(origLimitOrgCreation),
APIRateLimitingEnabled: Bool(origAPIRateLimitEnabled),
APIRateLimit: Int(origAPIRateLimit),
DefaultRemoteStateAccess: Bool(origDefaultRemoteState),
ApplyTimeout: &origApplyTimeout,
PlanTimeout: &origPlanTimeout,
})
require.NoError(t, err)
assert.Equal(t, origLimitOrgCreation, generalSettings.LimitUserOrganizationCreation)
assert.Equal(t, origAPIRateLimitEnabled, generalSettings.APIRateLimitingEnabled)
assert.Equal(t, origAPIRateLimit, generalSettings.APIRateLimit)
assert.Equal(t, origDefaultRemoteState, generalSettings.DefaultRemoteStateAccess)
assert.Equal(t, origApplyTimeout, generalSettings.ApplyTimeout)
assert.Equal(t, origPlanTimeout, generalSettings.PlanTimeout)
}
================================================
FILE: admin_setting_oidc.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
)
// Compile-time proof of interface implementation.
var _ OIDCSettings = (*adminOIDCSettings)(nil)
// OidcSettings describes all the OIDC admin settings for the Admin Setting API.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/settings
type OIDCSettings interface {
// Rotate the key used for signing OIDC tokens for workload identity
RotateKey(ctx context.Context) error
// Trim old version of the key used for signing OIDC tokens for workload identity
TrimKey(ctx context.Context) error
}
type adminOIDCSettings struct {
client *Client
}
// Rotate the key used for signing OIDC tokens for workload identity
func (a *adminOIDCSettings) RotateKey(ctx context.Context) error {
req, err := a.client.NewRequest("POST", "admin/oidc-settings/actions/rotate-key", nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// Trim old version of the key used for signing OIDC tokens for workload identity
func (a *adminOIDCSettings) TrimKey(ctx context.Context) error {
req, err := a.client.NewRequest("POST", "admin/oidc-settings/actions/trim-key", nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
================================================
FILE: admin_setting_oidc_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type wellKnownJwks struct {
Keys []struct {
Kid string `json:"kid"`
} `json:"keys"`
}
func TestAdminSettings_Oidc_RotateKey(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
jwksClient := http.Client{
Timeout: time.Second * 2,
}
baseURL := client.baseURL
token := client.token
ctx := context.Background()
jwks, err := getJwks(jwksClient, baseURL, token)
require.NoError(t, err)
// Don't assume there is only 1 key to start
originalNumKeys := len(jwks.Keys)
err = client.Admin.Settings.OIDC.RotateKey(ctx)
require.NoError(t, err)
jwks, err = getJwks(jwksClient, baseURL, token)
require.NoError(t, err)
newNumKeys := len(jwks.Keys)
// Rotate should add 1 additional key
assert.Equal(t, originalNumKeys+1, newNumKeys)
}
func TestAdminSettings_Oidc_TrimKey(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
jwksClient := http.Client{
Timeout: time.Second * 2,
}
baseURL := client.baseURL
token := client.token
ctx := context.Background()
jwks, err := getJwks(jwksClient, baseURL, token)
require.NoError(t, err)
// Don't assume there is only 1 key to start
originalNumKeys := len(jwks.Keys)
originalKids := make([]string, originalNumKeys)
for i := 0; i < originalNumKeys; i++ {
originalKids[i] = jwks.Keys[i].Kid
}
err = client.Admin.Settings.OIDC.RotateKey(ctx)
require.NoError(t, err)
jwks, err = getJwks(jwksClient, baseURL, token)
require.NoError(t, err)
beforeTrimNumKeys := len(jwks.Keys)
assert.Equal(t, originalNumKeys+1, beforeTrimNumKeys)
err = client.Admin.Settings.OIDC.TrimKey(ctx)
require.NoError(t, err)
jwks, err = getJwks(jwksClient, baseURL, token)
require.NoError(t, err)
afterTrimNumKeys := len(jwks.Keys)
assert.Equal(t, 1, afterTrimNumKeys)
// Make sure we actually trimmed the keys
assert.NotContains(t, originalKids, jwks.Keys[0].Kid)
}
func getJwks(client http.Client, baseURL *url.URL, token string) (*wellKnownJwks, error) {
jwksEndpoint, err := baseURL.Parse("/.well-known/jwks")
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodGet, jwksEndpoint.String(), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/json")
res, getErr := client.Do(req)
if getErr != nil {
return nil, getErr
}
if res.Body != nil {
defer res.Body.Close()
}
if res.StatusCode != 200 {
return nil, fmt.Errorf("unexpected status code: %d. Expected a 200 response", res.StatusCode)
}
var result wellKnownJwks
jsonErr := json.NewDecoder(res.Body).Decode(&result)
if jsonErr != nil {
return nil, jsonErr
}
return &result, nil
}
================================================
FILE: admin_setting_saml.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
)
// Compile-time proof of interface implementation.
var _ SAMLSettings = (*adminSAMLSettings)(nil)
// SAMLSettings describes all the SAML admin settings for the Admin Setting API.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/settings
type SAMLSettings interface {
// Read returns the SAML settings.
Read(ctx context.Context) (*AdminSAMLSetting, error)
// Update updates the SAML settings.
Update(ctx context.Context, options AdminSAMLSettingsUpdateOptions) (*AdminSAMLSetting, error)
// RevokeIdpCert revokes the older IdP certificate when the new IdP
// certificate is known to be functioning correctly.
RevokeIdpCert(ctx context.Context) (*AdminSAMLSetting, error)
}
type adminSAMLSettings struct {
client *Client
}
// SAMLProviderType represents the SAML identity provider type.
type SAMLProviderType string
// SAMLProviderType constants define the supported SAML identity provider types.
const (
SAMLProviderTypeOkta SAMLProviderType = "okta"
SAMLProviderTypeEntra SAMLProviderType = "entra"
SAMLProviderTypeGeneric SAMLProviderType = "saml"
SAMLProviderTypeUnknown SAMLProviderType = "unknown"
)
// AdminSAMLSetting represents the SAML settings in Terraform Enterprise.
type AdminSAMLSetting struct {
ID string `jsonapi:"primary,saml-settings"`
Enabled bool `jsonapi:"attr,enabled"`
Debug bool `jsonapi:"attr,debug"`
AuthnRequestsSigned bool `jsonapi:"attr,authn-requests-signed"`
WantAssertionsSigned bool `jsonapi:"attr,want-assertions-signed"`
TeamManagementEnabled bool `jsonapi:"attr,team-management-enabled"`
OldIDPCert string `jsonapi:"attr,old-idp-cert"`
IDPCert string `jsonapi:"attr,idp-cert"`
SLOEndpointURL string `jsonapi:"attr,slo-endpoint-url"`
SSOEndpointURL string `jsonapi:"attr,sso-endpoint-url"`
AttrUsername string `jsonapi:"attr,attr-username"`
AttrGroups string `jsonapi:"attr,attr-groups"`
AttrSiteAdmin string `jsonapi:"attr,attr-site-admin"`
SiteAdminRole string `jsonapi:"attr,site-admin-role"`
SSOAPITokenSessionTimeout int `jsonapi:"attr,sso-api-token-session-timeout"`
ACSConsumerURL string `jsonapi:"attr,acs-consumer-url"`
MetadataURL string `jsonapi:"attr,metadata-url"`
Certificate string `jsonapi:"attr,certificate"`
PrivateKey string `jsonapi:"attr,private-key"`
SignatureSigningMethod string `jsonapi:"attr,signature-signing-method"`
SignatureDigestMethod string `jsonapi:"attr,signature-digest-method"`
ProviderType SAMLProviderType `jsonapi:"attr,provider-type"`
}
// Read returns the SAML settings.
func (a *adminSAMLSettings) Read(ctx context.Context) (*AdminSAMLSetting, error) {
req, err := a.client.NewRequest("GET", "admin/saml-settings", nil)
if err != nil {
return nil, err
}
saml := &AdminSAMLSetting{}
err = req.Do(ctx, saml)
if err != nil {
return nil, err
}
return saml, nil
}
// AdminSAMLSettingsUpdateOptions represents the admin options for updating
// SAML settings.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/settings#request-body-2
type AdminSAMLSettingsUpdateOptions struct {
Enabled *bool `jsonapi:"attr,enabled,omitempty"`
Debug *bool `jsonapi:"attr,debug,omitempty"`
IDPCert *string `jsonapi:"attr,idp-cert,omitempty"`
Certificate *string `jsonapi:"attr,certificate,omitempty"`
PrivateKey *string `jsonapi:"attr,private-key,omitempty"`
SLOEndpointURL *string `jsonapi:"attr,slo-endpoint-url,omitempty"`
SSOEndpointURL *string `jsonapi:"attr,sso-endpoint-url,omitempty"`
AttrUsername *string `jsonapi:"attr,attr-username,omitempty"`
AttrGroups *string `jsonapi:"attr,attr-groups,omitempty"`
AttrSiteAdmin *string `jsonapi:"attr,attr-site-admin,omitempty"`
SiteAdminRole *string `jsonapi:"attr,site-admin-role,omitempty"`
SSOAPITokenSessionTimeout *int `jsonapi:"attr,sso-api-token-session-timeout,omitempty"`
TeamManagementEnabled *bool `jsonapi:"attr,team-management-enabled,omitempty"`
AuthnRequestsSigned *bool `jsonapi:"attr,authn-requests-signed,omitempty"`
WantAssertionsSigned *bool `jsonapi:"attr,want-assertions-signed,omitempty"`
SignatureSigningMethod *string `jsonapi:"attr,signature-signing-method,omitempty"`
SignatureDigestMethod *string `jsonapi:"attr,signature-digest-method,omitempty"`
ProviderType *SAMLProviderType `jsonapi:"attr,provider-type,omitempty"`
}
// Update updates the SAML settings.
func (a *adminSAMLSettings) Update(ctx context.Context, options AdminSAMLSettingsUpdateOptions) (*AdminSAMLSetting, error) {
if options.ProviderType != nil {
switch *options.ProviderType {
case SAMLProviderTypeOkta, SAMLProviderTypeEntra, SAMLProviderTypeGeneric, SAMLProviderTypeUnknown:
default:
return nil, ErrInvalidSAMLProviderType
}
}
req, err := a.client.NewRequest("PATCH", "admin/saml-settings", &options)
if err != nil {
return nil, err
}
saml := &AdminSAMLSetting{}
err = req.Do(ctx, saml)
if err != nil {
return nil, err
}
return saml, nil
}
// RevokeIdpCert revokes the older IdP certificate when the new IdP
// certificate is known to be functioning correctly.
func (a *adminSAMLSettings) RevokeIdpCert(ctx context.Context) (*AdminSAMLSetting, error) {
req, err := a.client.NewRequest("POST", "admin/saml-settings/actions/revoke-old-certificate", nil)
if err != nil {
return nil, err
}
saml := &AdminSAMLSetting{}
err = req.Do(ctx, saml)
if err != nil {
return nil, err
}
return saml, nil
}
================================================
FILE: admin_setting_saml_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAdminSettings_SAML_Read(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
samlSettings, err := client.Admin.Settings.SAML.Read(ctx)
require.NoError(t, err)
assert.Equal(t, "saml", samlSettings.ID)
assert.NotNil(t, samlSettings.Enabled)
assert.NotNil(t, samlSettings.Debug)
assert.NotNil(t, samlSettings.SLOEndpointURL)
assert.NotNil(t, samlSettings.SSOEndpointURL)
assert.NotNil(t, samlSettings.AttrUsername)
assert.NotNil(t, samlSettings.AttrGroups)
assert.NotNil(t, samlSettings.AttrSiteAdmin)
assert.NotNil(t, samlSettings.SiteAdminRole)
assert.NotNil(t, samlSettings.SSOAPITokenSessionTimeout)
assert.NotNil(t, samlSettings.ACSConsumerURL)
assert.NotNil(t, samlSettings.MetadataURL)
assert.NotNil(t, samlSettings.TeamManagementEnabled)
assert.NotNil(t, samlSettings.Certificate)
assert.NotNil(t, samlSettings.AuthnRequestsSigned)
assert.NotNil(t, samlSettings.WantAssertionsSigned)
assert.NotNil(t, samlSettings.PrivateKey)
assert.NotNil(t, samlSettings.SignatureSigningMethod)
assert.NotNil(t, samlSettings.SignatureDigestMethod)
assert.NotNil(t, samlSettings.ProviderType)
}
func TestAdminSettings_SAML_Update(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
_, err := client.Admin.Settings.SAML.Read(ctx)
require.NoError(t, err)
enabled := false
debug := false
samlSettings, err := client.Admin.Settings.SAML.Update(ctx, AdminSAMLSettingsUpdateOptions{
Enabled: Bool(enabled),
Debug: Bool(debug),
})
require.NoError(t, err)
assert.Equal(t, enabled, samlSettings.Enabled)
assert.Equal(t, debug, samlSettings.Debug)
assert.Empty(t, samlSettings.PrivateKey)
t.Run("with certificate defined", func(t *testing.T) {
cert := "testCert"
pKey := "testPrivateKey"
signatureSigningMethod := "SHA1"
signatureDigestMethod := "SHA1"
samlSettingsUpd, err := client.Admin.Settings.SAML.Update(ctx, AdminSAMLSettingsUpdateOptions{
Certificate: String(cert),
PrivateKey: String(pKey),
IDPCert: String(cert),
SLOEndpointURL: String("https://example.com/slo"),
SSOEndpointURL: String("https://example.com/sso"),
SignatureSigningMethod: String(signatureSigningMethod),
SignatureDigestMethod: String(signatureDigestMethod),
})
require.NoError(t, err)
assert.Equal(t, cert, samlSettingsUpd.Certificate)
assert.NotNil(t, samlSettingsUpd.PrivateKey)
assert.Equal(t, signatureSigningMethod, samlSettingsUpd.SignatureSigningMethod)
assert.Equal(t, signatureDigestMethod, samlSettingsUpd.SignatureDigestMethod)
})
t.Run("with team management enabled", func(t *testing.T) {
cert := "testCert"
pKey := "testPrivateKey"
signatureSigningMethod := "SHA1"
signatureDigestMethod := "SHA1"
samlSettingsUpd, err := client.Admin.Settings.SAML.Update(ctx, AdminSAMLSettingsUpdateOptions{
Enabled: Bool(true),
TeamManagementEnabled: Bool(true),
Certificate: String(cert),
PrivateKey: String(pKey),
SignatureSigningMethod: String(signatureSigningMethod),
SignatureDigestMethod: String(signatureDigestMethod),
})
require.NoError(t, err)
assert.True(t, samlSettingsUpd.TeamManagementEnabled)
})
t.Run("with invalid signature digest method", func(t *testing.T) {
_, err := client.Admin.Settings.SAML.Update(ctx, AdminSAMLSettingsUpdateOptions{
AuthnRequestsSigned: Bool(true),
SignatureDigestMethod: String("SHA1234"),
})
require.Error(t, err)
})
t.Run("with invalid signature signing method", func(t *testing.T) {
_, err := client.Admin.Settings.SAML.Update(ctx, AdminSAMLSettingsUpdateOptions{
AuthnRequestsSigned: Bool(true),
SignatureSigningMethod: String("SHA1234"),
})
require.Error(t, err)
})
t.Run("revoke IDP cert", func(t *testing.T) {
_, err := client.Admin.Settings.SAML.Update(ctx, AdminSAMLSettingsUpdateOptions{
Enabled: Bool(true),
IDPCert: String("anotherTestCert"),
})
require.NoError(t, err)
samlSettings, err = client.Admin.Settings.SAML.RevokeIdpCert(ctx)
require.NoError(t, err)
assert.NotNil(t, samlSettings.IDPCert)
})
t.Run("with provider type defined", func(t *testing.T) {
testCases := []struct {
name string
providerType SAMLProviderType
raiseError bool
}{
{"valid okta", SAMLProviderTypeOkta, false},
{"valid entra", SAMLProviderTypeEntra, false},
{"valid saml", SAMLProviderTypeGeneric, false},
{"valid unknown - for backward compatibility", SAMLProviderTypeUnknown, false},
{"invalid provider type", "error", true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, err := client.Admin.Settings.SAML.Update(ctx, AdminSAMLSettingsUpdateOptions{
Enabled: Bool(true),
ProviderType: SAMLProvider(tc.providerType),
})
if tc.raiseError {
require.Error(t, err)
return
}
require.NoError(t, err)
samlSettings, err = client.Admin.Settings.SAML.Read(ctx)
require.NoError(t, err)
assert.Equal(t, tc.providerType, samlSettings.ProviderType)
})
}
})
}
================================================
FILE: admin_setting_scim.go
================================================
// Copyright IBM Corp. 2018, 2026
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
)
// Compile-time proof of interface implementation.
var _ SCIMSettings = (*adminSCIMSettings)(nil)
// SCIMSettings describes all the scim settings related methods that the Terraform
// Enterprise API supports
//
// TFE API docs: https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/settings
type SCIMSettings interface {
// Read scim settings
Read(ctx context.Context) (*AdminSCIMSetting, error)
// Update scim settings
Update(ctx context.Context, options AdminSCIMSettingUpdateOptions) (*AdminSCIMSetting, error)
// Delete scim settings
Delete(ctx context.Context) error
}
// adminSCIMSettings implements SCIMSettings.
type adminSCIMSettings struct {
client *Client
}
// AdminSCIMSetting represents the SCIM setting in Terraform Enterprise
type AdminSCIMSetting struct {
ID string `jsonapi:"primary,scim-settings"`
Enabled bool `jsonapi:"attr,enabled"`
Paused bool `jsonapi:"attr,paused"`
SiteAdminGroupSCIMID string `jsonapi:"attr,site-admin-group-scim-id"`
SiteAdminGroupDisplayName string `jsonapi:"attr,site-admin-group-display-name"`
}
// AdminSCIMSettingUpdateOptions represents the options for updating an admin SCIM setting.
type AdminSCIMSettingUpdateOptions struct {
Enabled *bool `jsonapi:"attr,enabled,omitempty"`
Paused *bool `jsonapi:"attr,paused,omitempty"`
SiteAdminGroupSCIMID *string `jsonapi:"attr,site-admin-group-scim-id,omitempty"`
}
// Read scim setting.
func (a *adminSCIMSettings) Read(ctx context.Context) (*AdminSCIMSetting, error) {
req, err := a.client.NewRequest("GET", "admin/scim-settings", nil)
if err != nil {
return nil, err
}
scim := &AdminSCIMSetting{}
err = req.Do(ctx, scim)
if err != nil {
return nil, err
}
return scim, nil
}
// Update scim setting.
func (a *adminSCIMSettings) Update(ctx context.Context, options AdminSCIMSettingUpdateOptions) (*AdminSCIMSetting, error) {
req, err := a.client.NewRequest("PATCH", "admin/scim-settings", &options)
if err != nil {
return nil, err
}
scim := &AdminSCIMSetting{}
err = req.Do(ctx, scim)
if err != nil {
return nil, err
}
return scim, nil
}
// Delete scim setting.
func (a *adminSCIMSettings) Delete(ctx context.Context) error {
req, err := a.client.NewRequest("DELETE", "admin/scim-settings", nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
================================================
FILE: admin_setting_scim_groups.go
================================================
// Copyright IBM Corp. 2018, 2026
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
)
var _ AdminSCIMGroups = (*adminSCIMGroups)(nil)
// AdminSCIMGroups describes all the SCIM group related methods that the Terraform
// Enterprise API supports
//
// TFE API docs: https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/scim-groups
type AdminSCIMGroups interface {
// List all SCIM groups.
List(ctx context.Context, options *AdminSCIMGroupListOptions) (*AdminSCIMGroupList, error)
}
// adminSCIMGroups implements AdminSCIMGroups
type adminSCIMGroups struct {
client *Client
}
// AdminSCIMGroupList represents a list of SCIM groups
type AdminSCIMGroupList struct {
*Pagination
Items []*AdminSCIMGroup
}
// AdminSCIMGroup represents a Terraform Enterprise SCIM group
type AdminSCIMGroup struct {
ID string `jsonapi:"primary,scim-groups"`
Name string `jsonapi:"attr,name"`
}
// AdminSCIMGroupListOptions represents the options for listing SCIM groups
type AdminSCIMGroupListOptions struct {
ListOptions
Query string `url:"q,omitempty"`
}
func (o *AdminSCIMGroupListOptions) valid() error {
if o == nil {
return nil
}
if o.PageNumber < 0 || o.PageSize < 0 {
return ErrInvalidPagination
}
return nil
}
// List all SCIM groups.
func (a *adminSCIMGroups) List(ctx context.Context, options *AdminSCIMGroupListOptions) (*AdminSCIMGroupList, error) {
if err := options.valid(); err != nil {
return nil, err
}
req, err := a.client.NewRequest("GET", AdminSCIMGroupsPath, options)
if err != nil {
return nil, err
}
scimGroups := &AdminSCIMGroupList{}
err = req.Do(ctx, scimGroups)
if err != nil {
return nil, err
}
return scimGroups, nil
}
================================================
FILE: admin_setting_scim_groups_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2026
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAdminSCIMGroups_List(t *testing.T) {
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
scimClient := client.Admin.Settings.SCIM
enableSCIM(ctx, t, client, true)
defer enableSCIM(ctx, t, client, false)
scimToken, err := scimClient.Tokens.Create(ctx, "integration-test-token")
require.NoError(t, err)
const (
defaultPageSize = 20
maxGroupsToCreate = defaultPageSize + defaultPageSize/2
)
t.Run("basic list operations", func(t *testing.T) {
t.Run("empty list immediately after enabling SCIM", func(t *testing.T) {
scimGroups, err := scimClient.Groups.List(ctx, nil)
require.NoError(t, err)
assert.Len(t, scimGroups.Items, 0)
assert.Equal(t, 0, scimGroups.TotalCount)
})
t.Run("list all created groups", func(t *testing.T) {
var groupIDs []string
var expectedGroups []AdminSCIMGroup
t.Cleanup(func() {
for _, id := range groupIDs {
deleteSCIMGroup(ctx, t, client, id, scimToken.Token)
}
})
for range 2 {
groupName := randomStringWithoutSpecialChar(t)
id := createSCIMGroup(ctx, t, client, groupName, scimToken.Token)
groupIDs = append(groupIDs, id)
expectedGroups = append(expectedGroups, AdminSCIMGroup{ID: id, Name: groupName})
}
scimGroups, err := scimClient.Groups.List(ctx, nil)
require.NoError(t, err)
assert.Len(t, scimGroups.Items, 2)
assert.Equal(t, 2, scimGroups.TotalCount)
var found int
for _, eg := range expectedGroups {
for _, g := range scimGroups.Items {
if g.ID == eg.ID {
assert.Equal(t, eg.Name, g.Name)
found++
break
}
}
}
assert.Equal(t, 2, found, "all created groups should have matched ID and Name")
})
})
t.Run("filter groups using search query", func(t *testing.T) {
var groupIDs []string
t.Cleanup(func() {
for _, id := range groupIDs {
deleteSCIMGroup(ctx, t, client, id, scimToken.Token)
}
})
prefix := randomStringWithoutSpecialChar(t) + "-"
// Create a cohesive set of 10 groups that satisfy all query scenarios
groupIDs = append(groupIDs,
createSCIMGroup(ctx, t, client, prefix+"this-group-exists", scimToken.Token),
createSCIMGroup(ctx, t, client, prefix+"matching-group-1", scimToken.Token),
createSCIMGroup(ctx, t, client, prefix+"matching-group-2", scimToken.Token),
createSCIMGroup(ctx, t, client, prefix+"matching-group-3", scimToken.Token),
createSCIMGroup(ctx, t, client, prefix+"CaSe-InSeNsItIvE-gRoUp", scimToken.Token),
)
for range 5 {
id := createSCIMGroup(ctx, t, client, prefix+"random-"+randomStringWithoutSpecialChar(t), scimToken.Token)
groupIDs = append(groupIDs, id)
}
testCases := []struct {
name string
options AdminSCIMGroupListOptions
expectedGroupCount int
}{
{
name: "query returns no results for non-existent prefix",
options: AdminSCIMGroupListOptions{Query: prefix + "this-group-doesnot-exist"},
expectedGroupCount: 0,
},
{
name: "query returns exact match for specific group",
options: AdminSCIMGroupListOptions{Query: prefix + "this-group-exists"},
expectedGroupCount: 1,
},
{
name: "query returns multiple groups matching prefix",
options: AdminSCIMGroupListOptions{Query: prefix + "matching-group-"},
expectedGroupCount: 3,
},
{
name: "query performs case-insensitive match",
options: AdminSCIMGroupListOptions{Query: prefix + "case-insensitive-group"},
expectedGroupCount: 1,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
scimGroups, err := scimClient.Groups.List(ctx, &tc.options)
require.NoError(t, err)
assert.Len(t, scimGroups.Items, tc.expectedGroupCount)
assert.Equal(t, tc.expectedGroupCount, scimGroups.TotalCount)
})
}
})
t.Run("paginate through groups", func(t *testing.T) {
var groupIDs []string
t.Cleanup(func() {
for _, id := range groupIDs {
deleteSCIMGroup(ctx, t, client, id, scimToken.Token)
}
})
prefix := randomStringWithoutSpecialChar(t) + "-"
// Create groups to test default page size
for range maxGroupsToCreate {
groupName := prefix + randomStringWithoutSpecialChar(t)
id := createSCIMGroup(ctx, t, client, groupName, scimToken.Token)
groupIDs = append(groupIDs, id)
}
testCases := []struct {
name string
options AdminSCIMGroupListOptions
excludeOptions *AdminSCIMGroupListOptions
expectedGroupCount int
expectedTotalCount int
expectedTotalPages int
expectedPage int
expectedNextPage int
expectedPrevPage int
}{
{
name: fmt.Sprintf("default page size (%d) returns first page", defaultPageSize),
options: AdminSCIMGroupListOptions{Query: prefix, ListOptions: ListOptions{PageNumber: 1}},
expectedGroupCount: defaultPageSize,
expectedTotalCount: maxGroupsToCreate,
expectedTotalPages: 2,
expectedPage: 1,
expectedNextPage: 2,
expectedPrevPage: 0,
},
{
name: fmt.Sprintf("default page size (%d) returns second page", defaultPageSize),
options: AdminSCIMGroupListOptions{Query: prefix, ListOptions: ListOptions{PageNumber: 2}},
excludeOptions: &AdminSCIMGroupListOptions{Query: prefix, ListOptions: ListOptions{PageNumber: 1}},
expectedGroupCount: maxGroupsToCreate - defaultPageSize,
expectedTotalCount: maxGroupsToCreate,
expectedTotalPages: 2,
expectedPage: 2,
expectedNextPage: 0,
expectedPrevPage: 1,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
scimGroups, err := scimClient.Groups.List(ctx, &tc.options)
require.NoError(t, err)
assert.Len(
t, scimGroups.Items, tc.expectedGroupCount,
"expected default page size to be 20; if this fails, the API default page size may have changed",
)
assert.Equal(t, tc.expectedTotalCount, scimGroups.TotalCount)
assert.Equal(t, tc.expectedTotalPages, scimGroups.TotalPages)
assert.Equal(t, tc.expectedPage, scimGroups.CurrentPage)
assert.Equal(t, tc.expectedNextPage, scimGroups.NextPage)
assert.Equal(t, tc.expectedPrevPage, scimGroups.PreviousPage)
// Verify mutually exclusive items
if tc.excludeOptions != nil {
excludedGroups, err := scimClient.Groups.List(ctx, tc.excludeOptions)
require.NoError(t, err)
for _, g := range scimGroups.Items {
for _, exGroup := range excludedGroups.Items {
assert.NotEqual(t, g.ID, exGroup.ID)
}
}
}
})
}
})
t.Run("combine query filtering and pagination", func(t *testing.T) {
var groupIDs []string
t.Cleanup(func() {
for _, id := range groupIDs {
deleteSCIMGroup(ctx, t, client, id, scimToken.Token)
}
})
prefix := randomStringWithoutSpecialChar(t)
// Create 4 random groups
for range 4 {
groupName := prefix + "-" + randomStringWithoutSpecialChar(t)
id := createSCIMGroup(ctx, t, client, groupName, scimToken.Token)
groupIDs = append(groupIDs, id)
}
// Create 6 matching groups with same suffix "-idp-group"
for range 6 {
groupName := fmt.Sprintf("%s-idp-group-%s", prefix, randomStringWithoutSpecialChar(t))
id := createSCIMGroup(ctx, t, client, groupName, scimToken.Token)
groupIDs = append(groupIDs, id)
}
testCases := []struct {
name string
options AdminSCIMGroupListOptions
excludeOptions *AdminSCIMGroupListOptions
expectedGroupCount int
expectedTotalCount int
expectedTotalPages int
expectedPage int
}{
{
name: "first page of filtered results",
options: AdminSCIMGroupListOptions{
Query: prefix + "-idp-group",
ListOptions: ListOptions{PageSize: 3, PageNumber: 1},
},
expectedGroupCount: 3,
expectedTotalCount: 6,
expectedTotalPages: 2,
expectedPage: 1,
},
{
name: "second page of filtered results",
options: AdminSCIMGroupListOptions{
Query: prefix + "-idp-group",
ListOptions: ListOptions{PageSize: 3, PageNumber: 2},
},
excludeOptions: &AdminSCIMGroupListOptions{
Query: prefix + "-idp-group",
ListOptions: ListOptions{PageSize: 3, PageNumber: 1},
},
expectedGroupCount: 3,
expectedTotalCount: 6,
expectedTotalPages: 2,
expectedPage: 2,
},
{
name: "out of bounds page of filtered results returns empty list",
options: AdminSCIMGroupListOptions{
Query: prefix + "-idp-group",
ListOptions: ListOptions{PageSize: 3, PageNumber: 3},
},
expectedGroupCount: 0,
expectedTotalCount: 6,
expectedTotalPages: 2,
expectedPage: 3,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
scimGroups, err := scimClient.Groups.List(ctx, &tc.options)
require.NoError(t, err)
assert.Len(t, scimGroups.Items, tc.expectedGroupCount)
assert.Equal(t, tc.expectedTotalCount, scimGroups.TotalCount)
assert.Equal(t, tc.expectedTotalPages, scimGroups.TotalPages)
assert.Equal(t, tc.expectedPage, scimGroups.CurrentPage)
// Verify mutually exclusive items
if tc.excludeOptions != nil {
excludedGroups, err := scimClient.Groups.List(ctx, tc.excludeOptions)
require.NoError(t, err)
for _, g := range scimGroups.Items {
for _, exGroup := range excludedGroups.Items {
assert.NotEqual(t, g.ID, exGroup.ID)
}
}
}
})
}
})
}
================================================
FILE: admin_setting_scim_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2026
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAdminSettings_SCIM_Read(t *testing.T) {
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
t.Run("read scim settings with default values", func(t *testing.T) {
scimSettings, err := client.Admin.Settings.SCIM.Read(ctx)
require.NoError(t, err)
assert.Equal(t, "scim", scimSettings.ID)
assert.False(t, scimSettings.Enabled)
assert.False(t, scimSettings.Paused)
assert.Empty(t, scimSettings.SiteAdminGroupSCIMID)
assert.Empty(t, scimSettings.SiteAdminGroupDisplayName)
})
}
func TestAdminSettings_SCIM_Update(t *testing.T) {
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
enableSAML(ctx, t, client, true)
defer enableSAML(ctx, t, client, false)
scimClient := client.Admin.Settings.SCIM
t.Run("enable scim settings", func(t *testing.T) {
err := setSAMLProviderType(ctx, t, client, true)
require.NoErrorf(t, err, "failed to set SAML provider type")
defer cleanupSCIMSettings(ctx, t, client)
scimSettings, err := scimClient.Update(ctx, AdminSCIMSettingUpdateOptions{Enabled: Bool(true)})
require.NoError(t, err)
assert.True(t, scimSettings.Enabled)
})
t.Run("pause scim settings", func(t *testing.T) {
err := setSAMLProviderType(ctx, t, client, true)
require.NoErrorf(t, err, "failed to set SAML provider type")
defer cleanupSCIMSettings(ctx, t, client)
_, err = scimClient.Update(ctx, AdminSCIMSettingUpdateOptions{
Enabled: Bool(true),
})
require.NoError(t, err)
testCases := []struct {
name string
paused bool
}{
{"pause scim provisioning", true},
{"unpause scim provisioning", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, err := scimClient.Update(ctx, AdminSCIMSettingUpdateOptions{Paused: &tc.paused})
require.NoError(t, err)
scimSettings, err := scimClient.Read(ctx)
require.NoError(t, err)
assert.Equal(t, tc.paused, scimSettings.Paused)
})
}
})
t.Run("update site admin group scim id", func(t *testing.T) {
err := setSAMLProviderType(ctx, t, client, true)
require.NoErrorf(t, err, "failed to set SAML provider type")
defer cleanupSCIMSettings(ctx, t, client)
_, err = scimClient.Update(ctx, AdminSCIMSettingUpdateOptions{Enabled: Bool(true)})
require.NoError(t, err)
scimToken, err := scimClient.Tokens.Create(ctx, "scim integration test token")
require.NoError(t, err)
require.NotEmpty(t, scimToken.Token)
scimGroupID := createSCIMGroup(ctx, t, client, "foo", scimToken.Token)
testCases := []struct {
name string
scimGroupID string
raiseError bool
}{
{"link scim group to site admin role", scimGroupID, false},
{"trying to link non-existent group - should raise error", "this-group-doesn't-exist", true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, err := scimClient.Update(ctx, AdminSCIMSettingUpdateOptions{SiteAdminGroupSCIMID: &tc.scimGroupID})
if tc.raiseError {
require.Error(t, err)
return
}
require.NoError(t, err)
scimSettings, err := scimClient.Read(ctx)
require.NoError(t, err)
assert.Equal(t, tc.scimGroupID, scimSettings.SiteAdminGroupSCIMID)
assert.Equal(t, "foo", scimSettings.SiteAdminGroupDisplayName)
})
}
})
}
func TestAdminSettings_SCIM_Delete(t *testing.T) {
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
enableSAML(ctx, t, client, true)
defer enableSAML(ctx, t, client, false)
scimClient := client.Admin.Settings.SCIM
t.Run("disable scim settings", func(t *testing.T) {
err := setSAMLProviderType(ctx, t, client, true)
require.NoErrorf(t, err, "failed to set SAML provider type")
defer cleanupSCIMSettings(ctx, t, client)
testCases := []struct {
name string
isScimEnabled bool
}{
{"disable scim provisioning when it's already enabled", true},
{"disable scim provisioning when it's already disabled - should not raise error", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.isScimEnabled {
_, err := scimClient.Update(ctx, AdminSCIMSettingUpdateOptions{Enabled: Bool(true)})
require.NoError(t, err)
}
err := scimClient.Delete(ctx)
require.NoError(t, err)
scimSettings, err := scimClient.Read(ctx)
require.NoError(t, err)
assert.False(t, scimSettings.Enabled)
})
}
})
}
// cleanup scim settings by disabling scim provisioning and setting saml provider type to unknown.
func cleanupSCIMSettings(ctx context.Context, t *testing.T, client *Client) {
t.Helper()
scimSettings, err := client.Admin.Settings.SCIM.Read(ctx)
if err == nil && scimSettings.Enabled {
err = client.Admin.Settings.SCIM.Delete(ctx)
require.NoErrorf(t, err, "failed to disable SCIM provisioning")
}
err = setSAMLProviderType(ctx, t, client, false)
require.NoErrorf(t, err, "failed to set SAML provider type")
}
================================================
FILE: admin_setting_scim_token.go
================================================
// Copyright IBM Corp. 2018, 2026
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"time"
)
var _ AdminSCIMTokens = (*adminSCIMTokens)(nil)
// AdminSCIMTokens describes all the Admin SCIM token related methods that the Terraform
// Enterprise API supports
//
// TFE API docs: https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/scim-tokens
type AdminSCIMTokens interface {
// List all Admin SCIM tokens.
List(ctx context.Context) (*AdminSCIMTokenList, error)
// Create an Admin SCIM token.
Create(ctx context.Context, description string) (*AdminSCIMToken, error)
// Create an Admin SCIM token with options.
CreateWithOptions(ctx context.Context, options AdminSCIMTokenCreateOptions) (*AdminSCIMToken, error)
// Read an Admin SCIM token by its ID.
Read(ctx context.Context, scimTokenID string) (*AdminSCIMToken, error)
// Delete an Admin SCIM token.
Delete(ctx context.Context, scimTokenID string) error
}
// adminSCIMTokens implements AdminSCIMTokens
type adminSCIMTokens struct {
client *Client
}
// AdminSCIMTokenList represents a list of Admin SCIM tokens
type AdminSCIMTokenList struct {
Items []*AdminSCIMToken
}
// AdminSCIMToken represents a Terraform Enterprise Admin SCIM token.
type AdminSCIMToken struct {
ID string `jsonapi:"primary,authentication-tokens"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
LastUsedAt time.Time `jsonapi:"attr,last-used-at,iso8601"`
ExpiredAt time.Time `jsonapi:"attr,expired-at,iso8601"`
Description string `jsonapi:"attr,description"`
Token string `jsonapi:"attr,token,omitempty"`
}
// AdminSCIMTokenCreateOptions represents the options for creating an Admin SCIM token
type AdminSCIMTokenCreateOptions struct {
// Required: A human-readable description of the token's purpose
// (for example, Okta SCIM Integration).
Description *string `jsonapi:"attr,description"`
// Optional: Optional ISO-8601 timestamp for token expiration.
// Defaults to 365 days in the future. Must be between 29 and 365 days in the future.
ExpiredAt *time.Time `jsonapi:"attr,expired-at,iso8601,omitempty"`
}
// List all Admin SCIM tokens.
func (a *adminSCIMTokens) List(ctx context.Context) (*AdminSCIMTokenList, error) {
req, err := a.client.NewRequest("GET", AdminSCIMTokensPath, nil)
if err != nil {
return nil, err
}
scimTokens := &AdminSCIMTokenList{}
err = req.Do(ctx, scimTokens)
if err != nil {
return nil, err
}
return scimTokens, nil
}
// Create an Admin SCIM token.
func (a *adminSCIMTokens) Create(ctx context.Context, description string) (*AdminSCIMToken, error) {
return a.CreateWithOptions(ctx, AdminSCIMTokenCreateOptions{
Description: &description,
})
}
// Create an Admin SCIM token with options.
func (a *adminSCIMTokens) CreateWithOptions(ctx context.Context, options AdminSCIMTokenCreateOptions) (*AdminSCIMToken, error) {
if !validString(options.Description) {
return nil, ErrSCIMTokenDescription
}
req, err := a.client.NewRequest("POST", AdminSCIMTokensPath, &options)
if err != nil {
return nil, err
}
scimToken := &AdminSCIMToken{}
err = req.Do(ctx, scimToken)
if err != nil {
return nil, err
}
return scimToken, nil
}
// Read an Admin SCIM token by its ID.
func (a *adminSCIMTokens) Read(ctx context.Context, scimTokenID string) (*AdminSCIMToken, error) {
if !validStringID(&scimTokenID) {
return nil, ErrInvalidTokenID
}
u := fmt.Sprintf("%s/%s", AdminSCIMTokensPath, url.PathEscape(scimTokenID))
req, err := a.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
scimToken := &AdminSCIMToken{}
err = req.Do(ctx, scimToken)
if err != nil {
return nil, err
}
return scimToken, nil
}
// Delete an Admin SCIM token.
func (a *adminSCIMTokens) Delete(ctx context.Context, scimTokenID string) error {
if !validStringID(&scimTokenID) {
return ErrInvalidTokenID
}
u := fmt.Sprintf(AuthenticationTokensPath, url.PathEscape(scimTokenID))
req, err := a.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
================================================
FILE: admin_setting_scim_token_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2026
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAdminSCIMTokens_Create(t *testing.T) {
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
enableSCIM(ctx, t, client, true)
defer enableSCIM(ctx, t, client, false)
scimTokenClient := client.Admin.Settings.SCIM.Tokens
t.Run("create token", func(t *testing.T) {
testCases := []struct {
name string
description string
raiseError bool
}{
{"with valid description", "Test Description", false},
{"with empty description", "", true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
scimToken, err := scimTokenClient.Create(ctx, tc.description)
if tc.raiseError {
require.Error(t, err)
return
}
t.Cleanup(func() {
err := scimTokenClient.Delete(ctx, scimToken.ID)
if err != nil && err != ErrResourceNotFound {
t.Logf("failed to cleanup SCIM token %q: %v", scimToken.ID, err)
}
})
require.NoError(t, err)
require.NotNil(t, scimToken)
assert.NotEmpty(t, scimToken)
assert.NotEmpty(t, scimToken.ID)
assert.NotEmpty(t, scimToken.Token)
assert.NotEmpty(t, scimToken.Description)
assert.Equal(t, tc.description, scimToken.Description)
assert.WithinDuration(t, time.Now(), scimToken.CreatedAt, 10*time.Second)
assert.WithinDuration(t, time.Now().Add(365*24*time.Hour), scimToken.ExpiredAt, 10*time.Second)
})
}
})
}
func TestAdminSCIMTokens_CreateWithOptions(t *testing.T) {
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
enableSCIM(ctx, t, client, true)
defer enableSCIM(ctx, t, client, false)
scimTokenClient := client.Admin.Settings.SCIM.Tokens
t.Run("create token", func(t *testing.T) {
testCases := []struct {
name string
options AdminSCIMTokenCreateOptions
raiseError bool
}{
{"with no options - should fail", AdminSCIMTokenCreateOptions{}, true},
{
"with nil description - should fail`",
AdminSCIMTokenCreateOptions{
Description: nil,
},
true,
},
{
"with empty description - should fail`",
AdminSCIMTokenCreateOptions{
Description: String(""),
},
true,
},
{
"with description",
AdminSCIMTokenCreateOptions{
Description: String("Test Description"),
},
false,
},
{
"with only expiration - should fail",
AdminSCIMTokenCreateOptions{
ExpiredAt: Ptr(time.Now().Add(30 * 24 * time.Hour)),
},
true,
},
{
"with description and expiration",
AdminSCIMTokenCreateOptions{
Description: String("Test Description"),
ExpiredAt: Ptr(time.Now().Add(60 * 24 * time.Hour)),
},
false,
},
{
"with expiration in 20 days - should fail",
AdminSCIMTokenCreateOptions{
ExpiredAt: Ptr(time.Now().Add(20 * 24 * time.Hour)),
},
true,
},
{
"with expiration in 400 days - should fail",
AdminSCIMTokenCreateOptions{
ExpiredAt: Ptr(time.Now().Add(400 * 24 * time.Hour)),
},
true,
},
{
"with expiration in 29 days",
AdminSCIMTokenCreateOptions{
Description: String("Test Description"),
ExpiredAt: Ptr(time.Now().Add(29*24*time.Hour + 10*time.Second)), // adding 10 sec to account for any delays in test execution
},
false,
},
{
"with expiration in 365 days",
AdminSCIMTokenCreateOptions{
Description: String("Test Description"),
ExpiredAt: Ptr(time.Now().Add(365 * 24 * time.Hour)),
},
false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var scimToken *AdminSCIMToken
var err error
scimToken, err = scimTokenClient.CreateWithOptions(ctx, tc.options)
if tc.raiseError {
require.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, scimToken)
assert.NotEmpty(t, scimToken)
assert.NotEmpty(t, scimToken.ID)
t.Cleanup(func() {
err := scimTokenClient.Delete(ctx, scimToken.ID)
if err != nil && err != ErrResourceNotFound {
t.Logf("failed to cleanup SCIM token %q: %v", scimToken.ID, err)
}
})
if tc.options.ExpiredAt != nil {
assert.WithinDuration(t, *tc.options.ExpiredAt, scimToken.ExpiredAt, 10*time.Second)
} else {
expectedExpiredAt := scimToken.CreatedAt.Add(365 * 24 * time.Hour)
assert.WithinDuration(t, expectedExpiredAt, scimToken.ExpiredAt, 10*time.Second)
}
})
}
})
}
func TestAdminSCIMTokens_List(t *testing.T) {
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
enableSCIM(ctx, t, client, true)
defer enableSCIM(ctx, t, client, false)
scimTokenClient := client.Admin.Settings.SCIM.Tokens
t.Run("list tokens", func(t *testing.T) {
// create tokens to ensure there is data to list
var scimTokens []*AdminSCIMToken
for i := 0; i < 3; i++ {
scimToken, err := scimTokenClient.Create(ctx, fmt.Sprintf("foo token %d", i))
require.NoError(t, err)
tokenID := scimToken.ID
t.Cleanup(func() {
err := scimTokenClient.Delete(ctx, tokenID)
if err != nil && err != ErrResourceNotFound {
t.Logf("failed to cleanup SCIM token %q: %v", tokenID, err)
}
})
scimTokens = append(scimTokens, scimToken)
}
tokenList, err := scimTokenClient.List(ctx)
require.NoError(t, err)
require.NotNil(t, tokenList)
assert.NotEmpty(t, tokenList.Items)
var expectedIDs []string
var actualIDs []string
for _, listedToken := range tokenList.Items {
actualIDs = append(actualIDs, listedToken.ID)
}
for _, token := range scimTokens {
expectedIDs = append(expectedIDs, token.ID)
assert.Contains(t, actualIDs, token.ID)
}
assert.Subset(t, actualIDs, expectedIDs)
})
}
func TestAdminSCIMTokens_Read(t *testing.T) {
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
enableSCIM(ctx, t, client, true)
defer enableSCIM(ctx, t, client, false)
scimTokenClient := client.Admin.Settings.SCIM.Tokens
t.Run("read token", func(t *testing.T) {
// create a token to ensure there is data to read
scimToken, err := scimTokenClient.CreateWithOptions(ctx, AdminSCIMTokenCreateOptions{
Description: String("Test Desc"),
ExpiredAt: Ptr(time.Now().Add(60 * 24 * time.Hour)),
})
require.NoError(t, err)
require.NotNil(t, scimToken)
t.Cleanup(func() {
err := scimTokenClient.Delete(ctx, scimToken.ID)
if err != nil && err != ErrResourceNotFound {
t.Logf("failed to cleanup SCIM token %q: %v", scimToken.ID, err)
}
})
testCases := []struct {
name string
tokenID string
raiseError bool
}{
{"with valid token ID", scimToken.ID, false},
{"with invalid token ID", "invalid id", true},
{"with empty token ID", "", true},
{"with non-existent token ID", "this-does-not-exist", true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
token, err := scimTokenClient.Read(ctx, tc.tokenID)
if tc.raiseError {
require.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, token)
assert.Equal(t, tc.tokenID, token.ID)
// Verify specific field properties for the valid token
if !tc.raiseError {
assert.Equal(t, scimToken.Description, token.Description)
assert.WithinDuration(t, scimToken.ExpiredAt, token.ExpiredAt, time.Second)
assert.NotEmpty(t, scimToken.Token)
assert.Empty(t, token.Token)
}
})
}
})
}
func TestAdminSCIMTokens_Delete(t *testing.T) {
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
enableSCIM(ctx, t, client, true)
defer enableSCIM(ctx, t, client, false)
scimTokenClient := client.Admin.Settings.SCIM.Tokens
t.Run("delete token", func(t *testing.T) {
// create a token to ensure there is data to delete
scimToken, err := scimTokenClient.Create(ctx, "foo token")
require.NoError(t, err)
require.NotNil(t, scimToken)
t.Cleanup(func() {
err := scimTokenClient.Delete(ctx, scimToken.ID)
if err != nil && err != ErrResourceNotFound {
t.Logf("failed to cleanup SCIM token %q: %v", scimToken.ID, err)
}
})
testCases := []struct {
name string
tokenID string
raiseError bool
}{
{"with valid token ID", scimToken.ID, false},
{"with invalid token ID", "invalid id", true},
{"with empty token ID", "", true},
{"with non-existent token ID", "this-does-not-exist", true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err = scimTokenClient.Delete(ctx, tc.tokenID)
if tc.raiseError {
require.Error(t, err)
if tc.tokenID == "this-does-not-exist" {
assert.ErrorIs(t, err, ErrResourceNotFound)
} else {
assert.ErrorIs(t, err, ErrInvalidTokenID)
}
return
}
require.NoError(t, err)
// verify deletion
_, err = scimTokenClient.Read(ctx, tc.tokenID)
require.Error(t, err)
})
}
})
}
================================================
FILE: admin_setting_smtp.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
)
// Compile-time proof of interface implementation.
var _ SMTPSettings = (*adminSMTPSettings)(nil)
// SMTPSettings describes all the SMTP admin settings for the Admin Setting API https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/settings
type SMTPSettings interface {
// Read returns the SMTP settings.
Read(ctx context.Context) (*AdminSMTPSetting, error)
// Update updates SMTP settings.
Update(ctx context.Context, options AdminSMTPSettingsUpdateOptions) (*AdminSMTPSetting, error)
}
type adminSMTPSettings struct {
client *Client
}
// SMTPAuthType represents valid SMTP Auth types.
type SMTPAuthType string
// List of all SMTP auth types.
const (
SMTPAuthNone SMTPAuthType = "none"
SMTPAuthPlain SMTPAuthType = "plain"
SMTPAuthLogin SMTPAuthType = "login"
)
// AdminSMTPSetting represents a the SMTP settings in Terraform Enterprise.
type AdminSMTPSetting struct {
ID string `jsonapi:"primary,smtp-settings"`
Enabled bool `jsonapi:"attr,enabled"`
Host string `jsonapi:"attr,host"`
Port int `jsonapi:"attr,port"`
Sender string `jsonapi:"attr,sender"`
Auth SMTPAuthType `jsonapi:"attr,auth"`
Username string `jsonapi:"attr,username"`
}
// Read returns the SMTP settings.
func (a *adminSMTPSettings) Read(ctx context.Context) (*AdminSMTPSetting, error) {
req, err := a.client.NewRequest("GET", "admin/smtp-settings", nil)
if err != nil {
return nil, err
}
smtp := &AdminSMTPSetting{}
err = req.Do(ctx, smtp)
if err != nil {
return nil, err
}
return smtp, nil
}
// AdminSMTPSettingsUpdateOptions represents the admin options for updating
// SMTP settings.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/settings#request-body-3
type AdminSMTPSettingsUpdateOptions struct {
Enabled *bool `jsonapi:"attr,enabled,omitempty"`
Host *string `jsonapi:"attr,host,omitempty"`
Port *int `jsonapi:"attr,port,omitempty"`
Sender *string `jsonapi:"attr,sender,omitempty"`
Auth *SMTPAuthType `jsonapi:"attr,auth,omitempty"`
Username *string `jsonapi:"attr,username,omitempty"`
Password *string `jsonapi:"attr,password,omitempty"`
TestEmailAddress *string `jsonapi:"attr,test-email-address,omitempty"`
}
// Update updates the SMTP settings.
func (a *adminSMTPSettings) Update(ctx context.Context, options AdminSMTPSettingsUpdateOptions) (*AdminSMTPSetting, error) {
if err := options.valid(); err != nil {
return nil, err
}
req, err := a.client.NewRequest("PATCH", "admin/smtp-settings", &options)
if err != nil {
return nil, err
}
smtp := &AdminSMTPSetting{}
err = req.Do(ctx, smtp)
if err != nil {
return nil, err
}
return smtp, nil
}
func (o AdminSMTPSettingsUpdateOptions) valid() error {
if validString((*string)(o.Auth)) {
if err := validateAdminSettingSMTPAuth(*o.Auth); err != nil {
return err
}
}
return nil
}
func validateAdminSettingSMTPAuth(authVal SMTPAuthType) error {
switch authVal {
case SMTPAuthNone, SMTPAuthPlain, SMTPAuthLogin:
// do nothing
default:
return ErrInvalidSMTPAuth
}
return nil
}
================================================
FILE: admin_setting_smtp_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAdminSettings_SMTP_Read(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
smtpSettings, err := client.Admin.Settings.SMTP.Read(ctx)
require.NoError(t, err)
assert.Equal(t, "smtp", smtpSettings.ID)
assert.NotNil(t, smtpSettings.Enabled)
assert.NotNil(t, smtpSettings.Host)
assert.NotNil(t, smtpSettings.Port)
assert.NotNil(t, smtpSettings.Sender)
assert.NotNil(t, smtpSettings.Auth)
assert.NotNil(t, smtpSettings.Username)
}
func TestAdminSettings_SMTP_Update(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
enabled := true
disabled := false
t.Run("with Auth option defined", func(t *testing.T) {
smtpSettings, err := client.Admin.Settings.SMTP.Update(ctx, AdminSMTPSettingsUpdateOptions{
Enabled: Bool(disabled),
Auth: SMTPAuthValue(SMTPAuthNone),
})
require.NoError(t, err)
assert.Equal(t, disabled, smtpSettings.Enabled)
})
t.Run("with no Auth option", func(t *testing.T) {
smtpSettings, err := client.Admin.Settings.SMTP.Update(ctx, AdminSMTPSettingsUpdateOptions{
Enabled: Bool(disabled),
TestEmailAddress: String("test@example.com"),
Host: String("123"),
Port: Int(123),
})
require.NoError(t, err)
assert.Equal(t, SMTPAuthNone, smtpSettings.Auth)
assert.Equal(t, disabled, smtpSettings.Enabled)
})
t.Run("with invalid Auth option", func(t *testing.T) {
var SMTPAuthPlained SMTPAuthType = "plained"
_, err := client.Admin.Settings.SMTP.Update(ctx, AdminSMTPSettingsUpdateOptions{
Enabled: Bool(enabled),
Auth: &SMTPAuthPlained,
})
assert.Equal(t, err, ErrInvalidSMTPAuth)
})
}
================================================
FILE: admin_setting_twilio.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
)
// Compile-time proof of interface implementation.
var _ TwilioSettings = (*adminTwilioSettings)(nil)
// TwilioSettings describes all the Twilio admin settings for the Admin Setting API.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/settings
type TwilioSettings interface {
// Read returns the Twilio settings.
Read(ctx context.Context) (*AdminTwilioSetting, error)
// Update updates Twilio settings.
Update(ctx context.Context, options AdminTwilioSettingsUpdateOptions) (*AdminTwilioSetting, error)
// Verify verifies Twilio settings.
Verify(ctx context.Context, options AdminTwilioSettingsVerifyOptions) error
}
type adminTwilioSettings struct {
client *Client
}
// AdminTwilioSetting represents the Twilio settings in Terraform Enterprise.
type AdminTwilioSetting struct {
ID string `jsonapi:"primary,twilio-settings"`
Enabled bool `jsonapi:"attr,enabled"`
AccountSid string `jsonapi:"attr,account-sid"`
FromNumber string `jsonapi:"attr,from-number"`
}
// Read returns the Twilio settings.
func (a *adminTwilioSettings) Read(ctx context.Context) (*AdminTwilioSetting, error) {
req, err := a.client.NewRequest("GET", "admin/twilio-settings", nil)
if err != nil {
return nil, err
}
twilio := &AdminTwilioSetting{}
err = req.Do(ctx, twilio)
if err != nil {
return nil, err
}
return twilio, nil
}
// AdminTwilioSettingsUpdateOptions represents the admin options for updating
// Twilio settings.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/settings#request-body-4
type AdminTwilioSettingsUpdateOptions struct {
Enabled *bool `jsonapi:"attr,enabled,omitempty"`
AccountSid *string `jsonapi:"attr,account-sid,omitempty"`
AuthToken *string `jsonapi:"attr,auth-token,omitempty"`
FromNumber *string `jsonapi:"attr,from-number,omitempty"`
}
// AdminTwilioSettingsVerifyOptions represents the test number to verify Twilio.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/settings#verify-twilio-settings
type AdminTwilioSettingsVerifyOptions struct {
TestNumber *string `jsonapi:"attr,test-number"` // Required
}
// Update updates the Twilio settings.
func (a *adminTwilioSettings) Update(ctx context.Context, options AdminTwilioSettingsUpdateOptions) (*AdminTwilioSetting, error) {
req, err := a.client.NewRequest("PATCH", "admin/twilio-settings", &options)
if err != nil {
return nil, err
}
twilio := &AdminTwilioSetting{}
err = req.Do(ctx, twilio)
if err != nil {
return nil, err
}
return twilio, nil
}
// Verify verifies Twilio settings.
func (a *adminTwilioSettings) Verify(ctx context.Context, options AdminTwilioSettingsVerifyOptions) error {
if err := options.valid(); err != nil {
return err
}
req, err := a.client.NewRequest("PATCH", "admin/twilio-settings/verify", &options)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o AdminTwilioSettingsVerifyOptions) valid() error {
if !validString(o.TestNumber) {
return ErrRequiredTestNumber
}
return nil
}
================================================
FILE: admin_setting_twilio_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAdminSettings_Twilio_Read(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
twilioSettings, err := client.Admin.Settings.Twilio.Read(ctx)
require.NoError(t, err)
assert.Equal(t, "twilio", twilioSettings.ID)
assert.NotNil(t, twilioSettings.Enabled)
assert.NotNil(t, twilioSettings.AccountSid)
assert.NotNil(t, twilioSettings.FromNumber)
}
func TestAdminSettings_Twilio_Update(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
twilioSettings, err := client.Admin.Settings.Twilio.Update(ctx, AdminTwilioSettingsUpdateOptions{
Enabled: Bool(false),
})
require.NoError(t, err)
assert.Equal(t, false, twilioSettings.Enabled)
}
func TestAdminSettings_Twilio_Verify(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
err := client.Admin.Settings.Twilio.Verify(ctx, AdminTwilioSettingsVerifyOptions{})
assert.Equal(t, err, ErrRequiredTestNumber)
}
================================================
FILE: admin_terraform_version.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"reflect"
"time"
)
// Compile-time proof of interface implementation.
var _ AdminTerraformVersions = (*adminTerraformVersions)(nil)
const (
linux = "linux"
amd64 = "amd64"
arm64 = "arm64"
)
// AdminTerraformVersions describes all the admin terraform versions related methods that
// the Terraform Enterprise API supports.
// Note that admin terraform versions are only available in Terraform Enterprise.
//
// TFE API docs: https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/terraform-versions
type AdminTerraformVersions interface {
// List all the terraform versions.
List(ctx context.Context, options *AdminTerraformVersionsListOptions) (*AdminTerraformVersionsList, error)
// Read a terraform version by its ID.
Read(ctx context.Context, id string) (*AdminTerraformVersion, error)
// Create a terraform version.
Create(ctx context.Context, options AdminTerraformVersionCreateOptions) (*AdminTerraformVersion, error)
// Update a terraform version.
Update(ctx context.Context, id string, options AdminTerraformVersionUpdateOptions) (*AdminTerraformVersion, error)
// Delete a terraform version
Delete(ctx context.Context, id string) error
}
// adminTerraformVersions implements AdminTerraformVersions.
type adminTerraformVersions struct {
client *Client
}
// AdminTerraformVersion represents a Terraform Version
type AdminTerraformVersion struct {
ID string `jsonapi:"primary,terraform-versions"`
Version string `jsonapi:"attr,version"`
URL string `jsonapi:"attr,url,omitempty"`
Sha string `jsonapi:"attr,sha,omitempty"`
Deprecated bool `jsonapi:"attr,deprecated"`
DeprecatedReason *string `jsonapi:"attr,deprecated-reason,omitempty"`
Official bool `jsonapi:"attr,official"`
Enabled bool `jsonapi:"attr,enabled"`
Beta bool `jsonapi:"attr,beta"`
Usage int `jsonapi:"attr,usage"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Archs []*ToolVersionArchitecture `jsonapi:"attr,archs,omitempty"`
}
type ToolVersionArchitecture struct {
URL string `jsonapi:"attr,url"`
Sha string `jsonapi:"attr,sha"`
OS string `jsonapi:"attr,os"`
Arch string `jsonapi:"attr,arch"`
}
// AdminTerraformVersionsListOptions represents the options for listing
// terraform versions.
type AdminTerraformVersionsListOptions struct {
ListOptions
// Optional: A query string to find an exact version
Filter string `url:"filter[version],omitempty"`
// Optional: A search query string to find all versions that match version substring
Search string `url:"search[version],omitempty"`
}
// AdminTerraformVersionCreateOptions for creating a terraform version.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/terraform-versions#request-body
type AdminTerraformVersionCreateOptions struct {
Type string `jsonapi:"primary,terraform-versions"`
Version *string `jsonapi:"attr,version"` // Required
URL *string `jsonapi:"attr,url,omitempty"`
Sha *string `jsonapi:"attr,sha,omitempty"`
Official *bool `jsonapi:"attr,official,omitempty"`
Deprecated *bool `jsonapi:"attr,deprecated,omitempty"`
DeprecatedReason *string `jsonapi:"attr,deprecated-reason,omitempty"`
Enabled *bool `jsonapi:"attr,enabled,omitempty"`
Beta *bool `jsonapi:"attr,beta,omitempty"`
Archs []*ToolVersionArchitecture `jsonapi:"attr,archs,omitempty"`
}
// AdminTerraformVersionUpdateOptions for updating terraform version.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/terraform-versions#request-body
type AdminTerraformVersionUpdateOptions struct {
Type string `jsonapi:"primary,terraform-versions"`
Version *string `jsonapi:"attr,version,omitempty"`
URL *string `jsonapi:"attr,url,omitempty"`
Sha *string `jsonapi:"attr,sha,omitempty"`
Official *bool `jsonapi:"attr,official,omitempty"`
Deprecated *bool `jsonapi:"attr,deprecated,omitempty"`
DeprecatedReason *string `jsonapi:"attr,deprecated-reason,omitempty"`
Enabled *bool `jsonapi:"attr,enabled,omitempty"`
Beta *bool `jsonapi:"attr,beta,omitempty"`
Archs []*ToolVersionArchitecture `jsonapi:"attr,archs,omitempty"`
}
// AdminTerraformVersionsList represents a list of terraform versions.
type AdminTerraformVersionsList struct {
*Pagination
Items []*AdminTerraformVersion
}
// List all the terraform versions.
func (a *adminTerraformVersions) List(ctx context.Context, options *AdminTerraformVersionsListOptions) (*AdminTerraformVersionsList, error) {
req, err := a.client.NewRequest("GET", "admin/terraform-versions", options)
if err != nil {
return nil, err
}
tvl := &AdminTerraformVersionsList{}
err = req.Do(ctx, tvl)
if err != nil {
return nil, err
}
return tvl, nil
}
// Read a terraform version by its ID.
func (a *adminTerraformVersions) Read(ctx context.Context, id string) (*AdminTerraformVersion, error) {
if !validStringID(&id) {
return nil, ErrInvalidTerraformVersionID
}
u := fmt.Sprintf("admin/terraform-versions/%s", url.PathEscape(id))
req, err := a.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
tfv := &AdminTerraformVersion{}
err = req.Do(ctx, tfv)
if err != nil {
return nil, err
}
return tfv, nil
}
// Create a new terraform version.
func (a *adminTerraformVersions) Create(ctx context.Context, options AdminTerraformVersionCreateOptions) (*AdminTerraformVersion, error) {
if err := options.valid(); err != nil {
return nil, err
}
req, err := a.client.NewRequest("POST", "admin/terraform-versions", &options)
if err != nil {
return nil, err
}
tfv := &AdminTerraformVersion{}
err = req.Do(ctx, tfv)
if err != nil {
return nil, err
}
return tfv, nil
}
// Update an existing terraform version.
func (a *adminTerraformVersions) Update(ctx context.Context, id string, options AdminTerraformVersionUpdateOptions) (*AdminTerraformVersion, error) {
if !validStringID(&id) {
return nil, ErrInvalidTerraformVersionID
}
u := fmt.Sprintf("admin/terraform-versions/%s", url.PathEscape(id))
req, err := a.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
tfv := &AdminTerraformVersion{}
err = req.Do(ctx, tfv)
if err != nil {
return nil, err
}
return tfv, nil
}
// Delete a terraform version.
func (a *adminTerraformVersions) Delete(ctx context.Context, id string) error {
if !validStringID(&id) {
return ErrInvalidTerraformVersionID
}
u := fmt.Sprintf("admin/terraform-versions/%s", url.PathEscape(id))
req, err := a.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o AdminTerraformVersionCreateOptions) valid() error {
if (reflect.DeepEqual(o, AdminTerraformVersionCreateOptions{})) {
return ErrRequiredTFVerCreateOps
}
if !validString(o.Version) {
return ErrRequiredVersion
}
if !o.validArchs() {
return ErrRequiredArchsOrURLAndSha
}
return nil
}
func (o AdminTerraformVersionCreateOptions) validArchs() bool {
if o.Archs == nil && validString(o.URL) && validString(o.Sha) {
return true
}
for _, a := range o.Archs {
if !validArch(a) {
return false
}
}
return true
}
func validArch(a *ToolVersionArchitecture) bool {
return a.URL != "" && a.Sha != "" && a.OS == linux && (a.Arch == amd64 || a.Arch == arm64)
}
================================================
FILE: admin_terraform_version_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAdminTerraformVersions_List(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
t.Run("without list options", func(t *testing.T) {
tfList, err := client.Admin.TerraformVersions.List(ctx, nil)
require.NoError(t, err)
assert.NotEmpty(t, tfList.Items)
})
t.Run("with list options", func(t *testing.T) {
tfList, err := client.Admin.TerraformVersions.List(ctx, &AdminTerraformVersionsListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
// Out of range page number, so the items should be empty
assert.Empty(t, tfList.Items)
assert.Equal(t, 999, tfList.CurrentPage)
tfList, err = client.Admin.TerraformVersions.List(ctx, &AdminTerraformVersionsListOptions{
ListOptions: ListOptions{
PageNumber: 1,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Equal(t, 1, tfList.CurrentPage)
for _, item := range tfList.Items {
assert.NotNil(t, item.ID)
assert.NotNil(t, item.Version)
assert.NotNil(t, item.URL)
assert.NotNil(t, item.Sha)
assert.NotNil(t, item.Official)
assert.NotNil(t, item.Deprecated)
if item.Deprecated {
assert.NotNil(t, item.DeprecatedReason)
} else {
assert.Nil(t, item.DeprecatedReason)
}
assert.NotNil(t, item.Enabled)
assert.NotNil(t, item.Beta)
assert.NotNil(t, item.Usage)
assert.NotNil(t, item.CreatedAt)
assert.NotNil(t, item.Archs)
}
})
t.Run("with filter query string", func(t *testing.T) {
tfList, err := client.Admin.TerraformVersions.List(ctx, &AdminTerraformVersionsListOptions{
Filter: "1.0.4",
})
require.NoError(t, err)
assert.Equal(t, 1, len(tfList.Items))
// Query for a Terraform version that does not exist
tfList, err = client.Admin.TerraformVersions.List(ctx, &AdminTerraformVersionsListOptions{
Filter: "1000.1000.42",
})
require.NoError(t, err)
assert.Empty(t, tfList.Items)
})
t.Run("with search version query string", func(t *testing.T) {
searchVersion := "1.0"
tfList, err := client.Admin.TerraformVersions.List(ctx, &AdminTerraformVersionsListOptions{
Search: searchVersion,
})
require.NoError(t, err)
assert.NotEmpty(t, tfList.Items)
t.Run("ensure each version matches substring", func(t *testing.T) {
for _, item := range tfList.Items {
assert.Equal(t, true, strings.Contains(item.Version, searchVersion))
}
})
})
}
func TestAdminTerraformVersions_CreateDelete(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
amd64Sha := genSha(t)
url := "https://www.hashicorp.com"
t.Run("with valid options including top level url & sha and archs", func(t *testing.T) {
opts := AdminTerraformVersionCreateOptions{
Version: String(genSafeRandomTerraformVersion()),
URL: String(url),
Sha: &amd64Sha,
Deprecated: Bool(true),
DeprecatedReason: String("Test Reason"),
Official: Bool(false),
Enabled: Bool(false),
Beta: Bool(false),
Archs: []*ToolVersionArchitecture{
{
URL: url,
Sha: amd64Sha,
OS: linux,
Arch: amd64,
},
{
URL: url,
Sha: genSha(t),
OS: linux,
Arch: arm64,
}},
}
tfv, err := client.Admin.TerraformVersions.Create(ctx, opts)
require.NoError(t, err)
defer func() {
deleteErr := client.Admin.TerraformVersions.Delete(ctx, tfv.ID)
require.NoError(t, deleteErr)
}()
assert.Equal(t, *opts.Version, tfv.Version)
assert.Equal(t, *opts.URL, tfv.URL)
assert.Equal(t, *opts.Sha, tfv.Sha)
assert.Equal(t, *opts.Official, tfv.Official)
assert.Equal(t, *opts.Deprecated, tfv.Deprecated)
assert.Equal(t, *opts.DeprecatedReason, *tfv.DeprecatedReason)
assert.Equal(t, *opts.Enabled, tfv.Enabled)
assert.Equal(t, *opts.Beta, tfv.Beta)
for i, arch := range opts.Archs {
assert.Equal(t, arch.URL, tfv.Archs[i].URL)
assert.Equal(t, arch.Sha, tfv.Archs[i].Sha)
assert.Equal(t, arch.OS, tfv.Archs[i].OS)
assert.Equal(t, arch.Arch, tfv.Archs[i].Arch)
}
})
t.Run("with valid options including archs", func(t *testing.T) {
opts := AdminTerraformVersionCreateOptions{
Version: String(genSafeRandomTerraformVersion()),
Deprecated: Bool(true),
DeprecatedReason: String("Test Reason"),
Official: Bool(false),
Enabled: Bool(false),
Beta: Bool(false),
Archs: []*ToolVersionArchitecture{
{
URL: url,
Sha: amd64Sha,
OS: linux,
Arch: amd64,
},
{
URL: url,
Sha: *String(genSha(t)),
OS: linux,
Arch: arm64,
}},
}
tfv, err := client.Admin.TerraformVersions.Create(ctx, opts)
require.NoError(t, err)
defer func() {
deleteErr := client.Admin.TerraformVersions.Delete(ctx, tfv.ID)
require.NoError(t, deleteErr)
}()
assert.Equal(t, *opts.Version, tfv.Version)
assert.Equal(t, *opts.Official, tfv.Official)
assert.Equal(t, *opts.Deprecated, tfv.Deprecated)
assert.Equal(t, *opts.DeprecatedReason, *tfv.DeprecatedReason)
assert.Equal(t, *opts.Enabled, tfv.Enabled)
assert.Equal(t, *opts.Beta, tfv.Beta)
assert.Equal(t, len(opts.Archs), len(tfv.Archs))
for i, arch := range opts.Archs {
assert.Equal(t, arch.URL, tfv.Archs[i].URL)
assert.Equal(t, arch.Sha, tfv.Archs[i].Sha)
assert.Equal(t, arch.OS, tfv.Archs[i].OS)
assert.Equal(t, arch.Arch, tfv.Archs[i].Arch)
}
})
t.Run("with valid options including url and sha", func(t *testing.T) {
opts := AdminTerraformVersionCreateOptions{
Version: String(genSafeRandomTerraformVersion()),
URL: &url,
Sha: &amd64Sha,
Deprecated: Bool(true),
DeprecatedReason: String("Test Reason"),
Official: Bool(false),
Enabled: Bool(false),
Beta: Bool(false),
}
tfv, err := client.Admin.TerraformVersions.Create(ctx, opts)
require.NoError(t, err)
defer func() {
deleteErr := client.Admin.TerraformVersions.Delete(ctx, tfv.ID)
require.NoError(t, deleteErr)
}()
assert.Equal(t, *opts.Version, tfv.Version)
assert.Equal(t, *opts.URL, tfv.URL)
assert.Equal(t, *opts.Sha, tfv.Sha)
assert.Equal(t, *opts.Official, tfv.Official)
assert.Equal(t, *opts.Deprecated, tfv.Deprecated)
assert.Equal(t, opts.DeprecatedReason, tfv.DeprecatedReason)
assert.Equal(t, *opts.Enabled, tfv.Enabled)
assert.Equal(t, *opts.Beta, tfv.Beta)
assert.Equal(t, 1, len(tfv.Archs))
assert.Equal(t, *opts.URL, tfv.Archs[0].URL)
assert.Equal(t, *opts.Sha, tfv.Archs[0].Sha)
assert.Equal(t, linux, tfv.Archs[0].OS)
assert.Equal(t, amd64, tfv.Archs[0].Arch)
})
t.Run("with only required options including tool version url and sha", func(t *testing.T) {
version := genSafeRandomTerraformVersion()
opts := AdminTerraformVersionCreateOptions{
Version: String(version),
URL: &url,
Sha: &amd64Sha,
}
tfv, err := client.Admin.TerraformVersions.Create(ctx, opts)
require.NoError(t, err)
defer func() {
deleteErr := client.Admin.TerraformVersions.Delete(ctx, tfv.ID)
require.NoError(t, deleteErr)
}()
assert.Equal(t, *opts.Version, tfv.Version)
assert.Equal(t, *opts.URL, tfv.URL)
assert.Equal(t, *opts.Sha, tfv.Sha)
assert.Equal(t, false, tfv.Official)
assert.Equal(t, false, tfv.Deprecated)
assert.Nil(t, tfv.DeprecatedReason)
assert.Equal(t, true, tfv.Enabled)
assert.Equal(t, false, tfv.Beta)
assert.Equal(t, 1, len(tfv.Archs))
assert.Equal(t, *opts.URL, tfv.Archs[0].URL)
assert.Equal(t, *opts.Sha, tfv.Archs[0].Sha)
assert.Equal(t, linux, tfv.Archs[0].OS)
assert.Equal(t, amd64, tfv.Archs[0].Arch)
})
t.Run("with only required options including archs", func(t *testing.T) {
version := genSafeRandomTerraformVersion()
opts := AdminTerraformVersionCreateOptions{
Version: String(version),
Archs: []*ToolVersionArchitecture{
{
URL: url,
Sha: amd64Sha,
OS: linux,
Arch: amd64,
},
{
URL: url,
Sha: *String(genSha(t)),
OS: linux,
Arch: arm64,
}},
}
tfv, err := client.Admin.TerraformVersions.Create(ctx, opts)
require.NoError(t, err)
defer func() {
deleteErr := client.Admin.TerraformVersions.Delete(ctx, tfv.ID)
require.NoError(t, deleteErr)
}()
assert.Equal(t, *opts.Version, tfv.Version)
assert.Equal(t, false, tfv.Official)
assert.Equal(t, false, tfv.Deprecated)
assert.Nil(t, tfv.DeprecatedReason)
assert.Equal(t, true, tfv.Enabled)
assert.Equal(t, false, tfv.Beta)
})
t.Run("with empty options", func(t *testing.T) {
_, err := client.Admin.TerraformVersions.Create(ctx, AdminTerraformVersionCreateOptions{})
require.Equal(t, err, ErrRequiredTFVerCreateOps)
})
}
func TestAdminTerraformVersions_ReadUpdate(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
url := "https://www.hashicorp.com"
amd64Sha := genSha(t)
t.Run("reads and updates", func(t *testing.T) {
version := genSafeRandomTerraformVersion()
opts := AdminTerraformVersionCreateOptions{
Version: String(version),
URL: String(url),
Sha: &amd64Sha,
Official: Bool(false),
Deprecated: Bool(true),
DeprecatedReason: String("Test Reason"),
Enabled: Bool(false),
Beta: Bool(false),
Archs: []*ToolVersionArchitecture{{
URL: url,
Sha: amd64Sha,
OS: linux,
Arch: amd64,
}, {
URL: url,
Sha: genSha(t),
OS: linux,
Arch: arm64,
}},
}
tfv, err := client.Admin.TerraformVersions.Create(ctx, opts)
require.NoError(t, err)
id := tfv.ID
defer func() {
deleteErr := client.Admin.TerraformVersions.Delete(ctx, id)
require.NoError(t, deleteErr)
}()
tfv, err = client.Admin.TerraformVersions.Read(ctx, id)
require.NoError(t, err)
assert.Equal(t, *opts.Version, tfv.Version)
assert.Equal(t, 2, len(tfv.Archs))
assert.Equal(t, opts.Archs[0].URL, tfv.URL)
assert.Equal(t, opts.Archs[0].Sha, tfv.Sha)
assert.Equal(t, *opts.Official, tfv.Official)
assert.Equal(t, *opts.Deprecated, tfv.Deprecated)
assert.Equal(t, *opts.DeprecatedReason, *tfv.DeprecatedReason)
assert.Equal(t, *opts.Enabled, tfv.Enabled)
assert.Equal(t, *opts.Beta, tfv.Beta)
updateVersion := genSafeRandomTerraformVersion()
updateURL := "https://app.terraform.io/"
updateSha := genSha(t)
updateOpts := AdminTerraformVersionUpdateOptions{
Version: String(updateVersion),
URL: String(updateURL),
Sha: &updateSha,
Deprecated: Bool(false),
}
tfv, err = client.Admin.TerraformVersions.Update(ctx, id, updateOpts)
require.NoError(t, err)
assert.Equal(t, updateVersion, tfv.Version)
assert.Equal(t, updateURL, tfv.URL)
assert.Equal(t, updateSha, tfv.Sha)
assert.Equal(t, *opts.Official, tfv.Official)
assert.Equal(t, *updateOpts.Deprecated, tfv.Deprecated)
assert.Equal(t, *opts.Enabled, tfv.Enabled)
assert.Equal(t, *opts.Beta, tfv.Beta)
assert.Equal(t, 1, len(tfv.Archs))
assert.Equal(t, updateURL, tfv.Archs[0].URL)
assert.Equal(t, updateSha, tfv.Archs[0].Sha)
assert.Equal(t, linux, tfv.Archs[0].OS)
assert.Equal(t, amd64, tfv.Archs[0].Arch)
})
t.Run("update with Archs", func(t *testing.T) {
version := genSafeRandomTerraformVersion()
sha := String(genSha(t))
opts := AdminTerraformVersionCreateOptions{
Version: String(version),
URL: String("https://www.hashicorp.com"),
Sha: String(genSha(t)),
Official: Bool(false),
Deprecated: Bool(true),
DeprecatedReason: String("Test Reason"),
Enabled: Bool(false),
Beta: Bool(false),
Archs: []*ToolVersionArchitecture{{
URL: "https://www.hashicorp.com",
Sha: *sha,
OS: linux,
Arch: amd64,
}},
}
tfv, err := client.Admin.TerraformVersions.Create(ctx, opts)
require.NoError(t, err)
id := tfv.ID
defer func() {
deleteErr := client.Admin.TerraformVersions.Delete(ctx, id)
require.NoError(t, deleteErr)
}()
updateArchOpts := AdminTerraformVersionUpdateOptions{
Archs: []*ToolVersionArchitecture{{
URL: "https://www.hashicorp.com",
Sha: *sha,
OS: linux,
Arch: arm64,
}},
}
tfv, err = client.Admin.TerraformVersions.Update(ctx, id, updateArchOpts)
require.NoError(t, err)
assert.Equal(t, *opts.Version, tfv.Version)
assert.Equal(t, "", tfv.URL)
assert.Equal(t, "", tfv.Sha)
assert.Equal(t, *opts.Official, tfv.Official)
assert.Equal(t, *opts.Deprecated, tfv.Deprecated)
assert.Equal(t, *opts.Enabled, tfv.Enabled)
assert.Equal(t, *opts.Beta, tfv.Beta)
assert.Equal(t, 1, len(tfv.Archs))
assert.Equal(t, updateArchOpts.Archs[0].URL, tfv.Archs[0].URL)
assert.Equal(t, updateArchOpts.Archs[0].Sha, tfv.Archs[0].Sha)
assert.Equal(t, updateArchOpts.Archs[0].OS, tfv.Archs[0].OS)
assert.Equal(t, updateArchOpts.Archs[0].Arch, tfv.Archs[0].Arch)
})
t.Run("with non-existent terraform version", func(t *testing.T) {
randomID := "random-id"
_, err := client.Admin.TerraformVersions.Read(ctx, randomID)
require.Error(t, err)
})
}
================================================
FILE: admin_user.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ AdminUsers = (*adminUsers)(nil)
// AdminUsers describes all the admin user related methods that the Terraform
// Enterprise API supports.
// It contains endpoints to help site administrators manage their users.
//
// TFE API docs: https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/users
type AdminUsers interface {
// List all the users of the given installation.
List(ctx context.Context, options *AdminUserListOptions) (*AdminUserList, error)
// Delete a user by its ID.
Delete(ctx context.Context, userID string) error
// Suspend a user by its ID.
Suspend(ctx context.Context, userID string) (*AdminUser, error)
// Unsuspend a user by its ID.
Unsuspend(ctx context.Context, userID string) (*AdminUser, error)
// GrantAdmin grants admin privileges to a user by its ID.
GrantAdmin(ctx context.Context, userID string) (*AdminUser, error)
// RevokeAdmin revokees admin privileges to a user by its ID.
RevokeAdmin(ctx context.Context, userID string) (*AdminUser, error)
// Disable2FA disables a user's two-factor authentication in the situation
// where they have lost access to their device and recovery codes.
Disable2FA(ctx context.Context, userID string) (*AdminUser, error)
}
// adminUsers implements the AdminUsers interface.
type adminUsers struct {
client *Client
}
// AdminUser represents a user as seen by an Admin.
type AdminUser struct {
ID string `jsonapi:"primary,users"`
Email string `jsonapi:"attr,email"`
Username string `jsonapi:"attr,username"`
AvatarURL string `jsonapi:"attr,avatar-url"`
TwoFactor *TwoFactor `jsonapi:"attr,two-factor"`
IsAdmin bool `jsonapi:"attr,is-admin"`
IsSuspended bool `jsonapi:"attr,is-suspended"`
IsServiceAccount bool `jsonapi:"attr,is-service-account"`
// Relations
Organizations []*Organization `jsonapi:"relation,organizations"`
}
// AdminUserList represents a list of users.
type AdminUserList struct {
*Pagination
Items []*AdminUser
}
// AdminUserIncludeOpt represents the available options for include query params.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/users#available-related-resources
type AdminUserIncludeOpt string
const AdminUserOrgs AdminUserIncludeOpt = "organizations"
// AdminUserListOptions represents the options for listing users.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/users#query-parameters
type AdminUserListOptions struct {
ListOptions
// Optional: A search query string. Users are searchable by username and email address.
Query string `url:"q,omitempty"`
// Optional: Can be "true" or "false" to show only administrators or non-administrators.
Administrators string `url:"filter[admin],omitempty"`
// Optional: Can be "true" or "false" to show only suspended users or users who are not suspended.
SuspendedUsers string `url:"filter[suspended],omitempty"`
// Optional: A list of relations to include. See available resources
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/users#available-related-resources
Include []AdminUserIncludeOpt `url:"include,omitempty"`
}
// List all user accounts in the Terraform Enterprise installation
func (a *adminUsers) List(ctx context.Context, options *AdminUserListOptions) (*AdminUserList, error) {
if err := options.valid(); err != nil {
return nil, err
}
u := "admin/users"
req, err := a.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
aul := &AdminUserList{}
err = req.Do(ctx, aul)
if err != nil {
return nil, err
}
return aul, nil
}
// Delete a user by its ID.
func (a *adminUsers) Delete(ctx context.Context, userID string) error {
if !validStringID(&userID) {
return ErrInvalidUserValue
}
u := fmt.Sprintf("admin/users/%s", url.PathEscape(userID))
req, err := a.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// Suspend a user by its ID.
func (a *adminUsers) Suspend(ctx context.Context, userID string) (*AdminUser, error) {
if !validStringID(&userID) {
return nil, ErrInvalidUserValue
}
u := fmt.Sprintf("admin/users/%s/actions/suspend", url.PathEscape(userID))
req, err := a.client.NewRequest("POST", u, nil)
if err != nil {
return nil, err
}
au := &AdminUser{}
err = req.Do(ctx, au)
if err != nil {
return nil, err
}
return au, nil
}
// Unsuspend a user by its ID.
func (a *adminUsers) Unsuspend(ctx context.Context, userID string) (*AdminUser, error) {
if !validStringID(&userID) {
return nil, ErrInvalidUserValue
}
u := fmt.Sprintf("admin/users/%s/actions/unsuspend", url.PathEscape(userID))
req, err := a.client.NewRequest("POST", u, nil)
if err != nil {
return nil, err
}
au := &AdminUser{}
err = req.Do(ctx, au)
if err != nil {
return nil, err
}
return au, nil
}
// GrantAdmin grants admin privileges to a user by its ID.
func (a *adminUsers) GrantAdmin(ctx context.Context, userID string) (*AdminUser, error) {
if !validStringID(&userID) {
return nil, ErrInvalidUserValue
}
u := fmt.Sprintf("admin/users/%s/actions/grant_admin", url.PathEscape(userID))
req, err := a.client.NewRequest("POST", u, nil)
if err != nil {
return nil, err
}
au := &AdminUser{}
err = req.Do(ctx, au)
if err != nil {
return nil, err
}
return au, nil
}
// RevokeAdmin revokes admin privileges to a user by its ID.
func (a *adminUsers) RevokeAdmin(ctx context.Context, userID string) (*AdminUser, error) {
if !validStringID(&userID) {
return nil, ErrInvalidUserValue
}
u := fmt.Sprintf("admin/users/%s/actions/revoke_admin", url.PathEscape(userID))
req, err := a.client.NewRequest("POST", u, nil)
if err != nil {
return nil, err
}
au := &AdminUser{}
err = req.Do(ctx, au)
if err != nil {
return nil, err
}
return au, nil
}
// Disable2FA disables a user's two-factor authentication in the situation
// where they have lost access to their device and recovery codes.
func (a *adminUsers) Disable2FA(ctx context.Context, userID string) (*AdminUser, error) {
if !validStringID(&userID) {
return nil, ErrInvalidUserValue
}
u := fmt.Sprintf("admin/users/%s/actions/disable_two_factor", url.PathEscape(userID))
req, err := a.client.NewRequest("POST", u, nil)
if err != nil {
return nil, err
}
au := &AdminUser{}
err = req.Do(ctx, au)
if err != nil {
return nil, err
}
return au, nil
}
func (o *AdminUserListOptions) valid() error {
return nil
}
================================================
FILE: admin_user_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAdminUsers_List(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
currentUser, err := client.Users.ReadCurrent(ctx)
require.NoError(t, err)
org, orgCleanup := createOrganization(t, client)
defer orgCleanup()
t.Run("without list options", func(t *testing.T) {
ul, err := client.Admin.Users.List(ctx, nil)
require.NoError(t, err)
assert.NotEmpty(t, ul.Items)
})
t.Run("with list options", func(t *testing.T) {
ul, err := client.Admin.Users.List(ctx, &AdminUserListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
// Out of range page number, so the items should be empty
assert.Empty(t, ul.Items)
assert.Equal(t, 999, ul.CurrentPage)
ul, err = client.Admin.Users.List(ctx, &AdminUserListOptions{
ListOptions: ListOptions{
PageNumber: 1,
PageSize: 100,
},
})
require.NoError(t, err)
assert.NotEmpty(t, ul.Items)
assert.Equal(t, 1, ul.CurrentPage)
})
t.Run("query by username or email", func(t *testing.T) {
ul, err := client.Admin.Users.List(ctx, &AdminUserListOptions{
Query: "admin-security-maintenance",
})
require.NoError(t, err)
assert.Equal(t, 1, ul.CurrentPage)
assert.Equal(t, true, ul.TotalCount == 1)
member, memberCleanup := createOrganizationMembership(t, client, org)
defer memberCleanup()
ul, err = client.Admin.Users.List(ctx, &AdminUserListOptions{
Query: member.User.Email,
})
require.NoError(t, err)
assert.Equal(t, member.User.Email, ul.Items[0].Email)
assert.Equal(t, 1, ul.CurrentPage)
assert.Equal(t, true, ul.TotalCount == 1)
})
t.Run("with organization included", func(t *testing.T) {
ul, err := client.Admin.Users.List(ctx, &AdminUserListOptions{
Include: []AdminUserIncludeOpt{AdminUserOrgs},
})
require.NoError(t, err)
require.NotEmpty(t, ul.Items)
require.NotEmpty(t, ul.Items[0].Organizations)
assert.NotEmpty(t, ul.Items[0].Organizations[0].Name)
})
t.Run("filter by admin", func(t *testing.T) {
ul, err := client.Admin.Users.List(ctx, &AdminUserListOptions{
Administrators: "true",
})
require.NoError(t, err)
require.NotEmpty(t, ul.Items)
require.NotNil(t, ul.Items[0])
// We use this `includesEmail` helper function because throughout
// the tests, there could be multiple admins, depending on the
// ordering of the test runs.
assert.Equal(t, true, includesEmail(currentUser.Email, ul.Items))
})
}
func TestAdminUsers_Delete(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
org, orgCleanup := createOrganization(t, client)
defer orgCleanup()
t.Run("an existing user", func(t *testing.T) {
// Avoid the member cleanup function because the user
// gets deleted below.
member, _ := createOrganizationMembership(t, client, org)
ul, err := client.Admin.Users.List(ctx, &AdminUserListOptions{
Query: member.User.Email,
})
require.NoError(t, err)
assert.Equal(t, member.User.Email, ul.Items[0].Email)
assert.Equal(t, 1, ul.CurrentPage)
assert.Equal(t, true, ul.TotalCount == 1)
err = client.Admin.Users.Delete(ctx, member.User.ID)
require.NoError(t, err)
ul, err = client.Admin.Users.List(ctx, &AdminUserListOptions{
Query: member.User.Email,
})
require.NoError(t, err)
assert.Empty(t, ul.Items)
assert.Equal(t, 1, ul.CurrentPage)
assert.Equal(t, 0, ul.TotalCount)
})
t.Run("an non-existing user", func(t *testing.T) {
err := client.Admin.Users.Delete(ctx, "non-existing-user-id")
require.Error(t, err)
})
}
func TestAdminUsers_Disable2FA(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
org, orgCleanup := createOrganization(t, client)
defer orgCleanup()
member, memberCleanup := createOrganizationMembership(t, client, org)
defer memberCleanup()
if !member.User.TwoFactor.Enabled {
t.Skip("User does not have 2FA enabled. Skipping")
}
user, err := client.Admin.Users.Disable2FA(ctx, member.User.ID)
require.NoError(t, err)
require.NotNil(t, user)
}
func includesEmail(email string, userList []*AdminUser) bool {
for _, user := range userList {
if user.Email == email {
return true
}
}
return false
}
================================================
FILE: admin_workspace.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ AdminWorkspaces = (*adminWorkspaces)(nil)
// AdminWorkspaces describes all the admin workspace related methods that the Terraform Enterprise API supports.
// Note that admin settings are only available in Terraform Enterprise.
//
// TFE API docs: https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/workspaces
type AdminWorkspaces interface {
// List all the workspaces within a workspace.
List(ctx context.Context, options *AdminWorkspaceListOptions) (*AdminWorkspaceList, error)
// Read a workspace by its ID.
Read(ctx context.Context, workspaceID string) (*AdminWorkspace, error)
// Delete a workspace by its ID.
Delete(ctx context.Context, workspaceID string) error
}
// adminWorkspaces implements AdminWorkspaces interface.
type adminWorkspaces struct {
client *Client
}
// AdminVCSRepo represents a VCS repository
type AdminVCSRepo struct {
Identifier string `jsonapi:"attr,identifier"`
}
// AdminWorkspaces represents a Terraform Enterprise admin workspace.
type AdminWorkspace struct {
ID string `jsonapi:"primary,workspaces"`
Name string `jsonapi:"attr,name"`
Locked bool `jsonapi:"attr,locked"`
VCSRepo *AdminVCSRepo `jsonapi:"attr,vcs-repo"`
// Relations
Organization *Organization `jsonapi:"relation,organization"`
CurrentRun *Run `jsonapi:"relation,current-run"`
}
// AdminWorkspaceIncludeOpt represents the available options for include query params.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/workspaces#available-related-resources
type AdminWorkspaceIncludeOpt string
const (
AdminWorkspaceOrg AdminWorkspaceIncludeOpt = "organization"
AdminWorkspaceCurrentRun AdminWorkspaceIncludeOpt = "current_run"
AdminWorkspaceOrgOwners AdminWorkspaceIncludeOpt = "organization.owners"
)
// AdminWorkspaceListOptions represents the options for listing workspaces.
type AdminWorkspaceListOptions struct {
ListOptions
// A query string (partial workspace name) used to filter the results.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/workspaces#query-parameters
Query string `url:"q,omitempty"`
// Optional: A list of relations to include. See available resources
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/workspaces#available-related-resources
Include []AdminWorkspaceIncludeOpt `url:"include,omitempty"`
// Optional: A comma-separated list of Run statuses to restrict results. See available resources
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/workspaces#query-parameters
Filter string `url:"filter[current_run][status],omitempty"`
// Optional: May sort on "name" (the default) and "current-run.created-at" (which sorts by the time of the current run)
// Prepending a hyphen to the sort parameter will reverse the order (e.g. "-name" to reverse the default order)
Sort string `url:"sort,omitempty"`
}
// AdminWorkspaceList represents a list of workspaces.
type AdminWorkspaceList struct {
*Pagination
Items []*AdminWorkspace
}
// List all the workspaces within a workspace.
func (s *adminWorkspaces) List(ctx context.Context, options *AdminWorkspaceListOptions) (*AdminWorkspaceList, error) {
if err := options.valid(); err != nil {
return nil, err
}
u := "admin/workspaces"
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
awl := &AdminWorkspaceList{}
err = req.Do(ctx, awl)
if err != nil {
return nil, err
}
return awl, nil
}
// Read a workspace by its ID.
func (s *adminWorkspaces) Read(ctx context.Context, workspaceID string) (*AdminWorkspace, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceValue
}
u := fmt.Sprintf("admin/workspaces/%s", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
aw := &AdminWorkspace{}
err = req.Do(ctx, aw)
if err != nil {
return nil, err
}
return aw, nil
}
// Delete a workspace by its ID.
func (s *adminWorkspaces) Delete(ctx context.Context, workspaceID string) error {
if !validStringID(&workspaceID) {
return ErrInvalidWorkspaceValue
}
u := fmt.Sprintf("admin/workspaces/%s", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o *AdminWorkspaceListOptions) valid() error {
return nil
}
================================================
FILE: admin_workspace_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// BEWARE: The admin workspaces API can view all of the workspaces created by
// EVERY test organization in EVERY concurrent test run (or other usage) for the
// current HCP Terraform instance. It's generally not safe to assume that the workspaces
// you create in a given test will be within the first page of list results, so
// you might have to get creative and/or settle for less when testing the
// behavior of these endpoints.
func TestAdminWorkspaces_ListWithFilter_RunDependent(t *testing.T) {
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
org, orgCleanup := createOrganization(t, client)
defer orgCleanup()
wTest1, wTest1Cleanup := createWorkspace(t, client, org)
defer wTest1Cleanup()
wTest2, wTest2Cleanup := createWorkspace(t, client, org)
defer wTest2Cleanup()
t.Run("when filtering workspaces on a current run status", func(t *testing.T) {
_, appliedCleanup := createRunApply(t, client, wTest1)
t.Cleanup(appliedCleanup)
_, unAppliedCleanup := createRunUnapplied(t, client, wTest2)
t.Cleanup(unAppliedCleanup)
wl, err := client.Admin.Workspaces.List(ctx, &AdminWorkspaceListOptions{
Filter: string(RunApplied), Include: []AdminWorkspaceIncludeOpt{AdminWorkspaceCurrentRun},
})
require.NoError(t, err)
require.NotEmpty(t, wl.Items)
assert.Equal(t, wl.Items[0].CurrentRun.Status, RunApplied)
assert.NotContains(t, wl.Items, wTest2)
})
}
func TestAdminWorkspaces_ListWithSort_RunDependent(t *testing.T) {
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
org, orgCleanup := createOrganization(t, client)
defer orgCleanup()
wTest1, wTest1Cleanup := createWorkspace(t, client, org)
defer wTest1Cleanup()
wTest2, wTest2Cleanup := createWorkspace(t, client, org)
defer wTest2Cleanup()
t.Run("when sorting by workspace names", func(t *testing.T) {
wl, err := client.Admin.Workspaces.List(ctx, &AdminWorkspaceListOptions{
Sort: "name",
})
require.NoError(t, err)
require.NotEmpty(t, wl.Items)
require.GreaterOrEqual(t, len(wl.Items), 2)
assert.Equal(t, wl.Items[0].Name < wl.Items[1].Name, true)
})
t.Run("when sorting workspaces on current-run.created-at", func(t *testing.T) {
_, unappliedCleanup1 := createRunUnapplied(t, client, wTest1)
t.Cleanup(unappliedCleanup1)
_, unappliedCleanup2 := createRunUnapplied(t, client, wTest2)
t.Cleanup(unappliedCleanup2)
wl, err := client.Admin.Workspaces.List(ctx, &AdminWorkspaceListOptions{
Include: []AdminWorkspaceIncludeOpt{AdminWorkspaceCurrentRun},
Sort: "current-run.created-at",
})
require.NoError(t, err)
require.NotEmpty(t, wl.Items)
require.GreaterOrEqual(t, len(wl.Items), 2)
assert.True(t, wl.Items[1].CurrentRun.CreatedAt.After(wl.Items[0].CurrentRun.CreatedAt))
})
}
func TestAdminWorkspaces_List_RunDependent(t *testing.T) {
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
org, orgCleanup := createOrganization(t, client)
defer orgCleanup()
wTest1, wTest1Cleanup := createWorkspace(t, client, org)
defer wTest1Cleanup()
wTest2, wTest2Cleanup := createWorkspace(t, client, org)
defer wTest2Cleanup()
t.Run("without list options", func(t *testing.T) {
wl, err := client.Admin.Workspaces.List(ctx, nil)
require.NoError(t, err)
require.GreaterOrEqual(t, len(wl.Items), 2)
})
t.Run("with list options", func(t *testing.T) {
wl, err := client.Admin.Workspaces.List(ctx, &AdminWorkspaceListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
// Out of range page number, so the items should be empty
assert.Empty(t, wl.Items)
assert.Equal(t, 999, wl.CurrentPage)
wl, err = client.Admin.Workspaces.List(ctx, &AdminWorkspaceListOptions{
ListOptions: ListOptions{
PageNumber: 1,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Equal(t, 1, wl.CurrentPage)
assert.Equal(t, adminWorkspaceItemsContainsID(wl.Items, wTest1.ID), true)
assert.Equal(t, adminWorkspaceItemsContainsID(wl.Items, wTest2.ID), true)
})
t.Run("when searching a known workspace", func(t *testing.T) {
// Use a known workspace prefix as search attribute. The result
// should be successful and only contain the matching workspace.
wl, err := client.Admin.Workspaces.List(ctx, &AdminWorkspaceListOptions{
Query: wTest1.Name,
})
require.NoError(t, err)
assert.Equal(t, adminWorkspaceItemsContainsID(wl.Items, wTest1.ID), true)
assert.Equal(t, adminWorkspaceItemsContainsID(wl.Items, wTest2.ID), false)
assert.Equal(t, 1, wl.CurrentPage)
assert.Equal(t, true, wl.TotalCount == 1)
})
t.Run("when searching an unknown workspace", func(t *testing.T) {
// Use a nonexisting workspace name as search attribute. The result
// should be successful, but return no results.
wl, err := client.Admin.Workspaces.List(ctx, &AdminWorkspaceListOptions{
Query: "nonexisting",
})
require.NoError(t, err)
assert.Empty(t, wl.Items)
assert.Equal(t, 1, wl.CurrentPage)
assert.Equal(t, 0, wl.TotalCount)
})
t.Run("with organization included", func(t *testing.T) {
wl, err := client.Admin.Workspaces.List(ctx, &AdminWorkspaceListOptions{
Include: []AdminWorkspaceIncludeOpt{AdminWorkspaceOrg},
})
require.NoError(t, err)
require.NotEmpty(t, wl.Items)
require.NotNil(t, wl.Items[0].Organization)
assert.NotEmpty(t, wl.Items[0].Organization.Name)
})
// This sub-test should remain last because it creates a run that does not apply
// Any subsequent runs will be queued until a timeout is triggered
t.Run("with current_run included", func(t *testing.T) {
cvTest, cvCleanup := createUploadedConfigurationVersion(t, client, wTest1)
defer cvCleanup()
runOpts := RunCreateOptions{
ConfigurationVersion: cvTest,
Workspace: wTest1,
}
run, err := client.Runs.Create(ctx, runOpts)
require.NoError(t, err)
wl, err := client.Admin.Workspaces.List(ctx, &AdminWorkspaceListOptions{
Include: []AdminWorkspaceIncludeOpt{AdminWorkspaceCurrentRun},
})
require.NoError(t, err)
require.NotEmpty(t, wl.Items)
require.NotNil(t, wl.Items[0].CurrentRun)
assert.Equal(t, wl.Items[0].CurrentRun.ID, run.ID)
})
}
func TestAdminWorkspaces_Read(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
t.Run("it fails to read a workspace with an invalid name", func(t *testing.T) {
workspace, err := client.Admin.Workspaces.Read(ctx, "")
require.Error(t, err)
assert.EqualError(t, err, ErrInvalidWorkspaceValue.Error())
assert.Nil(t, workspace)
})
t.Run("it fails to read a workspace that is non existent", func(t *testing.T) {
workspaceID := fmt.Sprintf("non-existent-%s", randomString(t))
adminWorkspace, err := client.Admin.Workspaces.Read(ctx, workspaceID)
require.Error(t, err)
assert.EqualError(t, err, ErrResourceNotFound.Error())
assert.Nil(t, adminWorkspace)
})
t.Run("it reads a workspace successfully", func(t *testing.T) {
org, orgCleanup := createOrganization(t, client)
defer orgCleanup()
workspace, workspaceCleanup := createWorkspace(t, client, org)
defer workspaceCleanup()
adminWorkspace, err := client.Admin.Workspaces.Read(ctx, workspace.ID)
require.NoError(t, err)
require.NotNilf(t, adminWorkspace, "Admin Workspace is not nil")
assert.Equal(t, adminWorkspace.ID, workspace.ID)
assert.Equal(t, adminWorkspace.Name, workspace.Name)
assert.Equal(t, adminWorkspace.Locked, workspace.Locked)
})
}
func TestAdminWorkspaces_Delete(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
t.Run("it fails to delete an organization with an invalid id", func(t *testing.T) {
err := client.Admin.Workspaces.Delete(ctx, "")
require.Error(t, err)
assert.EqualError(t, err, ErrInvalidWorkspaceValue.Error())
})
t.Run("it fails to delete an organization with an bad org name", func(t *testing.T) {
workspaceID := fmt.Sprintf("non-existent-%s", randomString(t))
err := client.Admin.Workspaces.Delete(ctx, workspaceID)
require.Error(t, err)
assert.EqualError(t, err, ErrResourceNotFound.Error())
})
t.Run("it deletes a workspace successfully", func(t *testing.T) {
org, orgCleanup := createOrganization(t, client)
defer orgCleanup()
workspace, _ := createWorkspace(t, client, org)
adminWorkspace, err := client.Admin.Workspaces.Read(ctx, workspace.ID)
require.NoError(t, err)
require.NotNilf(t, adminWorkspace, "Admin Workspace is not nil")
assert.Equal(t, adminWorkspace.ID, workspace.ID)
err = client.Admin.Workspaces.Delete(ctx, adminWorkspace.ID)
require.NoError(t, err)
// Cannot find deleted workspace
_, err = client.Admin.Workspaces.Read(ctx, workspace.ID)
assert.Error(t, err)
assert.EqualError(t, err, ErrResourceNotFound.Error())
})
}
func adminWorkspaceItemsContainsID(items []*AdminWorkspace, id string) bool {
hasID := false
for _, item := range items {
if item.ID == id {
hasID = true
break
}
}
return hasID
}
func TestAdminWorkspace_Unmarshal(t *testing.T) {
t.Parallel()
data := map[string]interface{}{
"data": map[string]interface{}{
"type": "workspaces",
"id": "workspaces-VCsNJXa59eUza53R",
"attributes": map[string]interface{}{
"name": "workspace-name",
"locked": false,
"vcs-repo": map[string]string{
"identifier": "github",
},
},
},
}
byteData, err := json.Marshal(data)
if err != nil {
t.Fatal(err)
}
adminWorkspace := &AdminWorkspace{}
responseBody := bytes.NewReader(byteData)
err = unmarshalResponse(responseBody, adminWorkspace)
require.NoError(t, err)
assert.Equal(t, adminWorkspace.ID, "workspaces-VCsNJXa59eUza53R")
assert.Equal(t, adminWorkspace.Name, "workspace-name")
assert.Equal(t, adminWorkspace.Locked, false)
assert.Equal(t, adminWorkspace.VCSRepo.Identifier, "github")
}
================================================
FILE: agent.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ Agents = (*agents)(nil)
// Agents describes all the agent-related methods that the
// HCP Terraform API supports.
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/agents
type Agents interface {
// Read an agent by its ID.
Read(ctx context.Context, agentID string) (*Agent, error)
// List all the agents of the given pool.
List(ctx context.Context, agentPoolID string, options *AgentListOptions) (*AgentList, error)
}
// agents implements Agents.
type agents struct {
client *Client
}
// AgentList represents a list of agents.
type AgentList struct {
*Pagination
Items []*Agent
}
// Agent represents a HCP Terraform agent.
type Agent struct {
ID string `jsonapi:"primary,agents"`
Name string `jsonapi:"attr,name"`
IP string `jsonapi:"attr,ip-address"`
Status string `jsonapi:"attr,status"`
LastPingAt string `jsonapi:"attr,last-ping-at"`
}
type AgentListOptions struct {
ListOptions
//Optional:
LastPingSince time.Time `url:"filter[last-ping-since],omitempty,iso8601"`
// Optional: Allows sorting the agents by "created-by"
Sort string `url:"sort,omitempty"`
}
// Read a single agent by its ID
func (s *agents) Read(ctx context.Context, agentID string) (*Agent, error) {
if !validStringID(&agentID) {
return nil, ErrInvalidAgentID
}
u := fmt.Sprintf("agents/%s", url.PathEscape(agentID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
agent := &Agent{}
err = req.Do(ctx, agent)
if err != nil {
return nil, err
}
return agent, nil
}
// List all the agents of the given organization.
func (s *agents) List(ctx context.Context, agentPoolID string, options *AgentListOptions) (*AgentList, error) {
if !validStringID(&agentPoolID) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf("agent-pools/%s/agents", url.PathEscape(agentPoolID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
agentList := &AgentList{}
err = req.Do(ctx, agentList)
if err != nil {
return nil, err
}
return agentList, nil
}
================================================
FILE: agent_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAgentsRead(t *testing.T) {
t.Parallel()
skipUnlessLinuxAMD64(t)
client := testClient(t)
ctx := context.Background()
org, orgCleanup := createOrganization(t, client)
t.Cleanup(orgCleanup)
upgradeOrganizationSubscription(t, client, org)
agent, _, agentCleanup := createAgent(t, client, org)
t.Cleanup(agentCleanup)
t.Run("when the agent exists", func(t *testing.T) {
k, err := client.Agents.Read(ctx, agent.ID)
require.NoError(t, err)
assert.Equal(t, agent, k)
})
t.Run("when the agent does not exist", func(t *testing.T) {
k, err := client.Agents.Read(ctx, "nonexistent")
assert.Nil(t, k)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("without a valid agent ID", func(t *testing.T) {
k, err := client.Agents.Read(ctx, badIdentifier)
assert.Nil(t, k)
assert.EqualError(t, err, ErrInvalidAgentID.Error())
})
}
func TestAgentsList(t *testing.T) {
t.Parallel()
skipUnlessLinuxAMD64(t)
client := testClient(t)
ctx := context.Background()
org, orgCleanup := createOrganization(t, client)
t.Cleanup(orgCleanup)
upgradeOrganizationSubscription(t, client, org)
_, agentPool, agentCleanup := createAgent(t, client, org)
t.Cleanup(agentCleanup)
t.Run("expect an agent to exist", func(t *testing.T) {
agent, err := client.Agents.List(ctx, agentPool.ID, nil)
require.NoError(t, err)
require.NotEmpty(t, agent.Items)
assert.NotEmpty(t, agent.Items[0].ID)
})
t.Run("without a valid agent pool ID", func(t *testing.T) {
agent, err := client.Agents.List(ctx, badIdentifier, nil)
assert.Nil(t, agent)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
================================================
FILE: agent_pool.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ AgentPools = (*agentPools)(nil)
// AgentPools describes all the agent pool related methods that the HCP Terraform
// API supports. Note that agents are not available in Terraform Enterprise.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/agents
type AgentPools interface {
// List all the agent pools of the given organization.
List(ctx context.Context, organization string, options *AgentPoolListOptions) (*AgentPoolList, error)
// Create a new agent pool with the given options.
Create(ctx context.Context, organization string, options AgentPoolCreateOptions) (*AgentPool, error)
// Read an agent pool by its ID.
Read(ctx context.Context, agentPoolID string) (*AgentPool, error)
// Read an agent pool by its ID with the given options.
ReadWithOptions(ctx context.Context, agentPoolID string, options *AgentPoolReadOptions) (*AgentPool, error)
// Update an agent pool by its ID.
Update(ctx context.Context, agentPool string, options AgentPoolUpdateOptions) (*AgentPool, error)
// UpdateAllowedWorkspaces updates the list of allowed workspaces associated with an agent pool.
UpdateAllowedWorkspaces(ctx context.Context, agentPool string, options AgentPoolAllowedWorkspacesUpdateOptions) (*AgentPool, error)
// UpdateAllowedProjects updates the list of allowed projects associated with an agent pool.
UpdateAllowedProjects(ctx context.Context, agentPool string, options AgentPoolAllowedProjectsUpdateOptions) (*AgentPool, error)
// UpdateExcludedWorkspaces updates the list of excluded workspaces associated with an agent pool.
UpdateExcludedWorkspaces(ctx context.Context, agentPool string, options AgentPoolExcludedWorkspacesUpdateOptions) (*AgentPool, error)
// Delete an agent pool by its ID.
Delete(ctx context.Context, agentPoolID string) error
}
// agentPools implements AgentPools.
type agentPools struct {
client *Client
}
// AgentPoolList represents a list of agent pools.
type AgentPoolList struct {
*Pagination
Items []*AgentPool
}
// AgentPool represents a HCP Terraform agent pool.
type AgentPool struct {
ID string `jsonapi:"primary,agent-pools"`
Name string `jsonapi:"attr,name"`
AgentCount int `jsonapi:"attr,agent-count"`
OrganizationScoped bool `jsonapi:"attr,organization-scoped"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
// Relations
Organization *Organization `jsonapi:"relation,organization"`
HYOKConfigurations []*HYOKConfiguration `jsonapi:"relation,hyok-configurations"`
Workspaces []*Workspace `jsonapi:"relation,workspaces"`
AllowedWorkspaces []*Workspace `jsonapi:"relation,allowed-workspaces"`
AllowedProjects []*Project `jsonapi:"relation,allowed-projects"`
ExcludedWorkspaces []*Workspace `jsonapi:"relation,excluded-workspaces"`
}
// A list of relations to include
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/agents#available-related-resources
type AgentPoolIncludeOpt string
const (
AgentPoolWorkspaces AgentPoolIncludeOpt = "workspaces"
AgentPoolHYOKConfigurations AgentPoolIncludeOpt = "hyok-configurations"
)
type AgentPoolReadOptions struct {
Include []AgentPoolIncludeOpt `url:"include,omitempty"`
}
// AgentPoolListOptions represents the options for listing agent pools.
type AgentPoolListOptions struct {
ListOptions
// Optional: A list of relations to include. See available resources
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/agents#available-related-resources
Include []AgentPoolIncludeOpt `url:"include,omitempty"`
// Optional: A search query string used to filter agent pool. Agent pools are searchable by name
Query string `url:"q,omitempty"`
// Optional: String (workspace name) used to filter the results.
AllowedWorkspacesName string `url:"filter[allowed_workspaces][name],omitempty"`
// Optional: String (project name) used to filter the results.
AllowedProjectsName string `url:"filter[allowed_projects][name],omitempty"`
// Optional: Allows sorting the agent pools by "created-by" or "name"
Sort string `url:"sort,omitempty"`
}
// AgentPoolCreateOptions represents the options for creating an agent pool.
type AgentPoolCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,agent-pools"`
// Required: A name to identify the agent pool.
Name *string `jsonapi:"attr,name"`
// True if the agent pool is organization scoped, false otherwise.
OrganizationScoped *bool `jsonapi:"attr,organization-scoped,omitempty"`
// List of workspaces that are associated with an agent pool.
AllowedWorkspaces []*Workspace `jsonapi:"relation,allowed-workspaces,omitempty"`
// List of projects that are associated with an agent pool.
AllowedProjects []*Project `jsonapi:"relation,allowed-projects,omitempty"`
// List of workspaces that are excluded from the scope of an agent pool.
ExcludedWorkspaces []*Workspace `jsonapi:"relation,excluded-workspaces,omitempty"`
}
// List all the agent pools of the given organization.
func (s *agentPools) List(ctx context.Context, organization string, options *AgentPoolListOptions) (*AgentPoolList, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("organizations/%s/agent-pools", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
poolList := &AgentPoolList{}
err = req.Do(ctx, poolList)
if err != nil {
return nil, err
}
return poolList, nil
}
// Create a new agent pool with the given options.
func (s *agentPools) Create(ctx context.Context, organization string, options AgentPoolCreateOptions) (*AgentPool, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("organizations/%s/agent-pools", url.PathEscape(organization))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
pool := &AgentPool{}
err = req.Do(ctx, pool)
if err != nil {
return nil, err
}
return pool, nil
}
// Read a single agent pool by its ID
func (s *agentPools) Read(ctx context.Context, agentpoolID string) (*AgentPool, error) {
return s.ReadWithOptions(ctx, agentpoolID, nil)
}
// Read a single agent pool by its ID with options.
func (s *agentPools) ReadWithOptions(ctx context.Context, agentpoolID string, options *AgentPoolReadOptions) (*AgentPool, error) {
if !validStringID(&agentpoolID) {
return nil, ErrInvalidAgentPoolID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("agent-pools/%s", url.PathEscape(agentpoolID))
req, err := s.client.NewRequest("GET", u, &options)
if err != nil {
return nil, err
}
pool := &AgentPool{}
err = req.Do(ctx, pool)
if err != nil {
return nil, err
}
return pool, nil
}
// AgentPoolUpdateOptions represents the options for updating an agent pool.
type AgentPoolUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,agent-pools"`
// A new name to identify the agent pool.
Name *string `jsonapi:"attr,name,omitempty"`
// True if the agent pool is organization scoped, false otherwise.
OrganizationScoped *bool `jsonapi:"attr,organization-scoped,omitempty"`
// A new list of workspaces that are associated with an agent pool.
AllowedWorkspaces []*Workspace `jsonapi:"relation,allowed-workspaces,omitempty"`
// A new list of projects that are associated with an agent pool.
AllowedProjects []*Project `jsonapi:"relation,allowed-projects,omitempty"`
// A new list of workspaces that are excluded from the scope of an agent pool.
ExcludedWorkspaces []*Workspace `jsonapi:"relation,excluded-workspaces,omitempty"`
}
// AgentPoolAllowedWorkspacesUpdateOptions represents the options for updating the allowed workspace on an agent pool
type AgentPoolAllowedWorkspacesUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,agent-pools"`
// A new list of workspaces that are associated with an agent pool.
AllowedWorkspaces []*Workspace `jsonapi:"relation,allowed-workspaces"`
}
// AgentPoolAllowedProjectsUpdateOptions represents the options for updating the allowed projects on an agent pool
type AgentPoolAllowedProjectsUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,agent-pools"`
// A new list of projects that are associated with an agent pool.
AllowedProjects []*Project `jsonapi:"relation,allowed-projects"`
}
// AgentPoolExcludedWorkspacesUpdateOptions represents the options for updating the excluded workspace on an agent pool
type AgentPoolExcludedWorkspacesUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,agent-pools"`
// A new list of workspaces that are excluded from the scope of an agent pool.
ExcludedWorkspaces []*Workspace `jsonapi:"relation,excluded-workspaces"`
}
// Update an agent pool by its ID.
// **Note:** This method cannot be used to clear the allowed workspaces, allowed projects, or excluded workspaces fields.
// instead use UpdateAllowedWorkspaces, UpdateAllowedProjects, or UpdateExcludedWorkspaces methods respectively.
func (s *agentPools) Update(ctx context.Context, agentPoolID string, options AgentPoolUpdateOptions) (*AgentPool, error) {
if !validStringID(&agentPoolID) {
return nil, ErrInvalidAgentPoolID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("agent-pools/%s", url.PathEscape(agentPoolID))
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
k := &AgentPool{}
err = req.Do(ctx, k)
if err != nil {
return nil, err
}
return k, nil
}
func (s *agentPools) UpdateAllowedWorkspaces(ctx context.Context, agentPoolID string, options AgentPoolAllowedWorkspacesUpdateOptions) (*AgentPool, error) {
return s.updateArrayAttribute(ctx, agentPoolID, &options)
}
func (s *agentPools) UpdateAllowedProjects(ctx context.Context, agentPoolID string, options AgentPoolAllowedProjectsUpdateOptions) (*AgentPool, error) {
return s.updateArrayAttribute(ctx, agentPoolID, &options)
}
func (s *agentPools) UpdateExcludedWorkspaces(ctx context.Context, agentPoolID string, options AgentPoolExcludedWorkspacesUpdateOptions) (*AgentPool, error) {
return s.updateArrayAttribute(ctx, agentPoolID, &options)
}
// Delete an agent pool by its ID.
func (s *agentPools) Delete(ctx context.Context, agentPoolID string) error {
if !validStringID(&agentPoolID) {
return ErrInvalidAgentPoolID
}
u := fmt.Sprintf("agent-pools/%s", url.PathEscape(agentPoolID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// updateArrayAttribute is a helper function to update array attributes of an agent pool, such as allowed workspaces, allowed projects, or excluded workspaces.
// Note: This function does not validate the options parameter, so it should be used with caution. It is intended to be used with options structs
// (e.g. AgentPoolAllowedWorkspacesUpdateOptions, AgentPoolAllowedProjectsUpdateOptions, AgentPoolExcludedWorkspacesUpdateOptions) whose array
// attributes are NOT marked `omitempty`, so that an empty array is sent to the API to clear the existing values.
func (s *agentPools) updateArrayAttribute(ctx context.Context, agentPoolID string, options any) (*AgentPool, error) {
if !validStringID(&agentPoolID) {
return nil, ErrInvalidAgentPoolID
}
u := fmt.Sprintf("agent-pools/%s", url.PathEscape(agentPoolID))
req, err := s.client.NewRequest("PATCH", u, options)
if err != nil {
return nil, err
}
k := &AgentPool{}
err = req.Do(ctx, k)
if err != nil {
return nil, err
}
return k, nil
}
func (o AgentPoolCreateOptions) valid() error {
if !validString(o.Name) {
return ErrRequiredName
}
if !validStringID(o.Name) {
return ErrInvalidName
}
return nil
}
func (o AgentPoolUpdateOptions) valid() error {
if o.Name != nil && !validStringID(o.Name) {
return ErrInvalidName
}
return nil
}
func (o *AgentPoolReadOptions) valid() error {
return nil
}
func (o *AgentPoolListOptions) valid() error {
return nil
}
================================================
FILE: agent_pool_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAgentPoolsList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
agentPool, agentPoolCleanup := createAgentPool(t, client, orgTest)
t.Cleanup(agentPoolCleanup)
t.Run("without list options", func(t *testing.T) {
pools, err := client.AgentPools.List(ctx, orgTest.Name, nil)
require.NoError(t, err)
assert.Contains(t, pools.Items, agentPool)
assert.Equal(t, 1, pools.CurrentPage)
assert.Equal(t, 1, pools.TotalCount)
})
t.Run("with Include option", func(t *testing.T) {
_, wTestCleanup := createWorkspaceWithVCS(t, client, orgTest, WorkspaceCreateOptions{
Name: String("bar"),
ExecutionMode: String("agent"),
AgentPoolID: String(agentPool.ID),
})
t.Cleanup(wTestCleanup)
k, err := client.AgentPools.List(ctx, orgTest.Name, &AgentPoolListOptions{
Include: []AgentPoolIncludeOpt{AgentPoolWorkspaces},
})
require.NoError(t, err)
require.NotEmpty(t, k.Items)
require.NotEmpty(t, k.Items[0].Workspaces)
assert.NotNil(t, k.Items[0].Workspaces[0])
})
t.Run("with list options", func(t *testing.T) {
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
pools, err := client.AgentPools.List(ctx, orgTest.Name, &AgentPoolListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, pools.Items)
assert.Equal(t, 999, pools.CurrentPage)
assert.Equal(t, 1, pools.TotalCount)
})
t.Run("with sorting", func(t *testing.T) {
agentPool2, agentPoolCleanup2 := createAgentPool(t, client, orgTest)
t.Cleanup(agentPoolCleanup2)
pools, err := client.AgentPools.List(ctx, orgTest.Name, &AgentPoolListOptions{
Sort: "created-at",
})
require.NoError(t, err)
require.NotNil(t, pools)
require.Len(t, pools.Items, 2)
require.Equal(t, []string{agentPool.ID, agentPool2.ID}, []string{pools.Items[0].ID, pools.Items[1].ID})
pools, err = client.AgentPools.List(ctx, orgTest.Name, &AgentPoolListOptions{
Sort: "-created-at",
})
require.NoError(t, err)
require.NotNil(t, pools)
require.Len(t, pools.Items, 2)
require.Equal(t, []string{agentPool2.ID, agentPool.ID}, []string{pools.Items[0].ID, pools.Items[1].ID})
})
t.Run("without a valid organization", func(t *testing.T) {
pools, err := client.AgentPools.List(ctx, badIdentifier, nil)
assert.Nil(t, pools)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("with query options", func(t *testing.T) {
pools, err := client.AgentPools.List(ctx, orgTest.Name, &AgentPoolListOptions{
Query: agentPool.Name,
})
require.NoError(t, err)
assert.Equal(t, len(pools.Items), 1)
pools, err = client.AgentPools.List(ctx, orgTest.Name, &AgentPoolListOptions{
Query: agentPool.Name + "not_going_to_match",
})
require.NoError(t, err)
assert.Empty(t, pools.Items)
})
t.Run("with allowed workspace name filter", func(t *testing.T) {
ws1, ws1TestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(ws1TestCleanup)
ws2, ws2TestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(ws2TestCleanup)
organizationScoped := false
ap, apCleanup := createAgentPoolWithOptions(t, client, orgTest, AgentPoolCreateOptions{
Name: String("a-pool"),
OrganizationScoped: &organizationScoped,
AllowedWorkspaces: []*Workspace{ws1},
})
t.Cleanup(apCleanup)
ap2, ap2Cleanup := createAgentPoolWithOptions(t, client, orgTest, AgentPoolCreateOptions{
Name: String("b-pool"),
OrganizationScoped: &organizationScoped,
AllowedWorkspaces: []*Workspace{ws2},
})
t.Cleanup(ap2Cleanup)
pools, err := client.AgentPools.List(ctx, orgTest.Name, &AgentPoolListOptions{
AllowedWorkspacesName: ws1.Name,
})
require.NoError(t, err)
assert.NotEmpty(t, pools.Items)
assert.Contains(t, pools.Items, ap)
assert.Contains(t, pools.Items, agentPool)
assert.Equal(t, 2, pools.TotalCount)
pools, err = client.AgentPools.List(ctx, orgTest.Name, &AgentPoolListOptions{
AllowedWorkspacesName: ws2.Name,
})
require.NoError(t, err)
assert.NotEmpty(t, pools.Items)
assert.Contains(t, pools.Items, agentPool)
assert.Contains(t, pools.Items, ap2)
assert.Equal(t, 2, pools.TotalCount)
})
t.Run("with allowed projects name filter", func(t *testing.T) {
proj1, proj1TestCleanup := createProject(t, client, orgTest)
t.Cleanup(proj1TestCleanup)
proj2, proj2TestCleanup := createProject(t, client, orgTest)
t.Cleanup(proj2TestCleanup)
organizationScoped := false
ap, apCleanup := createAgentPoolWithOptions(t, client, orgTest, AgentPoolCreateOptions{
Name: String("a-pool"),
OrganizationScoped: &organizationScoped,
AllowedProjects: []*Project{proj1},
})
t.Cleanup(apCleanup)
ap2, ap2Cleanup := createAgentPoolWithOptions(t, client, orgTest, AgentPoolCreateOptions{
Name: String("b-pool"),
OrganizationScoped: &organizationScoped,
AllowedProjects: []*Project{proj2},
})
t.Cleanup(ap2Cleanup)
pools, err := client.AgentPools.List(ctx, orgTest.Name, &AgentPoolListOptions{
AllowedProjectsName: proj1.Name,
})
require.NoError(t, err)
assert.NotEmpty(t, pools.Items)
assert.Contains(t, pools.Items, ap)
assert.Contains(t, pools.Items, agentPool)
assert.Equal(t, 2, pools.TotalCount)
pools, err = client.AgentPools.List(ctx, orgTest.Name, &AgentPoolListOptions{
AllowedProjectsName: proj2.Name,
})
require.NoError(t, err)
assert.NotEmpty(t, pools.Items)
assert.Contains(t, pools.Items, agentPool)
assert.Contains(t, pools.Items, ap2)
assert.Equal(t, 2, pools.TotalCount)
})
}
func TestAgentPoolsCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
t.Run("with valid options", func(t *testing.T) {
options := AgentPoolCreateOptions{
Name: String("cool-pool"),
}
pool, err := client.AgentPools.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
// Get a refreshed view from the API.
refreshed, err := client.AgentPools.Read(ctx, pool.ID)
require.NoError(t, err)
for _, item := range []*AgentPool{
pool,
refreshed,
} {
assert.NotEmpty(t, item.ID)
}
})
t.Run("when options is missing name", func(t *testing.T) {
k, err := client.AgentPools.Create(ctx, "foo", AgentPoolCreateOptions{})
assert.Nil(t, k)
assert.EqualError(t, err, ErrRequiredName.Error())
})
t.Run("with an invalid organization", func(t *testing.T) {
pool, err := client.AgentPools.Create(ctx, badIdentifier, AgentPoolCreateOptions{
Name: String("cool-pool"),
})
assert.Nil(t, pool)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("with allowed-workspaces options", func(t *testing.T) {
workspaceTest, workspaceTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(workspaceTestCleanup)
organizationScoped := false
options := AgentPoolCreateOptions{
Name: String("a-pool"),
OrganizationScoped: &organizationScoped,
AllowedWorkspaces: []*Workspace{
workspaceTest,
},
}
pool, err := client.AgentPools.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
require.Equal(t, 1, len(pool.AllowedWorkspaces))
assert.Equal(t, workspaceTest.ID, pool.AllowedWorkspaces[0].ID)
// Get a refreshed view from the API.
refreshed, err := client.AgentPools.Read(ctx, pool.ID)
require.NoError(t, err)
for _, item := range []*AgentPool{
pool,
refreshed,
} {
assert.NotEmpty(t, item.ID)
}
})
t.Run("with allowed-projects options", func(t *testing.T) {
projectTest, projectTestCleanup := createProject(t, client, orgTest)
t.Cleanup(projectTestCleanup)
organizationScoped := false
options := AgentPoolCreateOptions{
Name: String("a-pool-2"),
OrganizationScoped: &organizationScoped,
AllowedProjects: []*Project{
projectTest,
},
}
pool, err := client.AgentPools.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
require.Equal(t, 1, len(pool.AllowedProjects))
assert.Equal(t, projectTest.ID, pool.AllowedProjects[0].ID)
// Get a refreshed view from the API.
refreshed, err := client.AgentPools.Read(ctx, pool.ID)
require.NoError(t, err)
for _, item := range []*AgentPool{
pool,
refreshed,
} {
assert.NotEmpty(t, item.ID)
}
})
t.Run("with excluded-workspaces options", func(t *testing.T) {
workspaceTest, workspaceTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(workspaceTestCleanup)
organizationScoped := false
options := AgentPoolCreateOptions{
Name: String("a-pool-3"),
OrganizationScoped: &organizationScoped,
ExcludedWorkspaces: []*Workspace{
workspaceTest,
},
}
pool, err := client.AgentPools.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
require.Equal(t, 1, len(pool.ExcludedWorkspaces))
assert.Equal(t, workspaceTest.ID, pool.ExcludedWorkspaces[0].ID)
// Get a refreshed view from the API.
refreshed, err := client.AgentPools.Read(ctx, pool.ID)
require.NoError(t, err)
for _, item := range []*AgentPool{
pool,
refreshed,
} {
assert.NotEmpty(t, item.ID)
}
})
}
func TestAgentPoolsRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
pool, poolCleanup := createAgentPool(t, client, orgTest)
t.Cleanup(poolCleanup)
t.Run("when the agent pool exists", func(t *testing.T) {
k, err := client.AgentPools.Read(ctx, pool.ID)
require.NoError(t, err)
assert.Equal(t, pool, k)
})
t.Run("when the agent pool does not exist", func(t *testing.T) {
k, err := client.AgentPools.Read(ctx, "nonexisting")
assert.Nil(t, k)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("without a valid agent pool ID", func(t *testing.T) {
k, err := client.AgentPools.Read(ctx, badIdentifier)
assert.Nil(t, k)
assert.EqualError(t, err, ErrInvalidAgentPoolID.Error())
})
t.Run("with Include option", func(t *testing.T) {
_, wTestCleanup := createWorkspaceWithVCS(t, client, orgTest, WorkspaceCreateOptions{
Name: String("foo"),
ExecutionMode: String("agent"),
AgentPoolID: String(pool.ID),
})
t.Cleanup(wTestCleanup)
k, err := client.AgentPools.ReadWithOptions(ctx, pool.ID, &AgentPoolReadOptions{
Include: []AgentPoolIncludeOpt{AgentPoolWorkspaces},
})
require.NoError(t, err)
assert.NotEmpty(t, k.Workspaces[0])
})
t.Run("read hyok configurations of an agent pool", func(t *testing.T) {
skipHYOKIntegrationTests(t)
// replace the environment variable with a valid agent pool ID that has HYOK configurations
hyokPoolID := os.Getenv("HYOK_POOL_ID")
if hyokPoolID == "" {
t.Fatal("Export a valid HYOK_POOL_ID before running this test!")
}
k, err := client.AgentPools.ReadWithOptions(ctx, hyokPoolID, &AgentPoolReadOptions{
Include: []AgentPoolIncludeOpt{AgentPoolHYOKConfigurations},
})
require.NoError(t, err)
assert.NotEmpty(t, k.HYOKConfigurations)
})
}
func TestAgentPoolsReadCreatedAt(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
pool, poolCleanup := createAgentPool(t, client, orgTest)
defer poolCleanup()
k, err := client.AgentPools.Read(ctx, pool.ID)
assert.NotEmpty(t, k.CreatedAt)
require.NoError(t, err)
}
func TestAgentPoolsUpdate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
t.Run("with valid options", func(t *testing.T) {
kBefore, kTestCleanup := createAgentPool(t, client, orgTest)
t.Cleanup(kTestCleanup)
kAfter, err := client.AgentPools.Update(ctx, kBefore.ID, AgentPoolUpdateOptions{
Name: String(randomString(t)),
})
require.NoError(t, err)
assert.Equal(t, kBefore.ID, kAfter.ID)
assert.NotEqual(t, kBefore.Name, kAfter.Name)
})
t.Run("when updating only the name", func(t *testing.T) {
workspaceTest, workspaceTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(workspaceTestCleanup)
projectTest, projectTestCleanup := createProject(t, client, orgTest)
t.Cleanup(projectTestCleanup)
excludedWorkspaceTest, excludedWorkspaceTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(excludedWorkspaceTestCleanup)
organizationScoped := false
options := AgentPoolCreateOptions{
Name: String("a-pool"),
OrganizationScoped: &organizationScoped,
AllowedWorkspaces: []*Workspace{
workspaceTest,
},
AllowedProjects: []*Project{
projectTest,
},
ExcludedWorkspaces: []*Workspace{
excludedWorkspaceTest,
},
}
kBefore, err := client.AgentPools.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
kAfter, err := client.AgentPools.Update(ctx, kBefore.ID, AgentPoolUpdateOptions{
Name: String("updated-key-name"),
})
require.NoError(t, err)
assert.Equal(t, kBefore.ID, kAfter.ID)
assert.Equal(t, "updated-key-name", kAfter.Name)
require.Equal(t, 1, len(kAfter.AllowedWorkspaces))
assert.Equal(t, workspaceTest.ID, kAfter.AllowedWorkspaces[0].ID)
require.Equal(t, 1, len(kAfter.AllowedProjects))
assert.Equal(t, projectTest.ID, kAfter.AllowedProjects[0].ID)
require.Equal(t, 1, len(kAfter.ExcludedWorkspaces))
assert.Equal(t, excludedWorkspaceTest.ID, kAfter.ExcludedWorkspaces[0].ID)
})
t.Run("without a valid agent pool ID", func(t *testing.T) {
w, err := client.AgentPools.Update(ctx, badIdentifier, AgentPoolUpdateOptions{})
assert.Nil(t, w)
assert.EqualError(t, err, ErrInvalidAgentPoolID.Error())
})
t.Run("when updating organization scope", func(t *testing.T) {
kBefore, kTestCleanup := createAgentPool(t, client, orgTest)
t.Cleanup(kTestCleanup)
organizationScoped := false
kAfter, err := client.AgentPools.Update(ctx, kBefore.ID, AgentPoolUpdateOptions{
Name: String(kBefore.Name),
OrganizationScoped: &organizationScoped,
})
require.NoError(t, err)
assert.NotEqual(t, kBefore.OrganizationScoped, kAfter.OrganizationScoped)
assert.Equal(t, organizationScoped, kAfter.OrganizationScoped)
})
t.Run("when updating allowed-workspaces", func(t *testing.T) {
kBefore, kTestCleanup := createAgentPool(t, client, orgTest)
t.Cleanup(kTestCleanup)
workspaceTest, workspaceTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(workspaceTestCleanup)
kAfter, err := client.AgentPools.Update(ctx, kBefore.ID, AgentPoolUpdateOptions{
AllowedWorkspaces: []*Workspace{
workspaceTest,
},
})
require.NoError(t, err)
assert.Equal(t, kBefore.Name, kAfter.Name)
assert.NotEqual(t, kBefore.AllowedWorkspaces, kAfter.AllowedWorkspaces)
require.Equal(t, 1, len(kAfter.AllowedWorkspaces))
assert.Equal(t, workspaceTest.ID, kAfter.AllowedWorkspaces[0].ID)
})
t.Run("when updating allowed-projects", func(t *testing.T) {
kBefore, kTestCleanup := createAgentPool(t, client, orgTest)
t.Cleanup(kTestCleanup)
projectTest, projectTestCleanup := createProject(t, client, orgTest)
t.Cleanup(projectTestCleanup)
kAfter, err := client.AgentPools.Update(ctx, kBefore.ID, AgentPoolUpdateOptions{
AllowedProjects: []*Project{
projectTest,
},
})
require.NoError(t, err)
assert.Equal(t, kBefore.Name, kAfter.Name)
assert.NotEqual(t, kBefore.AllowedProjects, kAfter.AllowedProjects)
require.Equal(t, 1, len(kAfter.AllowedProjects))
assert.Equal(t, projectTest.ID, kAfter.AllowedProjects[0].ID)
})
t.Run("when updating excluded-workspaces", func(t *testing.T) {
kBefore, kTestCleanup := createAgentPool(t, client, orgTest)
t.Cleanup(kTestCleanup)
workspaceTest, workspaceTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(workspaceTestCleanup)
kAfter, err := client.AgentPools.Update(ctx, kBefore.ID, AgentPoolUpdateOptions{
ExcludedWorkspaces: []*Workspace{
workspaceTest,
},
})
require.NoError(t, err)
assert.Equal(t, kBefore.Name, kAfter.Name)
assert.NotEqual(t, kBefore.ExcludedWorkspaces, kAfter.ExcludedWorkspaces)
require.Equal(t, 1, len(kAfter.ExcludedWorkspaces))
assert.Equal(t, workspaceTest.ID, kAfter.ExcludedWorkspaces[0].ID)
})
}
func TestAgentPoolsUpdateAllowedWorkspaces(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
t.Run("when updating allowed-workspaces", func(t *testing.T) {
kBefore, kTestCleanup := createAgentPool(t, client, orgTest)
t.Cleanup(kTestCleanup)
workspaceTest, workspaceTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(workspaceTestCleanup)
kAfter, err := client.AgentPools.UpdateAllowedWorkspaces(ctx, kBefore.ID, AgentPoolAllowedWorkspacesUpdateOptions{
AllowedWorkspaces: []*Workspace{
workspaceTest,
},
})
require.NoError(t, err)
assert.Equal(t, kBefore.Name, kAfter.Name)
assert.NotEqual(t, kBefore.AllowedWorkspaces, kAfter.AllowedWorkspaces)
require.Equal(t, 1, len(kAfter.AllowedWorkspaces))
assert.Equal(t, workspaceTest.ID, kAfter.AllowedWorkspaces[0].ID)
})
t.Run("when removing all the allowed-workspaces", func(t *testing.T) {
workspaceTest, workspaceTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(workspaceTestCleanup)
organizationScoped := false
options := AgentPoolCreateOptions{
Name: String("a-pool"),
OrganizationScoped: &organizationScoped,
AllowedWorkspaces: []*Workspace{
workspaceTest,
},
}
kBefore, kTestCleanup := createAgentPoolWithOptions(t, client, orgTest, options)
t.Cleanup(kTestCleanup)
kAfter, err := client.AgentPools.UpdateAllowedWorkspaces(ctx, kBefore.ID, AgentPoolAllowedWorkspacesUpdateOptions{
AllowedWorkspaces: []*Workspace{},
})
require.NoError(t, err)
assert.Equal(t, kBefore.ID, kAfter.ID)
assert.Equal(t, "a-pool", kAfter.Name)
assert.Empty(t, kAfter.AllowedWorkspaces)
})
}
func TestAgentPoolsUpdateAllowedProjects(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
t.Run("when updating allowed-projects", func(t *testing.T) {
kBefore, kTestCleanup := createAgentPool(t, client, orgTest)
t.Cleanup(kTestCleanup)
projectTest, projectTestCleanup := createProject(t, client, orgTest)
t.Cleanup(projectTestCleanup)
kAfter, err := client.AgentPools.UpdateAllowedProjects(ctx, kBefore.ID, AgentPoolAllowedProjectsUpdateOptions{
AllowedProjects: []*Project{
projectTest,
},
})
require.NoError(t, err)
assert.Equal(t, kBefore.Name, kAfter.Name)
assert.NotEqual(t, kBefore.AllowedProjects, kAfter.AllowedProjects)
require.Equal(t, 1, len(kAfter.AllowedProjects))
assert.Equal(t, projectTest.ID, kAfter.AllowedProjects[0].ID)
})
t.Run("when removing all the allowed-projects", func(t *testing.T) {
projectTest, projectTestCleanup := createProject(t, client, orgTest)
t.Cleanup(projectTestCleanup)
organizationScoped := false
options := AgentPoolCreateOptions{
Name: String("a-pool"),
OrganizationScoped: &organizationScoped,
AllowedProjects: []*Project{
projectTest,
},
}
kBefore, kTestCleanup := createAgentPoolWithOptions(t, client, orgTest, options)
t.Cleanup(kTestCleanup)
kAfter, err := client.AgentPools.UpdateAllowedProjects(ctx, kBefore.ID, AgentPoolAllowedProjectsUpdateOptions{
AllowedProjects: []*Project{},
})
require.NoError(t, err)
assert.Equal(t, kBefore.ID, kAfter.ID)
assert.Equal(t, "a-pool", kAfter.Name)
assert.Empty(t, kAfter.AllowedProjects)
})
}
func TestAgentPoolsUpdateExcludedWorkspaces(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
t.Run("when updating excluded-workspaces", func(t *testing.T) {
kBefore, kTestCleanup := createAgentPool(t, client, orgTest)
t.Cleanup(kTestCleanup)
workspaceTest, workspaceTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(workspaceTestCleanup)
kAfter, err := client.AgentPools.UpdateExcludedWorkspaces(ctx, kBefore.ID, AgentPoolExcludedWorkspacesUpdateOptions{
ExcludedWorkspaces: []*Workspace{
workspaceTest,
},
})
require.NoError(t, err)
assert.Equal(t, kBefore.Name, kAfter.Name)
assert.NotEqual(t, kBefore.ExcludedWorkspaces, kAfter.ExcludedWorkspaces)
require.Equal(t, 1, len(kAfter.ExcludedWorkspaces))
assert.Equal(t, workspaceTest.ID, kAfter.ExcludedWorkspaces[0].ID)
})
t.Run("when removing all the excluded-workspaces", func(t *testing.T) {
workspaceTest, workspaceTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(workspaceTestCleanup)
organizationScoped := false
options := AgentPoolCreateOptions{
Name: String("a-pool"),
OrganizationScoped: &organizationScoped,
ExcludedWorkspaces: []*Workspace{
workspaceTest,
},
}
kBefore, kTestCleanup := createAgentPoolWithOptions(t, client, orgTest, options)
t.Cleanup(kTestCleanup)
kAfter, err := client.AgentPools.UpdateExcludedWorkspaces(ctx, kBefore.ID, AgentPoolExcludedWorkspacesUpdateOptions{
ExcludedWorkspaces: []*Workspace{},
})
require.NoError(t, err)
assert.Equal(t, kBefore.ID, kAfter.ID)
assert.Equal(t, "a-pool", kAfter.Name)
assert.Empty(t, kAfter.ExcludedWorkspaces)
})
}
func TestAgentPoolsDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
agentPool, _ := createAgentPool(t, client, orgTest)
t.Run("with valid options", func(t *testing.T) {
err := client.AgentPools.Delete(ctx, agentPool.ID)
require.NoError(t, err)
// Try loading the agent pool - it should fail.
_, err = client.AgentPools.Read(ctx, agentPool.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the agent pool does not exist", func(t *testing.T) {
err := client.AgentPools.Delete(ctx, agentPool.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the agent pool ID is invalid", func(t *testing.T) {
err := client.AgentPools.Delete(ctx, badIdentifier)
assert.EqualError(t, err, ErrInvalidAgentPoolID.Error())
})
}
================================================
FILE: agent_token.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ AgentTokens = (*agentTokens)(nil)
// AgentTokens describes all the agent token related methods that the
// HCP Terraform API supports.
//
// TFE API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/agent-tokens
type AgentTokens interface {
// List all the agent tokens of the given agent pool.
List(ctx context.Context, agentPoolID string) (*AgentTokenList, error)
// Create a new agent token with the given options.
Create(ctx context.Context, agentPoolID string, options AgentTokenCreateOptions) (*AgentToken, error)
// Read an agent token by its ID.
Read(ctx context.Context, agentTokenID string) (*AgentToken, error)
// Delete an agent token by its ID.
Delete(ctx context.Context, agentTokenID string) error
}
// agentTokens implements AgentTokens.
type agentTokens struct {
client *Client
}
// AgentToken represents a HCP Terraform agent token.
type AgentToken struct {
ID string `jsonapi:"primary,authentication-tokens"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Description string `jsonapi:"attr,description"`
LastUsedAt time.Time `jsonapi:"attr,last-used-at,iso8601"`
Token string `jsonapi:"attr,token"`
// Relations
CreatedBy *User `jsonapi:"relation,created-by"`
}
// AgentTokenList represents a list of agent tokens.
type AgentTokenList struct {
*Pagination
Items []*AgentToken
}
// AgentTokenCreateOptions represents the options for creating an agent token.
type AgentTokenCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,agent-tokens"`
// Description of the token
Description *string `jsonapi:"attr,description"`
}
// List all the agent tokens of the given agent pool.
func (s *agentTokens) List(ctx context.Context, agentPoolID string) (*AgentTokenList, error) {
if !validStringID(&agentPoolID) {
return nil, ErrInvalidAgentPoolID
}
u := fmt.Sprintf("agent-pools/%s/authentication-tokens", url.PathEscape(agentPoolID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
tokenList := &AgentTokenList{}
err = req.Do(ctx, tokenList)
if err != nil {
return nil, err
}
return tokenList, nil
}
// Create a new agent token with the given options.
func (s *agentTokens) Create(ctx context.Context, agentPoolID string, options AgentTokenCreateOptions) (*AgentToken, error) {
if !validStringID(&agentPoolID) {
return nil, ErrInvalidAgentPoolID
}
if !validString(options.Description) {
return nil, ErrAgentTokenDescription
}
u := fmt.Sprintf("agent-pools/%s/authentication-tokens", url.PathEscape(agentPoolID))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
at := &AgentToken{}
err = req.Do(ctx, at)
if err != nil {
return nil, err
}
return at, err
}
// Read an agent token by its ID.
func (s *agentTokens) Read(ctx context.Context, agentTokenID string) (*AgentToken, error) {
if !validStringID(&agentTokenID) {
return nil, ErrInvalidAgentTokenID
}
u := fmt.Sprintf(AuthenticationTokensPath, url.PathEscape(agentTokenID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
at := &AgentToken{}
err = req.Do(ctx, at)
if err != nil {
return nil, err
}
return at, err
}
// Delete an agent token by its ID.
func (s *agentTokens) Delete(ctx context.Context, agentTokenID string) error {
if !validStringID(&agentTokenID) {
return ErrInvalidAgentTokenID
}
u := fmt.Sprintf(AuthenticationTokensPath, url.PathEscape(agentTokenID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
================================================
FILE: agent_token_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAgentTokensList(t *testing.T) {
t.Parallel()
skipIfEnterprise(t)
client := testClient(t)
ctx := context.Background()
apTest, apTestCleanup := createAgentPool(t, client, nil)
defer apTestCleanup()
agentToken1, agentToken1Cleanup := createAgentToken(t, client, apTest)
defer agentToken1Cleanup()
_, agentToken2Cleanup := createAgentToken(t, client, apTest)
defer agentToken2Cleanup()
t.Run("with no list options", func(t *testing.T) {
tokenlist, err := client.AgentTokens.List(ctx, apTest.ID)
require.NoError(t, err)
var found bool
for _, j := range tokenlist.Items {
if j.ID == agentToken1.ID {
found = true
break
}
}
if !found {
t.Fatalf("agent token (%s) not found in token list", agentToken1.ID)
}
assert.Equal(t, 1, tokenlist.CurrentPage)
assert.Equal(t, 2, tokenlist.TotalCount)
})
t.Run("without a valid agent pool ID", func(t *testing.T) {
tokenlist, err := client.AgentTokens.List(ctx, badIdentifier)
assert.Nil(t, tokenlist)
assert.EqualError(t, err, ErrInvalidAgentPoolID.Error())
})
}
func TestAgentTokensCreate(t *testing.T) {
t.Parallel()
skipIfEnterprise(t)
client := testClient(t)
ctx := context.Background()
apTest, apTestCleanup := createAgentPool(t, client, nil)
defer apTestCleanup()
t.Run("with valid description", func(t *testing.T) {
token, err := client.AgentTokens.Create(ctx, apTest.ID, AgentTokenCreateOptions{
Description: String(randomString(t)),
})
require.NoError(t, err)
require.NotEmpty(t, token.Token)
})
t.Run("without valid description", func(t *testing.T) {
at, err := client.AgentTokens.Create(ctx, badIdentifier, AgentTokenCreateOptions{})
assert.Nil(t, at)
assert.EqualError(t, err, ErrInvalidAgentPoolID.Error())
})
t.Run("without valid agent pool ID", func(t *testing.T) {
at, err := client.AgentTokens.Create(ctx, badIdentifier, AgentTokenCreateOptions{
Description: String(randomString(t)),
})
assert.Nil(t, at)
assert.EqualError(t, err, ErrInvalidAgentPoolID.Error())
})
}
func TestAgentTokensRead(t *testing.T) {
t.Parallel()
skipIfEnterprise(t)
client := testClient(t)
ctx := context.Background()
apTest, apTestCleanup := createAgentPool(t, client, nil)
defer apTestCleanup()
token, tokenTestCleanup := createAgentToken(t, client, apTest)
defer tokenTestCleanup()
t.Run("read token with valid token ID", func(t *testing.T) {
at, err := client.AgentTokens.Read(ctx, token.ID)
require.NoError(t, err)
// The initial API call to create a token will return a value in the token
// object. Empty that out for comparison
token.Token = ""
assert.Equal(t, token, at)
})
t.Run("read token without valid token ID", func(t *testing.T) {
_, err := client.AgentTokens.Read(ctx, badIdentifier)
assert.EqualError(t, err, ErrInvalidAgentTokenID.Error())
})
}
func TestAgentTokensReadCreatedBy(t *testing.T) {
t.Parallel()
skipIfEnterprise(t)
client := testClient(t)
ctx := context.Background()
apTest, apTestCleanup := createAgentPool(t, client, nil)
defer apTestCleanup()
token, tokenTestCleanup := createAgentToken(t, client, apTest)
defer tokenTestCleanup()
at, err := client.AgentTokens.Read(ctx, token.ID)
require.NoError(t, err)
require.NotNil(t, at.CreatedBy)
}
func TestAgentTokensDelete(t *testing.T) {
t.Parallel()
skipIfEnterprise(t)
client := testClient(t)
ctx := context.Background()
apTest, apTestCleanup := createAgentPool(t, client, nil)
defer apTestCleanup()
token, _ := createAgentToken(t, client, apTest)
t.Run("with valid token ID", func(t *testing.T) {
err := client.AgentTokens.Delete(ctx, token.ID)
require.NoError(t, err)
})
t.Run("without valid token ID", func(t *testing.T) {
err := client.AgentTokens.Delete(ctx, badIdentifier)
assert.EqualError(t, err, ErrInvalidAgentTokenID.Error())
})
}
================================================
FILE: apply.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"io"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ Applies = (*applies)(nil)
// Applies describes all the apply related methods that the Terraform
// Enterprise API supports.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/applies
type Applies interface {
// Read an apply by its ID.
Read(ctx context.Context, applyID string) (*Apply, error)
// Logs retrieves the logs of an apply.
Logs(ctx context.Context, applyID string) (io.Reader, error)
}
// applies implements Applies interface.
type applies struct {
client *Client
}
// ApplyStatus represents an apply state.
type ApplyStatus string
// List all available apply statuses.
const (
ApplyCanceled ApplyStatus = "canceled"
ApplyCreated ApplyStatus = "created"
ApplyErrored ApplyStatus = "errored"
ApplyFinished ApplyStatus = "finished"
ApplyMFAWaiting ApplyStatus = "mfa_waiting"
ApplyPending ApplyStatus = "pending"
ApplyQueued ApplyStatus = "queued"
ApplyRunning ApplyStatus = "running"
ApplyUnreachable ApplyStatus = "unreachable"
)
// Apply represents a Terraform Enterprise apply.
type Apply struct {
ID string `jsonapi:"primary,applies"`
LogReadURL string `jsonapi:"attr,log-read-url"`
ResourceAdditions int `jsonapi:"attr,resource-additions"`
ResourceChanges int `jsonapi:"attr,resource-changes"`
ResourceDestructions int `jsonapi:"attr,resource-destructions"`
ResourceImports int `jsonapi:"attr,resource-imports"`
Status ApplyStatus `jsonapi:"attr,status"`
StatusTimestamps *ApplyStatusTimestamps `jsonapi:"attr,status-timestamps"`
}
// ApplyStatusTimestamps holds the timestamps for individual apply statuses.
type ApplyStatusTimestamps struct {
CanceledAt time.Time `jsonapi:"attr,canceled-at,rfc3339"`
ErroredAt time.Time `jsonapi:"attr,errored-at,rfc3339"`
FinishedAt time.Time `jsonapi:"attr,finished-at,rfc3339"`
ForceCanceledAt time.Time `jsonapi:"attr,force-canceled-at,rfc3339"`
QueuedAt time.Time `jsonapi:"attr,queued-at,rfc3339"`
StartedAt time.Time `jsonapi:"attr,started-at,rfc3339"`
}
// Read an apply by its ID.
func (s *applies) Read(ctx context.Context, applyID string) (*Apply, error) {
if !validStringID(&applyID) {
return nil, ErrInvalidApplyID
}
u := fmt.Sprintf("applies/%s", url.PathEscape(applyID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
a := &Apply{}
err = req.Do(ctx, a)
if err != nil {
return nil, err
}
return a, nil
}
// Logs retrieves the logs of an apply.
func (s *applies) Logs(ctx context.Context, applyID string) (io.Reader, error) {
if !validStringID(&applyID) {
return nil, ErrInvalidApplyID
}
// Get the apply to make sure it exists.
a, err := s.Read(ctx, applyID)
if err != nil {
return nil, err
}
// Return an error if the log URL is empty.
if a.LogReadURL == "" {
return nil, fmt.Errorf("apply %s does not have a log URL", applyID)
}
u, err := url.Parse(a.LogReadURL)
if err != nil {
return nil, fmt.Errorf("invalid log URL: %w", err)
}
done := func() (bool, error) {
a, err := s.Read(ctx, a.ID)
if err != nil {
return false, err
}
switch a.Status {
case ApplyCanceled, ApplyErrored, ApplyFinished, ApplyUnreachable:
return true, nil
default:
return false, nil
}
}
return &LogReader{
client: s.client,
ctx: ctx,
done: done,
logURL: u,
}, nil
}
================================================
FILE: apply_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"encoding/json"
"io"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAppliesRead_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
wTest, wTestCleanup := createWorkspace(t, client, nil)
defer wTestCleanup()
rTest, rTestCleanup := createRunApply(t, client, wTest)
defer rTestCleanup()
t.Run("when the plan exists", func(t *testing.T) {
a, err := client.Applies.Read(ctx, rTest.Apply.ID)
require.NoError(t, err)
assert.NotEmpty(t, a.LogReadURL)
assert.Equal(t, a.Status, ApplyFinished)
assert.NotEmpty(t, a.StatusTimestamps)
})
t.Run("when the apply does not exist", func(t *testing.T) {
a, err := client.Applies.Read(ctx, "nonexisting")
assert.Nil(t, a)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("with invalid apply ID", func(t *testing.T) {
a, err := client.Applies.Read(ctx, badIdentifier)
assert.Nil(t, a)
assert.EqualError(t, err, ErrInvalidApplyID.Error())
})
}
func TestAppliesLogs_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
rTest, rTestCleanup := createRunApply(t, client, nil)
defer rTestCleanup()
t.Run("when the log exists", func(t *testing.T) {
a, err := client.Applies.Read(ctx, rTest.Apply.ID)
require.NoError(t, err)
logReader, err := client.Applies.Logs(ctx, a.ID)
require.NoError(t, err)
logs, err := io.ReadAll(logReader)
require.NoError(t, err)
assert.Contains(t, string(logs), "1 added, 0 changed, 0 destroyed")
})
t.Run("when the log does not exist", func(t *testing.T) {
logs, err := client.Applies.Logs(ctx, "nonexisting")
assert.Nil(t, logs)
assert.Error(t, err)
})
}
func TestApplies_Unmarshal(t *testing.T) {
t.Parallel()
data := map[string]interface{}{
"data": map[string]interface{}{
"type": "applies",
"id": "apply-47MBvjwzBG8YKc2v",
"attributes": map[string]interface{}{
"log-read-url": "hashicorp.com",
"resource-additions": 1,
"resource-changes": 1,
"resource-destructions": 1,
"status": ApplyCanceled,
"status-timestamps": map[string]string{
"queued-at": "2020-03-16T23:15:59+00:00",
"errored-at": "2019-03-16T23:23:59+00:00",
},
},
},
}
byteData, err := json.Marshal(data)
require.NoError(t, err)
responseBody := bytes.NewReader(byteData)
apply := &Apply{}
err = unmarshalResponse(responseBody, apply)
require.NoError(t, err)
queuedParsedTime, err := time.Parse(time.RFC3339, "2020-03-16T23:15:59+00:00")
require.NoError(t, err)
erroredParsedTime, err := time.Parse(time.RFC3339, "2019-03-16T23:23:59+00:00")
require.NoError(t, err)
assert.Equal(t, apply.ID, "apply-47MBvjwzBG8YKc2v")
assert.Equal(t, apply.ResourceAdditions, 1)
assert.Equal(t, apply.ResourceChanges, 1)
assert.Equal(t, apply.ResourceDestructions, 1)
assert.Equal(t, apply.Status, ApplyCanceled)
assert.Equal(t, apply.StatusTimestamps.QueuedAt, queuedParsedTime)
assert.Equal(t, apply.StatusTimestamps.ErroredAt, erroredParsedTime)
}
================================================
FILE: audit_trail.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"encoding/json"
"io"
"net/http"
"time"
"github.com/google/go-querystring/query"
retryablehttp "github.com/hashicorp/go-retryablehttp"
)
// Compile-time proof of interface implementation
var _ AuditTrails = (*auditTrails)(nil)
// AuditTrails describes all the audit event related methods that the HCP Terraform
// API supports.
// **Note:** These methods require the client to be configured with an organization token for
// an organization in the Business tier. Furthermore, these methods are only available in HCP Terraform.
//
// HCP Terraform API Docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/audit-trails
type AuditTrails interface {
// Read all the audit events in an organization.
List(ctx context.Context, options *AuditTrailListOptions) (*AuditTrailList, error)
}
// auditTrails implements AuditTrails
type auditTrails struct {
client *Client
}
// AuditTrailRequest represents the request details of the audit event.
type AuditTrailRequest struct {
ID string `json:"id"`
}
// AuditTrailAuth represents the details of the actor that invoked the audit event.
type AuditTrailAuth struct {
AccessorID string `json:"accessor_id"`
Description string `json:"description"`
Type string `json:"type"`
ImpersonatorID *string `json:"impersonator_id"`
OrganizationID string `json:"organization_id"`
}
// AuditTrailResource represents the details of the API resource in the audit event.
type AuditTrailResource struct {
ID string `json:"id"`
Type string `json:"type"`
Action string `json:"action"`
Meta map[string]interface{} `json:"meta"`
}
type AuditTrailPagination struct {
CurrentPage int `json:"current_page"`
PreviousPage int `json:"prev_page"`
NextPage int `json:"next_page"`
TotalPages int `json:"total_pages"`
TotalCount int `json:"total_count"`
}
// AuditTrail represents an event in the HCP Terraform audit log.
type AuditTrail struct {
ID string `json:"id"`
Version string `json:"version"`
Type string `json:"type"`
Timestamp time.Time `json:"timestamp"`
Auth AuditTrailAuth `json:"auth"`
Request AuditTrailRequest `json:"request"`
Resource AuditTrailResource `json:"resource"`
}
// AuditTrailList represents a list of audit trails.
type AuditTrailList struct {
*AuditTrailPagination `json:"pagination"`
Items []*AuditTrail `json:"data"`
}
// AuditTrailListOptions represents the options for listing audit trails.
type AuditTrailListOptions struct {
// Optional: Returns only audit trails created after this date
Since time.Time `url:"since,omitempty"`
*ListOptions
}
// List all the audit events in an organization.
func (s *auditTrails) List(ctx context.Context, options *AuditTrailListOptions) (*AuditTrailList, error) {
u, err := s.client.baseURL.Parse("/api/v2/organization/audit-trail")
if err != nil {
return nil, err
}
headers := make(http.Header)
headers.Set("User-Agent", _userAgent)
headers.Set("Authorization", "Bearer "+s.client.token)
headers.Set("Content-Type", "application/json")
if options != nil {
q, err := query.Values(options)
if err != nil {
return nil, err
}
u.RawQuery = encodeQueryParams(q)
}
req, err := retryablehttp.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, err
}
// Attach the headers to the request
for k, v := range headers {
req.Header[k] = v
}
if err := s.client.limiter.Wait(ctx); err != nil {
return nil, err
}
resp, err := s.client.http.Do(req.WithContext(ctx))
if err != nil {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
return nil, err
}
}
defer resp.Body.Close() //nolint:errcheck
if err := checkResponseCode(resp); err != nil {
return nil, err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
atl := &AuditTrailList{}
if err := json.Unmarshal(body, atl); err != nil {
return nil, err
}
return atl, nil
}
================================================
FILE: audit_trail_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAuditTrailsList(t *testing.T) {
t.Parallel()
skipIfEnterprise(t)
userClient := testClient(t)
ctx := context.Background()
org, orgCleanup := createOrganization(t, userClient)
t.Cleanup(orgCleanup)
auditTrailClient := testAuditTrailClient(t, userClient, org)
// First let's generate some audit events in this test organization
ws, wkspace1Cleanup := createWorkspace(t, userClient, org)
t.Cleanup(wkspace1Cleanup)
_, err := userClient.Workspaces.Lock(ctx, ws.ID, WorkspaceLockOptions{})
require.NoError(t, err)
_, err = userClient.Workspaces.Unlock(ctx, ws.ID)
require.NoError(t, err)
_, err = userClient.Workspaces.Lock(ctx, ws.ID, WorkspaceLockOptions{})
require.NoError(t, err)
_, err = userClient.Workspaces.Unlock(ctx, ws.ID)
require.NoError(t, err)
_, err = userClient.Workspaces.Lock(ctx, ws.ID, WorkspaceLockOptions{})
require.NoError(t, err)
_, err = userClient.Workspaces.Unlock(ctx, ws.ID)
require.NoError(t, err)
t.Run("with no specified timeframe", func(t *testing.T) {
atl, err := auditTrailClient.AuditTrails.List(ctx, nil)
require.NoError(t, err)
require.NotEmpty(t, atl.Items)
require.Equal(t, len(atl.Items), 8)
t.Run("pagination parameters", func(t *testing.T) {
page1, err := auditTrailClient.AuditTrails.List(ctx, &AuditTrailListOptions{
ListOptions: &ListOptions{
PageNumber: 1,
PageSize: 4,
},
})
require.NoError(t, err)
assert.NotEmpty(t, page1.Items)
assert.Equal(t, 1, page1.CurrentPage)
assert.Equal(t, 2, page1.TotalPages)
assert.Equal(t, 8, page1.TotalCount)
page2, err := auditTrailClient.AuditTrails.List(ctx, &AuditTrailListOptions{
ListOptions: &ListOptions{
PageNumber: 2,
PageSize: 4,
},
})
require.NoError(t, err)
assert.NotEmpty(t, page2.Items)
assert.Equal(t, 2, page2.CurrentPage)
assert.Equal(t, 0, page2.NextPage)
})
log := atl.Items[0]
assert.NotEmpty(t, log.ID)
assert.NotEmpty(t, log.Timestamp)
assert.NotEmpty(t, log.Type)
assert.NotEmpty(t, log.Version)
require.NotNil(t, log.Resource)
require.NotNil(t, log.Auth)
require.NotNil(t, log.Request)
t.Run("with resource deserialized correctly", func(t *testing.T) {
assert.NotEmpty(t, log.Resource.ID)
assert.NotEmpty(t, log.Resource.Type)
assert.NotEmpty(t, log.Resource.Action)
// we don't test against log.Resource.Meta since we don't know the nature
// of the audit trail log we're testing against as it can be nil or contain a k-v map
})
t.Run("with auth deserialized correctly", func(t *testing.T) {
assert.NotEmpty(t, log.Auth.AccessorID)
assert.NotEmpty(t, log.Auth.Description)
assert.NotEmpty(t, log.Auth.Type)
assert.NotEmpty(t, log.Auth.OrganizationID)
})
t.Run("with request deserialized correctly", func(t *testing.T) {
assert.NotEmpty(t, log.Request.ID)
})
})
t.Run("using since query param", func(t *testing.T) {
since := time.Now()
// Wait some time before creating the event
// otherwise comparing time values can be flaky
time.Sleep(1 * time.Second)
// Let's create an event that is sent to the audit log
_, wsCleanup := createWorkspace(t, userClient, org)
t.Cleanup(wsCleanup)
atl, err := auditTrailClient.AuditTrails.List(ctx, &AuditTrailListOptions{
Since: since,
ListOptions: &ListOptions{
PageNumber: 1,
PageSize: 20,
},
})
require.NoError(t, err)
require.Greater(t, len(atl.Items), 0)
assert.LessOrEqual(t, len(atl.Items), 20)
for _, log := range atl.Items {
assert.True(t, log.Timestamp.After(since))
}
})
}
================================================
FILE: aws_oidc_configuration.go
================================================
package tfe
import (
"context"
"fmt"
"net/url"
)
const OIDCConfigPathFormat = "oidc-configurations/%s"
// AWSOIDCConfigurations describes all the AWS OIDC configuration related methods that the HCP Terraform API supports.
// HCP Terraform API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/hold-your-own-key/oidc-configurations/aws
type AWSOIDCConfigurations interface {
Create(ctx context.Context, organization string, options AWSOIDCConfigurationCreateOptions) (*AWSOIDCConfiguration, error)
Read(ctx context.Context, oidcID string) (*AWSOIDCConfiguration, error)
Update(ctx context.Context, oidcID string, options AWSOIDCConfigurationUpdateOptions) (*AWSOIDCConfiguration, error)
Delete(ctx context.Context, oidcID string) error
}
type awsOIDCConfigurations struct {
client *Client
}
var _ AWSOIDCConfigurations = &awsOIDCConfigurations{}
type AWSOIDCConfiguration struct {
ID string `jsonapi:"primary,aws-oidc-configurations"`
RoleARN string `jsonapi:"attr,role-arn"`
Organization *Organization `jsonapi:"relation,organization"`
}
type AWSOIDCConfigurationCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,aws-oidc-configurations"`
// Attributes
RoleARN string `jsonapi:"attr,role-arn"`
}
type AWSOIDCConfigurationUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,aws-oidc-configurations"`
// Attributes
RoleARN string `jsonapi:"attr,role-arn"`
}
func (o *AWSOIDCConfigurationCreateOptions) valid() error {
if o.RoleARN == "" {
return ErrRequiredRoleARN
}
return nil
}
func (o *AWSOIDCConfigurationUpdateOptions) valid() error {
if o.RoleARN == "" {
return ErrRequiredRoleARN
}
return nil
}
func (aoc *awsOIDCConfigurations) Create(ctx context.Context, organization string, options AWSOIDCConfigurationCreateOptions) (*AWSOIDCConfiguration, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
req, err := aoc.client.NewRequest("POST", fmt.Sprintf("organizations/%s/oidc-configurations", url.PathEscape(organization)), &options)
if err != nil {
return nil, err
}
awsOIDCConfiguration := &AWSOIDCConfiguration{}
err = req.Do(ctx, awsOIDCConfiguration)
if err != nil {
return nil, err
}
return awsOIDCConfiguration, nil
}
func (aoc *awsOIDCConfigurations) Read(ctx context.Context, oidcID string) (*AWSOIDCConfiguration, error) {
req, err := aoc.client.NewRequest("GET", fmt.Sprintf(OIDCConfigPathFormat, url.PathEscape(oidcID)), nil)
if err != nil {
return nil, err
}
awsOIDCConfiguration := &AWSOIDCConfiguration{}
err = req.Do(ctx, awsOIDCConfiguration)
if err != nil {
return nil, err
}
return awsOIDCConfiguration, nil
}
func (aoc *awsOIDCConfigurations) Update(ctx context.Context, oidcID string, options AWSOIDCConfigurationUpdateOptions) (*AWSOIDCConfiguration, error) {
if !validStringID(&oidcID) {
return nil, ErrInvalidOIDC
}
if err := options.valid(); err != nil {
return nil, err
}
req, err := aoc.client.NewRequest("PATCH", fmt.Sprintf(OIDCConfigPathFormat, url.PathEscape(oidcID)), &options)
if err != nil {
return nil, err
}
awsOIDCConfiguration := &AWSOIDCConfiguration{}
err = req.Do(ctx, awsOIDCConfiguration)
if err != nil {
return nil, err
}
return awsOIDCConfiguration, nil
}
func (aoc *awsOIDCConfigurations) Delete(ctx context.Context, oidcID string) error {
if !validStringID(&oidcID) {
return ErrInvalidOIDC
}
req, err := aoc.client.NewRequest("DELETE", fmt.Sprintf(OIDCConfigPathFormat, url.PathEscape(oidcID)), nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
================================================
FILE: aws_oidc_configuration_integration_test.go
================================================
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// These tests are intended for local execution only, as OIDC configurations for HYOK requires specific conditions.
// To run them locally, follow the instructions outlined in hyok_configuration_integration_test.go
func TestAWSOIDCConfigurationCreateDelete(t *testing.T) {
t.Parallel()
skipHYOKIntegrationTests(t)
client := testClient(t)
ctx := context.Background()
orgTest := testHyokOrganization(t, client)
t.Run("with valid options", func(t *testing.T) {
opts := AWSOIDCConfigurationCreateOptions{
RoleARN: "arn:aws:iam::123456789012:role/some-role",
}
oidcConfig, err := client.AWSOIDCConfigurations.Create(ctx, orgTest.Name, opts)
require.NoError(t, err)
require.NotNil(t, oidcConfig)
assert.Equal(t, oidcConfig.RoleARN, opts.RoleARN)
// delete the created configuration
err = client.AWSOIDCConfigurations.Delete(ctx, oidcConfig.ID)
require.NoError(t, err)
})
t.Run("missing role ARN", func(t *testing.T) {
opts := AWSOIDCConfigurationCreateOptions{}
_, err := client.AWSOIDCConfigurations.Create(ctx, orgTest.Name, opts)
assert.ErrorIs(t, err, ErrRequiredRoleARN)
})
}
func TestAWSOIDCConfigurationRead(t *testing.T) {
t.Parallel()
skipHYOKIntegrationTests(t)
client := testClient(t)
ctx := context.Background()
orgTest := testHyokOrganization(t, client)
oidcConfig, oidcConfigCleanup := createAWSOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcConfigCleanup)
t.Run("fetch existing configuration", func(t *testing.T) {
fetched, err := client.AWSOIDCConfigurations.Read(ctx, oidcConfig.ID)
require.NoError(t, err)
require.NotEmpty(t, fetched)
})
t.Run("fetching non-existing configuration", func(t *testing.T) {
_, err := client.AWSOIDCConfigurations.Read(ctx, "awsoidc-notreal")
assert.ErrorIs(t, err, ErrResourceNotFound)
})
}
func TestAWSOIDCConfigurationsUpdate(t *testing.T) {
t.Parallel()
skipHYOKIntegrationTests(t)
client := testClient(t)
ctx := context.Background()
orgTest := testHyokOrganization(t, client)
oidcConfig, oidcConfigCleanup := createAWSOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcConfigCleanup)
t.Run("with valid options", func(t *testing.T) {
opts := AWSOIDCConfigurationUpdateOptions{
RoleARN: "arn:aws:iam::123456789012:role/some-role-2",
}
updated, err := client.AWSOIDCConfigurations.Update(ctx, oidcConfig.ID, opts)
require.NoError(t, err)
require.NotEmpty(t, updated)
assert.Equal(t, opts.RoleARN, updated.RoleARN)
})
t.Run("missing role ARN", func(t *testing.T) {
opts := AWSOIDCConfigurationUpdateOptions{}
_, err := client.AWSOIDCConfigurations.Update(ctx, oidcConfig.ID, opts)
assert.ErrorIs(t, err, ErrRequiredRoleARN)
})
}
================================================
FILE: azure_oidc_configuration.go
================================================
package tfe
import (
"context"
"fmt"
"net/url"
)
// AzureOIDCConfigurations describes all the Azure OIDC configuration related methods that the HCP Terraform API supports.
// HCP Terraform API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/hold-your-own-key/oidc-configurations/azure
type AzureOIDCConfigurations interface {
Create(ctx context.Context, organization string, options AzureOIDCConfigurationCreateOptions) (*AzureOIDCConfiguration, error)
Read(ctx context.Context, oidcID string) (*AzureOIDCConfiguration, error)
Update(ctx context.Context, oidcID string, options AzureOIDCConfigurationUpdateOptions) (*AzureOIDCConfiguration, error)
Delete(ctx context.Context, oidcID string) error
}
type azureOIDCConfigurations struct {
client *Client
}
var _ AzureOIDCConfigurations = &azureOIDCConfigurations{}
type AzureOIDCConfiguration struct {
ID string `jsonapi:"primary,azure-oidc-configurations"`
ClientID string `jsonapi:"attr,client-id"`
SubscriptionID string `jsonapi:"attr,subscription-id"`
TenantID string `jsonapi:"attr,tenant-id"`
Organization *Organization `jsonapi:"relation,organization"`
}
type AzureOIDCConfigurationCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,azure-oidc-configurations"`
// Attributes
ClientID string `jsonapi:"attr,client-id"`
SubscriptionID string `jsonapi:"attr,subscription-id"`
TenantID string `jsonapi:"attr,tenant-id"`
}
type AzureOIDCConfigurationUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,azure-oidc-configurations"`
// Attributes
ClientID *string `jsonapi:"attr,client-id,omitempty"`
SubscriptionID *string `jsonapi:"attr,subscription-id,omitempty"`
TenantID *string `jsonapi:"attr,tenant-id,omitempty"`
}
func (o *AzureOIDCConfigurationCreateOptions) valid() error {
if o.ClientID == "" {
return ErrRequiredClientID
}
if o.SubscriptionID == "" {
return ErrRequiredSubscriptionID
}
if o.TenantID == "" {
return ErrRequiredTenantID
}
return nil
}
func (aoc *azureOIDCConfigurations) Create(ctx context.Context, organization string, options AzureOIDCConfigurationCreateOptions) (*AzureOIDCConfiguration, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
req, err := aoc.client.NewRequest("POST", fmt.Sprintf("organizations/%s/oidc-configurations", url.PathEscape(organization)), &options)
if err != nil {
return nil, err
}
azureOIDCConfiguration := &AzureOIDCConfiguration{}
err = req.Do(ctx, azureOIDCConfiguration)
if err != nil {
return nil, err
}
return azureOIDCConfiguration, nil
}
func (aoc *azureOIDCConfigurations) Read(ctx context.Context, oidcID string) (*AzureOIDCConfiguration, error) {
req, err := aoc.client.NewRequest("GET", fmt.Sprintf(OIDCConfigPathFormat, url.PathEscape(oidcID)), nil)
if err != nil {
return nil, err
}
azureOIDCConfiguration := &AzureOIDCConfiguration{}
err = req.Do(ctx, azureOIDCConfiguration)
if err != nil {
return nil, err
}
return azureOIDCConfiguration, nil
}
func (aoc *azureOIDCConfigurations) Update(ctx context.Context, oidcID string, options AzureOIDCConfigurationUpdateOptions) (*AzureOIDCConfiguration, error) {
if !validStringID(&oidcID) {
return nil, ErrInvalidOIDC
}
req, err := aoc.client.NewRequest("PATCH", fmt.Sprintf(OIDCConfigPathFormat, url.PathEscape(oidcID)), &options)
if err != nil {
return nil, err
}
azureOIDCConfiguration := &AzureOIDCConfiguration{}
err = req.Do(ctx, azureOIDCConfiguration)
if err != nil {
return nil, err
}
return azureOIDCConfiguration, nil
}
func (aoc *azureOIDCConfigurations) Delete(ctx context.Context, oidcID string) error {
if !validStringID(&oidcID) {
return ErrInvalidOIDC
}
req, err := aoc.client.NewRequest("DELETE", fmt.Sprintf(OIDCConfigPathFormat, url.PathEscape(oidcID)), nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
================================================
FILE: azure_oidc_configuration_integration_test.go
================================================
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// These tests are intended for local execution only, as OIDC configurations for HYOK requires specific conditions.
// To run them locally, follow the instructions outlined in hyok_configuration_integration_test.go
func TestAzureOIDCConfigurationCreateDelete(t *testing.T) {
t.Parallel()
skipHYOKIntegrationTests(t)
client := testClient(t)
ctx := context.Background()
orgTest := testHyokOrganization(t, client)
t.Run("with valid options", func(t *testing.T) {
opts := AzureOIDCConfigurationCreateOptions{
ClientID: "your-azure-client-id",
SubscriptionID: "your-azure-subscription-id",
TenantID: "your-azure-tenant-id",
}
oidcConfig, err := client.AzureOIDCConfigurations.Create(ctx, orgTest.Name, opts)
require.NoError(t, err)
require.NotNil(t, oidcConfig)
assert.Equal(t, oidcConfig.ClientID, opts.ClientID)
assert.Equal(t, oidcConfig.SubscriptionID, opts.SubscriptionID)
assert.Equal(t, oidcConfig.TenantID, opts.TenantID)
// delete the created configuration
err = client.AzureOIDCConfigurations.Delete(ctx, oidcConfig.ID)
require.NoError(t, err)
})
t.Run("missing client ID", func(t *testing.T) {
opts := AzureOIDCConfigurationCreateOptions{
SubscriptionID: "your-azure-subscription-id",
TenantID: "your-azure-tenant-id",
}
_, err := client.AzureOIDCConfigurations.Create(ctx, orgTest.Name, opts)
assert.ErrorIs(t, err, ErrRequiredClientID)
})
t.Run("missing subscription ID", func(t *testing.T) {
opts := AzureOIDCConfigurationCreateOptions{
ClientID: "your-azure-client-id",
TenantID: "your-azure-tenant-id",
}
_, err := client.AzureOIDCConfigurations.Create(ctx, orgTest.Name, opts)
assert.ErrorIs(t, err, ErrRequiredSubscriptionID)
})
t.Run("missing tenant ID", func(t *testing.T) {
opts := AzureOIDCConfigurationCreateOptions{
ClientID: "your-azure-client-id",
SubscriptionID: "your-azure-subscription-id",
}
_, err := client.AzureOIDCConfigurations.Create(ctx, orgTest.Name, opts)
assert.ErrorIs(t, err, ErrRequiredTenantID)
})
}
func TestAzureOIDCConfigurationRead(t *testing.T) {
t.Parallel()
skipHYOKIntegrationTests(t)
client := testClient(t)
ctx := context.Background()
orgTest := testHyokOrganization(t, client)
oidcConfig, oidcConfigCleanup := createAzureOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcConfigCleanup)
t.Run("fetch existing configuration", func(t *testing.T) {
fetched, err := client.AzureOIDCConfigurations.Read(ctx, oidcConfig.ID)
require.NoError(t, err)
require.NotEmpty(t, fetched)
})
t.Run("fetching non-existing configuration", func(t *testing.T) {
_, err := client.AzureOIDCConfigurations.Read(ctx, "azoidc-notreal")
assert.ErrorIs(t, err, ErrResourceNotFound)
})
}
func TestAzureOIDCConfigurationUpdate(t *testing.T) {
t.Parallel()
skipHYOKIntegrationTests(t)
client := testClient(t)
ctx := context.Background()
orgTest := testHyokOrganization(t, client)
t.Run("update all fields", func(t *testing.T) {
oidcConfig, oidcConfigCleanup := createAzureOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcConfigCleanup)
clientID := "your-azure-client-id"
subscriptionID := "your-azure-subscription-id"
tenantID := "your-azure-tenant-id"
opts := AzureOIDCConfigurationUpdateOptions{
ClientID: &clientID,
SubscriptionID: &subscriptionID,
TenantID: &tenantID,
}
updated, err := client.AzureOIDCConfigurations.Update(ctx, oidcConfig.ID, opts)
require.NoError(t, err)
require.NotEmpty(t, updated)
assert.Equal(t, *opts.ClientID, updated.ClientID)
assert.Equal(t, *opts.SubscriptionID, updated.SubscriptionID)
assert.Equal(t, *opts.TenantID, updated.TenantID)
})
t.Run("client ID not provided", func(t *testing.T) {
oidcConfig, oidcConfigCleanup := createAzureOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcConfigCleanup)
subscriptionID := "your-azure-subscription-id"
tenantID := "your-azure-tenant-id"
opts := AzureOIDCConfigurationUpdateOptions{
SubscriptionID: &subscriptionID,
TenantID: &tenantID,
}
updated, err := client.AzureOIDCConfigurations.Update(ctx, oidcConfig.ID, opts)
require.NoError(t, err)
require.NotEmpty(t, updated)
assert.Equal(t, oidcConfig.ClientID, updated.ClientID) // not updated
assert.Equal(t, *opts.SubscriptionID, updated.SubscriptionID)
assert.Equal(t, *opts.TenantID, updated.TenantID)
})
t.Run("subscription ID not provided", func(t *testing.T) {
oidcConfig, oidcConfigCleanup := createAzureOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcConfigCleanup)
clientID := "your-azure-client-id"
tenantID := "your-azure-tenant-id"
opts := AzureOIDCConfigurationUpdateOptions{
ClientID: &clientID,
TenantID: &tenantID,
}
updated, err := client.AzureOIDCConfigurations.Update(ctx, oidcConfig.ID, opts)
require.NoError(t, err)
require.NotEmpty(t, updated)
assert.Equal(t, *opts.ClientID, updated.ClientID)
assert.Equal(t, oidcConfig.SubscriptionID, updated.SubscriptionID) // not updated
assert.Equal(t, *opts.TenantID, updated.TenantID)
})
t.Run("tenant ID not provided", func(t *testing.T) {
oidcConfig, oidcConfigCleanup := createAzureOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcConfigCleanup)
clientID := "your-azure-client-id"
subscriptionID := "your-azure-subscription-id"
opts := AzureOIDCConfigurationUpdateOptions{
ClientID: &clientID,
SubscriptionID: &subscriptionID,
}
updated, err := client.AzureOIDCConfigurations.Update(ctx, oidcConfig.ID, opts)
require.NoError(t, err)
require.NotEmpty(t, updated)
assert.Equal(t, *opts.ClientID, updated.ClientID)
assert.Equal(t, *opts.SubscriptionID, updated.SubscriptionID)
assert.Equal(t, oidcConfig.TenantID, updated.TenantID) // not updated
})
}
================================================
FILE: comment.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ Comments = (*comments)(nil)
// Comments describes all the comment related methods that the
// Terraform Enterprise API supports.
//
// TFE API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/comments
type Comments interface {
// List all comments of the given run.
List(ctx context.Context, runID string) (*CommentList, error)
// Read a comment by its ID.
Read(ctx context.Context, commentID string) (*Comment, error)
// Create a new comment with the given options.
Create(ctx context.Context, runID string, options CommentCreateOptions) (*Comment, error)
}
// Comments implements Comments.
type comments struct {
client *Client
}
// CommentList represents a list of comments.
type CommentList struct {
*Pagination
Items []*Comment
}
// Comment represents a Terraform Enterprise comment.
type Comment struct {
ID string `jsonapi:"primary,comments"`
Body string `jsonapi:"attr,body"`
}
type CommentCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,comments"`
// Required: Body of the comment.
Body string `jsonapi:"attr,body"`
}
// List all comments of the given run.
func (s *comments) List(ctx context.Context, runID string) (*CommentList, error) {
if !validStringID(&runID) {
return nil, ErrInvalidRunID
}
u := fmt.Sprintf("runs/%s/comments", url.PathEscape(runID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
cl := &CommentList{}
err = req.Do(ctx, cl)
if err != nil {
return nil, err
}
return cl, nil
}
// Create a new comment with the given options.
func (s *comments) Create(ctx context.Context, runID string, options CommentCreateOptions) (*Comment, error) {
if err := options.valid(); err != nil {
return nil, err
}
if !validStringID(&runID) {
return nil, ErrInvalidRunID
}
u := fmt.Sprintf("runs/%s/comments", url.PathEscape(runID))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
comm := &Comment{}
err = req.Do(ctx, comm)
if err != nil {
return nil, err
}
return comm, err
}
// Read a comment by its ID.
func (s *comments) Read(ctx context.Context, commentID string) (*Comment, error) {
if !validStringID(&commentID) {
return nil, ErrInvalidCommentID
}
u := fmt.Sprintf("comments/%s", url.PathEscape(commentID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
comm := &Comment{}
err = req.Do(ctx, comm)
if err != nil {
return nil, err
}
return comm, nil
}
func (o CommentCreateOptions) valid() error {
if !validString(&o.Body) {
return ErrInvalidCommentBody
}
return nil
}
================================================
FILE: comment_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCommentsList_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wTest1, wTest1Cleanup := createWorkspace(t, client, orgTest)
defer wTest1Cleanup()
rTest, rTest1Cleanup := createRun(t, client, wTest1)
defer rTest1Cleanup()
commentBody1 := "1st comment test"
commentBody2 := "2nd comment test"
t.Run("without comments", func(t *testing.T) {
_, err := client.Comments.List(ctx, rTest.ID)
require.NoError(t, err)
})
t.Run("without a valid run", func(t *testing.T) {
cl, err := client.Comments.List(ctx, badIdentifier)
assert.Nil(t, cl)
assert.EqualError(t, err, ErrInvalidRunID.Error())
})
t.Run("create a comment", func(t *testing.T) {
options := CommentCreateOptions{
Body: commentBody1,
}
cl, err := client.Comments.Create(ctx, rTest.ID, options)
require.NoError(t, err)
assert.Equal(t, commentBody1, cl.Body)
})
t.Run("create 2nd comment", func(t *testing.T) {
options := CommentCreateOptions{
Body: commentBody2,
}
cl, err := client.Comments.Create(ctx, rTest.ID, options)
require.NoError(t, err)
assert.Equal(t, commentBody2, cl.Body)
})
t.Run("list comments", func(t *testing.T) {
commentsList, err := client.Comments.List(ctx, rTest.ID)
require.NoError(t, err)
assert.Len(t, commentsList.Items, 2)
assert.Equal(t, true, commentItemsContainsBody(commentsList.Items, commentBody1))
assert.Equal(t, true, commentItemsContainsBody(commentsList.Items, commentBody2))
})
}
func commentItemsContainsBody(items []*Comment, body string) bool {
hasBody := false
for _, item := range items {
if item.Body == body {
hasBody = true
break
}
}
return hasBody
}
================================================
FILE: configuration_version.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"fmt"
"io"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ ConfigurationVersions = (*configurationVersions)(nil)
// ConfigurationVersions describes all the configuration version related
// methods that the Terraform Enterprise API supports.
//
// TFE API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/configuration-versions
type ConfigurationVersions interface {
// List returns all configuration versions of a workspace.
List(ctx context.Context, workspaceID string, options *ConfigurationVersionListOptions) (*ConfigurationVersionList, error)
// Create is used to create a new configuration version. The created
// configuration version will be usable once data is uploaded to it.
Create(ctx context.Context, workspaceID string, options ConfigurationVersionCreateOptions) (*ConfigurationVersion, error)
// CreateForRegistryModule is used to create a new configuration version
// keyed to a registry module instead of a workspace. The created
// configuration version will be usable once data is uploaded to it.
//
// **Note: This function is still in BETA and subject to change.**
CreateForRegistryModule(ctx context.Context, moduleID RegistryModuleID) (*ConfigurationVersion, error)
// Read a configuration version by its ID.
Read(ctx context.Context, cvID string) (*ConfigurationVersion, error)
// ReadWithOptions reads a configuration version by its ID using the options supplied
ReadWithOptions(ctx context.Context, cvID string, options *ConfigurationVersionReadOptions) (*ConfigurationVersion, error)
// Upload packages and uploads Terraform configuration files. It requires
// the upload URL from a configuration version and the full path to the
// configuration files on disk.
Upload(ctx context.Context, url string, path string) error
// Upload a tar gzip archive to the specified configuration version upload URL.
UploadTarGzip(ctx context.Context, url string, archive io.Reader) error
// Archive a configuration version. This can only be done on configuration versions that
// were created with the API or CLI, are in an uploaded state, and have no runs in progress.
Archive(ctx context.Context, cvID string) error
// Download a configuration version. Only configuration versions in the uploaded state may be downloaded.
Download(ctx context.Context, cvID string) ([]byte, error)
// SoftDeleteBackingData soft deletes the configuration version's backing data
// **Note: This functionality is only available in Terraform Enterprise.**
SoftDeleteBackingData(ctx context.Context, svID string) error
// RestoreBackingData restores a soft deleted configuration version's backing data
// **Note: This functionality is only available in Terraform Enterprise.**
RestoreBackingData(ctx context.Context, svID string) error
// PermanentlyDeleteBackingData permanently deletes a soft deleted configuration version's backing data
// **Note: This functionality is only available in Terraform Enterprise.**
PermanentlyDeleteBackingData(ctx context.Context, svID string) error
}
// configurationVersions implements ConfigurationVersions.
type configurationVersions struct {
client *Client
}
// ConfigurationStatus represents a configuration version status.
type ConfigurationStatus string
// List all available configuration version statuses.
const (
ConfigurationArchived ConfigurationStatus = "archived"
ConfigurationErrored ConfigurationStatus = "errored"
ConfigurationFetching ConfigurationStatus = "fetching"
ConfigurationPending ConfigurationStatus = "pending"
ConfigurationUploaded ConfigurationStatus = "uploaded"
)
// ConfigurationSource represents a source of a configuration version.
type ConfigurationSource string
// List all available configuration version sources.
const (
ConfigurationSourceAPI ConfigurationSource = "tfe-api"
ConfigurationSourceBitbucket ConfigurationSource = "bitbucket"
ConfigurationSourceGithub ConfigurationSource = "github"
ConfigurationSourceGitlab ConfigurationSource = "gitlab"
ConfigurationSourceAdo ConfigurationSource = "ado"
ConfigurationSourceTerraform ConfigurationSource = "terraform"
)
// ConfigurationVersionList represents a list of configuration versions.
type ConfigurationVersionList struct {
*Pagination
Items []*ConfigurationVersion
}
// ConfigurationVersion is a representation of an uploaded or ingressed
// Terraform configuration in TFE. A workspace must have at least one
// configuration version before any runs may be queued on it.
type ConfigurationVersion struct {
ID string `jsonapi:"primary,configuration-versions"`
AutoQueueRuns bool `jsonapi:"attr,auto-queue-runs"`
Error string `jsonapi:"attr,error"`
ErrorMessage string `jsonapi:"attr,error-message"`
Source ConfigurationSource `jsonapi:"attr,source"`
Speculative bool `jsonapi:"attr,speculative"`
Provisional bool `jsonapi:"attr,provisional"`
Status ConfigurationStatus `jsonapi:"attr,status"`
StatusTimestamps *CVStatusTimestamps `jsonapi:"attr,status-timestamps"`
UploadURL string `jsonapi:"attr,upload-url"`
// Relations
IngressAttributes *IngressAttributes `jsonapi:"relation,ingress-attributes"`
}
// CVStatusTimestamps holds the timestamps for individual configuration version
// statuses.
type CVStatusTimestamps struct {
ArchivedAt time.Time `jsonapi:"attr,archived-at,rfc3339"`
FetchingAt time.Time `jsonapi:"attr,fetching-at,rfc3339"`
FinishedAt time.Time `jsonapi:"attr,finished-at,rfc3339"`
QueuedAt time.Time `jsonapi:"attr,queued-at,rfc3339"`
StartedAt time.Time `jsonapi:"attr,started-at,rfc3339"`
}
// ConfigVerIncludeOpt represents the available options for include query params.
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/configuration-versions#available-related-resources
type ConfigVerIncludeOpt string
const (
ConfigVerIngressAttributes ConfigVerIncludeOpt = "ingress_attributes"
ConfigVerRun ConfigVerIncludeOpt = "run"
)
// ConfigurationVersionReadOptions represents the options for reading a configuration version.
type ConfigurationVersionReadOptions struct {
// Optional: A list of relations to include. See available resources:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/configuration-versions#available-related-resources
Include []ConfigVerIncludeOpt `url:"include,omitempty"`
}
// ConfigurationVersionListOptions represents the options for listing
// configuration versions.
type ConfigurationVersionListOptions struct {
ListOptions
// Optional: A list of relations to include. See available resources:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/configuration-versions#available-related-resources
Include []ConfigVerIncludeOpt `url:"include,omitempty"`
}
// ConfigurationVersionCreateOptions represents the options for creating a
// configuration version.
type ConfigurationVersionCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,configuration-versions"`
// Optional: When true, runs are queued automatically when the configuration version
// is uploaded.
AutoQueueRuns *bool `jsonapi:"attr,auto-queue-runs,omitempty"`
// Optional: When true, this configuration version can only be used for planning.
Speculative *bool `jsonapi:"attr,speculative,omitempty"`
// Optional: When true, does not become the workspace's current configuration until
// a run referencing it is ultimately applied.
Provisional *bool `jsonapi:"attr,provisional,omitempty"`
}
// IngressAttributes include commit information associated with configuration versions sourced from VCS.
type IngressAttributes struct {
ID string `jsonapi:"primary,ingress-attributes"`
Branch string `jsonapi:"attr,branch"`
CloneURL string `jsonapi:"attr,clone-url"`
CommitMessage string `jsonapi:"attr,commit-message"`
CommitSHA string `jsonapi:"attr,commit-sha"`
CommitURL string `jsonapi:"attr,commit-url"`
CompareURL string `jsonapi:"attr,compare-url"`
Identifier string `jsonapi:"attr,identifier"`
IsPullRequest bool `jsonapi:"attr,is-pull-request"`
OnDefaultBranch bool `jsonapi:"attr,on-default-branch"`
PullRequestNumber int `jsonapi:"attr,pull-request-number"`
PullRequestURL string `jsonapi:"attr,pull-request-url"`
PullRequestTitle string `jsonapi:"attr,pull-request-title"`
PullRequestBody string `jsonapi:"attr,pull-request-body"`
Tag string `jsonapi:"attr,tag"`
SenderUsername string `jsonapi:"attr,sender-username"`
SenderAvatarURL string `jsonapi:"attr,sender-avatar-url"`
SenderHTMLURL string `jsonapi:"attr,sender-html-url"`
// Links
Links map[string]interface{} `jsonapi:"links,omitempty"`
}
// List returns all configuration versions of a workspace.
func (s *configurationVersions) List(ctx context.Context, workspaceID string, options *ConfigurationVersionListOptions) (*ConfigurationVersionList, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("workspaces/%s/configuration-versions", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
cvl := &ConfigurationVersionList{}
err = req.Do(ctx, cvl)
if err != nil {
return nil, err
}
return cvl, nil
}
// Create is used to create a new configuration version. The created
// configuration version will be usable once data is uploaded to it.
func (s *configurationVersions) Create(ctx context.Context, workspaceID string, options ConfigurationVersionCreateOptions) (*ConfigurationVersion, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
u := fmt.Sprintf("workspaces/%s/configuration-versions", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
cv := &ConfigurationVersion{}
err = req.Do(ctx, cv)
if err != nil {
return nil, err
}
return cv, nil
}
func (s *configurationVersions) CreateForRegistryModule(ctx context.Context, moduleID RegistryModuleID) (*ConfigurationVersion, error) {
if err := moduleID.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("%s/configuration-versions", testRunsPath(moduleID))
req, err := s.client.NewRequest("POST", u, nil)
if err != nil {
return nil, err
}
cv := &ConfigurationVersion{}
err = req.Do(ctx, cv)
if err != nil {
return nil, err
}
return cv, nil
}
// Read a configuration version by its ID.
func (s *configurationVersions) Read(ctx context.Context, cvID string) (*ConfigurationVersion, error) {
return s.ReadWithOptions(ctx, cvID, nil)
}
// Read a configuration version by its ID with the given options.
func (s *configurationVersions) ReadWithOptions(ctx context.Context, cvID string, options *ConfigurationVersionReadOptions) (*ConfigurationVersion, error) {
if !validStringID(&cvID) {
return nil, ErrInvalidConfigVersionID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("configuration-versions/%s", url.PathEscape(cvID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
cv := &ConfigurationVersion{}
err = req.Do(ctx, cv)
if err != nil {
return nil, err
}
return cv, nil
}
// Upload packages and uploads Terraform configuration files. It requires the
// upload URL from a configuration version and the path to the configuration
// files on disk.
func (s *configurationVersions) Upload(ctx context.Context, uploadURL, path string) error {
body, err := packContents(path)
if err != nil {
return err
}
return s.UploadTarGzip(ctx, uploadURL, body)
}
// UploadTarGzip is used to upload Terraform configuration files contained a tar gzip archive.
// Any stream implementing io.Reader can be passed into this method. This method is also
// particularly useful for tar streams created by non-default go-slug configurations.
//
// **Note**: This method does not validate the content being uploaded and is therefore the caller's
// responsibility to ensure the raw content is a valid Terraform configuration.
func (s *configurationVersions) UploadTarGzip(ctx context.Context, uploadURL string, archive io.Reader) error {
return s.client.doForeignPUTRequest(ctx, uploadURL, archive)
}
// Archive a configuration version. This can only be done on configuration versions that
// were created with the API or CLI, are in an uploaded state, and have no runs in progress.
func (s *configurationVersions) Archive(ctx context.Context, cvID string) error {
if !validStringID(&cvID) {
return ErrInvalidConfigVersionID
}
body := bytes.NewBuffer(nil)
u := fmt.Sprintf("configuration-versions/%s/actions/archive", url.PathEscape(cvID))
req, err := s.client.NewRequest("POST", u, body)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o *ConfigurationVersionReadOptions) valid() error {
return nil
}
func (o *ConfigurationVersionListOptions) valid() error {
return nil
}
// Download a configuration version. Only configuration versions in the uploaded state may be downloaded.
func (s *configurationVersions) Download(ctx context.Context, cvID string) ([]byte, error) {
if !validStringID(&cvID) {
return nil, ErrInvalidConfigVersionID
}
u := fmt.Sprintf("configuration-versions/%s/download", url.PathEscape(cvID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
var buf bytes.Buffer
err = req.Do(ctx, &buf)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (s *configurationVersions) SoftDeleteBackingData(ctx context.Context, cvID string) error {
return s.manageBackingData(ctx, cvID, "soft_delete_backing_data")
}
func (s *configurationVersions) RestoreBackingData(ctx context.Context, cvID string) error {
return s.manageBackingData(ctx, cvID, "restore_backing_data")
}
func (s *configurationVersions) PermanentlyDeleteBackingData(ctx context.Context, cvID string) error {
return s.manageBackingData(ctx, cvID, "permanently_delete_backing_data")
}
func (s *configurationVersions) manageBackingData(ctx context.Context, cvID, action string) error {
if !validStringID(&cvID) {
return ErrInvalidConfigVersionID
}
u := fmt.Sprintf("configuration-versions/%s/actions/%s", cvID, action)
req, err := s.client.NewRequest("POST", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
================================================
FILE: configuration_version_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"encoding/json"
"errors"
"os"
"testing"
"time"
slug "github.com/hashicorp/go-slug"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestConfigurationVersionsList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
wTest, wTestCleanup := createWorkspace(t, client, nil)
defer wTestCleanup()
cvTest1, cvTest1Cleanup := createConfigurationVersion(t, client, wTest)
defer cvTest1Cleanup()
cvTest2, cvTest2Cleanup := createConfigurationVersion(t, client, wTest)
defer cvTest2Cleanup()
t.Run("without list options", func(t *testing.T) {
cvl, err := client.ConfigurationVersions.List(ctx, wTest.ID, nil)
require.NoError(t, err)
// We need to strip the upload URL as that is a dynamic link.
cvTest1.UploadURL = ""
cvTest2.UploadURL = ""
// And for the retrieved configuration versions as well.
for _, cv := range cvl.Items {
cv.UploadURL = ""
}
assert.Contains(t, cvl.Items, cvTest1)
assert.Contains(t, cvl.Items, cvTest2)
assert.Equal(t, 1, cvl.CurrentPage)
assert.Equal(t, 2, cvl.TotalCount)
})
t.Run("with list options", func(t *testing.T) {
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
options := &ConfigurationVersionListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
}
cvl, err := client.ConfigurationVersions.List(ctx, wTest.ID, options)
require.NoError(t, err)
assert.Empty(t, cvl.Items)
assert.Equal(t, 999, cvl.CurrentPage)
assert.Equal(t, 2, cvl.TotalCount)
})
t.Run("without a valid organization", func(t *testing.T) {
cvl, err := client.ConfigurationVersions.List(ctx, badIdentifier, nil)
assert.Nil(t, cvl)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
}
func TestConfigurationVersionsCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
wTest, wTestCleanup := createWorkspace(t, client, nil)
defer wTestCleanup()
t.Run("with valid options", func(t *testing.T) {
cv, err := client.ConfigurationVersions.Create(ctx,
wTest.ID,
ConfigurationVersionCreateOptions{},
)
assert.NotEmpty(t, cv.UploadURL)
require.NoError(t, err)
// Get a refreshed view of the configuration version.
refreshed, err := client.ConfigurationVersions.Read(ctx, cv.ID)
require.NoError(t, err)
assert.Empty(t, refreshed.UploadURL)
for _, item := range []*ConfigurationVersion{
cv,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Empty(t, item.Error)
assert.Equal(t, item.Source, ConfigurationSourceAPI)
assert.Equal(t, item.Status, ConfigurationPending)
}
})
t.Run("with invalid workspace id", func(t *testing.T) {
cv, err := client.ConfigurationVersions.Create(
ctx,
badIdentifier,
ConfigurationVersionCreateOptions{},
)
assert.Nil(t, cv)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
t.Run("provisional", func(t *testing.T) {
cv, err := client.ConfigurationVersions.Create(ctx,
wTest.ID,
ConfigurationVersionCreateOptions{
Provisional: Bool(true),
},
)
require.NoError(t, err)
assert.True(t, cv.Provisional)
ws, err := client.Workspaces.ReadByID(ctx, wTest.ID)
require.NoError(t, err)
// Depends on "with valid options"
require.NotNil(t, ws.CurrentConfigurationVersion)
// Provisional configuration version is not the current one
assert.NotEqual(t, ws.CurrentConfigurationVersion.ID, cv.ID)
})
}
func TestConfigurationVersionsCreateForRegistryModule(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
rmTest, rmTestCleanup := createRegistryModule(t, client, orgTest, PrivateRegistry)
defer rmTestCleanup()
id := RegistryModuleID{
Organization: rmTest.Organization.Name,
Name: rmTest.Name,
Provider: rmTest.Provider,
Namespace: rmTest.Namespace,
RegistryName: rmTest.RegistryName,
}
t.Run("with valid options", func(t *testing.T) {
cv, err := client.ConfigurationVersions.CreateForRegistryModule(ctx, id)
assert.NotEmpty(t, cv.UploadURL)
require.NoError(t, err)
// Get a refreshed view of the configuration version.
refreshed, err := client.ConfigurationVersions.Read(ctx, cv.ID)
require.NoError(t, err)
assert.Empty(t, refreshed.UploadURL)
for _, item := range []*ConfigurationVersion{
cv,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Empty(t, item.Error)
assert.Equal(t, item.Source, ConfigurationSourceAPI)
assert.Equal(t, item.Status, ConfigurationPending)
}
})
t.Run("with invalid workspace id", func(t *testing.T) {
cv, err := client.ConfigurationVersions.CreateForRegistryModule(
ctx,
RegistryModuleID{},
)
assert.Nil(t, cv)
assert.Equal(t, ErrRequiredName, err)
})
}
func TestConfigurationVersionsRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
cvTest, cvTestCleanup := createConfigurationVersion(t, client, nil)
defer cvTestCleanup()
t.Run("when the configuration version exists", func(t *testing.T) {
cv, err := client.ConfigurationVersions.Read(ctx, cvTest.ID)
require.NoError(t, err)
// Don't compare the UploadURL because it will be generated twice in
// this test - once at creation of the configuration version, and
// again during the GET.
cvTest.UploadURL, cv.UploadURL = "", ""
assert.Equal(t, cvTest, cv)
})
t.Run("when the configuration version does not exist", func(t *testing.T) {
cv, err := client.ConfigurationVersions.Read(ctx, "nonexisting")
assert.Nil(t, cv)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("with invalid configuration version id", func(t *testing.T) {
cv, err := client.ConfigurationVersions.Read(ctx, badIdentifier)
assert.Nil(t, cv)
assert.EqualError(t, err, ErrInvalidConfigVersionID.Error())
})
}
func TestConfigurationVersionsReadWithOptions(t *testing.T) {
t.Parallel()
t.Skip("Skipping due to persistent failures - see TF-31172")
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wTest, wTestCleanup := createWorkspaceWithVCS(t, client, orgTest, WorkspaceCreateOptions{QueueAllRuns: Bool(true)})
defer wTestCleanup()
w, err := retry(func() (interface{}, error) {
w, err := client.Workspaces.ReadByIDWithOptions(ctx, wTest.ID, &WorkspaceReadOptions{
Include: []WSIncludeOpt{WSCurrentRunConfigVer},
})
if err != nil {
return nil, err
}
if w.CurrentRun == nil {
return nil, errors.New("A run was expected to be found on this workspace as a test pre-condition")
}
return w, nil
})
require.NoError(t, err)
ws, ok := w.(*Workspace)
require.True(t, ok, "Expected Workspace, got %T", w)
cv := ws.CurrentRun.ConfigurationVersion
t.Run("when the configuration version exists", func(t *testing.T) {
options := &ConfigurationVersionReadOptions{
Include: []ConfigVerIncludeOpt{ConfigVerIngressAttributes},
}
cv, err := client.ConfigurationVersions.ReadWithOptions(ctx, cv.ID, options)
require.NoError(t, err)
require.NotNil(t, cv.IngressAttributes)
assert.NotZero(t, cv.IngressAttributes.CommitURL)
assert.NotZero(t, cv.IngressAttributes.CommitSHA)
})
}
func TestConfigurationVersionsUpload(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
cv, cvCleanup := createConfigurationVersion(t, client, nil)
defer cvCleanup()
t.Run("with valid options", func(t *testing.T) {
err := client.ConfigurationVersions.Upload(
ctx,
cv.UploadURL,
"test-fixtures/config-version",
)
require.NoError(t, err)
WaitUntilStatus(t, client, cv, ConfigurationUploaded, 60)
})
t.Run("without a valid upload URL", func(t *testing.T) {
err := client.ConfigurationVersions.Upload(
ctx,
cv.UploadURL[:len(cv.UploadURL)-10]+"nonexisting",
"test-fixtures/config-version",
)
assert.Error(t, err)
})
t.Run("without a valid path", func(t *testing.T) {
err := client.ConfigurationVersions.Upload(
ctx,
cv.UploadURL,
"nonexisting",
)
assert.Error(t, err)
})
}
func TestConfigurationVersionsUploadTarGzip(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
cv, cvCleanup := createConfigurationVersion(t, client, nil)
t.Cleanup(cvCleanup)
t.Run("with custom go-slug", func(t *testing.T) {
packer, err := slug.NewPacker(
slug.DereferenceSymlinks(),
slug.ApplyTerraformIgnore(),
)
require.NoError(t, err)
body := bytes.NewBuffer(nil)
_, err = packer.Pack("test-fixtures/config-version", body)
require.NoError(t, err)
err = client.ConfigurationVersions.UploadTarGzip(ctx, cv.UploadURL, body)
require.NoError(t, err)
})
t.Run("with custom tar archive", func(t *testing.T) {
archivePath := "test-fixtures/config-archive.tar.gz"
createTarGzipArchive(t, []string{"test-fixtures/config-version/main.tf"}, archivePath)
archive, err := os.Open(archivePath)
require.NoError(t, err)
defer archive.Close()
err = client.ConfigurationVersions.UploadTarGzip(ctx, cv.UploadURL, archive)
require.NoError(t, err)
})
}
func TestConfigurationVersionsArchive(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
w, wCleanup := createWorkspace(t, client, nil)
defer wCleanup()
cv, cvCleanup := createConfigurationVersion(t, client, w)
defer cvCleanup()
t.Run("when the configuration version exists and has been uploaded", func(t *testing.T) {
err := client.ConfigurationVersions.Upload(
ctx,
cv.UploadURL,
"test-fixtures/config-version",
)
require.NoError(t, err)
WaitUntilStatus(t, client, cv, ConfigurationUploaded, 60)
// configuration version should not be archived, since it's the latest version
err = client.ConfigurationVersions.Archive(ctx, cv.ID)
assert.Error(t, err)
assert.ErrorContains(t, err, "transition not allowed")
assert.ErrorContains(t, err, "configuration could not be archived because it is current")
// create subsequent version, since the latest configuration version cannot be archived
newCv, newCvCleanup := createConfigurationVersion(t, client, w)
err = client.ConfigurationVersions.Upload(
ctx,
newCv.UploadURL,
"test-fixtures/config-version",
)
require.NoError(t, err)
defer newCvCleanup()
WaitUntilStatus(t, client, newCv, ConfigurationUploaded, 60)
err = client.ConfigurationVersions.Archive(ctx, cv.ID)
require.NoError(t, err)
WaitUntilStatus(t, client, cv, ConfigurationArchived, 60)
})
t.Run("when the configuration version does not exist", func(t *testing.T) {
err := client.ConfigurationVersions.Archive(ctx, "nonexisting")
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("with invalid configuration version id", func(t *testing.T) {
err := client.ConfigurationVersions.Archive(ctx, badIdentifier)
assert.EqualError(t, err, ErrInvalidConfigVersionID.Error())
})
}
func TestConfigurationVersionsDownload(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
t.Run("with a valid ID for downloadable configuration version", func(t *testing.T) {
uploadedCv, uploadedCvCleanup := createUploadedConfigurationVersion(t, client, nil)
defer uploadedCvCleanup()
expectedCvFile := bytes.NewBuffer(nil)
_, expectedCvFileErr := slug.Pack("test-fixtures/config-version", expectedCvFile, true)
if expectedCvFileErr != nil {
t.Fatal(expectedCvFileErr)
}
cvFile, err := client.ConfigurationVersions.Download(ctx, uploadedCv.ID)
assert.NotNil(t, cvFile)
require.NoError(t, err)
assert.True(t, bytes.Equal(cvFile, expectedCvFile.Bytes()), "Configuration version should match")
})
t.Run("with a valid ID for a non downloadable configuration version", func(t *testing.T) {
pendingCv, pendingCvCleanup := createConfigurationVersion(t, client, nil)
defer pendingCvCleanup()
cvFile, err := client.ConfigurationVersions.Download(ctx, pendingCv.ID)
assert.Nil(t, cvFile)
assert.EqualError(t, err, ErrResourceNotFound.Error())
})
t.Run("with an invalid ID", func(t *testing.T) {
cvFile, err := client.ConfigurationVersions.Download(ctx, "nonexistent")
assert.Nil(t, cvFile)
assert.EqualError(t, err, ErrResourceNotFound.Error())
})
}
func TestConfigurationVersions_Unmarshal(t *testing.T) {
t.Parallel()
data := map[string]interface{}{
"data": map[string]interface{}{
"type": "configuration-versions",
"id": "cv-ntv3HbhJqvFzamy7",
"attributes": map[string]interface{}{
"auto-queue-runs": true,
"error": "bad error",
"error-message": "message",
"source": ConfigurationSourceTerraform,
"status": ConfigurationUploaded,
"status-timestamps": map[string]string{
"finished-at": "2020-03-16T23:15:59+00:00",
"started-at": "2019-03-16T23:23:59+00:00",
},
"speculative": true,
"provisional": true,
},
},
}
byteData, err := json.Marshal(data)
require.NoError(t, err)
responseBody := bytes.NewReader(byteData)
cv := &ConfigurationVersion{}
err = unmarshalResponse(responseBody, cv)
require.NoError(t, err)
finishedParsedTime, err := time.Parse(time.RFC3339, "2020-03-16T23:15:59+00:00")
require.NoError(t, err)
startedParsedTime, err := time.Parse(time.RFC3339, "2019-03-16T23:23:59+00:00")
require.NoError(t, err)
assert.Equal(t, cv.ID, "cv-ntv3HbhJqvFzamy7")
assert.Equal(t, cv.AutoQueueRuns, true)
assert.Equal(t, cv.Error, "bad error")
assert.Equal(t, cv.ErrorMessage, "message")
assert.Equal(t, cv.Source, ConfigurationSourceTerraform)
assert.Equal(t, cv.Status, ConfigurationUploaded)
assert.Equal(t, cv.StatusTimestamps.FinishedAt, finishedParsedTime)
assert.Equal(t, cv.StatusTimestamps.StartedAt, startedParsedTime)
assert.Equal(t, cv.Provisional, true)
assert.Equal(t, cv.Speculative, true)
}
func TestConfigurationVersions_ManageBackingData(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
workspace, workspaceCleanup := createWorkspace(t, client, nil)
t.Cleanup(workspaceCleanup)
nonCurrentCv, uploadedCvCleanup := createUploadedConfigurationVersion(t, client, workspace)
defer uploadedCvCleanup()
_, uploadedCvCleanup = createUploadedConfigurationVersion(t, client, workspace)
defer uploadedCvCleanup()
t.Run("soft delete backing data", func(t *testing.T) {
err := client.ConfigurationVersions.SoftDeleteBackingData(ctx, nonCurrentCv.ID)
require.NoError(t, err)
_, err = client.ConfigurationVersions.Download(ctx, nonCurrentCv.ID)
assert.Equal(t, ErrResourceNotFound, err)
})
t.Run("restore backing data", func(t *testing.T) {
err := client.ConfigurationVersions.RestoreBackingData(ctx, nonCurrentCv.ID)
require.NoError(t, err)
_, err = client.ConfigurationVersions.Download(ctx, nonCurrentCv.ID)
require.NoError(t, err)
})
t.Run("permanently delete backing data", func(t *testing.T) {
err := client.ConfigurationVersions.SoftDeleteBackingData(ctx, nonCurrentCv.ID)
require.NoError(t, err)
err = client.ConfigurationVersions.PermanentlyDeleteBackingData(ctx, nonCurrentCv.ID)
require.NoError(t, err)
err = client.ConfigurationVersions.RestoreBackingData(ctx, nonCurrentCv.ID)
require.ErrorContainsf(t, err, "transition not allowed", "Restore backing data should fail")
_, err = client.ConfigurationVersions.Download(ctx, nonCurrentCv.ID)
assert.Equal(t, ErrResourceNotFound, err)
})
}
================================================
FILE: const.go
================================================
// Copyright IBM Corp. 2018, 2026
// SPDX-License-Identifier: MPL-2.0
package tfe
const (
// AuthenticationTokensPath is the API path for authentication tokens.
AuthenticationTokensPath = "authentication-tokens/%s"
// AdminSCIMTokensPath is the API path for admin SCIM tokens.
AdminSCIMTokensPath = "admin/scim-tokens"
// AdminSCIMGroupsPath is the API path for admin SCIM groups.
AdminSCIMGroupsPath = "admin/scim-groups"
)
================================================
FILE: cost_estimate.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"fmt"
"io"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ CostEstimates = (*costEstimates)(nil)
// CostEstimates describes all the costEstimate related methods that
// the Terraform Enterprise API supports.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/cost-estimates
type CostEstimates interface {
// Read a costEstimate by its ID.
Read(ctx context.Context, costEstimateID string) (*CostEstimate, error)
// Logs retrieves the logs of a costEstimate.
Logs(ctx context.Context, costEstimateID string) (io.Reader, error)
}
// costEstimates implements CostEstimates.
type costEstimates struct {
client *Client
}
// CostEstimateStatus represents a costEstimate state.
type CostEstimateStatus string
// List all available costEstimate statuses.
const (
CostEstimateCanceled CostEstimateStatus = "canceled"
CostEstimateErrored CostEstimateStatus = "errored"
CostEstimateFinished CostEstimateStatus = "finished"
CostEstimatePending CostEstimateStatus = "pending"
CostEstimateQueued CostEstimateStatus = "queued"
CostEstimateSkippedDueToTargeting CostEstimateStatus = "skipped_due_to_targeting"
)
// CostEstimate represents a Terraform Enterprise costEstimate.
type CostEstimate struct {
ID string `jsonapi:"primary,cost-estimates"`
DeltaMonthlyCost string `jsonapi:"attr,delta-monthly-cost"`
ErrorMessage string `jsonapi:"attr,error-message"`
MatchedResourcesCount int `jsonapi:"attr,matched-resources-count"`
PriorMonthlyCost string `jsonapi:"attr,prior-monthly-cost"`
ProposedMonthlyCost string `jsonapi:"attr,proposed-monthly-cost"`
ResourcesCount int `jsonapi:"attr,resources-count"`
Status CostEstimateStatus `jsonapi:"attr,status"`
StatusTimestamps *CostEstimateStatusTimestamps `jsonapi:"attr,status-timestamps"`
UnmatchedResourcesCount int `jsonapi:"attr,unmatched-resources-count"`
}
// CostEstimateStatusTimestamps holds the timestamps for individual costEstimate statuses.
type CostEstimateStatusTimestamps struct {
CanceledAt time.Time `jsonapi:"attr,canceled-at,rfc3339"`
ErroredAt time.Time `jsonapi:"attr,errored-at,rfc3339"`
FinishedAt time.Time `jsonapi:"attr,finished-at,rfc3339"`
PendingAt time.Time `jsonapi:"attr,pending-at,rfc3339"`
QueuedAt time.Time `jsonapi:"attr,queued-at,rfc3339"`
SkippedDueToTargetingAt time.Time `jsonapi:"attr,skipped-due-to-targeting-at,rfc3339"`
}
// Read a costEstimate by its ID.
func (s *costEstimates) Read(ctx context.Context, costEstimateID string) (*CostEstimate, error) {
if !validStringID(&costEstimateID) {
return nil, ErrInvalidCostEstimateID
}
u := fmt.Sprintf("cost-estimates/%s", url.PathEscape(costEstimateID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
ce := &CostEstimate{}
err = req.Do(ctx, ce)
if err != nil {
return nil, err
}
return ce, nil
}
// Logs retrieves the logs of a costEstimate.
func (s *costEstimates) Logs(ctx context.Context, costEstimateID string) (io.Reader, error) {
if !validStringID(&costEstimateID) {
return nil, ErrInvalidCostEstimateID
}
// Loop until the context is canceled or the cost estimate is finished
// running. The cost estimate logs are not streamed and so only available
// once the estimate is finished.
for {
// Get the costEstimate to make sure it exists.
ce, err := s.Read(ctx, costEstimateID)
if err != nil {
return nil, err
}
switch ce.Status {
case CostEstimateQueued:
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(1000 * time.Millisecond):
continue
}
}
u := fmt.Sprintf("cost-estimates/%s/output", url.PathEscape(costEstimateID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
logs := bytes.NewBuffer(nil)
err = req.Do(ctx, logs)
if err != nil {
return nil, err
}
return logs, nil
}
}
================================================
FILE: cost_estimate_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCostEstimatesRead_RunDependent(t *testing.T) {
skipIfEnterprise(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
// Enable cost estimation for the test organization.
orgTest, err := client.Organizations.Update(
ctx,
orgTest.Name,
OrganizationUpdateOptions{
CostEstimationEnabled: Bool(true),
},
)
require.NoError(t, err)
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
rTest, rTestCleanup := createCostEstimatedRun(t, client, wTest)
defer rTestCleanup()
t.Run("when the costEstimate exists", func(t *testing.T) {
ce, err := client.CostEstimates.Read(ctx, rTest.CostEstimate.ID)
require.NoError(t, err)
assert.Equal(t, ce.Status, CostEstimateFinished)
assert.NotEmpty(t, ce.StatusTimestamps)
})
t.Run("when the costEstimate does not exist", func(t *testing.T) {
ce, err := client.CostEstimates.Read(ctx, "nonexisting")
assert.Nil(t, ce)
assert.Equal(t, ErrResourceNotFound, err)
})
t.Run("with invalid costEstimate ID", func(t *testing.T) {
ce, err := client.CostEstimates.Read(ctx, badIdentifier)
assert.Nil(t, ce)
assert.EqualError(t, err, ErrInvalidCostEstimateID.Error())
})
}
func TestCostEsimate_Unmarshal(t *testing.T) {
t.Parallel()
data := map[string]interface{}{
"data": map[string]interface{}{
"type": "cost-estimates",
"id": "ce-ntv3HbhJqvFzamy7",
"attributes": map[string]interface{}{
"delta-monthly-cost": "100",
"error-message": "message",
"matched-resources-count": 1,
"prior-monthly-cost": "100",
"proposed-monthly-cost": "100",
"resources-count": 1,
"status": CostEstimateCanceled,
"status-timestamps": map[string]string{
"queued-at": "2020-03-16T23:15:59+00:00",
"errored-at": "2019-03-16T23:23:59+00:00",
},
},
},
}
byteData, err := json.Marshal(data)
require.NoError(t, err)
queuedParsedTime, err := time.Parse(time.RFC3339, "2020-03-16T23:15:59+00:00")
require.NoError(t, err)
erroredParsedTime, err := time.Parse(time.RFC3339, "2019-03-16T23:23:59+00:00")
require.NoError(t, err)
responseBody := bytes.NewReader(byteData)
ce := &CostEstimate{}
err = unmarshalResponse(responseBody, ce)
require.NoError(t, err)
assert.Equal(t, ce.ID, "ce-ntv3HbhJqvFzamy7")
assert.Equal(t, ce.DeltaMonthlyCost, "100")
assert.Equal(t, ce.ErrorMessage, "message")
assert.Equal(t, ce.MatchedResourcesCount, 1)
assert.Equal(t, ce.PriorMonthlyCost, "100")
assert.Equal(t, ce.ProposedMonthlyCost, "100")
assert.Equal(t, ce.ResourcesCount, 1)
assert.Equal(t, ce.Status, CostEstimateCanceled)
assert.Equal(t, ce.StatusTimestamps.QueuedAt, queuedParsedTime)
assert.Equal(t, ce.StatusTimestamps.ErroredAt, erroredParsedTime)
}
================================================
FILE: data_retention_policy.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import "regexp"
// DataRetentionPolicyChoice is a choice type struct that represents the possible types
// of a drp returned by a polymorphic relationship. If a value is available, exactly one field
// will be non-nil.
type DataRetentionPolicyChoice struct {
DataRetentionPolicy *DataRetentionPolicy
DataRetentionPolicyDeleteOlder *DataRetentionPolicyDeleteOlder
DataRetentionPolicyDontDelete *DataRetentionPolicyDontDelete
}
// Returns whether one of the choices is populated
func (d DataRetentionPolicyChoice) IsPopulated() bool {
return d.DataRetentionPolicy != nil ||
d.DataRetentionPolicyDeleteOlder != nil ||
d.DataRetentionPolicyDontDelete != nil
}
// Convert the DataRetentionPolicyChoice to the legacy DataRetentionPolicy struct
// Returns nil if the policy cannot be represented by a legacy DataRetentionPolicy
func (d *DataRetentionPolicyChoice) ConvertToLegacyStruct() *DataRetentionPolicy {
if d == nil {
return nil
}
if d.DataRetentionPolicy != nil {
// TFE v202311-1 and v202312-1 will return a deprecated DataRetentionPolicy in the DataRetentionPolicyChoice struct
return d.DataRetentionPolicy
} else if d.DataRetentionPolicyDeleteOlder != nil {
// DataRetentionPolicy was functionally replaced by DataRetentionPolicyDeleteOlder in TFE v202401
return &DataRetentionPolicy{
ID: d.DataRetentionPolicyDeleteOlder.ID,
DeleteOlderThanNDays: d.DataRetentionPolicyDeleteOlder.DeleteOlderThanNDays,
}
}
return nil
}
// DataRetentionPolicy describes the retention policy of deleting records older than the specified number of days.
//
// Deprecated: Use DataRetentionPolicyDeleteOlder instead. This is the original representation of a
// data retention policy, only present in TFE v202311-1 and v202312-1
type DataRetentionPolicy struct {
ID string `jsonapi:"primary,data-retention-policies"`
DeleteOlderThanNDays int `jsonapi:"attr,delete-older-than-n-days"`
}
// DataRetentionPolicySetOptions is the options for a creating a DataRetentionPolicy.
//
// Deprecated: Use DataRetentionPolicyDeleteOlder variations instead
type DataRetentionPolicySetOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,data-retention-policies"`
// DeleteOlderThanNDays is the number of days to retain records for.
DeleteOlderThanNDays int `jsonapi:"attr,delete-older-than-n-days"`
}
// DataRetentionPolicyDeleteOlder describes the retention policy of deleting records older than the specified number of days.
type DataRetentionPolicyDeleteOlder struct {
ID string `jsonapi:"primary,data-retention-policy-delete-olders"`
// DeleteOlderThanNDays is the number of days to retain records for.
DeleteOlderThanNDays int `jsonapi:"attr,delete-older-than-n-days"`
}
// DataRetentionPolicyDontDelete describes the retention policy of never deleting records.
type DataRetentionPolicyDontDelete struct {
ID string `jsonapi:"primary,data-retention-policy-dont-deletes"`
}
// DataRetentionPolicyDeleteOlderSetOptions describes the options for a creating a DataRetentionPolicyDeleteOlder.
type DataRetentionPolicyDeleteOlderSetOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,data-retention-policy-delete-olders"`
// DeleteOlderThanNDays is the number of days records will be retained for after their creation.
DeleteOlderThanNDays int `jsonapi:"attr,delete-older-than-n-days"`
}
// DataRetentionPolicyDontDeleteSetOptions describes the options for a creating a DataRetentionPolicyDontDelete.
type DataRetentionPolicyDontDeleteSetOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,data-retention-policy-dont-deletes"`
}
// error we get when trying to unmarshal a data retention policy from TFE v202401+ into the deprecated DataRetentionPolicy struct
var drpUnmarshalEr = regexp.MustCompile(`Trying to Unmarshal an object of type \".+\", but \"data-retention-policies\" does not match`)
================================================
FILE: docs/CONTRIBUTING.md
================================================
# Contributing to go-tfe
If you find an issue with this package, please create an issue in GitHub. If you'd like, we welcome any contributions. Fork this repository and submit a pull request.
## Adding new functionality or fixing relevant bugs
If you are adding a new endpoint, make sure to update the [coverage list in README.md](../README.md#API-Coverage) where we keep a list of the HCP Terraform APIs that this SDK supports.
If you are making relevant changes that is worth communicating to our users, please include a note about it in our CHANGELOG.md. You can include it as part of the PR where you are submitting your changes.
CHANGELOG.md should have the next minor version listed as `# v1.X.0 (Unreleased)` and any changes can go under there. But if you feel that your changes are better suited for a patch version (like a critical bug fix), you may list a new section for this version. You should repeat the same formatting style introduced by previous versions.
### Scoping pull requests that add new resources
There are instances where several new resources being added (i.e Workspace Run Tasks and Organization Run Tasks) are coalesced into one PR. In order to keep the review process as efficient and least error prone as possible, we ask that you please scope each PR to an individual resource even if the multiple resources you're adding share similarities. If joining multiple related PRs into one single PR makes more sense logistically, we'd ask that you organize your commit history by resource. A general convention for this repository is one commit for the implementation of the resource's methods, one for the integration test, and one for cleanup and housekeeping (e.g modifying the changelog/docs, generating mocks, etc).
**Note HashiCorp Employees Only:** When submitting a new set of endpoints please ensure that one of your respective team members approves the changes as well before merging.
## Linting
After opening a PR, our CI system will perform a series of code checks, one of which is linting. Linting is not strictly required for a change to be merged, but it helps smooth the review process and catch common mistakes early. If you'd like to run the linters manually, follow these steps:
1. Ensure you have [installed golangci-lint](https://golangci-lint.run/welcome/install/#local-installation)
2. Format your code by running `make fmt`
3. Run lint checks using `make lint`
## Writing Tests
The test suite contains many acceptance tests that are run against the latest version of Terraform Enterprise. You can read more about running the tests against your own Terraform Enterprise environment in [TESTS.md](TESTS.md). Our CI system (Github Actions) will not test your fork until a one-time approval takes place.
## Editor Settings
We've included VSCode settings to assist with configuring the go extension. For other editors that integrate with the [Go Language Server](https://github.com/golang/tools/tree/master/gopls), the main thing to do is to add the `integration` build tags so that the test files are found by the language server. See `.vscode/settings.json` for more details.
## Generating Mocks
Ensure you have installed the [mockgen](https://github.com/uber-go/mock) tool.
You'll need to generate mocks if an existing endpoint method is modified or a new method is added. To generate mocks, simply run `./generate_mocks.sh`.
If you're adding a new API resource to go-tfe, you'll need to add a new command to `generate_mocks.sh`. For example if someone creates `example_resource.go`, you'll add:
```
mockgen -source=example_resource.go -destination=mocks/example_resource_mocks.go -package=mocks
```
You can also use the Makefile target `mocks` to add the new command:
```
FILENAME=example_resource.go make mocks
```
## Adding API changes that are not generally available
In general, beta features should not be merged/released until generally available (GA). However, the maintainers recognize almost any reason to release beta features on a case-by-case basis. These could include: partial customer availability, software dependency, or any reason short of feature completeness.
Beta features, if released, should be clearly commented:
```
// **Note: This field is still in BETA and subject to change.**
ExampleNewField *bool `jsonapi:"attr,example-new-field,omitempty"`
```
When adding test cases, you can temporarily use the skipUnlessBeta() test helper to omit beta features from running in CI.
```
t.Run("with nested changes trigger", func (t *testing.T) {
skipUnlessBeta(t)
options := WorkspaceCreateOptions {
// rest of required fields here
ExampleNewField: Bool(true),
}
// the rest of your test logic here
})
```
**Note**: After your PR has been merged, and the feature either reaches general availability, you should remove the `skipUnlessBeta()` flag.
## Adding New Endpoints
### Scaffolding a Resource
When creating a new resource you can use the helper script `generate_resource` to quickly setup boilerplate code for adding a new set of endpoints related to that resource:
#### Running the script directly
```sh
cd ./scripts/generate_resource
go run . example_resource
```
#### Running the Makefile target `generate`
```sh
RESOURCE=example_resource make generate
```
### Guidelines for Adding New Endpoints
* An interface should cover one RESTful resource, which sometimes involves two or more endpoints.
* We require that each resource interface provides compile-time proof that it has been implemented.
* You'll need to add an integration test that covers each method of the resource's interface.
* Option structs serve as a proxy for either passing query params or request bodies:
- `ListOptions` and `ReadOptions` are values passed as query parameters.
- `CreateOptions` and `UpdateOptions` represent the request body.
* URL parameters should be defined as method parameters.
* Any resource specific errors must be defined in `errors.go`
Here is a more comprehensive example of what a resource looks like when implemented. The helper script `generate_resource` generates a subset of this example, focusing only on the core details that are required across all resources in go-tfe.
```go
package tfe
import (
"context"
"errors"
"fmt"
"net/url"
)
var ErrInvalidExampleID = errors.New("invalid value for example ID") // move this line to errors.go
// Compile-time proof of interface implementation
var _ ExampleResource = (*example)(nil)
// Example represents all the example methods in the context of an organization
// that the HCP Terraform and Terraform Enterprise API supports.
// If this API is in beta or pre-release state, include that warning here.
type ExampleResource interface {
// Create an example for an organization
Create(ctx context.Context, organization string, options ExampleCreateOptions) (*Example, error)
// List all examples for an organization
List(ctx context.Context, organization string, options *ExampleListOptions) (*ExampleList, error)
// Read an organization's example by ID
Read(ctx context.Context, exampleID string) (*Example, error)
// Read an organization's example by ID with given options
ReadWithOptions(ctx context.Context, exampleID string, options *ExampleReadOptions) (*Example, error)
// Update an example for an organization
Update(ctx context.Context, exampleID string, options ExampleUpdateOptions) (*Example, error)
// Delete an organization's example
Delete(ctx context.Context, exampleID string) error
}
// example implements Example
type example struct {
client *Client
}
// Example represents a HCP Terraform and Terraform Enterprise example resource
type Example struct {
ID string `jsonapi:"primary,examples"`
Name string `jsonapi:"attr,name"`
URL string `jsonapi:"attr,url"`
OptionalValue *string `jsonapi:"attr,optional-value,omitempty"`
Organization *Organization `jsonapi:"relation,organization"`
}
// ExampleCreateOptions represents the set of options for creating an example
type ExampleCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,examples"`
// Required: The name of the example
Name string `jsonapi:"attr,name"`
// Required: The URL to send in the example
URL string `jsonapi:"attr,url"`
// Optional: An optional value that is omitted if empty
OptionalValue *string `jsonapi:"attr,optional-value,omitempty"`
}
// ExampleIncludeOpt represents the available options for include query params.
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/examples#list-examples (replace this URL with the actual documentation URL)
type ExampleIncludeOpt string
const (
ExampleOrganization ExampleIncludeOpt = "organization"
ExampleRun ExampleIncludeOpt = "run"
)
// ExampleListOptions represents the set of options for listing examples
type ExampleListOptions struct {
ListOptions
// Optional: A list of relations to include with an example. See available resources:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/examples#list-examples (replace this URL with the actual documentation URL)
Include []ExampleIncludeOpt `url:"include,omitempty"`
}
// ExampleList represents a list of examples
type ExampleList struct {
*Pagination
Items []*Example
}
// ExampleReadOptions represents the set of options for reading an example
type ExampleReadOptions struct {
// Optional: A list of relations to include with an example. See available resources:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/examples#list-examples (replace this URL with the actual documentation URL)
Include []RunTaskIncludeOpt `url:"include,omitempty"`
}
// ExampleUpdateOptions represents the set of options for updating an organization's examples
type ExampleUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,examples"`
// Optional: The name of the example, defaults to previous value
Name *string `jsonapi:"attr,name,omitempty"`
// Optional: The URL to send a example payload, defaults to previous value
URL *string `jsonapi:"attr,url,omitempty"`
// Optional: An optional value
OptionalValue *string `jsonapi:"attr,optional-value,omitempty"`
}
// Create is used to create a new example for an organization
func (s *example) Create(ctx context.Context, organization string, options ExampleCreateOptions) (*Example, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("organizations/%s/tasks", url.PathEscape(organization))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
r := &Example{}
err = req.Do(ctx, r)
if err != nil {
return nil, err
}
return r, nil
}
// List all the examples for an organization
func (s *example) List(ctx context.Context, organization string, options *ExampleListOptions) (*ExampleList, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf("organizations/%s/examples", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
el := &ExampleList{}
err = req.Do(ctx, el)
if err != nil {
return nil, err
}
return el, nil
}
// Read is used to read an organization's example by ID
func (s *example) Read(ctx context.Context, exampleID string) (*Example, error) {
return s.ReadWithOptions(ctx, exampleID, nil)
}
// Read is used to read an organization's example by ID with options
func (s *example) ReadWithOptions(ctx context.Context, exampleID string, options *ExampleReadOptions) (*Example, error) {
if !validStringID(&exampleID) {
return nil, ErrInvalidExampleID
}
u := fmt.Sprintf("examples/%s", url.PathEscape(exampleID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
e := &Example{}
err = req.Do(ctx, e)
if err != nil {
return nil, err
}
return e, nil
}
// Update an existing example for an organization by ID
func (s *example) Update(ctx context.Context, exampleID string, options ExampleUpdateOptions) (*Example, error) {
if !validStringID(&exampleID) {
return nil, ErrInvalidExampleID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("examples/%s", url.PathEscape(exampleID))
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
r := &Example{}
err = req.Do(ctx, r)
if err != nil {
return nil, err
}
return r, nil
}
// Delete an existing example for an organization by ID
func (s *example) Delete(ctx context.Context, exampleID string) error {
if !validStringID(&exampleID) {
return ErrInvalidExampleID
}
u := fmt.Sprintf("examples/%s", exampleID)
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o *ExampleUpdateOptions) valid() error {
if o.Name != nil && !validString(o.Name) {
return ErrRequiredName
}
if o.URL != nil && !validString(o.URL) {
return ErrInvalidRunTaskURL
}
return nil
}
func (o *ExampleCreateOptions) valid() error {
if !validString(&o.Name) {
return ErrRequiredName
}
if !validString(&o.URL) {
return ErrInvalidRunTaskURL
}
return nil
}
```
## Rebasing a fork to trigger CI (Maintainers Only)
Pull requests that originate from a fork will not have access to this repository's secrets, thus resulting in the inability to test against our CI instance. In order to trigger the CI action workflow, there is a handy script `./scripts/rebase-fork.sh` that automates the steps for you. It will:
* Checkout the fork PR locally onto your machine and create a new branch prefixed as follows: `local/{name_of_fork_branch}`
* Push your newly created branch to Github, appending an empty commit stating the original branch that was rebased.
* Copy the contents of the fork's pull request (title and description) and create a new pull request, triggering the CI workflow.
**Important**: This script does not handle subsequent commits to the original PR and would require you to rebase them manually. Therefore, it is important that authors include test results in their description and changes are approved before this script is executed.
This script depends on `gh` and `jq`. It also requires you to `gh auth login`, providing a SSO-authorized personal access token with the following scopes enabled:
- repo
- read:org
- read:discussion
### Example Usage
```sh
./scripts/rebase-fork.sh 557
```
================================================
FILE: docs/RELEASES.md
================================================
## Release Process
go-tfe can be released as often as required. Documentation updates and test fixes that only touch test files don't require a release or tag. You can just merge these changes into `main` once they have been approved.
### Preparing a release
Start by comparing the main branch with the last release in order to fully understand which changes are being released. Compare the last release tag with main ([example](https://github.com/hashicorp/go-tfe/compare/v1.5.0...main)). For each meaningful change, double check the following:
1. Is the change added to CHANGELOG.md?
2. Does the public package API follow all endpoint conventions, such as naming, pointer usage, and options availability? Once these are released, they are permanent in the current major release version.
3. Are new features generally available in the HCP Terraform API? Or is there another considered reason to release them?
Steps to prepare the changelog for a new release:
1. Replace `# Unreleased` with the version you are releasing.
2. Ensure there is a line with `# Unreleased` at the top of the changelog for future changes. Ideally we don't ask authors to add this line; this will make it clear where they should add their changelog entry.
3. Ensure that each existing changelog entry for the new release has the author(s) attributed and a pull request linked, i.e `- Some new feature/bugfix by @some-github-user (#3)[link-to-pull-request]`
4. Open a pull request with these changes titled `vX.XX.XX Changelog`. Once approved and merged, you can go ahead and create the release.
### Creating a release
1. [Create a new release in GitHub](https://help.github.com/en/github/administering-a-repository/creating-releases) by clicking on "Releases" and then "Draft a new release"
2. Set the `Tag version` to a new tag, using [Semantic Versioning](https://semver.org/) as a guideline.
3. Set the `Target` as `main`.
4. Set the `Release title` to the tag you created, `vX.Y.Z`
5. Use the description section to describe why you're releasing and what changes you've made. You should include links to merged PRs. Use the following headers in the description of your release:
- BREAKING CHANGES: Use this for any changes that aren't backwards compatible. Include details on how to handle these changes.
- FEATURES: Use this for any large new features added,
- ENHANCEMENTS: Use this for smaller new features added
- BUG FIXES: Use this for any bugs that were fixed.
- NOTES: Use this section if you need to include any additional notes on things like upgrading, upcoming deprecations, or any other information you might want to highlight.
Markdown example:
```markdown
ENHANCEMENTS
* Add description of new small feature by @some-github-user (#3)[link-to-pull-request]
BUG FIXES
* Fix description of a bug by @some-github-user (#2)[link-to-pull-request]
* Fix description of another bug by @some-github-user (#1)[link-to-pull-request]
```
6. Don't attach any binaries. The zip and tar.gz assets are automatically created and attached after you publish your release.
7. Click "Publish release" to save and publish your release.
================================================
FILE: docs/TESTS.md
================================================
# Running tests
go-tfe relies on acceptance tests against either the HCP Terraform and Terraform Enterprise APIs. go-tfe is tested against HCP Terraform by our CI environment, and against Terraform Enterprise prior to release or otherwise as needed.
## 1. (Optional) Create repositories for policy sets and registry modules
If you are planning to run the full suite of tests or work on policy sets or registry modules, you'll need to set up repositories for them in GitHub.
Your policy set repository will need the following:
1. A policy set stored in a subdirectory `policy-sets/foo`
1. A branch other than `main` named `policies`
Alternatively, you can start with this [example repository for policy sets](https://github.com/hashicorp/test-policy-set) by forking the repository to your GitHub account, then setting `GITHUB_POLICY_SET_IDENTIFIER` to the forked repository identifier `your-github-handle/test-policy-set`.
Your registry module repository will need to be a [valid module](https://developer.hashicorp.com/terraform/cloud-docs/registry/publish-modules#preparing-a-module-repository).
It will need the following:
1. To be named `terraform--`
1. At least one valid SemVer tag in the format `x.y.z`
[terraform-random-module](https://github.com/caseylang/terraform-random-module) is a good example repo.
## 2. Set up environment variables (ENVVARS)
You'll need to have environment variables setup in your environment to run the tests. There are different options to facilitate setting up environment variables, using the tool [envchain](https://github.com/sorah/envchain) is one option:
1. Install envchain - [refer to the envchain README for details](https://github.com/sorah/envchain#installation)
1. Run the script `./scripts/setup-test-envvars.sh` to setup the env vars. This script uses envchain, will use a default namespace of `go-tfe` and will prompt you for environment variable values. To run: `sh ./scripts/setup-test-envvars.sh`
1. Or manually, pick a namespace for storing your environment variables, such as: `go-tfe`. Then, for each environment variable you need to set, run the following command:
```sh
envchain --set YOUR_NAMESPACE_HERE ENVIRONMENT_VARIABLE_HERE
```
**OR**
Set all of the environment variables at once with the following command:
```sh
envchain --set YOUR_NAMESPACE_HERE TFE_ADDRESS TFE_TOKEN OAUTH_CLIENT_GITHUB_TOKEN GITHUB_POLICY_SET_IDENTIFIER
```
### Required ENVVARS
Tests are run against an actual backend so they require a valid backend address and token:
1. `TFE_ADDRESS` - URL of a HCP Terraform or Terraform Enterprise instance to be used for testing, including scheme. Example: `https://tfe.local`
1. `TFE_TOKEN` - A [user API token](https://developer.hashicorp.com/terraform/cloud-docs/users-teams-organizations/users#tokens) for the HCP Terraform or Terraform Enterprise instance being used for testing.
**Note:** Alternatively, you can set `TFE_HOSTNAME` which serves as a fallback for `TFE_ADDRESS`. It will only be used if `TFE_ADDRESS` is not set and will resolve the host to an `https` scheme. Example: `tfe.local` => resolves to `https://tfe.local`
### Optional ENVVARS
1. `OAUTH_CLIENT_GITHUB_TOKEN` - [GitHub personal access token](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line). Required for running any tests that use VCS (OAuth clients, policy sets, etc).
2. `GITHUB_POLICY_SET_IDENTIFIER` - GitHub policy set repository identifier in the format `username/repository`. Required for running policy set tests.
3. `GITHUB_REGISTRY_MODULE_IDENTIFIER` - GitHub registry module repository identifier in the format `username/repository`. Required for running registry module tests.
4. `ENABLE_TFE` - Some tests are only applicable to Terraform Enterprise or HCP Terraforrm. By setting `ENABLE_TFE=1` you will enable Terraform Enterprise only tests and disable HCP Terraform only tests. In CI `ENABLE_TFE` is not set so if you are writing enterprise only features you should manually test with `ENABLE_TFE=1` against a Terraform Enterprise instance.
5. `ENABLE_BETA` - Some tests require access to beta features. By setting `ENABLE_BETA=1` you will enable tests that require access to beta features. IN CI `ENABLE_BETA` is not set so if you are writing beta only features you should manually test with `ENABLE_BETA=1` against a Terraform Enterprise instance with those features enabled.
6. `TFC_RUN_TASK_URL` - Run task integration tests require a URL to use when creating run tasks. To learn more about the Run Task API, [read here](https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run-tasks/run-tasks)
7. `GITHUB_APP_INSTALLATION_ID` - Required for running any tests that use GitHub App as the VCS provider (workspace, policy sets, registry module). These tests are skipped in the automated CI pipeline because in order to use this variable, the user has to have a GitHub App Installation setup done using the [HCP Terraform UI](https://developer.hashicorp.com/terraform/enterprise/admin/application/github-app-integration). And then the value can be fetched either from the UI or from the `GET /github-app/installations` API. The test command is listed below.
```sh
$ GITHUB_APP_INSTALLATION_ID=ghain-xxxx TFE_ADDRESS= https://tfe.local TFE_TOKEN=xxx GITHUB_POLICY_SET_IDENTIFIER=username/repository GITHUB_REGISTRY_MODULE_IDENTIFIER=username/repository go test -run "(GHA|GithubApp)" -v ./...
```
8. `GITHUB_REGISTRY_NO_CODE_MODULE_IDENTIFIER` - Required for running tests for workspaces using no-code modules.
## 3. Make sure run queue settings are correct
In order for the tests relating to queuing and capacity to pass, FRQ (fair run queuing) should be
enabled with a limit of 2 concurrent runs per organization on the HCP Terraform or Terraform Enterprise instance you are using for testing.
## 4. Run tests
For most situations, it's recommended to run specific tests because it takes about 20 minutes to run all of the tests.
### Running specific tests
Typically, you'll want to run specific tests. The commands below use notification configurations as an example.
#### With envchain:
```sh
$ envchain YOUR_NAMESPACE_HERE go test -run TestNotificationConfiguration -v ./...
```
#### Without envchain (Using TFE_ADDRESS):
```sh
$ TFE_TOKEN=xyz TFE_ADDRESS=https://tfe.local ENABLE_TFE=1 go test -run TestNotificationConfiguration -v ./...
```
#### Without envchain (Using TFE_HOSTNAME):
```sh
$ TFE_TOKEN=xyz TFE_HOSTNAME=tfe.local ENABLE_TFE=1 go test -run TestNotificationConfiguration -v ./...
```
#### Using Makefile target `test`
```sh
TFE_TOKEN=xyz TFE_ADDRESS=https://tfe.local TESTARGS="-run TestNotificationConfiguration" make test
```
### Running all tests
It takes about 20 minutes to run all of the tests, so specify a larger timeout when you run the tests (_the default timeout is 10 minutes_):
#### With envchain:
```sh
$ envchain YOUR_NAMESPACE_HERE go test ./... -timeout=30m
```
#### Without envchain (Using TFE_ADDRESS):
```sh
$ TFE_TOKEN=xyz TFE_ADDRESS=https://tfe.local ENABLE_TFE=1 go test ./... -timeout=30m
```
#### Without envchain (Using TFE_HOSTNAME):
```sh
$ TFE_TOKEN=xyz TFE_HOSTNAME=tfe.local ENABLE_TFE=1 go test ./... -timeout=30m
```
### Running tests for HCP Terraform features that require paid plans (HashiCorp Employees)
You can use the test helper `newSubscriptionUpdater()` to upgrade your test organization to a Business Plan, giving the organization access to all features in HCP Terraform. This method requires `TFE_TOKEN` to be a user token with administrator access in the target test environment. Furthermore, you **can not** have enterprise features enabled (`ENABLE_TFE=1`) in order to use this method since the API call fails against Terraform Enterprise test environments.
================================================
FILE: entitlement_helper_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"errors"
)
func getOrgEntitlements(client *Client, organizationName string) (*Entitlements, error) {
ctx := context.Background()
orgEntitlements, err := client.Organizations.ReadEntitlements(ctx, organizationName)
if err != nil {
return nil, err
}
if orgEntitlements == nil {
return nil, errors.New("The organization entitlements are empty.")
}
return orgEntitlements, nil
}
func hasGlobalRunTasks(client *Client, organizationName string) (bool, error) {
oe, err := getOrgEntitlements(client, organizationName)
if err != nil {
return false, err
}
return oe.GlobalRunTasks, nil
}
func hasPrivateRunTasks(client *Client, organizationName string) (bool, error) {
oe, err := getOrgEntitlements(client, organizationName)
if err != nil {
return false, err
}
return oe.PrivateRunTasks, nil
}
func hasAuditLogging(client *Client, organizationName string) (bool, error) {
oe, err := getOrgEntitlements(client, organizationName)
if err != nil {
return false, err
}
return oe.AuditLogging, nil
}
================================================
FILE: errors.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"errors"
"fmt"
)
// Generic errors applicable to all resources.
var (
// ErrUnauthorized is returned when receiving a 401.
ErrUnauthorized = errors.New("unauthorized")
// ErrResourceNotFound is returned when receiving a 404.
ErrResourceNotFound = errors.New("resource not found")
// ErrMissingDirectory is returned when the path does not have an existing directory.
ErrMissingDirectory = errors.New("path needs to be an existing directory")
// ErrNamespaceNotAuthorized is returned when a user attempts to perform an action
// on a namespace (organization) they do not have access to.
ErrNamespaceNotAuthorized = errors.New("namespace not authorized")
)
// Options/fields that cannot be defined
var (
ErrUnsupportedOperations = errors.New("operations is deprecated and cannot be specified when execution mode is used")
ErrUnsupportedPrivateKey = errors.New("private Key can only be present with Azure DevOps Server service provider")
ErrUnsupportedBothTagsRegexAndFileTriggersEnabled = errors.New(`"TagsRegex" cannot be populated when "FileTriggersEnabled" is true`)
ErrUnsupportedBothTagsRegexAndTriggerPatterns = errors.New(`"TagsRegex" and "TriggerPrefixes" cannot be populated at the same time`)
ErrUnsupportedBothTagsRegexAndTriggerPrefixes = errors.New(`"TagsRegex" and "TriggerPatterns" cannot be populated at the same time`)
ErrUnsupportedRunTriggerType = errors.New(`"RunTriggerType" must be "inbound" when requesting "include" query params`)
ErrUnsupportedBothTriggerPatternsAndPrefixes = errors.New(`"TriggerPatterns" and "TriggerPrefixes" cannot be populated at the same time`)
ErrUnsupportedBothNamespaceAndPrivateRegistryName = errors.New(`"Namespace" cannot be populated when "RegistryName" is "private"`)
)
// Library errors that usually indicate a bug in the implementation of go-tfe
var (
ErrItemsMustBeSlice = errors.New(`model field "Items" must be a slice`) // ErrItemsMustBeSlice is returned when an API response attribute called Items is not a slice
ErrInvalidRequestBody = errors.New("go-tfe bug: DELETE/PATCH/POST body must be nil, ptr, or ptr slice") // ErrInvalidRequestBody is returned when a request body for DELETE/PATCH/POST is not a reference type
ErrInvalidStructFormat = errors.New("go-tfe bug: struct can't use both json and jsonapi attributes") // ErrInvalidStructFormat is returned when a mix of json and jsonapi tagged fields are used in the same struct
)
// Resource Errors
var (
// ErrWorkspaceLocked is returned when trying to lock a locked workspace.
ErrWorkspaceLocked = errors.New("workspace already locked")
// ErrWorkspaceNotLocked is returned when trying to unlock a unlocked workspace.
ErrWorkspaceNotLocked = errors.New("workspace already unlocked")
// ErrWorkspaceLockedByRun is returned when trying to unlock a workspace locked by a run.
ErrWorkspaceLockedByRun = errors.New("unable to unlock workspace locked by run")
// ErrWorkspaceLockedByTeam is returned when trying to unlock a workspace locked by a team.
ErrWorkspaceLockedByTeam = errors.New("unable to unlock workspace locked by team")
// ErrWorkspaceLockedByUser is returned when trying to unlock a workspace locked by a user.
ErrWorkspaceLockedByUser = errors.New("unable to unlock workspace locked by user")
// ErrWorkspaceLockedStateVersionStillPending is returned when trying to unlock whose
// latest state version is still pending.
ErrWorkspaceLockedStateVersionStillPending = errors.New("unable to unlock workspace while state version upload is still pending")
// ErrWorkspaceStillProcessing is returned when a workspace is still processing state
// to determine if it is safe to delete. "conflict" followed by newline is used to
// preserve go-tfe version compatibility with the error constructed at runtime before it was
// defined here.
ErrWorkspaceStillProcessing = errors.New("conflict\nLatest workspace state is being processed to discover resources, please try again later")
// ErrWorkspaceNotSafeToDelete is returned when a workspace has processed state and
// is determined to still have resources present. "conflict" followed by newline is used to
// preserve go-tfe version compatibility with the error constructed at runtime before it was
// defined here.
ErrWorkspaceNotSafeToDelete = errors.New("conflict\nworkspace cannot be safely deleted because it is still managing resources")
// ErrWorkspaceLockedCannotDelete is returned when a workspace cannot be safely deleted when
// it is locked. "conflict" followed by newline is used to preserve go-tfe version
// compatibility with the error constructed at runtime before it was defined here.
ErrWorkspaceLockedCannotDelete = errors.New("conflict\nWorkspace is currently locked. Workspace must be unlocked before it can be safely deleted")
// ErrHYOKCannotBeDisabled is returned when attempting to disable HYOK on a workspace that already has it enabled.
ErrHYOKCannotBeDisabled = errors.New("bad request\n\nhyok may not be disabled once it has been turned on for a workspace")
)
// Invalid values for resources/struct fields
var (
ErrInvalidWorkspaceID = errors.New("invalid value for workspace ID")
ErrInvalidWorkspaceValue = errors.New("invalid value for workspace")
ErrInvalidTerraformVersionID = errors.New("invalid value for terraform version ID")
ErrInvalidTerraformVersionType = errors.New("invalid type for terraform version. Please use 'terraform-version'")
ErrInvalidOPAVersionID = errors.New("invalid value for OPA version ID")
ErrInvalidSentinelVersionID = errors.New("invalid value for Sentinel version ID")
ErrInvalidConfigVersionID = errors.New("invalid value for configuration version ID")
ErrInvalidCostEstimateID = errors.New("invalid value for cost estimate ID")
ErrInvalidSMTPAuth = errors.New("invalid smtp auth type")
ErrInvalidAgentPoolID = errors.New("invalid value for agent pool ID")
ErrInvalidAgentTokenID = errors.New("invalid value for agent token ID")
ErrInvalidRunID = errors.New("invalid value for run ID")
ErrInvalidRunEventID = errors.New("invalid value for run event ID")
ErrInvalidProjectID = errors.New("invalid value for project ID")
ErrInvalidPagination = errors.New("invalid value for page size or number")
ErrInvalidReservedTagKeyID = errors.New("invalid value for reserved tag key ID")
ErrInvalidRunTaskCategory = errors.New(`category must be "task"`)
ErrInvalidRunTaskID = errors.New("invalid value for run task ID")
ErrInvalidRunTaskURL = errors.New("invalid url for run task URL")
ErrInvalidWorkspaceRunTaskID = errors.New("invalid value for workspace run task ID")
ErrInvalidWorkspaceRunTaskType = errors.New(`invalid value for type, please use "workspace-tasks"`)
ErrInvalidTaskResultID = errors.New("invalid value for task result ID")
ErrInvalidTaskStageID = errors.New("invalid value for task stage ID")
ErrInvalidApplyID = errors.New("invalid value for apply ID")
ErrInvalidOrg = errors.New("invalid value for organization")
ErrInvalidName = errors.New("invalid value for name")
ErrInvalidNotificationConfigID = errors.New("invalid value for notification configuration ID")
ErrInvalidMembership = errors.New("invalid value for membership")
ErrInvalidMembershipIDs = errors.New("invalid value for organization membership ids")
ErrInvalidOauthClientID = errors.New("invalid value for OAuth client ID")
ErrInvalidOauthTokenID = errors.New("invalid value for OAuth token ID")
ErrInvalidPolicySetID = errors.New("invalid value for policy set ID")
ErrInvalidPolicyCheckID = errors.New("invalid value for policy check ID")
ErrInvalidPolicyEvaluationID = errors.New("invalid value for policy evaluation ID")
ErrInvalidPolicySetOutcomeID = errors.New("invalid value for policy set outcome ID")
ErrInvalidTag = errors.New("invalid tag id")
ErrInvalidPlanExportID = errors.New("invalid value for plan export ID")
ErrInvalidPlanID = errors.New("invalid value for plan ID")
ErrInvalidParamID = errors.New("invalid value for parameter ID")
ErrInvalidPolicyID = errors.New("invalid value for policy ID")
ErrInvalidProvider = errors.New("invalid value for provider")
ErrInvalidVersion = errors.New("invalid value for version")
ErrInvalidRunTriggerID = errors.New("invalid value for run trigger ID")
ErrInvalidRunTriggerType = errors.New(`invalid value or no value for RunTriggerType. It must be either "inbound" or "outbound"`)
ErrInvalidIncludeValue = errors.New(`invalid value for "include" field`)
ErrInvalidSHHKeyID = errors.New("invalid value for SSH key ID")
ErrInvalidStateVerID = errors.New("invalid value for state version ID")
ErrInvalidOutputID = errors.New("invalid value for state version output ID")
ErrInvalidAccessTeamID = errors.New("invalid value for team access ID")
ErrInvalidTeamProjectAccessID = errors.New("invalid value for team project access ID")
ErrInvalidTeamProjectAccessType = errors.New("invalid type for team project access")
ErrInvalidTeamID = errors.New("invalid value for team ID")
ErrInvalidUsernames = errors.New("invalid value for usernames")
ErrInvalidUserID = errors.New("invalid value for user ID")
ErrInvalidUserValue = errors.New("invalid value for user")
ErrInvalidTokenID = errors.New("invalid value for token ID")
ErrInvalidCategory = errors.New("category must be policy-set")
ErrInvalidPolicies = errors.New("must provide at least one policy")
ErrInvalidVariableID = errors.New("invalid value for variable ID")
ErrInvalidNotificationTrigger = errors.New("invalid value for notification trigger")
ErrInvalidVariableSetID = errors.New("invalid variable set ID")
ErrInvalidCommentID = errors.New("invalid value for comment ID")
ErrInvalidCommentBody = errors.New("invalid value for comment body")
ErrInvalidNamespace = errors.New("invalid value for namespace")
ErrInvalidKeyID = errors.New("invalid value for key-id")
ErrInvalidOS = errors.New("invalid value for OS")
ErrInvalidArch = errors.New("invalid value for arch")
ErrInvalidAgentID = errors.New("invalid value for Agent ID")
ErrInvalidModuleID = errors.New("invalid value for module ID")
ErrInvalidRegistryName = errors.New(`invalid value for registry-name. It must be either "private" or "public"`)
ErrInvalidCallbackURL = errors.New("invalid value for callback URL")
ErrInvalidAccessToken = errors.New("invalid value for access token")
ErrInvalidTaskResultsCallbackStatus = fmt.Errorf("invalid value for task result status. Must be either `%s`, `%s`, or `%s`", TaskFailed, TaskPassed, TaskRunning)
ErrInvalidDescriptionConflict = errors.New("invalid attributes\n\nValidation failed: Description has already been taken")
ErrInvalidOIDC = errors.New("invalid value for OIDC configuration ID")
ErrInvalidHYOK = errors.New("invalid value for HYOK configuration ID")
ErrInvalidHYOKCustomerKeyVersion = errors.New("invalid value for HYOK Customer key version ID")
ErrInvalidHYOKEncryptedDataKey = errors.New("invalid value for HYOK encrypted data key ID")
ErrInvalidStackID = errors.New("invalid value for stack ID")
ErrInvalidRemoteStateOptions = errors.New("invalid attribute\n\nProject remote state cannot be enabled when global remote state sharing is enabled")
ErrInvalidSAMLProviderType = errors.New("invalid SAML provider type")
)
var (
ErrRequiredAccess = errors.New("access is required")
ErrRequiredAgentPoolID = errors.New("'agent' execution mode requires an agent pool ID to be specified")
ErrRequiredAgentMode = errors.New("specifying an agent pool ID requires 'agent' execution mode")
ErrRequiredBranchWhenTestsEnabled = errors.New("VCS branch is required when enabling tests")
ErrBranchMustBeEmptyWhenTagsEnabled = errors.New("VCS branch must be empty to enable tags")
ErrRequiredCategory = errors.New("category is required")
ErrAgentPoolNotRequiredForRemoteExecution = errors.New("'remote' execution mode does not support agent pool IDs")
ErrRequiredDestinationType = errors.New("destination type is required")
ErrRequiredDataType = errors.New("data type is required")
ErrRequiredKey = errors.New("key is required")
ErrRequiredName = errors.New("name is required")
ErrRequiredQuery = errors.New("query cannot be empty")
ErrRequiredEnabled = errors.New("enabled is required")
ErrRequiredEnforce = errors.New("enforce or enforcement-level is required")
ErrConflictingEnforceEnforcementLevel = errors.New("enforce and enforcement-level may not both be specified together")
ErrRequiredEnforcementPath = errors.New("enforcement path is required")
ErrRequiredEnforcementMode = errors.New("enforcement mode is required")
ErrRequiredEmail = errors.New("email is required")
ErrRequiredM5 = errors.New("MD5 is required")
ErrRequiredURL = errors.New("url is required")
ErrRequiredArchsOrURLAndSha = errors.New("valid archs or url and sha are required")
ErrRequiredAPIURL = errors.New("API URL is required")
ErrRequiredHTTPURL = errors.New("HTTP URL is required")
ErrRequiredServiceProvider = errors.New("service provider is required")
ErrRequiredProvider = errors.New("provider is required")
ErrRequiredOauthToken = errors.New("OAuth token is required")
ErrRequiredOauthTokenOrGithubAppInstallationID = errors.New("either oauth token ID or github app installation ID is required")
ErrRequiredTestNumber = errors.New("TestNumber is required")
ErrMissingTagIdentifier = errors.New("must specify at least one tag by ID or name")
ErrAgentTokenDescription = errors.New("agent token description can't be blank")
ErrRequiredTagID = errors.New("you must specify at least one tag id to remove")
ErrRequiredTagWorkspaceID = errors.New("you must specify at least one workspace to add tag to")
ErrRequiredWorkspace = errors.New("workspace is required")
ErrRequiredProject = errors.New("project is required")
ErrRequiredWorkspaceID = errors.New("workspace ID is required")
ErrRequiredProjectID = errors.New("project ID is required")
ErrRequiredStackID = errors.New("stack ID is required")
ErrWorkspacesRequired = errors.New("workspaces is required")
ErrWorkspaceMinLimit = errors.New("must provide at least one workspace")
ErrProjectMinLimit = errors.New("must provide at least one project")
ErrRequiredPlan = errors.New("plan is required")
ErrRequiredPolicies = errors.New("policies is required")
ErrRequiredVersion = errors.New("version is required")
ErrRequiredVCSRepo = errors.New("vcs repo is required")
ErrRequiredIdentifier = errors.New("identifier is required")
ErrRequiredDisplayIdentifier = errors.New("display identifier is required")
ErrRequiredSha = errors.New("sha is required")
ErrRequiredSourceable = errors.New("sourceable is required")
ErrRequiredValue = errors.New("value is required")
ErrRequiredOrg = errors.New("organization is required")
ErrRequiredTeam = errors.New("team is required")
ErrRequiredStateVerListOps = errors.New("StateVersionListOptions is required")
ErrRequiredTeamAccessListOps = errors.New("TeamAccessListOptions is required")
ErrRequiredTeamProjectAccessListOps = errors.New("TeamProjectAccessListOptions is required")
ErrRequiredRunTriggerListOps = errors.New("RunTriggerListOptions is required")
ErrRequiredTFVerCreateOps = errors.New("version, URL and sha is required for AdminTerraformVersionCreateOptions")
ErrRequiredOPAVerCreateOps = errors.New("version, URL and sha is required for AdminOPAVersionCreateOptions")
ErrRequiredSentinelVerCreateOps = errors.New("version, URL and sha is required for AdminSentinelVersionCreateOptions")
ErrRequiredSerial = errors.New("serial is required")
ErrRequiredState = errors.New("state is required")
ErrRequiredSHHKeyID = errors.New("SSH key ID is required")
ErrRequiredOnlyOneField = errors.New("only one of usernames or organization membership ids can be provided")
ErrRequiredUsernameOrMembershipIds = errors.New("usernames or organization membership ids are required")
ErrRequiredGlobalFlag = errors.New("global flag is required")
ErrRequiredWorkspacesList = errors.New("no workspaces list provided")
ErrRequiredStacksList = errors.New("no stacks list provided")
ErrCommentBody = errors.New("comment body is required")
ErrEmptyTeamName = errors.New("team name can not be empty")
ErrInvalidEmail = errors.New("email is invalid")
ErrRequiredPrivateRegistry = errors.New("only private registry is allowed")
ErrRequiredOS = errors.New("OS is required")
ErrRequiredArch = errors.New("arch is required")
ErrRequiredShasum = errors.New("shasum is required")
ErrRequiredFilename = errors.New("filename is required")
ErrInvalidAsciiArmor = errors.New("ASCII Armor is invalid")
ErrRequiredNamespace = errors.New("namespace is required for public registry")
ErrRequiredRegistryModule = errors.New("registry module is required")
ErrRequiredTagBindings = errors.New("TagBindings are required")
ErrInvalidTestRunID = errors.New("invalid value for test run id")
ErrInvalidQueryRunID = errors.New("invalid value for query run id")
ErrTerraformVersionValidForPlanOnly = errors.New("setting terraform-version is only valid when plan-only is set to true")
ErrStateMustBeOmitted = errors.New("when uploading state, the State and JSONState strings must be omitted from options")
ErrRequiredRawState = errors.New("RawState is required")
ErrStateVersionUploadNotSupported = errors.New("upload not supported by this version of Terraform Enterprise")
ErrSanitizedStateUploadURLMissing = errors.New("sanitized state upload URL is missing")
ErrRequiredRoleARN = errors.New("role-arn is required for AWS OIDC configuration")
ErrRequiredServiceAccountEmail = errors.New("service-account-email is required for GCP OIDC configuration")
ErrRequiredProjectNumber = errors.New("project-number is required for GCP OIDC configuration")
ErrRequiredWorkloadProviderName = errors.New("workload-provider-name is required for GCP OIDC configuration")
ErrRequiredClientID = errors.New("client-id is required for Azure OIDC configuration")
ErrRequiredSubscriptionID = errors.New("subscription-id is required for Azure OIDC configuration")
ErrRequiredTenantID = errors.New("tenant-id is required for Azure OIDC configuration")
ErrRequiredVaultAddress = errors.New("address is required for Vault OIDC configuration")
ErrRequiredRoleName = errors.New("role is required for Vault OIDC configuration")
ErrRequiredKEKID = errors.New("kek-id is required for HYOK configuration")
ErrRequiredOIDCConfiguration = errors.New("oidc-configuration is required for HYOK configuration")
ErrRequiredAgentPool = errors.New("agent-pool is required for HYOK configuration")
ErrRequiredKMSOptions = errors.New("kms-options is required for HYOK configuration")
ErrRequiredKMSOptionsKeyRegion = errors.New("kms-options.key-region is required for HYOK configuration with AWS OIDC")
ErrRequiredKMSOptionsKeyLocation = errors.New("kms-options.key-location is required for HYOK configuration with GCP OIDC")
ErrRequiredKMSOptionsKeyRingID = errors.New("kms-options.key-ring-id is required for HYOK configuration with GCP OIDC")
ErrSCIMTokenDescription = errors.New("SCIM token description can't be blank")
)
================================================
FILE: example_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"crypto/md5"
"fmt"
"log"
"os"
slug "github.com/hashicorp/go-slug"
)
func ExampleOrganizations() {
config := &Config{
Token: "insert-your-token-here",
RetryServerErrors: true,
}
client, err := NewClient(config)
if err != nil {
log.Fatal(err)
}
// Create a context
ctx := context.Background()
// Create a new organization
options := OrganizationCreateOptions{
Name: String("example"),
Email: String("info@example.com"),
}
org, err := client.Organizations.Create(ctx, options)
if err != nil {
log.Fatal(err)
}
// Delete an organization
err = client.Organizations.Delete(ctx, org.Name)
if err != nil {
log.Fatal(err)
}
}
func ExampleWorkspaces() {
config := &Config{
Token: "insert-your-token-here",
RetryServerErrors: true,
}
client, err := NewClient(config)
if err != nil {
log.Fatal(err)
}
// Create a context
ctx := context.Background()
// Create a new workspace
w, err := client.Workspaces.Create(ctx, "org-name", WorkspaceCreateOptions{
Name: String("my-app-tst"),
})
if err != nil {
log.Fatal(err)
}
// Update the workspace
w, err = client.Workspaces.Update(ctx, "org-name", w.Name, WorkspaceUpdateOptions{
AutoApply: Bool(false),
TerraformVersion: String("0.11.1"),
WorkingDirectory: String("my-app/infra"),
})
if err != nil {
log.Fatal(err)
}
}
func ExampleConfigurationVersions_UploadTarGzip() {
ctx := context.Background()
client, err := NewClient(&Config{
Token: "insert-your-token-here",
RetryServerErrors: true,
})
if err != nil {
log.Fatal(err)
}
packer, err := slug.NewPacker(
slug.DereferenceSymlinks(), // dereferences symlinks
slug.ApplyTerraformIgnore(), // ignores paths specified in .terraformignore
)
if err != nil {
log.Fatal(err)
}
rawConfig := bytes.NewBuffer(nil)
// Pass in a path
_, err = packer.Pack("test-fixtures/config", rawConfig)
if err != nil {
log.Fatal(err)
}
// Create a configuration version
cv, err := client.ConfigurationVersions.Create(ctx, "ws-12345678", ConfigurationVersionCreateOptions{
AutoQueueRuns: Bool(false),
})
if err != nil {
log.Fatal(err)
}
// Upload the buffer
err = client.ConfigurationVersions.UploadTarGzip(ctx, cv.UploadURL, rawConfig)
if err != nil {
log.Fatal(err)
}
}
func ExampleRegistryModules_UploadTarGzip() {
ctx := context.Background()
client, err := NewClient(&Config{
Token: "insert-your-token-here",
RetryServerErrors: true,
})
if err != nil {
log.Fatal(err)
}
packer, err := slug.NewPacker(
slug.DereferenceSymlinks(), // dereferences symlinks
slug.ApplyTerraformIgnore(), // ignores paths specified in .terraformignore
)
if err != nil {
log.Fatal(err)
}
rawConfig := bytes.NewBuffer(nil)
// Pass in a path
_, err = packer.Pack("test-fixtures/config", rawConfig)
if err != nil {
log.Fatal(err)
}
// Create a registry module
rm, err := client.RegistryModules.Create(ctx, "hashicorp", RegistryModuleCreateOptions{
Name: String("my-module"),
Provider: String("provider"),
RegistryName: PrivateRegistry,
})
if err != nil {
log.Fatal(err)
}
opts := RegistryModuleCreateVersionOptions{
Version: String("1.1.0"),
}
// Create a registry module version
rmv, err := client.RegistryModules.CreateVersion(ctx, RegistryModuleID{
Organization: "hashicorp",
Name: rm.Name,
Provider: rm.Provider,
}, opts)
if err != nil {
log.Fatal(err)
}
uploadURL, ok := rmv.Links["upload"].(string)
if !ok {
log.Fatal("upload url must be a valid string")
}
// Upload the buffer
err = client.RegistryModules.UploadTarGzip(ctx, uploadURL, rawConfig)
if err != nil {
log.Fatal(err)
}
}
func ExampleStateVersions_Upload() {
ctx := context.Background()
client, err := NewClient(&Config{
Token: "insert-your-token-here",
RetryServerErrors: true,
})
if err != nil {
log.Fatal(err)
}
// Lock the workspace
if _, err = client.Workspaces.Lock(ctx, "ws-12345678", WorkspaceLockOptions{}); err != nil {
log.Fatal(err)
}
state, err := os.ReadFile("state.json")
if err != nil {
log.Fatal(err)
}
// Create upload options that does not contain a State attribute within the create options
options := StateVersionUploadOptions{
StateVersionCreateOptions: StateVersionCreateOptions{
Lineage: String("493f7758-da5e-229e-7872-ea1f78ebe50a"),
Serial: Int64(int64(2)),
MD5: String(fmt.Sprintf("%x", md5.Sum(state))),
Force: Bool(false),
},
RawState: state,
}
// Upload a state version
if _, err = client.StateVersions.Upload(ctx, "ws-12345678", options); err != nil {
log.Fatal(err)
}
// Unlock the workspace
if _, err = client.Workspaces.Unlock(ctx, "ws-12345678"); err != nil {
log.Fatal(err)
}
}
================================================
FILE: examples/backing_data/main.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package main
import (
"context"
"flag"
"fmt"
tfe "github.com/hashicorp/go-tfe"
"log"
"strings"
)
func main() {
action := flag.String("action", "", "Action (soft-delete|restore|permanently-delete")
externalId := flag.String("external-id", "", "External ID of StateVersion or ConfigurationVersion")
flag.Parse()
if action == nil || *action == "" {
log.Fatal("No Action provided")
}
if externalId == nil || *externalId == "" {
log.Fatal("No external ID provided")
}
ctx := context.Background()
client, err := tfe.NewClient(&tfe.Config{
RetryServerErrors: true,
})
if err != nil {
log.Fatal(err)
}
err = performAction(ctx, client, *action, *externalId)
if err != nil {
log.Fatalf("Error performing action: %v", err)
}
}
func performAction(ctx context.Context, client *tfe.Client, action string, id string) error {
externalIdParts := strings.Split(id, "-")
switch externalIdParts[0] {
case "cv":
switch action {
case "soft-delete":
return client.ConfigurationVersions.SoftDeleteBackingData(ctx, id)
case "restore":
return client.ConfigurationVersions.RestoreBackingData(ctx, id)
case "permanently-delete":
return client.ConfigurationVersions.PermanentlyDeleteBackingData(ctx, id)
default:
return fmt.Errorf("unsupported action: %s", action)
}
case "sv":
switch action {
case "soft-delete":
return client.StateVersions.SoftDeleteBackingData(ctx, id)
case "restore":
return client.StateVersions.RestoreBackingData(ctx, id)
case "permanently-delete":
return client.StateVersions.PermanentlyDeleteBackingData(ctx, id)
default:
return fmt.Errorf("unsupported action: %s", action)
}
default:
return fmt.Errorf("unsupported external ID: %s", id)
}
return nil
}
================================================
FILE: examples/configuration_versions/main.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package main
import (
"bytes"
"context"
"log"
"github.com/hashicorp/go-slug"
tfe "github.com/hashicorp/go-tfe"
)
func main() {
ctx := context.Background()
client, err := tfe.NewClient(&tfe.Config{
RetryServerErrors: true,
})
if err != nil {
log.Fatal(err)
}
packer, err := slug.NewPacker(
slug.DereferenceSymlinks(), // dereferences symlinks
slug.ApplyTerraformIgnore(), // ignores paths specified in .terraformignore
)
if err != nil {
log.Fatal(err)
}
rawConfig := bytes.NewBuffer(nil)
// Pass in a path
_, err = packer.Pack("test-fixtures/config", rawConfig)
if err != nil {
log.Fatal(err)
}
// Create a configuration version
cv, err := client.ConfigurationVersions.Create(ctx, "ws-12345678", tfe.ConfigurationVersionCreateOptions{
AutoQueueRuns: tfe.Bool(false),
})
if err != nil {
log.Fatal(err)
}
// Upload the configuration
err = client.ConfigurationVersions.UploadTarGzip(ctx, cv.UploadURL, rawConfig)
if err != nil {
log.Fatal(err)
}
}
================================================
FILE: examples/organizations/main.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package main
import (
"context"
"log"
tfe "github.com/hashicorp/go-tfe"
)
func main() {
config := &tfe.Config{
Token: "insert-your-token-here",
RetryServerErrors: true,
}
client, err := tfe.NewClient(config)
if err != nil {
log.Fatal(err)
}
// Create a context
ctx := context.Background()
// Create a new organization
options := tfe.OrganizationCreateOptions{
Name: tfe.String("example"),
Email: tfe.String("info@example.com"),
}
org, err := client.Organizations.Create(ctx, options)
if err != nil {
log.Fatal(err)
}
// Delete an organization
err = client.Organizations.Delete(ctx, org.Name)
if err != nil {
log.Fatal(err)
}
}
================================================
FILE: examples/projects/main.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package main
import (
"context"
"log"
tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/jsonapi"
)
func main() {
config := &tfe.Config{
Token: "insert-your-token-here",
RetryServerErrors: true,
}
client, err := tfe.NewClient(config)
if err != nil {
log.Fatal(err)
}
// Create a context
ctx := context.Background()
// Create a new project
p, err := client.Projects.Create(ctx, "org-test", tfe.ProjectCreateOptions{
Name: "my-app-tst",
})
if err != nil {
log.Fatal(err)
}
// Update the project auto destroy activity duration
p, err = client.Projects.Update(ctx, p.ID, tfe.ProjectUpdateOptions{
AutoDestroyActivityDuration: jsonapi.NewNullableAttrWithValue("3d"),
})
if err != nil {
log.Fatal(err)
}
// Disable auto destroy
p, err = client.Projects.Update(ctx, p.ID, tfe.ProjectUpdateOptions{
AutoDestroyActivityDuration: jsonapi.NewNullNullableAttr[string](),
})
if err != nil {
log.Fatal(err)
}
err = client.Projects.Delete(ctx, p.ID)
if err != nil {
log.Fatal(err)
}
}
================================================
FILE: examples/registry_modules/main.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package main
import (
"bytes"
"context"
"log"
"github.com/hashicorp/go-slug"
tfe "github.com/hashicorp/go-tfe"
)
func main() {
ctx := context.Background()
client, err := tfe.NewClient(&tfe.Config{
RetryServerErrors: true,
})
if err != nil {
log.Fatal(err)
}
packer, err := slug.NewPacker(
slug.DereferenceSymlinks(), // dereferences symlinks
slug.ApplyTerraformIgnore(), // ignores paths specified in .terraformignore
)
if err != nil {
log.Fatal(err)
}
rawConfig := bytes.NewBuffer(nil)
// Pass in the configuration path
_, err = packer.Pack("test-fixtures/config", rawConfig)
if err != nil {
log.Fatal(err)
}
// Create a registry module
rm, err := client.RegistryModules.Create(ctx, "hashicorp", tfe.RegistryModuleCreateOptions{
Name: tfe.String("my-module"),
Provider: tfe.String("provider"),
RegistryName: tfe.PrivateRegistry,
})
if err != nil {
log.Fatal(err)
}
opts := tfe.RegistryModuleCreateVersionOptions{
Version: tfe.String("1.1.0"),
}
// Create a registry module version
rmv, err := client.RegistryModules.CreateVersion(ctx, tfe.RegistryModuleID{
Organization: "hashicorp",
Name: rm.Name,
Provider: rm.Provider,
}, opts)
if err != nil {
log.Fatal(err)
}
uploadURL, ok := rmv.Links["upload"].(string)
if !ok {
log.Fatal("upload url must be a valid string")
}
// Upload the buffer
err = client.RegistryModules.UploadTarGzip(ctx, uploadURL, rawConfig)
if err != nil {
log.Fatal(err)
}
}
================================================
FILE: examples/run_errors/README.md
================================================
## Example: Parsing Run Errors
In this example, you'll use terraform to create a run with errors on HCP Terraform, then
execute the command to read the plan log and filter it for errors. It's important to use
Terraform to create the run, otherwise you will not get the structured log that this code
example requires.
#### Instructions
1. Change to the terraform directory, and run terraform init using Terraform 1.3+
`cd terraform`
`TF_CLOUD_ORGANIZATION="yourorg" terraform init`
2. Apply the changes (You should see an error "Error making request" or similar)
`TF_CLOUD_ORGANIZATION="yourorg" terraform apply`
3. Notice the run ID in the URL (it begins with "run-") and execute the example with the run ID as a flag:
`cd ../`
`TFE_TOKEN="YOURTOKEN" go run main.go run-RUN_ID_FROM_URL_ABOVE`
================================================
FILE: examples/run_errors/main.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"log"
"os"
"time"
tfe "github.com/hashicorp/go-tfe"
)
var (
pollInterval = 500 * time.Millisecond
)
// Diagnostic represents a diagnostic type message from Terraform, which is how errors
// are usually represented.
type Diagnostic struct {
Severity string `json:"severity"`
Summary string `json:"summary"`
Detail string `json:"detail"`
Address string `json:"address,omitempty"`
Range *DiagnosticRange `json:"range,omitempty"`
}
// Pos represents a position in the source code.
type Pos struct {
// Line is a one-based count for the line in the indicated file.
Line int `json:"line"`
// Column is a one-based count of Unicode characters from the start of the line.
Column int `json:"column"`
// Byte is a zero-based offset into the indicated file.
Byte int `json:"byte"`
}
// DiagnosticRange represents the filename and position of the diagnostic subject.
type DiagnosticRange struct {
Filename string `json:"filename"`
Start Pos `json:"start"`
End Pos `json:"end"`
}
// For full decoding, see https://github.com/hashicorp/terraform/blob/main/internal/command/jsonformat/renderer.go
type JSONLog struct {
Message string `json:"@message"`
Level string `json:"@level"`
Timestamp string `json:"@timestamp"`
Type string `json:"type"`
Diagnostic *Diagnostic `json:"diagnostic"`
}
// Given a
func logErrorsOnly(reader io.Reader) {
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
var jsonLog JSONLog
err := json.Unmarshal([]byte(scanner.Text()), &jsonLog)
// It's possible this log is not encoded as JSON at all, so errors will be ignored.
if err == nil && jsonLog.Level == "error" {
fmt.Println()
fmt.Println("--- Error Message")
fmt.Println(jsonLog.Message)
fmt.Println("---")
fmt.Println()
if jsonLog.Type == "diagnostic" {
fmt.Println("--- Diagnostic Details")
fmt.Println(jsonLog.Diagnostic.Detail)
fmt.Println("---")
fmt.Println()
}
}
}
}
func logRunErrors(ctx context.Context, client *tfe.Client, run *tfe.Run) {
var reader io.Reader
var err error
if run.Apply != nil && run.Apply.Status == tfe.ApplyErrored {
log.Printf("Reading apply logs from %q", run.Apply.LogReadURL)
reader, err = client.Applies.Logs(ctx, run.Apply.ID)
} else if run.Plan != nil && run.Plan.Status == tfe.PlanErrored {
log.Printf("Reading apply logs from %q", run.Plan.LogReadURL)
reader, err = client.Plans.Logs(ctx, run.Plan.ID)
} else {
log.Fatal("Failed to find an errored plan or apply.")
}
if err != nil {
log.Fatal("Failed to read error log: ", err)
}
logErrorsOnly(reader)
}
func readRun(ctx context.Context, client *tfe.Client, id string) *tfe.Run {
r, err := client.Runs.ReadWithOptions(ctx, id, &tfe.RunReadOptions{
Include: []tfe.RunIncludeOpt{tfe.RunApply, tfe.RunPlan},
})
if err != nil {
log.Fatal("Failed to read specified run: ", err)
}
return r
}
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage:")
fmt.Printf("\t%s \n", os.Args[0])
os.Exit(1)
}
ctx := context.Background()
client, err := tfe.NewClient(&tfe.Config{
Address: "https://app.terraform.io",
RetryServerErrors: true,
})
if err != nil {
log.Fatal("Failed to initialize client: ", err)
}
r := readRun(ctx, client, os.Args[1])
poll:
for {
<-time.After(pollInterval)
r := readRun(ctx, client, r.ID)
switch r.Status {
case tfe.RunApplied:
fmt.Println("Run finished!")
case tfe.RunErrored:
fmt.Println("Run had errors!")
logRunErrors(ctx, client, r)
break poll
default:
fmt.Printf("Waiting for run to error... Run status was %q...\n", r.Status)
}
}
}
================================================
FILE: examples/run_errors/terraform/main.tf
================================================
# Copyright IBM Corp. 2018, 2025
# SPDX-License-Identifier: MPL-2.0
terraform {
cloud {
workspaces {
name = "go-tfe-examples-run_errors"
}
}
}
# The following example should return an error
data "http" "example_head" {
url = "https://this-shall-not-exist.hashicorp.com/example"
method = "GET"
}
================================================
FILE: examples/state_versions/main.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package main
import (
"context"
"crypto/md5"
"fmt"
"log"
"os"
tfe "github.com/hashicorp/go-tfe"
)
func main() {
ctx := context.Background()
client, err := tfe.NewClient(&tfe.Config{
RetryServerErrors: true,
})
if err != nil {
log.Fatal(err)
}
// Lock the workspace
if _, err = client.Workspaces.Lock(ctx, "ws-12345678", tfe.WorkspaceLockOptions{}); err != nil {
log.Fatal(err)
}
state, err := os.ReadFile("state.json")
if err != nil {
log.Fatal(err)
}
// Create upload options that does not contain a State attribute within the create options
options := tfe.StateVersionUploadOptions{
StateVersionCreateOptions: tfe.StateVersionCreateOptions{
Lineage: tfe.String("493f7758-da5e-229e-7872-ea1f78ebe50a"),
Serial: tfe.Int64(int64(2)),
MD5: tfe.String(fmt.Sprintf("%x", md5.Sum(state))),
Force: tfe.Bool(false),
},
RawState: state,
}
// Upload a state version
if _, err = client.StateVersions.Upload(ctx, "ws-12345678", options); err != nil {
log.Fatal(err)
}
// Unlock the workspace
if _, err = client.Workspaces.Unlock(ctx, "ws-12345678"); err != nil {
log.Fatal(err)
}
}
================================================
FILE: examples/state_versions/state.json
================================================
{
"version": 4,
"terraform_version": "1.3.9",
"serial": 2,
"lineage": "493f7758-da5e-229e-7872-ea1f78ebe50a",
"outputs": {
"name": {
"value": "",
"type": "string"
}
},
"resources": [
{
"mode": "managed",
"type": "null_resource",
"name": "null",
"provider": "provider[\"registry.terraform.io/hashicorp/null\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"id": "6593301963468675161",
"triggers": {
"creating": "ai-generated content",
"critical": "code",
"even": "[2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50]",
"happy": "gilmore",
"key": "value2",
"napkin": "piano",
"odd": "[1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49]",
"product": "type",
"responsible": "damages to your mainframe",
"slam": "dunk",
"super": "heroes",
"system": "or otherwise",
"warning": "do not operate"
}
},
"sensitive_attributes": []
}
]
}
],
"check_results": null
}
================================================
FILE: examples/users/main.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package main
import (
"context"
"log"
tfe "github.com/hashicorp/go-tfe"
)
func main() {
config := &tfe.Config{
Token: "insert Your user token here",
RetryServerErrors: true,
}
client, err := tfe.NewClient(config)
if err != nil {
log.Fatal(err)
}
// Create a context
ctx := context.Background()
// Read Current User Details
user, err := client.Users.ReadCurrent(ctx)
if err != nil {
log.Fatal(err)
}
log.Printf("%v", user)
}
================================================
FILE: examples/workspaces/main.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package main
import (
"context"
"log"
"time"
tfe "github.com/hashicorp/go-tfe"
)
func main() {
config := &tfe.Config{
Token: "insert-your-token-here",
RetryServerErrors: true,
}
client, err := tfe.NewClient(config)
if err != nil {
log.Fatal(err)
}
// Create a context
ctx := context.Background()
// Create a new workspace
w, err := client.Workspaces.Create(ctx, "org-name", tfe.WorkspaceCreateOptions{
Name: tfe.String("my-app-tst"),
AutoDestroyAt: tfe.NullableTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)),
InheritsProjectAutoDestroy: tfe.Bool(false),
})
if err != nil {
log.Fatal(err)
}
// Update the workspace
w, err = client.Workspaces.Update(ctx, "org-name", w.Name, tfe.WorkspaceUpdateOptions{
AutoApply: tfe.Bool(false),
TerraformVersion: tfe.String("0.11.1"),
WorkingDirectory: tfe.String("my-app/infra"),
AutoDestroyAt: tfe.NullableTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)),
InheritsProjectAutoDestroy: tfe.Bool(false),
})
if err != nil {
log.Fatal(err)
}
// Disable auto destroy
w, err = client.Workspaces.Update(ctx, "org-name", w.Name, tfe.WorkspaceUpdateOptions{
AutoDestroyAt: tfe.NullTime(),
})
if err != nil {
log.Fatal(err)
}
}
================================================
FILE: gcp_oidc_configuration.go
================================================
package tfe
import (
"context"
"fmt"
"net/url"
)
// GCPOIDCConfigurations describes all the GCP OIDC configuration related methods that the HCP Terraform API supports.
// HCP Terraform API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/hold-your-own-key/oidc-configurations/gcp
type GCPOIDCConfigurations interface {
Create(ctx context.Context, organization string, options GCPOIDCConfigurationCreateOptions) (*GCPOIDCConfiguration, error)
Read(ctx context.Context, oidcID string) (*GCPOIDCConfiguration, error)
Update(ctx context.Context, oidcID string, options GCPOIDCConfigurationUpdateOptions) (*GCPOIDCConfiguration, error)
Delete(ctx context.Context, oidcID string) error
}
type gcpOIDCConfigurations struct {
client *Client
}
var _ GCPOIDCConfigurations = &gcpOIDCConfigurations{}
type GCPOIDCConfiguration struct {
ID string `jsonapi:"primary,gcp-oidc-configurations"`
ServiceAccountEmail string `jsonapi:"attr,service-account-email"`
ProjectNumber string `jsonapi:"attr,project-number"`
WorkloadProviderName string `jsonapi:"attr,workload-provider-name"`
Organization *Organization `jsonapi:"relation,organization"`
}
type GCPOIDCConfigurationCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,gcp-oidc-configurations"`
// Attributes
ServiceAccountEmail string `jsonapi:"attr,service-account-email"`
ProjectNumber string `jsonapi:"attr,project-number"`
WorkloadProviderName string `jsonapi:"attr,workload-provider-name"`
}
type GCPOIDCConfigurationUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,gcp-oidc-configurations"`
// Attributes
ServiceAccountEmail *string `jsonapi:"attr,service-account-email,omitempty"`
ProjectNumber *string `jsonapi:"attr,project-number,omitempty"`
WorkloadProviderName *string `jsonapi:"attr,workload-provider-name,omitempty"`
}
func (o *GCPOIDCConfigurationCreateOptions) valid() error {
if o.ServiceAccountEmail == "" {
return ErrRequiredServiceAccountEmail
}
if o.ProjectNumber == "" {
return ErrRequiredProjectNumber
}
if o.WorkloadProviderName == "" {
return ErrRequiredWorkloadProviderName
}
return nil
}
func (goc *gcpOIDCConfigurations) Create(ctx context.Context, organization string, options GCPOIDCConfigurationCreateOptions) (*GCPOIDCConfiguration, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
req, err := goc.client.NewRequest("POST", fmt.Sprintf("organizations/%s/oidc-configurations", url.PathEscape(organization)), &options)
if err != nil {
return nil, err
}
gcpOIDCConfiguration := &GCPOIDCConfiguration{}
err = req.Do(ctx, gcpOIDCConfiguration)
if err != nil {
return nil, err
}
return gcpOIDCConfiguration, nil
}
func (goc *gcpOIDCConfigurations) Read(ctx context.Context, oidcID string) (*GCPOIDCConfiguration, error) {
req, err := goc.client.NewRequest("GET", fmt.Sprintf(OIDCConfigPathFormat, url.PathEscape(oidcID)), nil)
if err != nil {
return nil, err
}
gcpOIDCConfiguration := &GCPOIDCConfiguration{}
err = req.Do(ctx, gcpOIDCConfiguration)
if err != nil {
return nil, err
}
return gcpOIDCConfiguration, nil
}
func (goc *gcpOIDCConfigurations) Update(ctx context.Context, oidcID string, options GCPOIDCConfigurationUpdateOptions) (*GCPOIDCConfiguration, error) {
if !validStringID(&oidcID) {
return nil, ErrInvalidOIDC
}
req, err := goc.client.NewRequest("PATCH", fmt.Sprintf(OIDCConfigPathFormat, url.PathEscape(oidcID)), &options)
if err != nil {
return nil, err
}
gcpOIDCConfiguration := &GCPOIDCConfiguration{}
err = req.Do(ctx, gcpOIDCConfiguration)
if err != nil {
return nil, err
}
return gcpOIDCConfiguration, nil
}
func (goc *gcpOIDCConfigurations) Delete(ctx context.Context, oidcID string) error {
if !validStringID(&oidcID) {
return ErrInvalidOIDC
}
req, err := goc.client.NewRequest("DELETE", fmt.Sprintf(OIDCConfigPathFormat, url.PathEscape(oidcID)), nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
================================================
FILE: gcp_oidc_configuration_integration_test.go
================================================
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// These tests are intended for local execution only, as OIDC configurations for HYOK requires specific conditions.
// To run them locally, follow the instructions outlined in hyok_configuration_integration_test.go
func TestGCPOIDCConfigurationCreateDelete(t *testing.T) {
t.Parallel()
skipHYOKIntegrationTests(t)
client := testClient(t)
ctx := context.Background()
orgTest := testHyokOrganization(t, client)
t.Run("with valid options", func(t *testing.T) {
opts := GCPOIDCConfigurationCreateOptions{
ServiceAccountEmail: "updated-service-account@example.iam.gserviceaccount.com",
ProjectNumber: "123456789012",
WorkloadProviderName: randomString(t),
}
oidcConfig, err := client.GCPOIDCConfigurations.Create(ctx, orgTest.Name, opts)
require.NoError(t, err)
require.NotNil(t, oidcConfig)
assert.Equal(t, oidcConfig.ServiceAccountEmail, opts.ServiceAccountEmail)
assert.Equal(t, oidcConfig.ProjectNumber, opts.ProjectNumber)
assert.Equal(t, oidcConfig.WorkloadProviderName, opts.WorkloadProviderName)
// delete the created configuration
err = client.GCPOIDCConfigurations.Delete(ctx, oidcConfig.ID)
require.NoError(t, err)
})
t.Run("missing workload provider name", func(t *testing.T) {
opts := GCPOIDCConfigurationCreateOptions{
ServiceAccountEmail: "updated-service-account@example.iam.gserviceaccount.com",
ProjectNumber: "123456789012",
}
_, err := client.GCPOIDCConfigurations.Create(ctx, orgTest.Name, opts)
assert.ErrorIs(t, err, ErrRequiredWorkloadProviderName)
})
t.Run("missing service account email", func(t *testing.T) {
opts := GCPOIDCConfigurationCreateOptions{
ProjectNumber: "123456789012",
WorkloadProviderName: randomString(t),
}
_, err := client.GCPOIDCConfigurations.Create(ctx, orgTest.Name, opts)
assert.ErrorIs(t, err, ErrRequiredServiceAccountEmail)
})
t.Run("missing project number", func(t *testing.T) {
opts := GCPOIDCConfigurationCreateOptions{
ServiceAccountEmail: "updated-service-account@example.iam.gserviceaccount.com",
WorkloadProviderName: randomString(t),
}
_, err := client.GCPOIDCConfigurations.Create(ctx, orgTest.Name, opts)
assert.ErrorIs(t, err, ErrRequiredProjectNumber)
})
}
func TestGCPOIDCConfigurationRead(t *testing.T) {
t.Parallel()
skipHYOKIntegrationTests(t)
client := testClient(t)
ctx := context.Background()
orgTest := testHyokOrganization(t, client)
oidcConfig, oidcConfigCleanup := createGCPOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcConfigCleanup)
t.Run("fetch existing configuration", func(t *testing.T) {
fetched, err := client.GCPOIDCConfigurations.Read(ctx, oidcConfig.ID)
require.NoError(t, err)
require.NotEmpty(t, fetched)
})
t.Run("fetching non-existing configuration", func(t *testing.T) {
_, err := client.GCPOIDCConfigurations.Read(ctx, "gcpoidc-notreal")
assert.ErrorIs(t, err, ErrResourceNotFound)
})
}
func TestGCPOIDCConfigurationUpdate(t *testing.T) {
t.Parallel()
skipHYOKIntegrationTests(t)
client := testClient(t)
ctx := context.Background()
orgTest := testHyokOrganization(t, client)
t.Run("update all fields", func(t *testing.T) {
oidcConfig, oidcConfigCleanup := createGCPOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcConfigCleanup)
serviceAccountEmail := "updated-service-account@example.iam.gserviceaccount.com"
projectNumber := "123456789012"
workloadProviderName := randomString(t)
opts := GCPOIDCConfigurationUpdateOptions{
ServiceAccountEmail: &serviceAccountEmail,
ProjectNumber: &projectNumber,
WorkloadProviderName: &workloadProviderName,
}
updated, err := client.GCPOIDCConfigurations.Update(ctx, oidcConfig.ID, opts)
require.NoError(t, err)
require.NotNil(t, updated)
assert.Equal(t, *opts.ServiceAccountEmail, updated.ServiceAccountEmail)
assert.Equal(t, *opts.ProjectNumber, updated.ProjectNumber)
assert.Equal(t, *opts.WorkloadProviderName, updated.WorkloadProviderName)
})
t.Run("workload provider name not provided", func(t *testing.T) {
oidcConfig, oidcConfigCleanup := createGCPOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcConfigCleanup)
serviceAccountEmail := "updated-service-account@example.iam.gserviceaccount.com"
projectNumber := "123456789012"
opts := GCPOIDCConfigurationUpdateOptions{
ServiceAccountEmail: &serviceAccountEmail,
ProjectNumber: &projectNumber,
}
updated, err := client.GCPOIDCConfigurations.Update(ctx, oidcConfig.ID, opts)
require.NoError(t, err)
require.NotNil(t, updated)
assert.Equal(t, *opts.ServiceAccountEmail, updated.ServiceAccountEmail)
assert.Equal(t, *opts.ProjectNumber, updated.ProjectNumber)
assert.Equal(t, oidcConfig.WorkloadProviderName, updated.WorkloadProviderName) // not updated
})
t.Run("service account email not provided", func(t *testing.T) {
oidcConfig, oidcConfigCleanup := createGCPOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcConfigCleanup)
projectNumber := "123456789012"
workloadProviderName := randomString(t)
opts := GCPOIDCConfigurationUpdateOptions{
ProjectNumber: &projectNumber,
WorkloadProviderName: &workloadProviderName,
}
updated, err := client.GCPOIDCConfigurations.Update(ctx, oidcConfig.ID, opts)
require.NoError(t, err)
require.NotNil(t, updated)
assert.Equal(t, oidcConfig.ServiceAccountEmail, updated.ServiceAccountEmail) // not updated
assert.Equal(t, *opts.ProjectNumber, updated.ProjectNumber)
assert.Equal(t, *opts.WorkloadProviderName, updated.WorkloadProviderName)
})
t.Run("project number not provided", func(t *testing.T) {
oidcConfig, oidcConfigCleanup := createGCPOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcConfigCleanup)
serviceAccountEmail := "updated-service-account@example.iam.gserviceaccount.com"
workloadProviderName := randomString(t)
opts := GCPOIDCConfigurationUpdateOptions{
ServiceAccountEmail: &serviceAccountEmail,
WorkloadProviderName: &workloadProviderName,
}
updated, err := client.GCPOIDCConfigurations.Update(ctx, oidcConfig.ID, opts)
require.NoError(t, err)
require.NotNil(t, updated)
assert.Equal(t, *opts.ServiceAccountEmail, updated.ServiceAccountEmail)
assert.Equal(t, oidcConfig.ProjectNumber, updated.ProjectNumber) // not updated
assert.Equal(t, *opts.WorkloadProviderName, updated.WorkloadProviderName)
})
}
================================================
FILE: generate_mocks.sh
================================================
#!/bin/bash
# Copyright IBM Corp. 2018, 2026
# SPDX-License-Identifier: MPL-2.0
set -euf -o pipefail
mockgen -source=admin_opa_version.go -destination=mocks/admin_opa_version_mocks.go -package=mocks
mockgen -source=admin_organization.go -destination=mocks/admin_organization_mocks.go -package=mocks
mockgen -source=admin_run.go -destination=mocks/admin_run_mocks.go -package=mocks
mockgen -source=admin_sentinel_version.go -destination=mocks/admin_sentinel_version_mocks.go -package=mocks
mockgen -source=admin_setting.go -destination=mocks/admin_setting_mocks.go -package=mocks
mockgen -source=admin_setting_cost_estimation.go -destination=mocks/admin_setting_cost_estimation_mocks.go -package=mocks
mockgen -source=admin_setting_customization.go -destination=mocks/admin_setting_customization_mocks.go -package=mocks
mockgen -source=admin_setting_general.go -destination=mocks/admin_setting_general_mocks.go -package=mocks
mockgen -source=admin_setting_oidc.go -destination=mocks/admin_setting_oidc_mocks.go -package=mocks
mockgen -source=admin_setting_saml.go -destination=mocks/admin_setting_saml_mocks.go -package=mocks
mockgen -source=admin_setting_scim.go -destination=mocks/admin_setting_scim_mocks.go -package=mocks
mockgen -source=admin_setting_scim_token.go -destination=mocks/admin_setting_scim_token_mocks.go -package=mocks
mockgen -source=admin_setting_scim_groups.go -destination=mocks/admin_setting_scim_groups_mocks.go -package=mocks
mockgen -source=admin_setting_smtp.go -destination=mocks/admin_setting_smtp_mocks.go -package=mocks
mockgen -source=admin_setting_twilio.go -destination=mocks/admin_setting_twilio_mocks.go -package=mocks
mockgen -source=admin_terraform_version.go -destination=mocks/admin_terraform_version_mocks.go -package=mocks
mockgen -source=admin_user.go -destination=mocks/admin_user_mocks.go -package=mocks
mockgen -source=admin_workspace.go -destination=mocks/admin_workspace_mocks.go -package=mocks
mockgen -source=agent.go -destination=mocks/agents.go -package=mocks
mockgen -source=agent_pool.go -destination=mocks/agent_pool_mocks.go -package=mocks
mockgen -source=agent_token.go -destination=mocks/agent_token_mocks.go -package=mocks
mockgen -source=apply.go -destination=mocks/apply_mocks.go -package=mocks
mockgen -source=audit_trail.go -destination=mocks/audit_trail_mocks.go -package=mocks
mockgen -source=comment.go -destination=mocks/comment_mocks.go -package=mocks
mockgen -source=configuration_version.go -destination=mocks/configuration_version_mocks.go -package=mocks
mockgen -source=cost_estimate.go -destination=mocks/cost_estimate_mocks.go -package=mocks
mockgen -source=github_app_installation.go -destination=mocks/github_app_installation_mocks.go -package=mocks
mockgen -source=gpg_key.go -destination=mocks/gpg_key_mocks.go -package=mocks
mockgen -source=ip_ranges.go -destination=mocks/ip_ranges_mocks.go -package=mocks
mockgen -source=logreader.go -destination=mocks/logreader_mocks.go -package=mocks
mockgen -source=notification_configuration.go -destination=mocks/notification_configuration_mocks.go -package=mocks
mockgen -source=oauth_client.go -destination=mocks/oauth_client_mocks.go -package=mocks
mockgen -source=oauth_token.go -destination=mocks/oauth_token_mocks.go -package=mocks
mockgen -source=organization.go -destination=mocks/organization_mocks.go -package=mocks
mockgen -source=organization_membership.go -destination=mocks/organization_membership_mocks.go -package=mocks
mockgen -source=organization_token.go -destination=mocks/organization_token_mocks.go -package=mocks
mockgen -source=plan.go -destination=mocks/plan_mocks.go -package=mocks
mockgen -source=plan_export.go -destination=mocks/plan_export_mocks.go -package=mocks
mockgen -source=policy.go -destination=mocks/policy_mocks.go -package=mocks
mockgen -source=policy_check.go -destination=mocks/policy_check_mocks.go -package=mocks
mockgen -source=policy_set.go -destination=mocks/policy_set_mocks.go -package=mocks
mockgen -source=policy_set_parameter.go -destination=mocks/policy_set_parameter_mocks.go -package=mocks
mockgen -source=policy_set_version.go -destination=mocks/policy_set_version_mocks.go -package=mocks
mockgen -source=registry_module.go -destination=mocks/registry_module_mocks.go -package=mocks
mockgen -source=registry_provider.go -destination=mocks/registry_provider_mocks.go -package=mocks
mockgen -source=registry_provider_platform.go -destination=mocks/registry_provider_platform_mocks.go -package=mocks
mockgen -source=registry_provider_version.go -destination=mocks/registry_provider_version_mocks.go -package=mocks
mockgen -source=query_runs.go -destination=mocks/query_runs_mocks.go -package=mocks
mockgen -source=run.go -destination=mocks/run_mocks.go -package=mocks
mockgen -source=run_event.go -destination=mocks/run_events_mocks.go -package=mocks
mockgen -source=run_task.go -destination=mocks/run_tasks_mocks.go -package=mocks
mockgen -source=run_trigger.go -destination=mocks/run_trigger_mocks.go -package=mocks
mockgen -source=ssh_key.go -destination=mocks/ssh_key_mocks.go -package=mocks
mockgen -source=state_version.go -destination=mocks/state_version_mocks.go -package=mocks
mockgen -source=state_version_output.go -destination=mocks/state_version_output_mocks.go -package=mocks
mockgen -source=tag.go -destination=mocks/tag_mocks.go -package=mocks
mockgen -source=task_result.go -destination=mocks/task_result_mocks.go -package=mocks
mockgen -source=task_stages.go -destination=mocks/task_stages_mocks.go -package=mocks
mockgen -source=team.go -destination=mocks/team_mocks.go -package=mocks
mockgen -source=team_access.go -destination=mocks/team_access_mocks.go -package=mocks
mockgen -source=team_member.go -destination=mocks/team_member_mocks.go -package=mocks
mockgen -source=team_project_access.go -destination=mocks/team_project_access_mocks.go -package=mocks
mockgen -source=team_token.go -destination=mocks/team_token_mocks.go -package=mocks
mockgen -source=test_run.go -destination=mocks/test_run_mocks.go -package=mocks
mockgen -source=test_variables.go -destination=mocks/test_variables_mocks.go -package=mocks
mockgen -source=user.go -destination=mocks/user_mocks.go -package=mocks
mockgen -source=user_token.go -destination=mocks/user_token_mocks.go -package=mocks
mockgen -source=variable.go -destination=mocks/variable_mocks.go -package=mocks
mockgen -source=variable_set.go -destination=mocks/variable_set_mocks.go -package=mocks
mockgen -source=variable_set_variable.go -destination=mocks/variable_set_variable_mocks.go -package=mocks
mockgen -source=workspace.go -destination=mocks/workspace_mocks.go -package=mocks
mockgen -source=workspace_run_task.go -destination=mocks/workspace_run_tasks_mocks.go -package=mocks
mockgen -source=policy_evaluation.go -destination=mocks/policy_evaluation.go -package=mocks
mockgen -source=project.go -destination=mocks/project_mocks.go -package=mocks
mockgen -source=registry_no_code_module.go -destination=mocks/registry_no_code_module_mocks.go -package=mocks
mockgen -source=registry_module.go -destination=mocks/registry_module_mocks.go -package=mocks
mockgen -source=workspace_resources.go -destination=mocks/workspace_resources.go -package=mocks
================================================
FILE: github_app_installation.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ GHAInstallations = (*gHAInstallations)(nil)
// GHAInstallations describes all the GitHub App Installation related methods that the
// Terraform Enterprise API supports. The APIs require the user token for the user who
// already has the GitHub App Installation set up via the UI.
// (https://developer.hashicorp.com/terraform/enterprise/admin/application/github-app-integration)
type GHAInstallations interface {
// List all the GitHub App Installations for the user.
List(ctx context.Context, options *GHAInstallationListOptions) (*GHAInstallationList, error)
// Read a GitHub App Installations by its external id.
Read(ctx context.Context, GHAInstallationID string) (*GHAInstallation, error)
}
// gHAInstallations implements GHAInstallations.
type gHAInstallations struct {
client *Client
}
// GHAInstallationList represents a list of github installations.
type GHAInstallationList struct {
*Pagination
Items []*GHAInstallation
}
// GHAInstallation represents a github app installation
type GHAInstallation struct {
ID *string `jsonapi:"primary,github-app-installations"`
IconURL *string `jsonapi:"attr,icon-url"`
InstallationID *int `jsonapi:"attr,installation-id"`
InstallationType *string `jsonapi:"attr,installation-type"`
InstallationURL *string `jsonapi:"attr,installation-url"`
Name *string `jsonapi:"attr,name"`
}
// GHAInstallationListOptions represents the options for listing.
type GHAInstallationListOptions struct {
ListOptions
}
// List all the github app installations.
func (s *gHAInstallations) List(ctx context.Context, options *GHAInstallationListOptions) (*GHAInstallationList, error) {
u := "github-app/installations"
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
ghil := &GHAInstallationList{}
err = req.Do(ctx, ghil)
if err != nil {
return nil, err
}
return ghil, nil
}
// Read a GitHub App Installations by its ID.
func (s *gHAInstallations) Read(ctx context.Context, id string) (*GHAInstallation, error) {
if !validStringID(&id) {
return nil, ErrInvalidOauthClientID
}
u := fmt.Sprintf("github-app/installation/%s", url.PathEscape(id))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
ghi := &GHAInstallation{}
err = req.Do(ctx, ghi)
if err != nil {
return nil, err
}
return ghi, err
}
================================================
FILE: github_app_installation_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGHAInstallationList(t *testing.T) {
t.Parallel()
gHAInstallationID := os.Getenv("GITHUB_APP_INSTALLATION_ID")
if gHAInstallationID == "" {
t.Skip("Export a valid GITHUB_APP_INSTALLATION_ID before running this test!")
}
client := testClient(t)
ctx := context.Background()
t.Run("without list options", func(t *testing.T) {
_, err := client.GHAInstallations.List(ctx, nil)
assert.NoError(t, err)
})
}
func TestGHAInstallationRead(t *testing.T) {
t.Parallel()
gHAInstallationID := os.Getenv("GITHUB_APP_INSTALLATION_ID")
if gHAInstallationID == "" {
t.Skip("Export a valid GITHUB_APP_INSTALLATION_ID before running this test!")
}
var GHAInstallationID = string(gHAInstallationID)
client := testClient(t)
ctx := context.Background()
t.Run("when installation id exists", func(t *testing.T) {
ghais, err := client.GHAInstallations.Read(ctx, GHAInstallationID)
require.NoError(t, err)
assert.NotEmpty(t, ghais.IconURL)
assert.NotEmpty(t, ghais.ID)
assert.NotEmpty(t, ghais.InstallationID)
assert.NotEmpty(t, ghais.InstallationType)
assert.NotEmpty(t, ghais.InstallationURL)
assert.NotEmpty(t, ghais.Name)
assert.Equal(t, *ghais.ID, gHAInstallationID)
})
}
================================================
FILE: go.mod
================================================
module github.com/hashicorp/go-tfe
go 1.25.0
require (
github.com/google/go-querystring v1.2.0
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-retryablehttp v0.7.8
github.com/hashicorp/go-slug v0.16.8
github.com/hashicorp/go-uuid v1.0.3
github.com/hashicorp/go-version v1.8.0
github.com/hashicorp/jsonapi v1.4.3-0.20250220162346-81a76b606f3e
github.com/stretchr/testify v1.11.1
go.uber.org/mock v0.4.0
golang.org/x/sync v0.19.0
golang.org/x/time v0.14.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.35.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: go.sum
================================================
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
github.com/hashicorp/go-slug v0.16.8 h1:f4/sDZqRsxx006HrE6e9BE5xO9lWXydKhVoH6Kb0v1M=
github.com/hashicorp/go-slug v0.16.8/go.mod h1:hB4mUcVHl4RPu0205s0fwmB9i31MxQgeafGkko3FD+Y=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/jsonapi v1.4.3-0.20250220162346-81a76b606f3e h1:xwy/1T0cxHWaLx2MM0g4BlaQc1BXn/9835mPrBqwSPU=
github.com/hashicorp/jsonapi v1.4.3-0.20250220162346-81a76b606f3e/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: gpg_key.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"strings"
"time"
)
// Compile-time proof of interface implementation
var _ GPGKeys = (*gpgKeys)(nil)
// GPGKeys describes all the GPG key related methods that the Terraform Private Registry API supports.
//
// TFE API Docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/private-registry/gpg-keys
type GPGKeys interface {
// Lists GPG keys in a private registry.
ListPrivate(ctx context.Context, options GPGKeyListOptions) (*GPGKeyList, error)
// Uploads a GPG Key to a private registry scoped with a namespace.
Create(ctx context.Context, registryName RegistryName, options GPGKeyCreateOptions) (*GPGKey, error)
// Read a GPG key.
Read(ctx context.Context, keyID GPGKeyID) (*GPGKey, error)
// Update a GPG key.
Update(ctx context.Context, keyID GPGKeyID, options GPGKeyUpdateOptions) (*GPGKey, error)
// Delete a GPG key.
Delete(ctx context.Context, keyID GPGKeyID) error
}
// gpgKeys implements GPGKeys
type gpgKeys struct {
client *Client
}
// GPGKeyList represents a list of GPG keys.
type GPGKeyList struct {
*Pagination
Items []*GPGKey
}
// GPGKey represents a signed GPG key for a HCP Terraform or Terraform Enterprise private provider.
type GPGKey struct {
ID string `jsonapi:"primary,gpg-keys"`
AsciiArmor string `jsonapi:"attr,ascii-armor"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
KeyID string `jsonapi:"attr,key-id"`
Namespace string `jsonapi:"attr,namespace"`
Source string `jsonapi:"attr,source"`
SourceURL *string `jsonapi:"attr,source-url"`
TrustSignature string `jsonapi:"attr,trust-signature"`
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
}
// GPGKeyID represents the set of identifiers used to fetch a GPG key.
type GPGKeyID struct {
RegistryName RegistryName
Namespace string
KeyID string
}
// GPGKeyListOptions represents all the available options to list keys in a registry.
type GPGKeyListOptions struct {
ListOptions
// Required: A list of one or more namespaces. Must be authorized HCP Terraform or Terraform Enterprise organization names.
Namespaces []string `url:"filter[namespace]"`
}
// GPGKeyCreateOptions represents all the available options used to create a GPG key.
type GPGKeyCreateOptions struct {
Type string `jsonapi:"primary,gpg-keys"`
Namespace string `jsonapi:"attr,namespace"`
AsciiArmor string `jsonapi:"attr,ascii-armor"`
}
// GPGKeyCreateOptions represents all the available options used to update a GPG key.
type GPGKeyUpdateOptions struct {
Type string `jsonapi:"primary,gpg-keys"`
Namespace string `jsonapi:"attr,namespace"`
}
// ListPrivate lists the private registry GPG keys for specified namespaces.
func (s *gpgKeys) ListPrivate(ctx context.Context, options GPGKeyListOptions) (*GPGKeyList, error) {
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("/api/registry/%s/v2/gpg-keys", url.PathEscape(string(PrivateRegistry)))
req, err := s.client.NewRequest("GET", u, &options)
if err != nil {
return nil, err
}
keyl := &GPGKeyList{}
err = req.Do(ctx, keyl)
if err != nil {
return nil, err
}
return keyl, nil
}
func (s *gpgKeys) Create(ctx context.Context, registryName RegistryName, options GPGKeyCreateOptions) (*GPGKey, error) {
if err := options.valid(); err != nil {
return nil, err
}
if registryName != PrivateRegistry {
return nil, ErrInvalidRegistryName
}
u := fmt.Sprintf("/api/registry/%s/v2/gpg-keys", url.PathEscape(string(registryName)))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
g := &GPGKey{}
err = req.Do(ctx, g)
if err != nil {
return nil, err
}
return g, nil
}
func (s *gpgKeys) Read(ctx context.Context, keyID GPGKeyID) (*GPGKey, error) {
if err := keyID.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("/api/registry/%s/v2/gpg-keys/%s/%s",
url.PathEscape(string(keyID.RegistryName)),
url.PathEscape(keyID.Namespace),
url.PathEscape(keyID.KeyID),
)
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
g := &GPGKey{}
err = req.Do(ctx, g)
if err != nil {
return nil, err
}
return g, nil
}
func (s *gpgKeys) Update(ctx context.Context, keyID GPGKeyID, options GPGKeyUpdateOptions) (*GPGKey, error) {
if err := options.valid(); err != nil {
return nil, err
}
if err := keyID.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("/api/registry/%s/v2/gpg-keys/%s/%s",
url.PathEscape(string(keyID.RegistryName)),
url.PathEscape(keyID.Namespace),
url.PathEscape(keyID.KeyID),
)
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
g := &GPGKey{}
err = req.Do(ctx, g)
if err != nil {
if strings.Contains(err.Error(), "namespace not authorized") {
return nil, ErrNamespaceNotAuthorized
}
return nil, err
}
return g, nil
}
func (s *gpgKeys) Delete(ctx context.Context, keyID GPGKeyID) error {
if err := keyID.valid(); err != nil {
return err
}
u := fmt.Sprintf("/api/registry/%s/v2/gpg-keys/%s/%s",
url.PathEscape(string(keyID.RegistryName)),
url.PathEscape(keyID.Namespace),
url.PathEscape(keyID.KeyID),
)
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o GPGKeyID) valid() error {
if o.RegistryName != PrivateRegistry {
return ErrInvalidRegistryName
}
if !validString(&o.Namespace) {
return ErrInvalidNamespace
}
if !validString(&o.KeyID) {
return ErrInvalidKeyID
}
return nil
}
func (o *GPGKeyListOptions) valid() error {
if len(o.Namespaces) == 0 {
return ErrInvalidNamespace
}
for _, namespace := range o.Namespaces {
if namespace == "" || !validString(&namespace) {
return ErrInvalidNamespace
}
}
return nil
}
func (o GPGKeyCreateOptions) valid() error {
if !validString(&o.Namespace) {
return ErrInvalidNamespace
}
if !validString(&o.AsciiArmor) {
return ErrInvalidAsciiArmor
}
return nil
}
func (o GPGKeyUpdateOptions) valid() error {
if !validString(&o.Namespace) {
return ErrInvalidNamespace
}
return nil
}
================================================
FILE: gpg_key_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGPGKeyList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
org1, org1Cleanup := createOrganization(t, client)
t.Cleanup(org1Cleanup)
org2, org2Cleanup := createOrganization(t, client)
t.Cleanup(org2Cleanup)
provider1, provider1Cleanup := createRegistryProvider(t, client, org1, PrivateRegistry)
t.Cleanup(provider1Cleanup)
provider2, provider2Cleanup := createRegistryProvider(t, client, org2, PrivateRegistry)
t.Cleanup(provider2Cleanup)
gpgKey1, gpgKey1Cleanup := createGPGKey(t, client, org1, provider1)
t.Cleanup(gpgKey1Cleanup)
gpgKey2, gpgKey2Cleanup := createGPGKey(t, client, org2, provider2)
t.Cleanup(gpgKey2Cleanup)
t.Run("with single namespace", func(t *testing.T) {
opts := GPGKeyListOptions{
Namespaces: []string{org1.Name},
}
keyl, err := client.GPGKeys.ListPrivate(ctx, opts)
require.NoError(t, err)
require.Len(t, keyl.Items, 1)
assert.Equal(t, gpgKey1.ID, keyl.Items[0].ID)
assert.Equal(t, gpgKey1.KeyID, keyl.Items[0].KeyID)
})
t.Run("with multiple namespaces", func(t *testing.T) {
t.Skip("Skipping due to GPG Key API not returning keys for multiple namespaces")
opts := GPGKeyListOptions{
Namespaces: []string{org1.Name, org2.Name},
}
keyl, err := client.GPGKeys.ListPrivate(ctx, opts)
require.NoError(t, err)
require.Len(t, keyl.Items, 2)
for i, key := range []*GPGKey{
gpgKey1,
gpgKey2,
} {
assert.Equal(t, key.ID, keyl.Items[i].ID)
assert.Equal(t, key.KeyID, keyl.Items[i].KeyID)
}
})
t.Run("with list options", func(t *testing.T) {
opts := GPGKeyListOptions{
Namespaces: []string{org1.Name},
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
}
keyl, err := client.GPGKeys.ListPrivate(ctx, opts)
require.NoError(t, err)
require.Empty(t, keyl.Items)
assert.Equal(t, 999, keyl.CurrentPage)
assert.Equal(t, 1, keyl.TotalCount)
})
t.Run("with invalid options", func(t *testing.T) {
t.Run("invalid namespace", func(t *testing.T) {
opts := GPGKeyListOptions{
Namespaces: []string{},
}
_, err := client.GPGKeys.ListPrivate(ctx, opts)
require.EqualError(t, err, ErrInvalidNamespace.Error())
})
})
}
func TestGPGKeyCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
org, orgCleanup := createOrganization(t, client)
t.Cleanup(orgCleanup)
provider, providerCleanup := createRegistryProvider(t, client, org, PrivateRegistry)
t.Cleanup(providerCleanup)
t.Run("with valid options", func(t *testing.T) {
opts := GPGKeyCreateOptions{
Namespace: provider.Organization.Name,
AsciiArmor: testGpgArmor,
}
gpgKey, err := client.GPGKeys.Create(ctx, PrivateRegistry, opts)
require.NoError(t, err)
assert.NotEmpty(t, gpgKey.ID)
assert.Equal(t, gpgKey.AsciiArmor, opts.AsciiArmor)
assert.Equal(t, gpgKey.Namespace, opts.Namespace)
assert.NotEmpty(t, gpgKey.CreatedAt)
assert.NotEmpty(t, gpgKey.UpdatedAt)
// The default value for these two fields is an empty string
assert.Empty(t, gpgKey.Source)
assert.Empty(t, gpgKey.TrustSignature)
})
t.Run("with invalid registry name", func(t *testing.T) {
opts := GPGKeyCreateOptions{
Namespace: provider.Organization.Name,
AsciiArmor: testGpgArmor,
}
_, err := client.GPGKeys.Create(ctx, "foobar", opts)
assert.ErrorIs(t, err, ErrInvalidRegistryName)
})
t.Run("with invalid options", func(t *testing.T) {
missingNamespaceOpts := GPGKeyCreateOptions{
Namespace: "",
AsciiArmor: testGpgArmor,
}
_, err := client.GPGKeys.Create(ctx, PrivateRegistry, missingNamespaceOpts)
assert.ErrorIs(t, err, ErrInvalidNamespace)
missingAsciiArmorOpts := GPGKeyCreateOptions{
Namespace: provider.Organization.Name,
AsciiArmor: "",
}
_, err = client.GPGKeys.Create(ctx, PrivateRegistry, missingAsciiArmorOpts)
assert.ErrorIs(t, err, ErrInvalidAsciiArmor)
})
}
func TestGPGKeyRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
org, orgCleanup := createOrganization(t, client)
t.Cleanup(orgCleanup)
provider, providerCleanup := createRegistryProvider(t, client, org, PrivateRegistry)
t.Cleanup(providerCleanup)
gpgKey, gpgKeyCleanup := createGPGKey(t, client, org, provider)
t.Cleanup(gpgKeyCleanup)
t.Run("when the gpg key exists", func(t *testing.T) {
fetched, err := client.GPGKeys.Read(ctx, GPGKeyID{
RegistryName: PrivateRegistry,
Namespace: provider.Organization.Name,
KeyID: gpgKey.KeyID,
})
require.NoError(t, err)
assert.NotEmpty(t, gpgKey.ID)
assert.NotEmpty(t, gpgKey.KeyID)
assert.Greater(t, len(gpgKey.AsciiArmor), 0)
assert.Equal(t, fetched.Namespace, provider.Organization.Name)
})
t.Run("when the key does not exist", func(t *testing.T) {
_, err := client.GPGKeys.Read(ctx, GPGKeyID{
RegistryName: PrivateRegistry,
Namespace: provider.Organization.Name,
KeyID: "foobar",
})
assert.ErrorIs(t, err, ErrResourceNotFound)
})
}
func TestGPGKeyUpdate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
org, orgCleanup := createOrganization(t, client)
t.Cleanup(orgCleanup)
provider, providerCleanup := createRegistryProvider(t, client, org, PrivateRegistry)
t.Cleanup(providerCleanup)
// We won't use the cleanup method here as the namespace
// is used to identify a key and that will change due to the update
// call. We'll need to manually delete the key.
gpgKey, _ := createGPGKey(t, client, org, provider)
t.Run("when using an invalid namespace", func(t *testing.T) {
keyID := GPGKeyID{
RegistryName: PrivateRegistry,
Namespace: provider.Organization.Name,
KeyID: gpgKey.KeyID,
}
opts := GPGKeyUpdateOptions{
Namespace: "invalid_namespace_org",
}
_, err := client.GPGKeys.Update(ctx, keyID, opts)
assert.ErrorIs(t, err, ErrNamespaceNotAuthorized)
})
t.Run("when updating to a valid namespace", func(t *testing.T) {
// Create a new namespace to update the key with
org2, org2Cleanup := createOrganization(t, client)
t.Cleanup(org2Cleanup)
provider2, provider2Cleanup := createRegistryProvider(t, client, org2, PrivateRegistry)
t.Cleanup(provider2Cleanup)
keyID := GPGKeyID{
RegistryName: PrivateRegistry,
Namespace: provider.Organization.Name,
KeyID: gpgKey.KeyID,
}
opts := GPGKeyUpdateOptions{
Namespace: provider2.Organization.Name,
}
updatedKey, err := client.GPGKeys.Update(ctx, keyID, opts)
require.NoError(t, err)
assert.Equal(t, gpgKey.KeyID, updatedKey.KeyID)
assert.Equal(t, updatedKey.Namespace, provider2.Organization.Name)
// Cleanup
err = client.GPGKeys.Delete(ctx, GPGKeyID{
RegistryName: PrivateRegistry,
Namespace: provider2.Organization.Name,
KeyID: updatedKey.KeyID,
})
require.NoError(t, err)
})
}
func TestGPGKeyDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
org, orgCleanup := createOrganization(t, client)
t.Cleanup(orgCleanup)
provider, providerCleanup := createRegistryProvider(t, client, org, PrivateRegistry)
t.Cleanup(providerCleanup)
gpgKey, _ := createGPGKey(t, client, org, provider)
t.Run("when a key exists", func(t *testing.T) {
err := client.GPGKeys.Delete(ctx, GPGKeyID{
RegistryName: PrivateRegistry,
Namespace: provider.Organization.Name,
KeyID: gpgKey.KeyID,
})
require.NoError(t, err)
})
}
================================================
FILE: helper_test.go
================================================
// Copyright IBM Corp. 2018, 2026
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"archive/tar"
"archive/zip"
"compress/gzip"
"context"
"crypto/hmac"
"crypto/md5"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"io"
"math/rand"
"net/http"
"net/http/httptest"
"net/url"
"os"
"os/exec"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
retryablehttp "github.com/hashicorp/go-retryablehttp"
uuid "github.com/hashicorp/go-uuid"
)
const badIdentifier = "! / nope" //nolint
const agentVersion = "1.3.0"
const testInitialClientToken = "insert-your-token-here"
const testTaskResultCallbackToken = "this-is-task-result-callback-token"
const defaultTokenExpirationYears = 2
var _testAccountDetails *TestAccountDetails
func testClient(t *testing.T) *Client {
client, err := NewClient(&Config{
RetryServerErrors: true,
})
if err != nil {
t.Fatal(err)
}
return client
}
type adminRoleType string
const (
siteAdmin adminRoleType = "site-admin"
configurationAdmin adminRoleType = "configuration"
provisionLicensesAdmin adminRoleType = "provision-licenses"
subscriptionAdmin adminRoleType = "subscription"
supportAdmin adminRoleType = "support"
securityMaintenanceAdmin adminRoleType = "security-maintenance"
versionMaintenanceAdmin adminRoleType = "version-maintenance"
)
func getTokenForAdminRole(adminRole adminRoleType) string {
token := ""
switch adminRole {
case siteAdmin:
token = os.Getenv("TFE_ADMIN_SITE_ADMIN_TOKEN")
case configurationAdmin:
token = os.Getenv("TFE_ADMIN_CONFIGURATION_TOKEN")
case provisionLicensesAdmin:
token = os.Getenv("TFE_ADMIN_PROVISION_LICENSES_TOKEN")
case subscriptionAdmin:
token = os.Getenv("TFE_ADMIN_SUBSCRIPTION_TOKEN")
case supportAdmin:
token = os.Getenv("TFE_ADMIN_SUPPORT_TOKEN")
case securityMaintenanceAdmin:
token = os.Getenv("TFE_ADMIN_SECURITY_MAINTENANCE_TOKEN")
case versionMaintenanceAdmin:
token = os.Getenv("TFE_ADMIN_VERSION_MAINTENANCE_TOKEN")
}
return token
}
func testAdminClient(t *testing.T, adminRole adminRoleType) *Client {
token := getTokenForAdminRole(adminRole)
if token == "" {
t.Fatal("missing API token for admin role " + adminRole)
}
client, err := NewClient(&Config{
Token: token,
RetryServerErrors: true,
})
if err != nil {
t.Fatal(err)
}
return client
}
func testAuditTrailClient(t *testing.T, userClient *Client, org *Organization) *Client {
upgradeOrganizationSubscription(t, userClient, org)
orgToken, orgTokenCleanup := createOrganizationToken(t, userClient, org)
t.Cleanup(orgTokenCleanup)
client, err := NewClient(&Config{
Token: orgToken.Token,
})
if err != nil {
t.Fatal(err)
}
return client
}
// TestAccountDetails represents the basic account information
// of a Terraform Enterprise or HCP Terraform user.
//
// See FetchTestAccountDetails for more information.
type TestAccountDetails struct {
ID string `jsonapi:"primary,users"`
Username string `jsonapi:"attr,username"`
Email string `jsonapi:"attr,email"`
}
func fetchTestAccountDetails(t *testing.T, client *Client) *TestAccountDetails {
t.Helper()
if _testAccountDetails == nil {
_testAccountDetails = &TestAccountDetails{}
req, err := client.NewRequest("GET", "account/details", nil)
if err != nil {
t.Fatalf("could not create account details request: %v", err)
}
ctx := context.Background()
err = req.Do(ctx, _testAccountDetails)
if err != nil {
t.Fatalf("could not fetch test user details: %v", err)
}
}
return _testAccountDetails
}
func downloadFile(filePath, fileURL string) error {
// Get the data
resp, err := http.Get(fileURL)
if err != nil {
return err
}
defer resp.Body.Close() //nolint:errcheck
// Create the file
out, err := os.Create(filePath)
if err != nil {
return err
}
defer out.Close()
// Write the body to file
_, err = io.Copy(out, resp.Body)
return err
}
func unzip(src, dest string) error {
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer func() {
if err := r.Close(); err != nil {
panic(err)
}
}()
if err := os.MkdirAll(dest, 0o755); err != nil {
return err
}
// Closure to address file descriptors issue with all the deferred .Close() methods
extractAndWriteFile := func(zf *zip.File) error {
rc, err := zf.Open()
if err != nil {
return err
}
defer func() {
if err := rc.Close(); err != nil {
panic(err)
}
}()
path := filepath.Join(dest, zf.Name)
// Check for ZipSlip (Directory traversal)
if !strings.HasPrefix(path, filepath.Clean(dest)+string(os.PathSeparator)) {
return fmt.Errorf("illegal file path: %s", path)
}
if zf.FileInfo().IsDir() {
return os.MkdirAll(path, zf.Mode())
}
if err := os.MkdirAll(filepath.Dir(path), zf.Mode()); err != nil {
return err
}
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, zf.Mode())
if err != nil {
return err
}
defer func() {
if err := f.Close(); err != nil {
panic(err)
}
}()
_, err = io.Copy(f, rc)
if err != nil {
return err
}
return nil
}
for _, f := range r.File {
err := extractAndWriteFile(f)
if err != nil {
return err
}
}
return nil
}
func downloadTFCAgent(t *testing.T) (string, error) {
t.Helper()
tmpDir, err := os.MkdirTemp("", "tfc-agent")
if err != nil {
return "", fmt.Errorf("cannot create temp dir: %w", err)
}
t.Cleanup(func() {
fmt.Printf("cleaning up %s \n", tmpDir)
os.RemoveAll(tmpDir)
})
agentPath := fmt.Sprintf("https://releases.hashicorp.com/tfc-agent/%s/tfc-agent_%s_linux_amd64.zip", agentVersion, agentVersion)
zipFile := fmt.Sprintf("%s/agent.zip", tmpDir)
if err = downloadFile(zipFile, agentPath); err != nil {
return "", fmt.Errorf("cannot download agent file: %w", err)
}
if err = unzip(zipFile, tmpDir); err != nil {
return "", fmt.Errorf("cannot unzip file: %w", err)
}
return fmt.Sprintf("%s/tfc-agent", tmpDir), nil
}
func createAgent(t *testing.T, client *Client, org *Organization) (*Agent, *AgentPool, func()) {
var orgCleanup func()
var agentPoolTokenCleanup func()
var agent *Agent
var ok bool
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
agentPool, agentPoolCleanup := createAgentPool(t, client, org)
upgradeOrganizationSubscription(t, client, org)
agentPoolToken, agentPoolTokenCleanup := createAgentToken(t, client, agentPool)
cleanup := func() {
agentPoolTokenCleanup()
if agentPoolCleanup != nil {
agentPoolCleanup()
}
if orgCleanup != nil {
orgCleanup()
}
}
agentPath, err := downloadTFCAgent(t)
if err != nil {
return agent, agentPool, cleanup
}
ctx := context.Background()
cmd := exec.Command(agentPath)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env,
"TFC_AGENT_TOKEN="+agentPoolToken.Token,
"TFC_AGENT_NAME="+"test-agent",
"TFC_ADDRESS="+DefaultConfig().Address,
)
go func() {
_, err := cmd.CombinedOutput()
if err != nil {
t.Logf("Could not run container: %s", err)
}
}()
t.Cleanup(func() {
if err := cmd.Process.Kill(); err != nil {
t.Error(err)
}
})
i, err := retry(func() (interface{}, error) {
agentList, err := client.Agents.List(ctx, agentPool.ID, nil)
if err != nil {
return nil, err
}
if agentList != nil && len(agentList.Items) > 0 {
return agentList.Items[0], nil
}
return nil, errors.New("no agent found")
})
if err != nil {
t.Fatalf("Could not return an agent %s", err)
}
agent, ok = i.(*Agent)
if !ok {
t.Fatalf("Expected type to be *Agent but got %T", agent)
}
return agent, agentPool, cleanup
}
func createAgentPool(t *testing.T, client *Client, org *Organization) (*AgentPool, func()) {
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
ctx := context.Background()
pool, err := client.AgentPools.Create(ctx, org.Name, AgentPoolCreateOptions{
Name: String(randomString(t)),
})
if err != nil {
t.Fatal(err)
}
return pool, func() {
if err := client.AgentPools.Delete(ctx, pool.ID); err != nil {
t.Logf("Error destroying agent pool! WARNING: Dangling resources "+
"may exist! The full error is shown below.\n\n"+
"Agent pool ID: %s\nError: %s", pool.ID, err)
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func createAgentPoolWithOptions(t *testing.T, client *Client, org *Organization, opts AgentPoolCreateOptions) (*AgentPool, func()) {
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
ctx := context.Background()
pool, err := client.AgentPools.Create(ctx, org.Name, opts)
if err != nil {
t.Fatal(err)
}
return pool, func() {
if err := client.AgentPools.Delete(ctx, pool.ID); err != nil {
t.Logf("Error destroying agent pool! WARNING: Dangling resources "+
"may exist! The full error is shown below.\n\n"+
"Agent pool ID: %s\nError: %s", pool.ID, err)
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func createAgentToken(t *testing.T, client *Client, ap *AgentPool) (*AgentToken, func()) {
var apCleanup func()
if ap == nil {
ap, apCleanup = createAgentPool(t, client, nil)
}
ctx := context.Background()
at, err := client.AgentTokens.Create(ctx, ap.ID, AgentTokenCreateOptions{
Description: String(randomString(t)),
})
if err != nil {
t.Fatal(err)
}
return at, func() {
if err := client.AgentTokens.Delete(ctx, at.ID); err != nil {
t.Errorf("Error destroying agent token! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"AgentToken: %s\nError: %s", at.ID, err)
}
if apCleanup != nil {
apCleanup()
}
}
}
func createConfigurationVersion(t *testing.T, client *Client, w *Workspace) (*ConfigurationVersion, func()) {
var wCleanup func()
if w == nil {
w, wCleanup = createWorkspace(t, client, nil)
}
ctx := context.Background()
cv, err := client.ConfigurationVersions.Create(
ctx,
w.ID,
ConfigurationVersionCreateOptions{AutoQueueRuns: Bool(false)},
)
if err != nil {
t.Fatal(err)
}
return cv, func() {
if wCleanup != nil {
wCleanup()
}
}
}
func createUploadedConfigurationVersion(t *testing.T, client *Client, w *Workspace) (*ConfigurationVersion, func()) {
cv, cvCleanup := createConfigurationVersion(t, client, w)
ctx := context.Background()
err := client.ConfigurationVersions.Upload(ctx, cv.UploadURL, "test-fixtures/config-version")
if err != nil {
cvCleanup()
t.Fatal(err)
}
WaitUntilStatus(t, client, cv, ConfigurationUploaded, 15)
return cv, cvCleanup
}
func createTestRunConfigurationVersion(t *testing.T, client *Client, rm *RegistryModule) (*ConfigurationVersion, func()) {
var rmCleanup func()
if rm == nil {
rm, rmCleanup = createRegistryModuleWithVersion(t, client, nil)
}
ctx := context.Background()
cv, err := client.ConfigurationVersions.CreateForRegistryModule(
ctx,
RegistryModuleID{
Organization: rm.Organization.Name,
Name: rm.Name,
Provider: rm.Provider,
Namespace: rm.Namespace,
RegistryName: rm.RegistryName,
})
if err != nil {
t.Fatal(err)
}
return cv, func() {
if rmCleanup != nil {
rmCleanup()
}
}
}
func createUploadedTestRunConfigurationVersion(t *testing.T, client *Client, rm *RegistryModule) (*ConfigurationVersion, func()) {
cv, cvCleanup := createTestRunConfigurationVersion(t, client, rm)
ctx := context.Background()
err := client.ConfigurationVersions.Upload(ctx, cv.UploadURL, "test-fixtures/config-version-with-test")
if err != nil {
cvCleanup()
t.Fatal(err)
}
WaitUntilStatus(t, client, cv, ConfigurationUploaded, 15)
return cv, cvCleanup
}
// helper to wait until a configuration version has reached a certain status
func WaitUntilStatus(t *testing.T, client *Client, cv *ConfigurationVersion, desiredStatus ConfigurationStatus, timeoutSeconds int) {
ctx := context.Background()
for i := 0; ; i++ {
refreshed, err := client.ConfigurationVersions.Read(ctx, cv.ID)
require.NoError(t, err)
if refreshed.Status == desiredStatus {
break
}
if i > timeoutSeconds {
t.Fatal("Timeout waiting for the configuration version to be archived")
}
time.Sleep(1 * time.Second)
}
}
func createGPGKey(t *testing.T, client *Client, org *Organization, provider *RegistryProvider) (*GPGKey, func()) {
var orgCleanup func()
var providerCleanup func()
ctx := context.Background()
if org == nil {
org, orgCleanup = createOrganization(t, client)
upgradeOrganizationSubscription(t, client, org)
}
if provider == nil {
provider, providerCleanup = createRegistryProvider(t, client, org, PrivateRegistry)
}
gpgKey, err := client.GPGKeys.Create(ctx, PrivateRegistry, GPGKeyCreateOptions{
Namespace: provider.Organization.Name,
AsciiArmor: testGpgArmor,
})
if err != nil {
t.Fatal(err)
}
return gpgKey, func() {
if err := client.GPGKeys.Delete(ctx, GPGKeyID{
RegistryName: PrivateRegistry,
Namespace: provider.Organization.Name,
KeyID: gpgKey.KeyID,
}); err != nil {
t.Errorf("Error removing GPG key! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"GPGKey: %s\nError: %s", gpgKey.KeyID, err)
}
if providerCleanup != nil {
providerCleanup()
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func createAWSOIDCConfiguration(t *testing.T, client *Client, org *Organization) (*AWSOIDCConfiguration, func()) {
var orgCleanup func()
ctx := context.Background()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
opts := AWSOIDCConfigurationCreateOptions{
RoleARN: fmt.Sprintf("arn:aws:iam::123456789012:role/%s", randomString(t)),
}
oidcConfig, err := client.AWSOIDCConfigurations.Create(ctx, org.Name, opts)
if err != nil {
t.Fatal(err)
}
return oidcConfig, func() {
if err := client.AWSOIDCConfigurations.Delete(ctx, oidcConfig.ID); err != nil {
t.Errorf("Error removing AWS OIDC Configuration! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"AWSOIDCConfigurations: %s\nError: %s", oidcConfig.ID, err)
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func (a *AWSOIDCConfiguration) createHYOKConfiguration(t *testing.T, client *Client, org *Organization, agentPool *AgentPool) (*HYOKConfiguration, func()) {
var orgCleanup func()
ctx := context.Background()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
opts := HYOKConfigurationsCreateOptions{
KEKID: "arn:aws:kms:us-east-1:123456789012:key/this-is-not-a-real-key",
Name: randomStringWithoutSpecialChar(t),
KMSOptions: &KMSOptions{KeyRegion: "us-east-1"},
AgentPool: agentPool,
OIDCConfiguration: &OIDCConfigurationTypeChoice{AWSOIDCConfiguration: a},
}
hyokConfig, err := client.HYOKConfigurations.Create(ctx, org.Name, opts)
if err != nil {
t.Fatal(err)
}
return hyokConfig, func() {
cleanupHYOKConfiguration(t, ctx, client, hyokConfig.ID)
if orgCleanup != nil {
orgCleanup()
}
}
}
func createAzureOIDCConfiguration(t *testing.T, client *Client, org *Organization) (*AzureOIDCConfiguration, func()) {
var orgCleanup func()
ctx := context.Background()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
opts := AzureOIDCConfigurationCreateOptions{
ClientID: randomString(t),
SubscriptionID: randomString(t),
TenantID: randomString(t),
}
oidcConfig, err := client.AzureOIDCConfigurations.Create(ctx, org.Name, opts)
if err != nil {
t.Fatal(err)
}
return oidcConfig, func() {
if err := client.AzureOIDCConfigurations.Delete(ctx, oidcConfig.ID); err != nil {
t.Errorf("Error removing Azure OIDC Configuration! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"AzureOIDCConfigurations: %s\nError: %s", oidcConfig.ID, err)
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func (a *AzureOIDCConfiguration) createHYOKConfiguration(t *testing.T, client *Client, org *Organization, agentPool *AgentPool) (*HYOKConfiguration, func()) {
var orgCleanup func()
ctx := context.Background()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
opts := HYOKConfigurationsCreateOptions{
KEKID: "https://vault-name.vault.azure.net/keys/key-name",
Name: randomStringWithoutSpecialChar(t),
AgentPool: agentPool,
OIDCConfiguration: &OIDCConfigurationTypeChoice{AzureOIDCConfiguration: a},
}
hyokConfig, err := client.HYOKConfigurations.Create(ctx, org.Name, opts)
if err != nil {
t.Fatal(err)
}
return hyokConfig, func() {
cleanupHYOKConfiguration(t, ctx, client, hyokConfig.ID)
if orgCleanup != nil {
orgCleanup()
}
}
}
func createGCPOIDCConfiguration(t *testing.T, client *Client, org *Organization) (*GCPOIDCConfiguration, func()) {
var orgCleanup func()
ctx := context.Background()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
opts := GCPOIDCConfigurationCreateOptions{
ServiceAccountEmail: randomString(t),
ProjectNumber: "123456789012",
WorkloadProviderName: randomString(t),
}
oidcConfig, err := client.GCPOIDCConfigurations.Create(ctx, org.Name, opts)
if err != nil {
t.Fatal(err)
}
return oidcConfig, func() {
if err := client.GCPOIDCConfigurations.Delete(ctx, oidcConfig.ID); err != nil {
t.Errorf("Error removing GCP OIDC Configuration! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"GCPOIDCConfigurations: %s\nError: %s", oidcConfig.ID, err)
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func (g *GCPOIDCConfiguration) createHYOKConfiguration(t *testing.T, client *Client, org *Organization, agentPool *AgentPool) (*HYOKConfiguration, func()) {
var orgCleanup func()
ctx := context.Background()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
opts := HYOKConfigurationsCreateOptions{
KEKID: randomStringWithoutSpecialChar(t),
Name: randomStringWithoutSpecialChar(t),
KMSOptions: &KMSOptions{KeyLocation: "global", KeyRingID: randomString(t)},
AgentPool: agentPool,
OIDCConfiguration: &OIDCConfigurationTypeChoice{GCPOIDCConfiguration: g},
}
hyokConfig, err := client.HYOKConfigurations.Create(ctx, org.Name, opts)
if err != nil {
t.Fatal(err)
}
return hyokConfig, func() {
cleanupHYOKConfiguration(t, ctx, client, hyokConfig.ID)
if orgCleanup != nil {
orgCleanup()
}
}
}
func createVaultOIDCConfiguration(t *testing.T, client *Client, org *Organization) (*VaultOIDCConfiguration, func()) {
var orgCleanup func()
ctx := context.Background()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
opts := VaultOIDCConfigurationCreateOptions{
Address: "https://vault.example.com",
RoleName: randomString(t),
Namespace: randomString(t),
JWTAuthPath: "jwt",
TLSCACertificate: randomString(t),
}
oidcConfig, err := client.VaultOIDCConfigurations.Create(ctx, org.Name, opts)
if err != nil {
t.Fatal(err)
}
return oidcConfig, func() {
if err := client.VaultOIDCConfigurations.Delete(ctx, oidcConfig.ID); err != nil {
t.Errorf("Error removing Vault OIDC Configuration! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"VaultOIDCConfigurations: %s\nError: %s", oidcConfig.ID, err)
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func (v *VaultOIDCConfiguration) createHYOKConfiguration(t *testing.T, client *Client, org *Organization, agentPool *AgentPool) (*HYOKConfiguration, func()) {
var orgCleanup func()
ctx := context.Background()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
opts := HYOKConfigurationsCreateOptions{
KEKID: randomStringWithoutSpecialChar(t),
Name: randomStringWithoutSpecialChar(t),
AgentPool: agentPool,
OIDCConfiguration: &OIDCConfigurationTypeChoice{VaultOIDCConfiguration: v},
}
hyokConfig, err := client.HYOKConfigurations.Create(ctx, org.Name, opts)
if err != nil {
t.Fatal(err)
}
return hyokConfig, func() {
cleanupHYOKConfiguration(t, ctx, client, hyokConfig.ID)
if orgCleanup != nil {
orgCleanup()
}
}
}
func waitForHYOKConfigurationStatus(t *testing.T, ctx context.Context, client *Client, hyokID string, status HYOKConfigurationStatus) (interface{}, error) {
t.Helper()
return retryPatiently(func() (interface{}, error) {
fetched, err := client.HYOKConfigurations.Read(ctx, hyokID, nil)
if err != nil {
return nil, err
}
if fetched.Status == status {
return fetched, nil
}
return nil, fmt.Errorf("HYOK Configuration is not %s! HYOKConfiguration: %s\nStatus: %s", status, hyokID, fetched.Status)
})
}
func cleanupHYOKConfiguration(t *testing.T, ctx context.Context, client *Client, hyokID string) {
_, err := waitForHYOKConfigurationStatus(t, ctx, client, hyokID, HYOKConfigurationTestFailed)
if err != nil {
t.Errorf("Timed out waiting for HYOK configuration %s to fail test", hyokID)
}
if err = client.HYOKConfigurations.Revoke(ctx, hyokID); err != nil {
t.Errorf("Error removing HYOK Configuration! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"HYOKConfigurations: %s\nError: %s", hyokID, err)
}
_, err = waitForHYOKConfigurationStatus(t, ctx, client, hyokID, HYOKConfigurationRevoked)
if err != nil {
t.Errorf("Timed out waiting for HYOK configuration %s to revoke", hyokID)
}
if err := client.HYOKConfigurations.Delete(ctx, hyokID); err != nil {
t.Errorf("Error removing HYOK Configuration! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"HYOKConfigurations: %s\nError: %s", hyokID, err)
}
}
func createNotificationConfiguration(t *testing.T, client *Client, w *Workspace, options *NotificationConfigurationCreateOptions) (*NotificationConfiguration, func()) {
var wCleanup func()
if w == nil {
w, wCleanup = createWorkspace(t, client, nil)
}
runTaskURL := os.Getenv("TFC_RUN_TASK_URL")
if runTaskURL == "" {
t.Skip("Cannot create a notification configuration with an empty URL. You must set TFC_RUN_TASK_URL for run task related tests.")
}
if options == nil {
options = &NotificationConfigurationCreateOptions{
DestinationType: NotificationDestination(NotificationDestinationTypeGeneric),
Enabled: Bool(false),
Name: String(randomString(t)),
Token: String(randomString(t)),
URL: String(runTaskURL),
Triggers: []NotificationTriggerType{NotificationTriggerCreated},
}
}
ctx := context.Background()
nc, err := client.NotificationConfigurations.Create(
ctx,
w.ID,
*options,
)
if err != nil {
t.Fatal(err)
}
return nc, func() {
if err := client.NotificationConfigurations.Delete(ctx, nc.ID); err != nil {
t.Errorf("Error destroying notification configuration! WARNING: Dangling\n"+
"resources may exist! The full error is shown below.\n\n"+
"NotificationConfiguration: %s\nError: %s", nc.ID, err)
}
if wCleanup != nil {
wCleanup()
}
}
}
func createTeamNotificationConfiguration(t *testing.T, client *Client, team *Team, options *NotificationConfigurationCreateOptions) (*NotificationConfiguration, func()) {
var tCleanup func()
if team == nil {
team, tCleanup = createTeam(t, client, nil)
}
// Team notification configurations do not actually require a run task, but we'll
// reuse this as a URL that returns a 200.
runTaskURL := os.Getenv("TFC_RUN_TASK_URL")
if runTaskURL == "" {
t.Error("You must set TFC_RUN_TASK_URL for run task related tests.")
}
if options == nil {
options = &NotificationConfigurationCreateOptions{
DestinationType: NotificationDestination(NotificationDestinationTypeGeneric),
Enabled: Bool(false),
Name: String(randomString(t)),
Token: String(randomString(t)),
URL: String(runTaskURL),
Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated},
SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: team},
}
}
ctx := context.Background()
nc, err := client.NotificationConfigurations.Create(
ctx,
team.ID,
*options,
)
if err != nil {
t.Fatal(err)
}
return nc, func() {
if err := client.NotificationConfigurations.Delete(ctx, nc.ID); err != nil {
t.Errorf("Error destroying team notification configuration! WARNING: Dangling\n"+
"resources may exist! The full error is shown below.\n\n"+
"NotificationConfiguration: %s\nError: %s", nc.ID, err)
}
if tCleanup != nil {
tCleanup()
}
}
}
func createPolicySetParameter(t *testing.T, client *Client, ps *PolicySet) (*PolicySetParameter, func()) {
var psCleanup func()
if ps == nil {
ps, psCleanup = createPolicySet(t, client, nil, nil, nil, nil, nil, "")
}
ctx := context.Background()
v, err := client.PolicySetParameters.Create(ctx, ps.ID, PolicySetParameterCreateOptions{
Key: String(randomKeyValue(t)),
Value: String(randomKeyValue(t)),
Category: Category(CategoryPolicySet),
})
if err != nil {
t.Fatal(err)
}
return v, func() {
if err := client.PolicySetParameters.Delete(ctx, ps.ID, v.ID); err != nil {
t.Errorf("Error destroying variable! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"Parameter: %s\nError: %s", v.Key, err)
}
if psCleanup != nil {
psCleanup()
}
}
}
func createPolicySet(t *testing.T, client *Client, org *Organization, policies []*Policy, workspaces []*Workspace,
excludedWorkspace []*Workspace, projects []*Project, kind PolicyKind) (*PolicySet, func()) {
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
ctx := context.Background()
ps, err := client.PolicySets.Create(ctx, org.Name, PolicySetCreateOptions{
Name: String(randomString(t)),
Policies: policies,
Workspaces: workspaces,
WorkspaceExclusions: excludedWorkspace,
Projects: projects,
Kind: kind,
})
if err != nil {
t.Fatal(err)
}
return ps, func() {
if err := client.PolicySets.Delete(ctx, ps.ID); err != nil {
t.Errorf("Error destroying policy set! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"PolicySet: %s\nError: %s", ps.ID, err)
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func createPolicySetWithOptions(t *testing.T, client *Client, org *Organization, policies []*Policy, workspaces, excludedWorkspace []*Workspace, projects []*Project, opts PolicySetCreateOptions) (*PolicySet, func()) {
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
ctx := context.Background()
ps, err := client.PolicySets.Create(ctx, org.Name, PolicySetCreateOptions{
Name: String(randomString(t)),
Policies: policies,
Workspaces: workspaces,
WorkspaceExclusions: excludedWorkspace,
Projects: projects,
Kind: opts.Kind,
Overridable: opts.Overridable,
AgentEnabled: opts.AgentEnabled,
PolicyToolVersion: opts.PolicyToolVersion,
ProjectExclusions: opts.ProjectExclusions,
Global: opts.Global,
})
if err != nil {
t.Fatal(err)
}
return ps, func() {
if err := client.PolicySets.Delete(ctx, ps.ID); err != nil {
t.Errorf("Error destroying policy set! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"PolicySet: %s\nError: %s", ps.ID, err)
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func createPolicySetVersion(t *testing.T, client *Client, ps *PolicySet) (*PolicySetVersion, func()) {
var psCleanup func()
if ps == nil {
ps, psCleanup = createPolicySet(t, client, nil, nil, nil, nil, nil, "")
}
ctx := context.Background()
psv, err := client.PolicySetVersions.Create(ctx, ps.ID)
if err != nil {
t.Fatal(err)
}
return psv, func() {
// Deleting a Policy Set Version is done through deleting a Policy Set.
if psCleanup != nil {
psCleanup()
}
}
}
func createPolicy(t *testing.T, client *Client, org *Organization) (*Policy, func()) {
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
name := randomString(t)
options := PolicyCreateOptions{
Name: String(name),
Enforce: []*EnforcementOptions{
{
Path: String(name + ".sentinel"),
Mode: EnforcementMode(EnforcementSoft),
},
},
}
ctx := context.Background()
p, err := client.Policies.Create(ctx, org.Name, options)
if err != nil {
t.Fatal(err)
}
return p, func() {
if err := client.Policies.Delete(ctx, p.ID); err != nil {
t.Errorf("Error destroying policy! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"Policy: %s\nError: %s", p.ID, err)
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func createPolicyWithOptions(t *testing.T, client *Client, org *Organization, opts PolicyCreateOptions) (*Policy, func()) {
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
name := randomString(t)
options := PolicyCreateOptions{
Name: String(name),
Kind: opts.Kind,
Query: opts.Query,
EnforcementLevel: opts.EnforcementLevel,
}
if len(opts.Enforce) > 0 {
path := name + ".sentinel"
if opts.Kind == OPA {
path = name + ".rego"
}
options.Enforce = []*EnforcementOptions{
{
Path: String(path),
Mode: opts.Enforce[0].Mode,
},
}
}
ctx := context.Background()
p, err := client.Policies.Create(ctx, org.Name, options)
if err != nil {
t.Fatal(err)
}
return p, func() {
if err := client.Policies.Delete(ctx, p.ID); err != nil {
t.Errorf("Error destroying policy! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"Policy: %s\nError: %s", p.ID, err)
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func createUploadedPolicy(t *testing.T, client *Client, pass bool, org *Organization) (*Policy, func()) {
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
p, pCleanup := createPolicy(t, client, org)
ctx := context.Background()
err := client.Policies.Upload(ctx, p.ID, []byte(fmt.Sprintf("main = rule { %t }", pass)))
if err != nil {
t.Fatal(err)
}
p, err = client.Policies.Read(ctx, p.ID)
if err != nil {
t.Fatal(err)
}
return p, func() {
pCleanup()
if orgCleanup != nil {
orgCleanup()
}
}
}
func createUploadedPolicyWithOptions(t *testing.T, client *Client, pass bool, org *Organization, opts PolicyCreateOptions) (*Policy, func()) {
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
p, pCleanup := createPolicyWithOptions(t, client, org, opts)
ctx := context.Background()
policy := fmt.Sprintf("main = rule { %t }", pass)
if opts.Kind == OPA {
policy = `package example rule["not allowed"] { false }`
if !pass {
policy = `package example rule["not allowed"] { true }`
}
}
err := client.Policies.Upload(ctx, p.ID, []byte(policy))
if err != nil {
t.Fatal(err)
}
p, err = client.Policies.Read(ctx, p.ID)
if err != nil {
t.Fatal(err)
}
return p, func() {
pCleanup()
if orgCleanup != nil {
orgCleanup()
}
}
}
func createOAuthClient(t *testing.T, client *Client, org *Organization, projects []*Project) (*OAuthClient, func()) {
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
githubToken := os.Getenv("OAUTH_CLIENT_GITHUB_TOKEN")
if githubToken == "" {
t.Skip("Export a valid OAUTH_CLIENT_GITHUB_TOKEN before running this test!")
}
options := OAuthClientCreateOptions{
APIURL: String("https://api.github.com"),
HTTPURL: String("https://github.com"),
OAuthToken: String(githubToken),
ServiceProvider: ServiceProvider(ServiceProviderGithub),
Projects: projects,
}
ctx := context.Background()
oc, err := client.OAuthClients.Create(ctx, org.Name, options)
if err != nil {
t.Fatal(err)
}
// This currently panics as the token will not be there when the client is
// created. To get a token, the client needs to be connected through the UI
// first. So the test using this (TestOAuthTokensList) is currently disabled.
return oc, func() {
if err := client.OAuthClients.Delete(ctx, oc.ID); err != nil {
t.Errorf("Error destroying OAuth client! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"OAuthClient: %s\nError: %s", oc.ID, err)
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func createOAuthToken(t *testing.T, client *Client, org *Organization) (*OAuthToken, func()) {
ocTest, ocTestCleanup := createOAuthClient(t, client, org, nil)
return ocTest.OAuthTokens[0], ocTestCleanup
}
// createOrganization creates an organization for tests using the special prefix
// "tst-" that the API uses especially to grant access to orgs for testing.
// Don't change this prefix unless we refactor the code!
func createOrganization(t *testing.T, client *Client) (*Organization, func()) {
return createOrganizationWithOptions(t, client, OrganizationCreateOptions{
Name: String("tst-" + randomString(t)),
Email: String(fmt.Sprintf("%s@tfe.local", randomString(t))),
CostEstimationEnabled: Bool(true),
StacksEnabled: Bool(true),
})
}
func createOrganizationWithOptions(t *testing.T, client *Client, options OrganizationCreateOptions) (*Organization, func()) {
ctx := context.Background()
org, err := client.Organizations.Create(ctx, options)
if err != nil {
t.Fatalf("Failed to create organization: %s", err)
}
return org, func() {
if err := client.Organizations.Delete(ctx, org.Name); err != nil {
t.Logf("Error destroying organization! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"Organization: %s\nError: %s", org.Name, err)
}
}
}
func createOrganizationWithDefaultAgentPool(t *testing.T, client *Client) (*Organization, func()) {
ctx := context.Background()
org, orgCleanup := createOrganizationWithOptions(t, client, OrganizationCreateOptions{
Name: String("tst-" + randomString(t)),
Email: String(fmt.Sprintf("%s@tfe.local", randomString(t))),
CostEstimationEnabled: Bool(true),
})
agentPool, _ := createAgentPool(t, client, org)
org, err := client.Organizations.Update(ctx, org.Name, OrganizationUpdateOptions{
DefaultExecutionMode: String("agent"),
DefaultAgentPool: agentPool,
})
if err != nil {
t.Fatal(err)
}
return org, func() {
// delete the org
orgCleanup()
}
}
func createOrganizationMembership(t *testing.T, client *Client, org *Organization) (*OrganizationMembership, func()) {
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
ctx := context.Background()
mem, err := client.OrganizationMemberships.Create(ctx, org.Name, OrganizationMembershipCreateOptions{
Email: String(fmt.Sprintf("%s@tfe.local", randomString(t))),
})
if err != nil {
t.Fatal(err)
}
return mem, func() {
if err := client.OrganizationMemberships.Delete(ctx, mem.ID); err != nil {
t.Errorf("Error destroying membership! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"Membership: %s\nError: %s", mem.ID, err)
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func createOrganizationToken(t *testing.T, client *Client, org *Organization) (*OrganizationToken, func()) {
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
ctx := context.Background()
tk, err := client.OrganizationTokens.Create(ctx, org.Name)
if err != nil {
t.Fatal(err)
}
return tk, func() {
if err := client.OrganizationTokens.Delete(ctx, org.Name); err != nil {
t.Errorf("Error destroying organization token! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"OrganizationToken: %s\nError: %s", tk.ID, err)
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func createOrganizationTokenWithOptions(t *testing.T, client *Client, org *Organization, options OrganizationTokenCreateOptions) (*OrganizationToken, func()) {
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
ctx := context.Background()
tk, err := client.OrganizationTokens.CreateWithOptions(ctx, org.Name, options)
if err != nil {
t.Fatal(err)
}
return tk, func() {
if err := client.OrganizationTokens.Delete(ctx, org.Name); err != nil {
t.Errorf("Error destroying organization token! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"OrganizationToken: %s\nError: %s", tk.ID, err)
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func createRunTrigger(t *testing.T, client *Client, w, sourceable *Workspace) (*RunTrigger, func()) {
var wCleanup func()
var sourceableCleanup func()
if w == nil {
w, wCleanup = createWorkspace(t, client, nil)
}
if sourceable == nil {
sourceable, sourceableCleanup = createWorkspace(t, client, nil)
}
ctx := context.Background()
rt, err := client.RunTriggers.Create(
ctx,
w.ID,
RunTriggerCreateOptions{
Sourceable: sourceable,
},
)
if err != nil {
t.Fatal(err)
}
return rt, func() {
if err := client.RunTriggers.Delete(ctx, rt.ID); err != nil {
t.Errorf("Error destroying run trigger! WARNING: Dangling\n"+
"resources may exist! The full error is shown below.\n\n"+
"RunTrigger: %s\nError: %s", rt.ID, err)
}
if wCleanup != nil {
wCleanup()
}
if sourceableCleanup != nil {
sourceableCleanup()
}
}
}
func createPolicyCheckedRun(t *testing.T, client *Client, w *Workspace) (*Run, func()) {
return createRunWaitForAnyStatuses(t, client, w, []RunStatus{RunPolicyChecked, RunPolicyOverride})
}
func createPlannedRun(t *testing.T, client *Client, w *Workspace) (*Run, func()) {
return createRunWaitForAnyStatuses(t, client, w, []RunStatus{RunCostEstimated, RunPlanned, RunPostPlanCompleted})
}
func createCostEstimatedRun(t *testing.T, client *Client, w *Workspace) (*Run, func()) {
return createRunWaitForStatus(t, client, w, RunCostEstimated)
}
func createRunApply(t *testing.T, client *Client, w *Workspace) (*Run, func()) {
ctx := context.Background()
run, rCleanup := createRunUnapplied(t, client, w)
timeout := 2 * time.Minute
// If the run was not in error, it must be applyable
applyRun(t, client, ctx, run)
ctxPollRunApplied, cancelPollApplied := context.WithTimeout(ctx, timeout)
run = pollRunStatus(t, client, ctxPollRunApplied, run, []RunStatus{RunApplied, RunErrored})
if run.Status == RunErrored {
fatalDumpRunLog(t, client, ctx, run)
}
return run, func() {
rCleanup()
cancelPollApplied()
}
}
func createRunUnapplied(t *testing.T, client *Client, w *Workspace) (*Run, func()) {
var rCleanup func()
ctx := context.Background()
r, rCleanup := createRun(t, client, w)
timeout := 2 * time.Minute
ctxPollRunReady, cancelPollRunReady := context.WithTimeout(ctx, timeout)
run := pollRunStatus(
t,
client,
ctxPollRunReady,
r,
append(applyableStatuses(r), RunErrored),
)
if run.Status == RunErrored {
fatalDumpRunLog(t, client, ctx, run)
}
return run, func() {
rCleanup()
cancelPollRunReady()
}
}
func createRunWaitForStatus(t *testing.T, client *Client, w *Workspace, status RunStatus) (*Run, func()) {
return createRunWaitForAnyStatuses(t, client, w, []RunStatus{status})
}
func createRunWaitForAnyStatuses(t *testing.T, client *Client, w *Workspace, statuses []RunStatus) (*Run, func()) {
var rCleanup func()
ctx := context.Background()
r, rCleanup := createRun(t, client, w)
timeout := 2 * time.Minute
ctxPollRunReady, cancelPollRunReady := context.WithTimeout(ctx, timeout)
run := pollRunStatus(
t,
client,
ctxPollRunReady,
r,
append(statuses, RunErrored),
)
if run.Status == RunErrored {
fatalDumpRunLog(t, client, ctx, run)
}
return run, func() {
rCleanup()
cancelPollRunReady()
}
}
func createQueryRunWaitForAnyStatuses(t *testing.T, client *Client, w *Workspace, statuses []QueryRunStatus) (*QueryRun, func()) {
ctx := context.Background()
qr := createQueryRun(t, client, w)
timeout := 2 * time.Minute
ctxPollQueryRunReady, cancelPollQueryRunReady := context.WithTimeout(ctx, timeout)
run := pollQueryRunStatus(
t,
client,
ctxPollQueryRunReady,
qr,
append(statuses, QueryRunErrored),
)
return run, func() {
cancelPollQueryRunReady()
}
}
func applyableStatuses(r *Run) []RunStatus {
if len(r.PolicyChecks) > 0 {
return []RunStatus{
RunPolicyChecked,
RunPolicyOverride,
}
} else if r.CostEstimate != nil {
return []RunStatus{RunCostEstimated}
}
return []RunStatus{RunPlanned, RunPostPlanCompleted}
}
// pollRunStatus will poll the given run until its status matches one of the given run statuses or the given context
// times out.
func pollRunStatus(t *testing.T, client *Client, ctx context.Context, r *Run, rss []RunStatus) *Run {
deadline, ok := ctx.Deadline()
if !ok {
t.Logf("No deadline was set to poll run %q which could result in an infinite loop", r.ID)
}
t.Logf("Polling run %q for status included in %q with deadline of %s", r.ID, rss, deadline)
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for finished := false; !finished; {
t.Log("...")
select {
case <-ctx.Done():
t.Fatalf("Run %q had status %q at deadline", r.ID, r.Status)
case <-ticker.C:
r = readRun(t, client, ctx, r)
t.Logf("Run %q had status %q", r.ID, r.Status)
for _, rs := range rss {
if rs == r.Status {
finished = true
break
}
}
}
}
return r
}
// pollQueryRunStatus will poll the given query run until its status matches one of the given run statuses or the given context
// times out.
func pollQueryRunStatus(t *testing.T, client *Client, ctx context.Context, q *QueryRun, rss []QueryRunStatus) *QueryRun {
deadline, ok := ctx.Deadline()
if !ok {
t.Logf("No deadline was set to poll query run %q which could result in an infinite loop", q.ID)
}
t.Logf("Polling query run %q for status included in %q with deadline of %s", q.ID, rss, deadline)
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for finished := false; !finished; {
t.Log("...")
select {
case <-ctx.Done():
t.Fatalf("Run %q had status %q at deadline", q.ID, q.Status)
case <-ticker.C:
q = readQueryRun(t, client, ctx, q)
t.Logf("Query Run %q had status %q", q.ID, q.Status)
for _, rs := range rss {
if rs == q.Status {
finished = true
break
}
}
}
}
return q
}
// pollStateVersionStatus will poll the given state version until its status
// matches one of the given statuses or the given context times out.
func pollStateVersionStatus(t *testing.T, client *Client, ctx context.Context, sv *StateVersion, statuses []StateVersionStatus) *StateVersion {
deadline, ok := ctx.Deadline()
if !ok {
t.Logf("No deadline was set to poll state version %q which could result in an infinite loop", sv.ID)
}
t.Logf("Polling state version %q for status included in %q with deadline of %s", sv.ID, statuses, deadline)
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
var err error
for finished := false; !finished; {
t.Log("...")
select {
case <-ctx.Done():
t.Fatalf("State version %q had status %q at deadline", sv.ID, sv.Status)
case <-ticker.C:
sv, err = client.StateVersions.Read(ctx, sv.ID)
if err != nil {
t.Fatalf("Could not read state version %q: %s", sv.ID, err)
}
t.Logf("State version %q had status %q", sv.ID, sv.Status)
for _, svst := range statuses {
if svst == sv.Status {
finished = true
break
}
}
}
}
return sv
}
// readRun will re-read the given run.
func readRun(t *testing.T, client *Client, ctx context.Context, r *Run) *Run {
t.Logf("Reading run %q", r.ID)
rr, err := client.Runs.Read(ctx, r.ID)
if err != nil {
t.Fatalf("Could not read run %q: %s", r.ID, err)
}
return rr
}
// readQueryRun will re-read the given query run.
func readQueryRun(t *testing.T, client *Client, ctx context.Context, r *QueryRun) *QueryRun {
t.Logf("Reading query run %q", r.ID)
qr, err := client.QueryRuns.Read(ctx, r.ID)
if err != nil {
t.Fatalf("Could not read run %q: %s", r.ID, err)
}
return qr
}
// applyRun will apply the given run.
func applyRun(t *testing.T, client *Client, ctx context.Context, r *Run) {
runDependentTestNameValidator(t)
t.Logf("Applying run %q", r.ID)
if err := client.Runs.Apply(ctx, r.ID, RunApplyOptions{}); err != nil {
t.Fatalf("Could not apply run %q: %s", r.ID, err)
}
}
// readPlan will read the given plan.
func readPlan(t *testing.T, client *Client, ctx context.Context, p *Plan) *Plan {
t.Logf("Reading plan %q", p.ID)
rp, err := client.Plans.Read(ctx, p.ID)
if err != nil {
t.Fatalf("Could not read plan %q: %s", p.ID, err)
}
return rp
}
// readPlanLogs will read the logs of the given plan.
func readPlanLogs(t *testing.T, client *Client, ctx context.Context, p *Plan) io.Reader {
t.Logf("Reading logs of plan %q", p.ID)
r, err := client.Plans.Logs(ctx, p.ID)
if err != nil {
t.Fatalf("Could not retrieve logs of plan %q: %s", p.ID, err)
}
return r
}
func fatalDumpRunLog(t *testing.T, client *Client, ctx context.Context, run *Run) {
t.Helper()
p := readPlan(t, client, ctx, run.Plan)
r := readPlanLogs(t, client, ctx, p)
l, err := io.ReadAll(r)
if err != nil {
t.Fatalf("Could not read logs of plan %q: %v", p.ID, err)
}
t.Log("Run errored - here's some logs to help figure out what happened")
t.Logf("---Start of logs---\n%s\n---End of logs---", l)
t.Fatalf("Run %q unexpectedly errored", run.ID)
}
func createRun(t *testing.T, client *Client, w *Workspace) (*Run, func()) {
runDependentTestNameValidator(t)
var wCleanup func()
if w == nil {
w, wCleanup = createWorkspace(t, client, nil)
}
cv, cvCleanup := createUploadedConfigurationVersion(t, client, w)
ctx := context.Background()
r, err := client.Runs.Create(ctx, RunCreateOptions{
ConfigurationVersion: cv,
Workspace: w,
})
if err != nil {
t.Fatal(err)
}
return r, func() {
cvCleanup()
if wCleanup != nil {
wCleanup()
}
}
}
func createTestRun(t *testing.T, client *Client, rm *RegistryModule, variables ...*RunVariable) (*TestRun, func()) {
var rmCleanup func()
if rm == nil {
rm, rmCleanup = createBranchBasedRegistryModule(t, client, nil)
}
cv, cvCleanup := createUploadedTestRunConfigurationVersion(t, client, rm)
ctx := context.Background()
tr, err := client.TestRuns.Create(ctx, TestRunCreateOptions{
Variables: variables,
ConfigurationVersion: cv,
RegistryModule: rm,
})
if err != nil {
t.Fatal(err)
}
return tr, func() {
cvCleanup()
if rmCleanup != nil {
rmCleanup()
}
}
}
func createTestVariable(t *testing.T, client *Client, rm *RegistryModule) (*Variable, func()) {
var rmCleanup func()
if rm == nil {
rm, rmCleanup = createBranchBasedRegistryModule(t, client, nil)
}
rmID := RegistryModuleID{
Organization: rm.Organization.Name,
Name: rm.Name,
Provider: rm.Provider,
Namespace: rm.Namespace,
RegistryName: rm.RegistryName,
}
ctx := context.Background()
v, err := client.TestVariables.Create(ctx, rmID, VariableCreateOptions{
Key: String(randomKeyValue(t)),
Value: String(randomStringWithoutSpecialChar(t)),
Category: Category(CategoryEnv),
Description: String(randomStringWithoutSpecialChar(t)),
})
if err != nil {
t.Fatal(err)
}
return v, func() {
if err := client.TestVariables.Delete(ctx, rmID, v.ID); err != nil {
t.Errorf("Error destroying variable! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"Variable: %s\nError: %s", v.Key, err)
}
if rmCleanup != nil {
rmCleanup()
}
}
}
// helper to wait until a test run has reached a certain status
func waitUntilTestRunStatus(t *testing.T, client *Client, rm RegistryModuleID, tr *TestRun, desiredStatus TestRunStatus, timeoutSeconds int) {
ctx := context.Background()
for i := 0; ; i++ {
refreshed, err := client.TestRuns.Read(ctx, rm, tr.ID)
require.NoError(t, err)
if refreshed.Status == desiredStatus {
break
}
if i > timeoutSeconds {
t.Fatalf("Timeout waiting for the test run status %s", string(desiredStatus))
}
time.Sleep(1 * time.Second)
}
}
func createPlanExport(t *testing.T, client *Client, r *Run) (*PlanExport, func()) {
var rCleanup func()
if r == nil {
r, rCleanup = createRunApply(t, client, nil)
}
ctx := context.Background()
pe, err := client.PlanExports.Create(ctx, PlanExportCreateOptions{
Plan: r.Plan,
DataType: PlanExportType(PlanExportSentinelMockBundleV0),
})
if err != nil {
t.Fatal(err)
}
timeout := 10 * time.Minute
ctxPollExportReady, cancelPollExportReady := context.WithTimeout(ctx, timeout)
t.Cleanup(cancelPollExportReady)
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
for {
t.Log("...")
select {
case <-ctxPollExportReady.Done():
rCleanup()
t.Fatalf("Run %q had status %q at deadline", r.ID, r.Status)
case <-ticker.C:
pe, err := client.PlanExports.Read(ctxPollExportReady, pe.ID)
if err != nil {
t.Fatal(err)
}
switch pe.Status {
case PlanExportFinished, PlanExportQueued:
return pe, func() {
if rCleanup != nil {
rCleanup()
}
}
case PlanExportErrored:
t.Fatal("Plan export failed")
default:
t.Logf("Waiting for plan export finished or queued but was %s", pe.Status)
}
}
}
}
func createBranchBasedRegistryModule(t *testing.T, client *Client, org *Organization) (*RegistryModule, func()) {
githubIdentifier := os.Getenv("GITHUB_REGISTRY_MODULE_IDENTIFIER")
if githubIdentifier == "" {
t.Skip("Export a valid GITHUB_REGISTRY_MODULE_IDENTIFIER before running this test")
}
githubBranch := os.Getenv("GITHUB_REGISTRY_MODULE_BRANCH")
if githubBranch == "" {
githubBranch = "main"
}
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
oauthTokenTest, oauthTokenTestCleanup := createOAuthToken(t, client, org)
ctx := context.Background()
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
OrganizationName: String(org.Name),
Identifier: String(githubIdentifier),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(githubIdentifier),
Branch: String(githubBranch),
},
InitialVersion: String("1.0.0"),
})
if err != nil {
oauthTokenTestCleanup()
if orgCleanup != nil {
orgCleanup()
}
t.Fatal(err)
}
return rm, func() {
if err := client.RegistryModules.Delete(ctx, org.Name, rm.Name); err != nil {
t.Errorf("Error destroying registry module! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"Registry Module: %s\nError: %s", rm.Name, err)
}
oauthTokenTestCleanup()
if orgCleanup != nil {
orgCleanup()
}
}
}
func createBranchBasedRegistryModuleWithTests(t *testing.T, client *Client, org *Organization) (*RegistryModule, func()) {
githubIdentifier := os.Getenv("GITHUB_REGISTRY_MODULE_IDENTIFIER")
if githubIdentifier == "" {
t.Skip("Export a valid GITHUB_REGISTRY_MODULE_IDENTIFIER before running this test")
}
githubBranch := os.Getenv("GITHUB_REGISTRY_MODULE_BRANCH")
if githubBranch == "" {
githubBranch = "main"
}
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
oauthTokenTest, oauthTokenTestCleanup := createOAuthToken(t, client, org)
ctx := context.Background()
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
OrganizationName: String(org.Name),
Identifier: String(githubIdentifier),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(githubIdentifier),
Branch: String(githubBranch),
},
InitialVersion: String("1.0.0"),
TestConfig: &RegistryModuleTestConfigOptions{
TestsEnabled: Bool(true),
},
})
if err != nil {
oauthTokenTestCleanup()
if orgCleanup != nil {
orgCleanup()
}
t.Fatal(err)
}
return rm, func() {
if err := client.RegistryModules.Delete(ctx, org.Name, rm.Name); err != nil {
t.Errorf("Error destroying registry module! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"Registry Module: %s\nError: %s", rm.Name, err)
}
oauthTokenTestCleanup()
if orgCleanup != nil {
orgCleanup()
}
}
}
func createRegistryModule(t *testing.T, client *Client, org *Organization, registryName RegistryName) (*RegistryModule, func()) {
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
ctx := context.Background()
options := RegistryModuleCreateOptions{
Name: String(randomString(t)),
Provider: String("provider"),
RegistryName: registryName,
}
if registryName == PublicRegistry {
options.Namespace = "namespace"
}
rm, err := client.RegistryModules.Create(ctx, org.Name, options)
if err != nil {
t.Fatal(err)
}
return rm, func() {
if err := client.RegistryModules.Delete(ctx, org.Name, rm.Name); err != nil {
t.Errorf("Error destroying registry module! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"Registry Module: %s\nError: %s", rm.Name, err)
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func createRegistryModuleWithVersion(t *testing.T, client *Client, org *Organization) (*RegistryModule, func()) {
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
ctx := context.Background()
options := RegistryModuleCreateOptions{
Name: String("name"),
Provider: String("provider"),
}
rm, err := client.RegistryModules.Create(ctx, org.Name, options)
if err != nil {
t.Fatal(err)
}
optionsModuleVersion := RegistryModuleCreateVersionOptions{
Version: String("1.0.0"),
}
rmID := RegistryModuleID{
Organization: org.Name,
Name: rm.Name,
Provider: rm.Provider,
}
_, err = client.RegistryModules.CreateVersion(ctx, rmID, optionsModuleVersion)
if err != nil {
t.Fatal(err)
}
rm, err = client.RegistryModules.Read(ctx, rmID)
if err != nil {
t.Fatal(err)
}
return rm, func() {
if err := client.RegistryModules.Delete(ctx, org.Name, rm.Name); err != nil {
t.Errorf("Error destroying registry module! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"Registry Module: %s\nError: %s", rm.Name, err)
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func createRunTask(t *testing.T, client *Client, org *Organization) (*RunTask, func()) {
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
runTaskURL := os.Getenv("TFC_RUN_TASK_URL")
if runTaskURL == "" {
t.Error("Cannot create a run task with an empty URL. You must set TFC_RUN_TASK_URL for run task related tests.")
}
ctx := context.Background()
description := randomString(t)
r, err := client.RunTasks.Create(ctx, org.Name, RunTaskCreateOptions{
Name: "tst-" + randomString(t),
URL: runTaskURL,
Description: &description,
Category: "task",
})
if err != nil {
t.Fatal(err)
}
return r, func() {
if err := client.RunTasks.Delete(ctx, r.ID); err != nil {
t.Errorf("Error removing Run Task! WARNING: Run task limit\n"+
"may be reached if not deleted! The full error is shown below.\n\n"+
"Run Task: %s\nError: %s", r.Name, err)
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func createRegistryProvider(t *testing.T, client *Client, org *Organization, registryName RegistryName) (*RegistryProvider, func()) {
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
if (registryName != PublicRegistry) && (registryName != PrivateRegistry) {
t.Fatal("RegistryName must be public or private")
}
ctx := context.Background()
namespaceName := "test-namespace-" + randomString(t)
if registryName == PrivateRegistry {
namespaceName = org.Name
}
options := RegistryProviderCreateOptions{
Name: "test-registry-provider-" + randomString(t),
Namespace: namespaceName,
RegistryName: registryName,
}
prv, err := client.RegistryProviders.Create(ctx, org.Name, options)
if err != nil {
t.Fatal(err)
}
prv.Organization = org
return prv, func() {
id := RegistryProviderID{
OrganizationName: org.Name,
RegistryName: prv.RegistryName,
Namespace: prv.Namespace,
Name: prv.Name,
}
if err := client.RegistryProviders.Delete(ctx, id); err != nil {
t.Errorf("Error destroying registry provider! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"Registry Provider: %s/%s\nError: %s", prv.Namespace, prv.Name, err)
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func createRegistryProviderPlatform(t *testing.T, client *Client, provider *RegistryProvider, version *RegistryProviderVersion, targetOS, arch string) (*RegistryProviderPlatform, func()) {
var providerCleanup func()
var versionCleanup func()
if provider == nil {
provider, providerCleanup = createRegistryProvider(t, client, nil, PrivateRegistry)
}
providerID := RegistryProviderID{
OrganizationName: provider.Organization.Name,
RegistryName: provider.RegistryName,
Namespace: provider.Namespace,
Name: provider.Name,
}
if version == nil {
version, versionCleanup = createRegistryProviderVersion(t, client, provider)
}
versionID := RegistryProviderVersionID{
RegistryProviderID: providerID,
Version: version.Version,
}
ctx := context.Background()
options := RegistryProviderPlatformCreateOptions{
OS: targetOS,
Arch: arch,
Shasum: genSha(t),
Filename: randomString(t),
}
if targetOS == "" {
options.OS = "linux"
}
if arch == "" {
options.Arch = "amd64"
}
rpp, err := client.RegistryProviderPlatforms.Create(ctx, versionID, options)
if err != nil {
t.Fatal(err)
}
return rpp, func() {
platformID := RegistryProviderPlatformID{
RegistryProviderVersionID: versionID,
OS: rpp.OS,
Arch: rpp.Arch,
}
if err := client.RegistryProviderPlatforms.Delete(ctx, platformID); err != nil {
t.Errorf("Error destroying registry provider platform! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"Registry Provider Version: %s/%s/%s/%s\nError: %s", rpp.RegistryProviderVersion.RegistryProvider.Namespace, rpp.RegistryProviderVersion.RegistryProvider.Name, rpp.OS, rpp.Arch, err)
}
if versionCleanup != nil {
versionCleanup()
}
if providerCleanup != nil {
providerCleanup()
}
}
}
func createRegistryProviderVersion(t *testing.T, client *Client, provider *RegistryProvider) (*RegistryProviderVersion, func()) {
var providerCleanup func()
if provider == nil {
provider, providerCleanup = createRegistryProvider(t, client, nil, PrivateRegistry)
}
providerID := RegistryProviderID{
OrganizationName: provider.Organization.Name,
RegistryName: provider.RegistryName,
Namespace: provider.Namespace,
Name: provider.Name,
}
ctx := context.Background()
options := RegistryProviderVersionCreateOptions{
Version: randomSemver(t),
KeyID: randomString(t),
Protocols: []string{"4.0", "5.0", "6.0"},
}
prvv, err := client.RegistryProviderVersions.Create(ctx, providerID, options)
if err != nil {
t.Fatal(err)
}
prvv.RegistryProvider = provider
return prvv, func() {
id := RegistryProviderVersionID{
Version: options.Version,
RegistryProviderID: providerID,
}
if err := client.RegistryProviderVersions.Delete(ctx, id); err != nil {
t.Errorf("Error destroying registry provider version! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"Registry Provider Version: %s/%s/%s\nError: %s", prvv.RegistryProvider.Namespace, prvv.RegistryProvider.Name, prvv.Version, err)
}
if providerCleanup != nil {
providerCleanup()
}
}
}
func createSSHKey(t *testing.T, client *Client, org *Organization) (*SSHKey, func()) {
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
ctx := context.Background()
key, err := client.SSHKeys.Create(ctx, org.Name, SSHKeyCreateOptions{
Name: String(randomString(t)),
Value: String(randomString(t)),
})
if err != nil {
t.Fatal(err)
}
return key, func() {
if err := client.SSHKeys.Delete(ctx, key.ID); err != nil {
t.Errorf("Error destroying SSH key! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"SSHKey: %s\nError: %s", key.Name, err)
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func createStateVersion(t *testing.T, client *Client, serial int64, w *Workspace) (*StateVersion, func()) {
var wCleanup func()
if w == nil {
w, wCleanup = createWorkspace(t, client, nil)
}
state, err := os.ReadFile("test-fixtures/state-version/terraform.tfstate")
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
_, err = client.Workspaces.Lock(ctx, w.ID, WorkspaceLockOptions{})
if err != nil {
t.Fatal(err)
}
defer func() {
_, err := client.Workspaces.Unlock(ctx, w.ID)
if err != nil {
t.Fatal(err)
}
}()
sv, err := client.StateVersions.Create(ctx, w.ID, StateVersionCreateOptions{
MD5: String(fmt.Sprintf("%x", md5.Sum(state))),
Serial: Int64(serial),
State: String(base64.StdEncoding.EncodeToString(state)),
})
if err != nil {
t.Fatal(err)
}
// Download URL may not be immediately available after creation
// Poll until download URL is ready
sv, err = retryPatientlyIf(
func() (any, error) {
return client.StateVersions.Read(ctx, sv.ID)
},
func(nsv *StateVersion) bool {
return nsv.DownloadURL == ""
},
)
if err != nil {
t.Fatal(err)
}
return sv, func() {
// There currently isn't a way to delete a state, so we
// can only cleanup by deleting the workspace.
if wCleanup != nil {
wCleanup()
}
}
}
func createTeam(t *testing.T, client *Client, org *Organization) (*Team, func()) {
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
ctx := context.Background()
tm, err := client.Teams.Create(ctx, org.Name, TeamCreateOptions{
Name: String(randomString(t)),
OrganizationAccess: &OrganizationAccessOptions{
ManagePolicies: Bool(true),
ManagePolicyOverrides: Bool(true),
DelegatePolicyOverrides: Bool(true),
ManageProviders: Bool(true),
ManageModules: Bool(true),
},
})
if err != nil {
t.Fatal(err)
}
return tm, func() {
if err := client.Teams.Delete(ctx, tm.ID); err != nil {
t.Errorf("Error destroying team! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"Team: %s\nError: %s", tm.Name, err)
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func createTeamAccess(t *testing.T, client *Client, tm *Team, w *Workspace, org *Organization) (*TeamAccess, func()) {
var orgCleanup, tmCleanup, wCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
if tm == nil {
tm, tmCleanup = createTeam(t, client, org)
}
if w == nil {
w, wCleanup = createWorkspace(t, client, org)
}
ctx := context.Background()
ta, err := client.TeamAccess.Add(ctx, TeamAccessAddOptions{
Access: Access(AccessAdmin),
Team: tm,
Workspace: w,
})
if err != nil {
t.Fatal(err)
}
return ta, func() {
if err := client.TeamAccess.Remove(ctx, ta.ID); err != nil {
t.Errorf("Error removing team access! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"TeamAccess: %s\nError: %s", ta.ID, err)
}
if tmCleanup != nil {
tmCleanup()
}
if orgCleanup != nil {
orgCleanup()
}
if wCleanup != nil {
wCleanup()
}
}
}
func createTeamProjectAccess(t *testing.T, client *Client, tm *Team, p *Project, org *Organization) (*TeamProjectAccess, func()) {
var orgCleanup, tmCleanup, pCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
if tm == nil {
tm, tmCleanup = createTeam(t, client, org)
}
if p == nil {
p, pCleanup = createProject(t, client, org)
}
ctx := context.Background()
tpa, err := client.TeamProjectAccess.Add(ctx, TeamProjectAccessAddOptions{
Access: *ProjectAccess(TeamProjectAccessAdmin),
Team: tm,
Project: p,
})
if err != nil {
t.Fatal(err)
}
return tpa, func() {
if err := client.TeamProjectAccess.Remove(ctx, tpa.ID); err != nil {
t.Errorf("Error removing team access! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"TeamAccess: %s\nError: %s", tpa.ID, err)
}
if tmCleanup != nil {
tmCleanup()
}
if orgCleanup != nil {
orgCleanup()
}
if pCleanup != nil {
pCleanup()
}
}
}
func createTeamToken(t *testing.T, client *Client, tm *Team) (*TeamToken, func()) {
var tmCleanup func()
if tm == nil {
tm, tmCleanup = createTeam(t, client, nil)
}
ctx := context.Background()
tt, err := client.TeamTokens.Create(ctx, tm.ID)
if err != nil {
t.Fatal(err)
}
return tt, func() {
if err := client.TeamTokens.Delete(ctx, tm.ID); err != nil {
t.Errorf("Error destroying team token! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"TeamToken: %s\nError: %s", tm.ID, err)
}
if tmCleanup != nil {
tmCleanup()
}
}
}
func createTeamTokenWithOptions(t *testing.T, client *Client, tm *Team, options TeamTokenCreateOptions) (*TeamToken, func()) {
var tmCleanup func()
if tm == nil {
tm, tmCleanup = createTeam(t, client, nil)
}
ctx := context.Background()
tt, err := client.TeamTokens.CreateWithOptions(ctx, tm.ID, options)
if err != nil {
t.Fatal(err)
}
return tt, func() {
if err := client.TeamTokens.DeleteByID(ctx, tt.ID); err != nil {
t.Errorf("Error destroying team token! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"TeamToken: %s\nError: %s", tm.ID, err)
}
if tmCleanup != nil {
tmCleanup()
}
}
}
func createVariable(t *testing.T, client *Client, w *Workspace) (*Variable, func()) {
options := VariableCreateOptions{
Key: String(randomString(t)),
Value: String(randomString(t)),
Category: Category(CategoryTerraform),
Description: String(randomString(t)),
}
return createVariableWithOptions(t, client, w, options)
}
func createVariableWithOptions(t *testing.T, client *Client, w *Workspace, options VariableCreateOptions) (*Variable, func()) {
var wCleanup func()
if w == nil {
w, wCleanup = createWorkspace(t, client, nil)
}
if options.Key == nil {
options.Key = String(randomString(t))
}
if options.Value == nil {
options.Value = String(randomString(t))
}
if options.Description == nil {
options.Description = String(randomString(t))
}
if options.Category == nil {
options.Category = Category(CategoryTerraform)
}
if options.HCL == nil {
options.HCL = Bool(false)
}
if options.Sensitive == nil {
options.Sensitive = Bool(false)
}
ctx := context.Background()
v, err := client.Variables.Create(ctx, w.ID, options)
if err != nil {
t.Fatal(err)
}
return v, func() {
if err := client.Variables.Delete(ctx, w.ID, v.ID); err != nil {
t.Errorf("Error destroying variable! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"Variable: %s\nError: %s", v.Key, err)
}
if wCleanup != nil {
wCleanup()
}
}
}
func createWorkspace(t *testing.T, client *Client, org *Organization) (*Workspace, func()) {
return createWorkspaceWithOptions(t, client, org, WorkspaceCreateOptions{
Name: String(randomString(t)),
})
}
func createWorkspaceWithOptions(t *testing.T, client *Client, org *Organization, options WorkspaceCreateOptions) (*Workspace, func()) {
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
ctx := context.Background()
w, err := client.Workspaces.Create(ctx, org.Name, options)
if err != nil {
t.Fatal(err)
}
return w, func() {
if err := client.Workspaces.DeleteByID(ctx, w.ID); err != nil {
t.Errorf("Error destroying workspace! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"Workspace: %s\nError: %s", w.ID, err)
}
if orgCleanup != nil {
orgCleanup()
}
}
}
// queueAllRuns: Whether runs should be queued immediately after workspace creation. When set to
// false, runs triggered by a VCS change will not be queued until at least one run is manually
// queued. If set to true, a run will be automatically started after the configuration is ingressed
// from VCS.
func createWorkspaceWithVCS(t *testing.T, client *Client, org *Organization, options WorkspaceCreateOptions) (*Workspace, func()) {
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
oc, ocCleanup := createOAuthToken(t, client, org)
githubIdentifier := os.Getenv("GITHUB_POLICY_SET_IDENTIFIER")
if githubIdentifier == "" {
t.Fatal("Export a valid GITHUB_POLICY_SET_IDENTIFIER before running this test!")
}
if options.Name == nil {
options.Name = String(randomString(t))
}
if options.VCSRepo == nil {
options.VCSRepo = &VCSRepoOptions{}
}
options.VCSRepo.Identifier = String(githubIdentifier)
options.VCSRepo.OAuthTokenID = String(oc.ID)
ctx := context.Background()
w, err := client.Workspaces.Create(ctx, org.Name, options)
if err != nil {
t.Fatal(err)
}
return w, func() {
if err := client.Workspaces.Delete(ctx, org.Name, w.Name); err != nil {
t.Errorf("Error destroying workspace! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"Workspace: %s\nError: %s", w.Name, err)
}
if ocCleanup != nil {
ocCleanup()
}
if orgCleanup != nil {
orgCleanup()
}
}
}
// This function is added to test setting up workspace's VCS connection via Github App Installation in place of
// Oauth token. For now the value of GHAInstallationID has to manually set to the correct value by the user.
func createWorkspaceWithGithubApp(t *testing.T, client *Client, org *Organization, options WorkspaceCreateOptions) (*Workspace, func()) {
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
gHAInstallationID := os.Getenv("GITHUB_APP_INSTALLATION_ID")
if gHAInstallationID == "" {
t.Skip("Export a valid GITHUB_APP_INSTALLATION_ID before running this test!")
}
options.VCSRepo.GHAInstallationID = String(gHAInstallationID)
githubIdentifier := os.Getenv("GITHUB_POLICY_SET_IDENTIFIER")
if githubIdentifier == "" {
t.Fatal("Export a valid GITHUB_POLICY_SET_IDENTIFIER before running this test!")
}
if options.Name == nil {
options.Name = String(randomString(t))
}
if options.VCSRepo == nil {
options.VCSRepo = &VCSRepoOptions{}
}
options.VCSRepo.Identifier = String(githubIdentifier)
ctx := context.Background()
w, err := client.Workspaces.Create(ctx, org.Name, options)
if err != nil {
t.Fatal(err)
}
return w, func() {
if err := client.Workspaces.Delete(ctx, org.Name, w.Name); err != nil {
t.Errorf("Error destroying workspace! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"Workspace: %s\nError: %s", w.Name, err)
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func createWorkspaceRunTask(t *testing.T, client *Client, workspace *Workspace, runTask *RunTask) (*WorkspaceRunTask, func()) {
var organization *Organization
var runTaskCleanup func()
var workspaceCleanup func()
var orgCleanup func()
if workspace == nil {
organization, orgCleanup = createOrganization(t, client)
workspace, workspaceCleanup = createWorkspace(t, client, organization)
}
if runTask == nil {
runTask, runTaskCleanup = createRunTask(t, client, organization)
}
ctx := context.Background()
wr, err := client.WorkspaceRunTasks.Create(ctx, workspace.ID, WorkspaceRunTaskCreateOptions{
EnforcementLevel: Advisory,
RunTask: runTask,
})
if err != nil {
t.Fatal(err)
}
return wr, func() {
if err := client.WorkspaceRunTasks.Delete(ctx, workspace.ID, wr.ID); err != nil {
t.Errorf("Error destroying workspace run task!\n"+
"Workspace: %s\n"+
"Workspace Run Task: %s\n"+
"Error: %s", workspace.ID, wr.ID, err)
}
if runTaskCleanup != nil {
runTaskCleanup()
}
if workspaceCleanup != nil {
workspaceCleanup()
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func createVariableSet(t *testing.T, client *Client, org *Organization, options VariableSetCreateOptions) (*VariableSet, func()) {
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
if options.Name == nil {
options.Name = String(randomString(t))
}
if options.Global == nil {
options.Global = Bool(false)
}
ctx := context.Background()
vs, err := client.VariableSets.Create(ctx, org.Name, &options)
if err != nil {
t.Fatal(err)
}
return vs, func() {
if err := client.VariableSets.Delete(ctx, vs.ID); err != nil {
t.Errorf("Error destroying variable set! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"VariableSet: %s\nError: %s", vs.Name, err)
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func applyVariableSetToWorkspace(t *testing.T, client *Client, vsID, wsID string) {
if vsID == "" {
t.Fatal("variable set ID must not be empty")
}
if wsID == "" {
t.Fatal("workspace ID must not be empty")
}
opts := &VariableSetApplyToWorkspacesOptions{}
opts.Workspaces = append(opts.Workspaces, &Workspace{ID: wsID})
ctx := context.Background()
if err := client.VariableSets.ApplyToWorkspaces(ctx, vsID, opts); err != nil {
t.Fatalf("Error applying variable set %s to workspace %s: %v", vsID, wsID, err)
}
t.Cleanup(func() {
removeOpts := &VariableSetRemoveFromWorkspacesOptions{}
removeOpts.Workspaces = append(removeOpts.Workspaces, &Workspace{ID: wsID})
if err := client.VariableSets.RemoveFromWorkspaces(ctx, vsID, removeOpts); err != nil {
t.Errorf("Error removing variable set from workspace! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"VariableSet ID: %s\nError: %s", vsID, err)
}
})
}
func applyVariableSetToProject(t *testing.T, client *Client, vsID, prjID string) {
t.Helper()
if vsID == "" {
t.Fatal("variable set ID must not be empty")
}
if prjID == "" {
t.Fatal("project ID must not be empty")
}
opts := VariableSetApplyToProjectsOptions{}
opts.Projects = append(opts.Projects, &Project{ID: prjID})
ctx := context.Background()
if err := client.VariableSets.ApplyToProjects(ctx, vsID, opts); err != nil {
t.Fatalf("Error applying variable set %s to project %s: %v", vsID, prjID, err)
}
t.Cleanup(func() {
removeOpts := VariableSetRemoveFromProjectsOptions{}
removeOpts.Projects = append(removeOpts.Projects, &Project{ID: prjID})
if err := client.VariableSets.RemoveFromProjects(ctx, vsID, removeOpts); err != nil {
t.Errorf("Error removing variable set from project! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"VariableSet ID: %s\nError: %s", vsID, err)
}
})
}
func createVariableSetVariable(t *testing.T, client *Client, vs *VariableSet, options VariableSetVariableCreateOptions) (*VariableSetVariable, func()) {
var vsCleanup func()
if vs == nil {
vs, vsCleanup = createVariableSet(t, client, nil, VariableSetCreateOptions{})
}
if options.Key == nil {
options.Key = String(randomString(t))
}
if options.Value == nil {
options.Value = String(randomString(t))
}
if options.Description == nil {
options.Description = String("")
}
if options.Category == nil {
options.Category = Category(CategoryTerraform)
}
if options.HCL == nil {
options.HCL = Bool(false)
}
if options.Sensitive == nil {
options.Sensitive = Bool(false)
}
ctx := context.Background()
v, err := client.VariableSetVariables.Create(ctx, vs.ID, &options)
if err != nil {
t.Fatal(err)
}
return v, func() {
if err := client.VariableSetVariables.Delete(ctx, vs.ID, v.ID); err != nil {
t.Errorf("Error destroying variable! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"Variable: %s\nError: %s", v.Key, err)
}
if vsCleanup != nil {
vsCleanup()
}
}
}
// Attempts to upgrade an organization to the business plan. Requires a user token with admin access.
// DEPRECATED : Please use the newSubscriptionUpdater instead.
func upgradeOrganizationSubscription(t *testing.T, _ *Client, organization *Organization) {
newSubscriptionUpdater(organization).WithBusinessPlan().Update(t)
}
func createProject(t *testing.T, client *Client, org *Organization) (*Project, func()) {
return createProjectWithOptions(t, client, org, ProjectCreateOptions{
Name: randomStringWithoutSpecialChar(t),
})
}
func createProjectWithOptions(t *testing.T, client *Client, org *Organization, options ProjectCreateOptions) (*Project, func()) {
var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}
ctx := context.Background()
p, err := client.Projects.Create(ctx, org.Name, options)
if err != nil {
t.Fatal(err)
}
return p, func() {
if err := client.Projects.Delete(ctx, p.ID); err != nil {
t.Logf("Error destroying project! WARNING: Dangling resources "+
"may exist! The full error is shown below.\n\n"+
"Project ID: %s\nError: %s", p.ID, err)
}
if orgCleanup != nil {
orgCleanup()
}
}
}
func createTarGzipArchive(t *testing.T, files []string, outputPath string) {
if len(files) == 0 {
t.Fatal("files to archive are empty")
}
out, err := os.Create(outputPath)
if err != nil {
t.Fatal(err)
}
defer out.Close()
gw := gzip.NewWriter(out)
defer gw.Close()
tw := tar.NewWriter(gw)
defer tw.Close()
for _, filename := range files {
func() {
file, err := os.Open(filename)
if err != nil {
t.Fatal(err)
}
defer file.Close()
info, err := file.Stat()
if err != nil {
t.Fatal(err)
}
header, err := tar.FileInfoHeader(info, info.Name())
if err != nil {
t.Fatal(err)
}
header.Name = filename
err = tw.WriteHeader(header)
if err != nil {
t.Fatal(err)
}
_, err = io.Copy(tw, file)
if err != nil {
t.Fatal(err)
}
}()
}
t.Cleanup(func() {
err := os.Remove(outputPath)
if err != nil {
t.Fatal("failed to delete archive: %w", err)
}
})
}
func waitForSVOutputs(t *testing.T, client *Client, svID string) {
t.Helper()
_, err := retryPatiently(func() (interface{}, error) {
outputs, err := client.StateVersions.ListOutputs(context.Background(), svID, nil)
if err != nil {
return nil, err
}
if len(outputs.Items) == 0 {
return nil, errors.New("no state version outputs found")
}
return outputs, nil
})
if err != nil {
t.Error(err)
}
}
func waitForRunLock(t *testing.T, client *Client, workspaceID string) {
t.Helper()
_, err := retry(func() (interface{}, error) {
ws, err := client.Workspaces.ReadByID(context.Background(), workspaceID)
if err != nil {
return nil, err
}
if !ws.Locked {
return nil, errors.New("workspace is not locked by run")
}
return ws, nil
})
if err != nil {
t.Error(err)
}
}
func retryTimes(maxRetries, secondsBetween int, f retryableFn) (interface{}, error) {
tick := time.NewTicker(time.Duration(secondsBetween) * time.Second)
retries := 0
defer tick.Stop()
for { //nolint
select {
case <-tick.C:
res, err := f()
if err == nil {
return res, nil
}
if retries >= maxRetries {
return nil, err
}
retries += 1
}
}
}
func retryTimesIf[T any](maxRetries, secondsBetween int, f retryableFn, c func(T) bool) (T, error) {
tick := time.NewTicker(time.Duration(secondsBetween) * time.Second)
retries := 0
defer tick.Stop()
var zero T
for {
<-tick.C
res, err := f()
obj, ok := res.(T)
if !ok {
return zero, fmt.Errorf("type assertion failed in retryTimesIf")
}
if err == nil && !c(obj) {
return obj, nil
}
if retries >= maxRetries {
return zero, err
}
retries += 1
}
}
func retryPatiently(f retryableFn) (interface{}, error) {
return retryTimes(39, 3, f) // 40 attempts over 120 seconds
}
func retry(f retryableFn) (interface{}, error) { //nolint
return retryTimes(9, 3, f) // 10 attempts over 30 seconds
}
func retryPatientlyIf[T any](f retryableFn, c func(T) bool) (T, error) {
return retryTimesIf[T](39, 3, f, c) // 40 attempts over 120 seconds
}
func retryIf[T any](f retryableFn, c func(T) bool) (T, error) {
return retryTimesIf[T](9, 3, f, c) // 10 attempts over 30 seconds
}
func genSha(t *testing.T) string {
t.Helper()
h := hmac.New(sha256.New, []byte("secret"))
_, err := h.Write([]byte("data"))
if err != nil {
t.Fatalf("error writing hmac: %s", err)
}
sha := hex.EncodeToString(h.Sum(nil))
return sha
}
// genSafeRandomTerraformVersion returns a random version number of the form
// `1.0.`, which HCP Terraform won't ever select as the latest available
// Terraform. (At the time of writing, a fresh HCP Terraform instance will include
// official Terraforms 1.2 and higher.) This is necessary because newly created
// workspaces default to the latest available version, and there's nothing
// preventing unrelated processes from creating workspaces during these tests.
func genSafeRandomTerraformVersion() string {
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()
// Avoid colliding with an official Terraform version. Highest 1.0 was
// 1.0.11, so add a little padding and call it good.
for rInt < 20 {
rInt = rand.New(rand.NewSource(time.Now().UnixNano())).Int()
}
return fmt.Sprintf("1.0.%d", rInt)
}
// createAdminSentinelVersion returns a random version number of the form
// `0.0.`
func createAdminSentinelVersion() string {
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()
return fmt.Sprintf("0.0.%d", rInt)
}
// createAdminOPAVersion returns a random OPA version number of the form
// `0.0.`
func createAdminOPAVersion() string {
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()
return fmt.Sprintf("0.0.%d", rInt)
}
func randomString(t *testing.T) string {
v, err := uuid.GenerateUUID()
if err != nil {
t.Fatal(err)
}
return v
}
func randomStringWithoutSpecialChar(t *testing.T) string {
v, err := uuid.GenerateUUID()
if err != nil {
t.Fatal(err)
}
uuidWithoutHyphens := strings.ReplaceAll(v, "-", "")
return uuidWithoutHyphens
}
func randomKeyValue(t *testing.T) string {
v, err := uuid.GenerateUUID()
if err != nil {
t.Fatal(err)
}
uuidWithoutHyphens := strings.ReplaceAll(v, "-", "")
return "t" + uuidWithoutHyphens
}
func containsProject(pl []*Project, str string) bool {
for _, p := range pl {
if p.Name == str {
return true
}
}
return false
}
func randomSemver(t *testing.T) string {
t.Helper()
return fmt.Sprintf("%d.%d.%d", rand.Intn(99)+3, rand.Intn(99)+1, rand.Intn(99)+1)
}
// skips a test if the environment is for HCP Terraform.
func skipUnlessEnterprise(t *testing.T) {
t.Helper()
if !enterpriseEnabled() {
t.Skip("Skipping test related to HCP Terraform. Set ENABLE_TFE=1 to run.")
}
}
// skips a test if the environment is for Terraform Enterprise
func skipIfEnterprise(t *testing.T) {
t.Helper()
if enterpriseEnabled() {
t.Skip("Skipping test related to Terraform Enterprise. Set ENABLE_TFE=0 to run.")
}
}
func skipHYOKIntegrationTests(t *testing.T) {
t.Helper()
if !hyokIntegrationTestsEnabled() {
t.Skip("Skipping test related to HYOK features. Set ENABLE_HYOK_INTEGRATION_TESTS=1 to run.")
}
}
// skips a test if the underlying beta feature is not available.
// **Note: ENABLE_BETA is always disabled in CI, so ensure you:
//
// 1. Run tests locally and paste the test output in the resulting pull request
// 2. Remove the beta requirements of your feature from go-tfe once the feature is generally available.
//
// See CONTRIBUTING.md for details
func skipUnlessBeta(t *testing.T) {
t.Helper()
if !betaFeaturesEnabled() {
t.Skip("Skipping test related to a HCP Terraform beta feature. Set ENABLE_BETA=1 to run.")
}
}
// skips a test if the architecture is not linux_amd64
func skipUnlessLinuxAMD64(t *testing.T) {
t.Helper()
if !linuxAmd64() {
t.Skip("Skipping test if architecture is not linux_amd64")
}
}
// Temporarily skip a test that may be experiencing API errors. This method
// purposefully errors after the set date to remind contributors to remove this check
// and verify that the API errors are no longer occurring.
func skipUnlessAfterDate(t *testing.T, d time.Time) {
today := time.Now()
if today.After(d) {
t.Fatalf("This test was temporarily skipped and has now expired. Remove this check to run this test.")
} else {
t.Skipf("Temporarily skipping test due to external issues: %s", t.Name())
}
}
func linuxAmd64() bool {
return runtime.GOOS == "linux" && runtime.GOARCH == "amd64"
}
// Checks to see if ENABLE_TFE is set to 1, thereby enabling enterprise tests.
func enterpriseEnabled() bool {
return os.Getenv("ENABLE_TFE") == "1"
}
// Checks to see if ENABLE_BETA is set to 1, thereby enabling tests for beta features.
func betaFeaturesEnabled() bool {
return os.Getenv("ENABLE_BETA") == "1"
}
// Checks to see if HYOK_INTEGRATION_TESTS is set to 1, thereby enabling tests for HYOK features.
func hyokIntegrationTestsEnabled() bool {
return os.Getenv("ENABLE_HYOK_INTEGRATION_TESTS") == "1"
}
func runDependentTestNameValidator(t *testing.T) {
t.Helper()
testName := t.Name()
testNameParts := strings.Split(testName, "/")
if len(testNameParts) == 0 {
return
}
rootTestName := testNameParts[0]
if rootTestName != "" && !strings.HasSuffix(rootTestName, "_RunDependent") {
t.Fatalf("Tests that start runs must have names ending with '_RunDependent', but got: %s", rootTestName)
}
}
func testHyokOrganization(t *testing.T, client *Client) *Organization {
ctx := context.Background()
// replace the environment variable with a valid organization name that has desired test configurations
hyokOrganizationName := os.Getenv("HYOK_ORGANIZATION_NAME")
if hyokOrganizationName == "" {
t.Fatal("Export a valid HYOK_ORGANIZATION_NAME before running this test!")
}
orgTest, err := client.Organizations.Read(ctx, hyokOrganizationName)
if err != nil {
t.Fatal(err)
}
return orgTest
}
// isEmpty gets whether the specified object is considered empty or not.
func isEmpty(object interface{}) bool {
// get nil case out of the way
if object == nil {
return true
}
objValue := reflect.ValueOf(object)
switch objValue.Kind() {
// collection types are empty when they have no element
case reflect.Chan, reflect.Map, reflect.Slice:
return objValue.Len() == 0
// pointers are empty if nil or if the value they point to is empty
case reflect.Ptr:
if objValue.IsNil() {
return true
}
deref := objValue.Elem().Interface()
return isEmpty(deref)
// for all other types, compare against the zero value
// array types are empty when they match their zero-initialized state
default:
zero := reflect.Zero(objValue.Type())
return reflect.DeepEqual(object, zero.Interface())
}
}
// requireExactlyOneNotEmpty accepts any number of values and calls t.Fatal if
// less or more than one is empty.
func requireExactlyOneNotEmpty(t *testing.T, v ...any) {
if len(v) == 0 {
t.Fatal("Expected some values for requireExactlyOneNotEmpty, but received none")
}
empty := 0
for _, value := range v {
if isEmpty(value) {
empty += 1
}
}
if empty != len(v)-1 {
t.Fatalf("Expected exactly one value to not be empty, but found %d empty values", empty)
}
}
func runTaskCallbackMockServer(t *testing.T) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
return
}
if r.Header.Get("Accept") != ContentTypeJSONAPI {
t.Fatalf("unexpected accept header: %q", r.Header.Get("Accept"))
}
if r.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", testTaskResultCallbackToken) {
t.Fatalf("unexpected authorization header: %q", r.Header.Get("Authorization"))
}
if r.Header.Get("Authorization") == fmt.Sprintf("Bearer %s", testInitialClientToken) {
t.Fatalf("authorization header is still the initial one: %q", r.Header.Get("Authorization"))
}
if r.Header.Get("User-Agent") != "go-tfe" {
t.Fatalf("unexpected user agent header: %q", r.Header.Get("User-Agent"))
}
}))
}
func enableSAML(ctx context.Context, t *testing.T, client *Client, enable bool) {
t.Helper()
var options AdminSAMLSettingsUpdateOptions
if enable {
options = AdminSAMLSettingsUpdateOptions{
Enabled: Bool(true),
SLOEndpointURL: String("https://example.com/slo"),
SSOEndpointURL: String("https://example.com/sso"),
Certificate: String("testCert"),
IDPCert: String("testCert"),
}
} else {
options = AdminSAMLSettingsUpdateOptions{
Enabled: Bool(false),
}
}
_, err := client.Admin.Settings.SAML.Update(ctx, options)
require.NoError(t, err)
}
func enableSCIM(ctx context.Context, t *testing.T, client *Client, enable bool) {
t.Helper()
if enable {
enableSAML(ctx, t, client, true)
err := setSAMLProviderType(ctx, t, client, true)
require.NoError(t, err, "error setting SAML provider type")
_, err = client.Admin.Settings.SCIM.Update(ctx, AdminSCIMSettingUpdateOptions{
Enabled: Bool(true),
})
require.NoError(t, err, "error enabling SCIM")
} else {
err := client.Admin.Settings.SCIM.Delete(ctx)
require.NoError(t, err, "error disabling SCIM")
err = setSAMLProviderType(ctx, t, client, false)
require.NoError(t, err, "error clearing SAML provider type")
enableSAML(ctx, t, client, false)
}
}
func setSAMLProviderType(ctx context.Context, t *testing.T, client *Client, setProvider bool) error {
t.Helper()
var provider SAMLProviderType
if setProvider {
provider = SAMLProviderTypeGeneric
} else {
provider = SAMLProviderTypeUnknown
}
_, err := client.Admin.Settings.SAML.Update(ctx, AdminSAMLSettingsUpdateOptions{ProviderType: &provider})
return err
}
func createSCIMGroup(ctx context.Context, t *testing.T, client *Client, groupName, scimToken string) string {
t.Helper()
payload := struct {
DisplayName string `json:"displayName"`
Schemas []string `json:"schemas"`
}{
DisplayName: groupName,
Schemas: []string{"urn:ietf:params:scim:schemas:core:2.0:Group"},
}
body, err := serializeRequestBody(&payload)
require.NoError(t, err)
u := client.BaseURL()
u.Path = "/scim/v2/Groups"
req, err := retryablehttp.NewRequest("POST", u.String(), body)
require.NoError(t, err)
req.Header = client.headers.Clone()
req.Header.Set("Authorization", "Bearer "+scimToken)
req.Header.Set("Accept", "application/scim+json")
req.Header.Set("Content-Type", "application/scim+json; charset=utf-8")
var res struct {
ID string `json:"id"`
}
err = (&ClientRequest{
retryableRequest: req,
http: client.http,
limiter: client.limiter,
Header: req.Header,
}).DoJSON(ctx, &res)
require.NoError(t, err)
require.NotEmpty(t, res.ID)
return res.ID
}
func deleteSCIMGroup(ctx context.Context, t *testing.T, client *Client, groupID, scimToken string) {
t.Helper()
u := client.BaseURL()
u.Path = "/scim/v2/Groups/" + url.PathEscape(groupID)
req, err := retryablehttp.NewRequest("DELETE", u.String(), nil)
require.NoError(t, err)
req.Header = client.headers.Clone()
req.Header.Set("Authorization", "Bearer "+scimToken)
err = (&ClientRequest{
retryableRequest: req,
http: client.http,
limiter: client.limiter,
Header: req.Header,
}).Do(ctx, nil)
require.NoError(t, err)
}
// Useless key but enough to pass validation in the API
const testGpgArmor string = `
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGKnWEYBEACsTJ9HEUrXBaBvQvXZAXEIMWloG96MVAdCj547jJviSS4TqMIQ
EST2pzDq7lEpqL+JkW3ptyLEAeQs6gJJeuhODGm2EcxjJ9/JM4ZH+p9zq2wBeXVe
0XJcP3HD8/7MesjMyGSsoX7tR7TcIhs5Y7zS+/L1xnoReYUsBgC6QdqjQwkuntaq
2y6yxdYG4gVlxb4yA0Ga6Qfy0VGIKjbCdPqCRyJ76YHE3t+Skq9oDCOV3VSiwKsU
V/ivf/MVZ1GyE03anW0+poVK38Ekogsd2+34uEjusbuoJGmHzh/20IDS8VnxQHIY
qdVwcZrW+a3O6nexL4dJJGMfXMbCdS87FxpSnC1FDGMSJ2c5cxlMuKuDboTpbRy5
Dd80p6voJQcLcpr0hKYIwwDGJYE336KMFqf/apCc6HbCFfN8kCYg3K7+4yganRWu
h/9qIhP0QaYOYEQl4RdjJTSyJSP3srAJ3F5OmrAhRXlHlLo1p00zxFxG7ZcJER6l
+uRubtL9WN2kgGbr9NDJbz/HeOTjJhCASdQuzstcL8RrFMDftE/P2K8LnkxUNIbT
dhZtwvkhnyIwOZIHwsQddeJboeHD445SlHJ+4vFsPKRTuNu5u9GhVSyZhoHmdeH0
FheD8p43+BKZ7KmD4xd+zfCQE1xO2cO9ZrCNV2hs9UVFbgZfjokqWkuHJQARAQAB
tBNmb28gPGFkbWluQHRmZS5jb20+iQJRBBMBCAA7FiEE/2esSrAATXzEQSanE9/s
yjtYzkoFAmKnWEYCGwMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQE9/s
yjtYzkq01g/9EgnW0NBD4DdtQSHg5jya0lx5iNHLK+umwL2x7abcSQ9iTIylhbHP
+he6jS/p4yzK7Gf7S+W3D9EZ58KrTMhu85iLr0uZ947pEbC0kDlQGkIfiK0CAyq2
IDj1RFgmeM0E2LkPOYCM+JPeBC9nZduFMYY9eFhCZXJ3ua1DP37ZBdZbjuImbiQ5
abt75a89NbQI3KRaACzqEjFpRYuoxbh8RznkTFf57AFzt4yMWy+4l47GSXTE8boS
1P7ZOfvJPuh2RRN9sSe0eTPCYnnSxPPo0LvgqSnLSk9yc65nkPZmlSXVdswV5Le+
7LlKG+rTwXljfGwLmj0VNn2gGCKe5IHs8FKt3parSiQOu4MXHCHshSQDEvXyIugJ
i2V2pcw4Hi6f2Znh3YYJamL6fDwCpDcTOCxZbvFi4OuBzbWcDLP1k52k3ZyYce92
1CK84HWtoRseNlVt1rieClPZH5T4b0HMPBWKK39/r+RABJDAfdGtn2ulKXK2JugH
AYXlhY9xh9+r1O7tsqExGkEYnp7nI0ArauJhIUWZybpGpPYP99kK4F64E4DRu1si
/3eeYoqKY1jAHoebRzn3XcRg5kro/lJYQQIhT4fHt5sAc/e8gDdaQaDPIftsmu7K
w4e6pMyztiMfRw7w0ZSjGlPsl0NiXA3nuG966gx4Bnx/ddJIHrghAi25Ag0EYqdY
RgEQAOGONFP+z45+9gvnT1yd9sJLqxYhtj5QRxKkXkLARPd0Yjdyff/lVd1YPtZ7
slLuEGlBDKdB6aIeu3b1C95Ie3qbTIwIp6ZYKGqUEwGW/0sPtBqqXanVrQkrY4ho
lqejgPraFgF6sDGrSxG7b8W985NJwKcm8Lx1/x4ZwvpUrQlCL4UajJcECmjVqU/e
ofjWZFZl7eR2oYh2BBzvA8mwkVKXs6kTGWLkK7VDeR2lCRl2fk4+5DydbOMIZXxT
jmYR8iu2Mr+gt//VmvvBjlFMI05kwD9iG3SRYBwpYEXETKCE12KKqcbhP/bwahIB
bcsaQkoky9jgtp7tizduPOkjkGhT9kF8L1O0VGxek40L7+QIDEnVHMAH5hSLmgau
vJF+Bd0W/TRZbmAJXoWPreftVTmWH7xH4N7v+3dvWziIJPt+N/1HHeZXBojJJAVk
6C+t1KpsSwGzzOjdsQVCklT7D4PmWtzz6FAjImPSbk5LbiVWis/lH+SEVZS4sG7j
pR3vRjUZTjCi/8CmHTjiWXL7g9kkt//a5Av3iArQq0pv0QNPG/uPeN2QTnkz5DAo
kM/qUx/G59i8AfEH2myh9oPCOzb3yFOsK9G/2Sy05cfdLozddHwt+hJVPx1Od9Nr
HAJMQspr9AaZPB9FnAa0Bv/RNEGJv6LJwzVWJkezL2wQAZdlABEBAAGJAjYEGAEI
ACAWIQT/Z6xKsABNfMRBJqcT3+zKO1jOSgUCYqdYRgIbDAAKCRAT3+zKO1jOSq9E
D/4hlNaCwY/etk7ZvMe4pupQATzrZF58d2qjx4niMd3CvCWmbrWMmoNxBjECXc8H
kp+0NURFFc/wiCn/Q6dhrMxKVCpsWpHA1Doi/vtzQtM081Ib6uIX6L6liyUexW1l
tvJwPurqJJVBW3ikOjICCnv70tp2zaS47uQjyFGTnzglIU961EXCWdNjH1vm8bFJ
BxXN87gHXhUUw8GZ3d2V75TAJIEqRVV+eI4flXcJ4Ld+Zbt2EiMwtQ05XCc8bgsc
QzZFizw936bC5Py7Iu6aEaShFlZlz8LgYcId32UYh5PG1xGNZv0C9Z/PJECx5zcx
RJszDpm3erpmdkkJf9UBuhjjTdQ9gheFjZRDi/rVJ0JPVxD7HTzEAWd5MqFXqh0V
j2xG1FhtfxSaMf9rsJjtwewLPyZylSuz2erz1j80Hx3Q6eSIDsNjnDTtfh9Z8gXz
gvF7mSC0lZu/RvDSRyHfCw4zCQ04HieIvq3hZLy+QS11ykJTSKAePKk77EmwtoLd
Je9n9FCKhLknUp1/dsu0lsznvttOLwYy6xFP4JNPgiq6iYlVHs417oib67DrGlsI
3Ki44OESW/vL3WAC091TOF4OYgGw+TMauB8SxZo0PLXrIwKeBsQEB4tf6bX66OvJ
UFpas2r53xTaraRDpu6+u66hLY+/XV9Uf5YzETuPQnX/nw==
=bBSS
-----END PGP PUBLIC KEY BLOCK-----
`
================================================
FILE: hyok_configuration.go
================================================
package tfe
import (
"context"
"fmt"
"net/url"
)
// HYOKConfigurations describes all the HYOK configuration related methods that the HCP Terraform API supports.
// HCP Terraform API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/hold-your-own-key/configurations
type HYOKConfigurations interface {
List(ctx context.Context, organization string, options *HYOKConfigurationsListOptions) (*HYOKConfigurationsList, error)
Create(ctx context.Context, organization string, options HYOKConfigurationsCreateOptions) (*HYOKConfiguration, error)
Read(ctx context.Context, hyokID string, options *HYOKConfigurationsReadOptions) (*HYOKConfiguration, error)
Update(ctx context.Context, hyokID string, options HYOKConfigurationsUpdateOptions) (*HYOKConfiguration, error)
Delete(ctx context.Context, hyokID string) error
// Test checks the HYOK configuration and returns success if the configuration is valid.
// It returns an error along with the error message if any issues are found.
Test(ctx context.Context, hyokID string) error
Revoke(ctx context.Context, hyokID string) error
}
type hyokConfigurations struct {
client *Client
}
var _ HYOKConfigurations = &hyokConfigurations{}
type HYOKConfigurationStatus string
const (
HYOKConfigurationUntested HYOKConfigurationStatus = "untested"
HYOKConfigurationTesting HYOKConfigurationStatus = "testing"
HYOKConfigurationTestFailed HYOKConfigurationStatus = "test_failed"
HYOKConfigurationAvailable HYOKConfigurationStatus = "available"
HYOKConfigurationErrored HYOKConfigurationStatus = "errored"
HYOKConfigurationRevoking HYOKConfigurationStatus = "revoking"
HYOKConfigurationRevoked HYOKConfigurationStatus = "revoked"
)
type OIDCConfigurationTypeChoice struct {
AWSOIDCConfiguration *AWSOIDCConfiguration
GCPOIDCConfiguration *GCPOIDCConfiguration
AzureOIDCConfiguration *AzureOIDCConfiguration
VaultOIDCConfiguration *VaultOIDCConfiguration
}
type KMSOptions struct {
// AWS
KeyRegion string `jsonapi:"attr,key-region,omitempty"`
// GCP
KeyLocation string `jsonapi:"attr,key-location,omitempty"`
KeyRingID string `jsonapi:"attr,key-ring-id,omitempty"`
}
type HYOKConfiguration struct {
ID string `jsonapi:"primary,hyok-configurations"`
// Attributes
KEKID string `jsonapi:"attr,kek-id"`
KMSOptions *KMSOptions `jsonapi:"attr,kms-options,omitempty"`
Name string `jsonapi:"attr,name"`
Primary bool `jsonapi:"attr,primary"`
Status HYOKConfigurationStatus `jsonapi:"attr,status"`
Error *string `jsonapi:"attr,error"`
// Relationships
Organization *Organization `jsonapi:"relation,organization"`
OIDCConfiguration *OIDCConfigurationTypeChoice `jsonapi:"polyrelation,oidc-configuration"`
AgentPool *AgentPool `jsonapi:"relation,agent-pool"`
KeyVersions []*HYOKCustomerKeyVersion `jsonapi:"relation,hyok-customer-key-versions"`
}
type HYOKConfigurationsList struct {
*Pagination
Items []*HYOKConfiguration
}
type HYOKConfigurationsIncludeOpt string
const (
HYOKConfigurationsIncludeHYOKCustomerKeyVersions HYOKConfigurationsIncludeOpt = "hyok_customer_key_versions"
HYOKConfigurationsIncludeOIDCConfiguration HYOKConfigurationsIncludeOpt = "oidc_configuration"
)
type HYOKConfigurationsListOptions struct {
ListOptions
SearchQuery string `url:"q,omitempty"`
Include []HYOKConfigurationsIncludeOpt `url:"include,omitempty"`
}
type HYOKConfigurationsCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,hyok-configurations"`
// Attributes
KEKID string `jsonapi:"attr,kek-id"`
KMSOptions *KMSOptions `jsonapi:"attr,kms-options"`
Name string `jsonapi:"attr,name"`
// Relationships
OIDCConfiguration *OIDCConfigurationTypeChoice `jsonapi:"polyrelation,oidc-configuration"`
AgentPool *AgentPool `jsonapi:"relation,agent-pool"`
}
type HYOKConfigurationsReadOptions struct {
Include []HYOKConfigurationsIncludeOpt `url:"include,omitempty"`
}
type HYOKConfigurationsUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,hyok-configurations"`
// Attributes
KEKID *string `jsonapi:"attr,kek-id,omitempty"`
KMSOptions *KMSOptions `jsonapi:"attr,kms-options,omitempty"`
Name *string `jsonapi:"attr,name,omitempty"`
Primary *bool `jsonapi:"attr,primary,omitempty"`
// Relationships
AgentPool *AgentPool `jsonapi:"relation,agent-pool,omitempty"`
}
func (h hyokConfigurations) List(ctx context.Context, organization string, options *HYOKConfigurationsListOptions) (*HYOKConfigurationsList, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
req, err := h.client.NewRequest("GET", fmt.Sprintf("organizations/%s/hyok-configurations", url.PathEscape(organization)), options)
if err != nil {
return nil, err
}
hyokConfigurationList := &HYOKConfigurationsList{}
err = req.Do(ctx, hyokConfigurationList)
if err != nil {
return nil, err
}
return hyokConfigurationList, nil
}
func (h hyokConfigurations) Read(ctx context.Context, hyokID string, options *HYOKConfigurationsReadOptions) (*HYOKConfiguration, error) {
if !validStringID(&hyokID) {
return nil, ErrInvalidHYOK
}
req, err := h.client.NewRequest("GET", fmt.Sprintf("hyok-configurations/%s", url.PathEscape(hyokID)), options)
if err != nil {
return nil, err
}
hyokConfiguration := &HYOKConfiguration{}
err = req.Do(ctx, hyokConfiguration)
if err != nil {
return nil, err
}
return hyokConfiguration, nil
}
func (h *HYOKConfigurationsCreateOptions) valid() error {
if h.KEKID == "" {
return ErrRequiredKEKID
}
if h.Name == "" {
return ErrRequiredName
}
if h.OIDCConfiguration == nil {
return ErrRequiredOIDCConfiguration
}
if h.AgentPool == nil {
return ErrRequiredAgentPool
}
if h.OIDCConfiguration.AWSOIDCConfiguration != nil {
if h.KMSOptions == nil {
return ErrRequiredKMSOptions
}
if h.KMSOptions.KeyRegion == "" {
return ErrRequiredKMSOptionsKeyRegion
}
}
if h.OIDCConfiguration.GCPOIDCConfiguration != nil {
if h.KMSOptions == nil {
return ErrRequiredKMSOptions
}
if h.KMSOptions.KeyLocation == "" {
return ErrRequiredKMSOptionsKeyLocation
}
if h.KMSOptions.KeyRingID == "" {
return ErrRequiredKMSOptionsKeyRingID
}
}
return nil
}
func (h hyokConfigurations) Create(ctx context.Context, organization string, options HYOKConfigurationsCreateOptions) (*HYOKConfiguration, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
req, err := h.client.NewRequest("POST", fmt.Sprintf("organizations/%s/hyok-configurations", url.PathEscape(organization)), &options)
if err != nil {
return nil, err
}
hyokConfiguration := &HYOKConfiguration{}
err = req.Do(ctx, hyokConfiguration)
if err != nil {
return nil, err
}
return hyokConfiguration, nil
}
func (h hyokConfigurations) Update(ctx context.Context, hyokID string, options HYOKConfigurationsUpdateOptions) (*HYOKConfiguration, error) {
if !validStringID(&hyokID) {
return nil, ErrInvalidHYOK
}
req, err := h.client.NewRequest("PATCH", fmt.Sprintf("hyok-configurations/%s", url.PathEscape(hyokID)), &options)
if err != nil {
return nil, err
}
hyokConfiguration := &HYOKConfiguration{}
err = req.Do(ctx, hyokConfiguration)
if err != nil {
return nil, err
}
return hyokConfiguration, nil
}
func (h hyokConfigurations) Delete(ctx context.Context, hyokID string) error {
if !validStringID(&hyokID) {
return ErrInvalidHYOK
}
req, err := h.client.NewRequest("DELETE", fmt.Sprintf("hyok-configurations/%s", url.PathEscape(hyokID)), nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (h hyokConfigurations) Test(ctx context.Context, hyokID string) error {
if !validStringID(&hyokID) {
return ErrInvalidHYOK
}
req, err := h.client.NewRequest("POST", fmt.Sprintf("hyok-configurations/%s/actions/test", url.PathEscape(hyokID)), nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (h hyokConfigurations) Revoke(ctx context.Context, hyokID string) error {
if !validStringID(&hyokID) {
return ErrInvalidHYOK
}
req, err := h.client.NewRequest("POST", fmt.Sprintf("hyok-configurations/%s/actions/revoke", url.PathEscape(hyokID)), nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
================================================
FILE: hyok_configuration_integration_test.go
================================================
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHYOKConfigurationCreateRevokeDelete(t *testing.T) {
t.Parallel()
skipHYOKIntegrationTests(t)
client := testClient(t)
ctx := context.Background()
orgTest := testHyokOrganization(t, client)
agentPool, agentPoolCleanup := createAgentPool(t, client, orgTest)
t.Cleanup(agentPoolCleanup)
t.Run("AWS with valid options", func(t *testing.T) {
awsOIDCConfig, configCleanup := createAWSOIDCConfiguration(t, client, orgTest)
t.Cleanup(configCleanup)
keyRegion := "us-east-1"
opts := HYOKConfigurationsCreateOptions{
Name: randomStringWithoutSpecialChar(t),
KMSOptions: &KMSOptions{
KeyRegion: keyRegion,
},
KEKID: "arn:aws:kms:us-east-1:123456789012:key/this-is-not-a-real-key",
AgentPool: agentPool,
OIDCConfiguration: &OIDCConfigurationTypeChoice{
AWSOIDCConfiguration: awsOIDCConfig,
},
}
created, err := client.HYOKConfigurations.Create(ctx, orgTest.Name, opts)
require.NoError(t, err)
require.NotNil(t, created)
assert.Equal(t, opts.Name, created.Name)
assert.Equal(t, opts.KEKID, created.KEKID)
assert.Equal(t, opts.KMSOptions.KeyRegion, created.KMSOptions.KeyRegion)
assert.Equal(t, opts.AgentPool.ID, created.AgentPool.ID)
assert.Equal(t, opts.OIDCConfiguration.AWSOIDCConfiguration.ID, created.OIDCConfiguration.AWSOIDCConfiguration.ID)
// Must first wait for test_failed status before revoking and deleting the HYOK config or else OIDC configs cannot be cleaned up
_, err = waitForHYOKConfigurationStatus(t, ctx, client, created.ID, HYOKConfigurationTestFailed)
require.NoError(t, err)
err = client.HYOKConfigurations.Revoke(ctx, created.ID)
require.NoError(t, err)
_, err = waitForHYOKConfigurationStatus(t, ctx, client, created.ID, HYOKConfigurationRevoked)
require.NoError(t, err, "Timed out waiting for HYOK configuration %s to revoke", created.ID)
err = client.HYOKConfigurations.Delete(ctx, created.ID)
require.NoError(t, err)
_, err = client.HYOKConfigurations.Read(ctx, created.ID, nil)
require.ErrorIs(t, err, ErrResourceNotFound)
})
t.Run("AWS with missing key region", func(t *testing.T) {
awsOIDCConfig, configCleanup := createAWSOIDCConfiguration(t, client, orgTest)
t.Cleanup(configCleanup)
opts := HYOKConfigurationsCreateOptions{
Name: randomStringWithoutSpecialChar(t),
KMSOptions: &KMSOptions{},
KEKID: "arn:aws:kms:us-east-1:123456789012:key/this-is-not-a-real-key",
AgentPool: agentPool,
OIDCConfiguration: &OIDCConfigurationTypeChoice{
AWSOIDCConfiguration: awsOIDCConfig,
},
}
_, err := client.HYOKConfigurations.Create(ctx, orgTest.Name, opts)
require.ErrorIs(t, err, ErrRequiredKMSOptionsKeyRegion)
})
t.Run("GCP with valid options", func(t *testing.T) {
gcpOIDCConfig, configCleanup := createGCPOIDCConfiguration(t, client, orgTest)
t.Cleanup(configCleanup)
keyLocation := "global"
keyRingID := randomStringWithoutSpecialChar(t)
opts := HYOKConfigurationsCreateOptions{
Name: randomStringWithoutSpecialChar(t),
KMSOptions: &KMSOptions{
KeyLocation: keyLocation,
KeyRingID: keyRingID,
},
KEKID: randomStringWithoutSpecialChar(t),
AgentPool: agentPool,
OIDCConfiguration: &OIDCConfigurationTypeChoice{
GCPOIDCConfiguration: gcpOIDCConfig,
},
}
created, err := client.HYOKConfigurations.Create(ctx, orgTest.Name, opts)
require.NoError(t, err)
require.NotNil(t, created)
assert.Equal(t, opts.Name, created.Name)
assert.Equal(t, opts.KEKID, created.KEKID)
assert.Equal(t, opts.KMSOptions.KeyLocation, created.KMSOptions.KeyLocation)
assert.Equal(t, opts.KMSOptions.KeyRingID, created.KMSOptions.KeyRingID)
assert.Equal(t, opts.AgentPool.ID, created.AgentPool.ID)
assert.Equal(t, opts.OIDCConfiguration.GCPOIDCConfiguration.ID, created.OIDCConfiguration.GCPOIDCConfiguration.ID)
// Must first wait for test_failed status before revoking and deleting the HYOK config or else OIDC configs cannot be cleaned up
_, err = waitForHYOKConfigurationStatus(t, ctx, client, created.ID, HYOKConfigurationTestFailed)
require.NoError(t, err)
err = client.HYOKConfigurations.Revoke(ctx, created.ID)
require.NoError(t, err)
_, err = waitForHYOKConfigurationStatus(t, ctx, client, created.ID, HYOKConfigurationRevoked)
require.NoError(t, err, "Timed out waiting for HYOK configuration %s to revoke", created.ID)
err = client.HYOKConfigurations.Delete(ctx, created.ID)
require.NoError(t, err)
_, err = client.HYOKConfigurations.Read(ctx, created.ID, nil)
require.ErrorIs(t, err, ErrResourceNotFound)
})
t.Run("GCP with missing key location", func(t *testing.T) {
gcpOIDCConfig, configCleanup := createGCPOIDCConfiguration(t, client, orgTest)
t.Cleanup(configCleanup)
keyRingID := randomStringWithoutSpecialChar(t)
opts := HYOKConfigurationsCreateOptions{
Name: randomStringWithoutSpecialChar(t),
KMSOptions: &KMSOptions{
KeyRingID: keyRingID,
},
KEKID: randomStringWithoutSpecialChar(t),
AgentPool: agentPool,
OIDCConfiguration: &OIDCConfigurationTypeChoice{
GCPOIDCConfiguration: gcpOIDCConfig,
},
}
_, err := client.HYOKConfigurations.Create(ctx, orgTest.Name, opts)
require.ErrorIs(t, err, ErrRequiredKMSOptionsKeyLocation)
})
t.Run("GCP with missing key ring ID", func(t *testing.T) {
gcpOIDCConfig, configCleanup := createGCPOIDCConfiguration(t, client, orgTest)
t.Cleanup(configCleanup)
keyLocation := "global"
opts := HYOKConfigurationsCreateOptions{
Name: randomStringWithoutSpecialChar(t),
KMSOptions: &KMSOptions{
KeyLocation: keyLocation,
},
KEKID: randomStringWithoutSpecialChar(t),
AgentPool: agentPool,
OIDCConfiguration: &OIDCConfigurationTypeChoice{
GCPOIDCConfiguration: gcpOIDCConfig,
},
}
_, err := client.HYOKConfigurations.Create(ctx, orgTest.Name, opts)
require.ErrorIs(t, err, ErrRequiredKMSOptionsKeyRingID)
})
t.Run("Vault with valid options", func(t *testing.T) {
vaultOIDCConfig, configCleanup := createVaultOIDCConfiguration(t, client, orgTest)
t.Cleanup(configCleanup)
opts := HYOKConfigurationsCreateOptions{
Name: randomStringWithoutSpecialChar(t),
KEKID: randomStringWithoutSpecialChar(t),
AgentPool: agentPool,
OIDCConfiguration: &OIDCConfigurationTypeChoice{
VaultOIDCConfiguration: vaultOIDCConfig,
},
}
created, err := client.HYOKConfigurations.Create(ctx, orgTest.Name, opts)
require.NoError(t, err)
require.NotNil(t, created)
assert.Equal(t, opts.Name, created.Name)
assert.Equal(t, opts.KEKID, created.KEKID)
assert.Equal(t, opts.AgentPool.ID, created.AgentPool.ID)
assert.Equal(t, opts.OIDCConfiguration.VaultOIDCConfiguration.ID, created.OIDCConfiguration.VaultOIDCConfiguration.ID)
// Must first wait for test_failed status before revoking and deleting the HYOK config or else OIDC configs cannot be cleaned up
_, err = waitForHYOKConfigurationStatus(t, ctx, client, created.ID, HYOKConfigurationTestFailed)
require.NoError(t, err)
err = client.HYOKConfigurations.Revoke(ctx, created.ID)
require.NoError(t, err)
_, err = waitForHYOKConfigurationStatus(t, ctx, client, created.ID, HYOKConfigurationRevoked)
require.NoError(t, err, "Timed out waiting for HYOK configuration %s to revoke", created.ID)
err = client.HYOKConfigurations.Delete(ctx, created.ID)
require.NoError(t, err)
_, err = client.HYOKConfigurations.Read(ctx, created.ID, nil)
require.ErrorIs(t, err, ErrResourceNotFound)
})
t.Run("Azure with valid options", func(t *testing.T) {
azureOIDCConfig, configCleanup := createAzureOIDCConfiguration(t, client, orgTest)
t.Cleanup(configCleanup)
opts := HYOKConfigurationsCreateOptions{
Name: randomStringWithoutSpecialChar(t),
KEKID: "https://random.vault.azure.net/keys/some-key",
AgentPool: agentPool,
OIDCConfiguration: &OIDCConfigurationTypeChoice{
AzureOIDCConfiguration: azureOIDCConfig,
},
}
created, err := client.HYOKConfigurations.Create(ctx, orgTest.Name, opts)
require.NoError(t, err)
require.NotNil(t, created)
assert.Equal(t, opts.Name, created.Name)
assert.Equal(t, opts.KEKID, created.KEKID)
assert.Equal(t, opts.AgentPool.ID, created.AgentPool.ID)
assert.Equal(t, opts.OIDCConfiguration.AzureOIDCConfiguration.ID, created.OIDCConfiguration.AzureOIDCConfiguration.ID)
// Must first wait for test_failed status before revoking and deleting the HYOK config or else OIDC configs cannot be cleaned up
_, err = waitForHYOKConfigurationStatus(t, ctx, client, created.ID, HYOKConfigurationTestFailed)
require.NoError(t, err)
err = client.HYOKConfigurations.Revoke(ctx, created.ID)
require.NoError(t, err)
_, err = waitForHYOKConfigurationStatus(t, ctx, client, created.ID, HYOKConfigurationRevoked)
require.NoError(t, err, "Timed out waiting for HYOK configuration %s to revoke", created.ID)
err = client.HYOKConfigurations.Delete(ctx, created.ID)
require.NoError(t, err)
_, err = client.HYOKConfigurations.Read(ctx, created.ID, nil)
require.ErrorIs(t, err, ErrResourceNotFound)
})
t.Run("with missing KEK ID", func(t *testing.T) {
awsOIDCConfig, configCleanup := createAWSOIDCConfiguration(t, client, orgTest)
t.Cleanup(configCleanup)
keyRegion := "us-east-1"
opts := HYOKConfigurationsCreateOptions{
Name: randomStringWithoutSpecialChar(t),
KMSOptions: &KMSOptions{
KeyRegion: keyRegion,
},
AgentPool: agentPool,
OIDCConfiguration: &OIDCConfigurationTypeChoice{
AWSOIDCConfiguration: awsOIDCConfig,
},
}
_, err := client.HYOKConfigurations.Create(ctx, orgTest.Name, opts)
require.ErrorIs(t, err, ErrRequiredKEKID)
})
t.Run("with missing agent pool", func(t *testing.T) {
awsOIDCConfig, configCleanup := createAWSOIDCConfiguration(t, client, orgTest)
t.Cleanup(configCleanup)
keyRegion := "us-east-1"
opts := HYOKConfigurationsCreateOptions{
Name: randomStringWithoutSpecialChar(t),
KMSOptions: &KMSOptions{
KeyRegion: keyRegion,
},
KEKID: randomStringWithoutSpecialChar(t),
OIDCConfiguration: &OIDCConfigurationTypeChoice{
AWSOIDCConfiguration: awsOIDCConfig,
},
}
_, err := client.HYOKConfigurations.Create(ctx, orgTest.Name, opts)
require.ErrorIs(t, err, ErrRequiredAgentPool)
})
t.Run("with missing OIDC config", func(t *testing.T) {
keyRegion := "us-east-1"
opts := HYOKConfigurationsCreateOptions{
Name: randomStringWithoutSpecialChar(t),
KMSOptions: &KMSOptions{
KeyRegion: keyRegion,
},
KEKID: randomStringWithoutSpecialChar(t),
AgentPool: agentPool,
}
_, err := client.HYOKConfigurations.Create(ctx, orgTest.Name, opts)
require.ErrorIs(t, err, ErrRequiredOIDCConfiguration)
})
}
func TestHyokConfigurationList(t *testing.T) {
t.Parallel()
skipHYOKIntegrationTests(t)
client := testClient(t)
ctx := context.Background()
orgTest := testHyokOrganization(t, client)
agentPool, agentPoolCleanup := createAgentPool(t, client, orgTest)
t.Cleanup(agentPoolCleanup)
azureOIDC, azureOIDCCleanup := createAzureOIDCConfiguration(t, client, orgTest)
t.Cleanup(azureOIDCCleanup)
hyok1, hyokCleanup1 := azureOIDC.createHYOKConfiguration(t, client, orgTest, agentPool)
t.Cleanup(hyokCleanup1)
awsOIDC, awsOIDCCleanup := createAWSOIDCConfiguration(t, client, orgTest)
t.Cleanup(awsOIDCCleanup)
hyok2, hyokCleanup2 := awsOIDC.createHYOKConfiguration(t, client, orgTest, agentPool)
t.Cleanup(hyokCleanup2)
gcpOIDC, gcpOIDCCleanup := createAWSOIDCConfiguration(t, client, orgTest)
t.Cleanup(gcpOIDCCleanup)
hyok3, hyokCleanup3 := gcpOIDC.createHYOKConfiguration(t, client, orgTest, agentPool)
t.Cleanup(hyokCleanup3)
vaultOIDC, vaultOIDCCleanup := createAWSOIDCConfiguration(t, client, orgTest)
t.Cleanup(vaultOIDCCleanup)
hyok4, hyokCleanup4 := vaultOIDC.createHYOKConfiguration(t, client, orgTest, agentPool)
t.Cleanup(hyokCleanup4)
t.Run("without list options", func(t *testing.T) {
results, err := client.HYOKConfigurations.List(ctx, orgTest.Name, nil)
var resultingIDs []string
for _, r := range results.Items {
resultingIDs = append(resultingIDs, r.ID)
}
require.NoError(t, err)
assert.Contains(t, resultingIDs, hyok1.ID)
assert.Contains(t, resultingIDs, hyok2.ID)
assert.Contains(t, resultingIDs, hyok3.ID)
assert.Contains(t, resultingIDs, hyok4.ID)
})
}
func TestHyokConfigurationRead(t *testing.T) {
t.Parallel()
skipHYOKIntegrationTests(t)
client := testClient(t)
ctx := context.Background()
orgTest := testHyokOrganization(t, client)
agentPool, agentPoolCleanup := createAgentPool(t, client, orgTest)
t.Cleanup(agentPoolCleanup)
t.Run("AWS", func(t *testing.T) {
oidc, oidcCleanup := createAWSOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcCleanup)
hyok, hyokCleanup := oidc.createHYOKConfiguration(t, client, orgTest, agentPool)
t.Cleanup(hyokCleanup)
fetched, err := client.HYOKConfigurations.Read(ctx, hyok.ID, nil)
require.NoError(t, err)
require.NotNil(t, fetched)
assert.Equal(t, hyok.Name, fetched.Name)
assert.Equal(t, hyok.KEKID, fetched.KEKID)
assert.Equal(t, hyok.KMSOptions.KeyRegion, fetched.KMSOptions.KeyRegion)
assert.Equal(t, hyok.Organization.Name, fetched.Organization.Name)
assert.Equal(t, hyok.AgentPool.ID, fetched.AgentPool.ID)
assert.Equal(t, hyok.OIDCConfiguration.AWSOIDCConfiguration.ID, fetched.OIDCConfiguration.AWSOIDCConfiguration.ID)
})
t.Run("Azure", func(t *testing.T) {
oidc, oidcCleanup := createAzureOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcCleanup)
hyok, hyokCleanup := oidc.createHYOKConfiguration(t, client, orgTest, agentPool)
t.Cleanup(hyokCleanup)
fetched, err := client.HYOKConfigurations.Read(ctx, hyok.ID, nil)
require.NoError(t, err)
require.NotNil(t, fetched)
assert.Equal(t, hyok.Name, fetched.Name)
assert.Equal(t, hyok.KEKID, fetched.KEKID)
assert.Equal(t, hyok.KMSOptions, fetched.KMSOptions)
assert.Equal(t, hyok.Organization.Name, fetched.Organization.Name)
assert.Equal(t, hyok.AgentPool.ID, fetched.AgentPool.ID)
assert.Equal(t, hyok.OIDCConfiguration.AzureOIDCConfiguration.ID, fetched.OIDCConfiguration.AzureOIDCConfiguration.ID)
})
t.Run("GCP", func(t *testing.T) {
oidc, oidcCleanup := createGCPOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcCleanup)
hyok, hyokCleanup := oidc.createHYOKConfiguration(t, client, orgTest, agentPool)
t.Cleanup(hyokCleanup)
fetched, err := client.HYOKConfigurations.Read(ctx, hyok.ID, nil)
require.NoError(t, err)
require.NotNil(t, fetched)
assert.Equal(t, hyok.Name, fetched.Name)
assert.Equal(t, hyok.KEKID, fetched.KEKID)
assert.Equal(t, hyok.KMSOptions.KeyLocation, fetched.KMSOptions.KeyLocation)
assert.Equal(t, hyok.KMSOptions.KeyRingID, fetched.KMSOptions.KeyRingID)
assert.Equal(t, hyok.Organization.Name, fetched.Organization.Name)
assert.Equal(t, hyok.AgentPool.ID, fetched.AgentPool.ID)
assert.Equal(t, hyok.OIDCConfiguration.GCPOIDCConfiguration.ID, fetched.OIDCConfiguration.GCPOIDCConfiguration.ID)
})
t.Run("Vault", func(t *testing.T) {
oidc, oidcCleanup := createVaultOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcCleanup)
hyok, hyokCleanup := oidc.createHYOKConfiguration(t, client, orgTest, agentPool)
t.Cleanup(hyokCleanup)
fetched, err := client.HYOKConfigurations.Read(ctx, hyok.ID, nil)
require.NoError(t, err)
require.NotNil(t, fetched)
assert.Equal(t, hyok.Name, fetched.Name)
assert.Equal(t, hyok.KEKID, fetched.KEKID)
assert.Equal(t, hyok.KMSOptions, fetched.KMSOptions)
assert.Equal(t, hyok.Organization.Name, fetched.Organization.Name)
assert.Equal(t, hyok.AgentPool.ID, fetched.AgentPool.ID)
assert.Equal(t, hyok.OIDCConfiguration.VaultOIDCConfiguration.ID, fetched.OIDCConfiguration.VaultOIDCConfiguration.ID)
})
t.Run("fetching non-existing configuration", func(t *testing.T) {
_, err := client.HYOKConfigurations.Read(ctx, "hyokc-notreal", nil)
assert.ErrorIs(t, err, ErrResourceNotFound)
})
}
func TestHYOKConfigurationUpdate(t *testing.T) {
t.Parallel()
skipHYOKIntegrationTests(t)
client := testClient(t)
ctx := context.Background()
orgTest := testHyokOrganization(t, client)
agentPool, agentPoolCleanup := createAgentPool(t, client, orgTest)
t.Cleanup(agentPoolCleanup)
t.Run("AWS with valid options", func(t *testing.T) {
oidc, oidcCleanup := createAWSOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcCleanup)
hyok, hyokCleanup := oidc.createHYOKConfiguration(t, client, orgTest, agentPool)
t.Cleanup(hyokCleanup)
name := randomStringWithoutSpecialChar(t)
kekID := "arn:aws:kms:us-east-1:123456789012:key/this-is-a-bad-key"
opts := HYOKConfigurationsUpdateOptions{
Name: &name,
KMSOptions: &KMSOptions{
KeyRegion: "us-east-2",
},
KEKID: &kekID,
AgentPool: agentPool,
}
updated, err := client.HYOKConfigurations.Update(ctx, hyok.ID, opts)
require.NoError(t, err)
assert.Equal(t, *opts.Name, updated.Name)
assert.Equal(t, *opts.KEKID, updated.KEKID)
assert.Equal(t, opts.KMSOptions.KeyRegion, updated.KMSOptions.KeyRegion)
})
t.Run("GCP with valid options", func(t *testing.T) {
oidc, oidcCleanup := createGCPOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcCleanup)
hyok, hyokCleanup := oidc.createHYOKConfiguration(t, client, orgTest, agentPool)
t.Cleanup(hyokCleanup)
name := randomStringWithoutSpecialChar(t)
kekID := randomStringWithoutSpecialChar(t)
opts := HYOKConfigurationsUpdateOptions{
Name: &name,
KMSOptions: &KMSOptions{
KeyLocation: "ca",
KeyRingID: randomStringWithoutSpecialChar(t),
},
KEKID: &kekID,
AgentPool: agentPool,
}
updated, err := client.HYOKConfigurations.Update(ctx, hyok.ID, opts)
require.NoError(t, err)
assert.Equal(t, *opts.Name, updated.Name)
assert.Equal(t, *opts.KEKID, updated.KEKID)
assert.Equal(t, opts.KMSOptions.KeyLocation, updated.KMSOptions.KeyLocation)
assert.Equal(t, opts.KMSOptions.KeyRingID, updated.KMSOptions.KeyRingID)
})
t.Run("Vault with valid options", func(t *testing.T) {
oidc, oidcCleanup := createVaultOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcCleanup)
hyok, hyokCleanup := oidc.createHYOKConfiguration(t, client, orgTest, agentPool)
t.Cleanup(hyokCleanup)
name := randomStringWithoutSpecialChar(t)
kekID := randomStringWithoutSpecialChar(t)
opts := HYOKConfigurationsUpdateOptions{
Name: &name,
KEKID: &kekID,
AgentPool: agentPool,
}
updated, err := client.HYOKConfigurations.Update(ctx, hyok.ID, opts)
require.NoError(t, err)
assert.Equal(t, *opts.Name, updated.Name)
assert.Equal(t, *opts.KEKID, updated.KEKID)
assert.Equal(t, opts.AgentPool.ID, updated.AgentPool.ID)
})
t.Run("Azure with valid options", func(t *testing.T) {
oidc, oidcCleanup := createAzureOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcCleanup)
hyok, hyokCleanup := oidc.createHYOKConfiguration(t, client, orgTest, agentPool)
t.Cleanup(hyokCleanup)
name := randomStringWithoutSpecialChar(t)
kekID := "https://random.vault.azure.net/keys/some-key-2"
opts := HYOKConfigurationsUpdateOptions{
Name: &name,
KEKID: &kekID,
AgentPool: agentPool,
}
updated, err := client.HYOKConfigurations.Update(ctx, hyok.ID, opts)
require.NoError(t, err)
assert.Equal(t, *opts.Name, updated.Name)
assert.Equal(t, *opts.KEKID, updated.KEKID)
assert.Equal(t, opts.AgentPool.ID, updated.AgentPool.ID)
})
}
================================================
FILE: hyok_customer_key_version.go
================================================
package tfe
import (
"context"
"fmt"
"net/url"
"time"
)
var _ HYOKCustomerKeyVersions = (*hyokCustomerKeyVersions)(nil)
// HYOKCustomerKeyVersions describes all the hyok customer key version related methods that the HCP Terraform API supports.
// HCP Terraform API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/hold-your-own-key/key-versions
type HYOKCustomerKeyVersions interface {
// List all hyok customer key versions associated to a HYOK configuration.
List(ctx context.Context, hyokConfigurationID string, options *HYOKCustomerKeyVersionListOptions) (*HYOKCustomerKeyVersionList, error)
// Read a hyok customer key version by its ID.
Read(ctx context.Context, hyokCustomerKeyVersionID string) (*HYOKCustomerKeyVersion, error)
// Revoke a hyok customer key version.
Revoke(ctx context.Context, hyokCustomerKeyVersionID string) error
// Delete a hyok customer key version.
Delete(ctx context.Context, hyokCustomerKeyVersionID string) error
}
// hyokCustomerKeyVersions implements HYOKCustomerKeyVersions
type hyokCustomerKeyVersions struct {
client *Client
}
// HYOKCustomerKeyVersionList represents a list of hyok customer key versions
type HYOKCustomerKeyVersionList struct {
*Pagination
Items []*HYOKCustomerKeyVersion
}
// HYOKCustomerKeyVersion represents the resource
type HYOKCustomerKeyVersion struct {
// Attributes
ID string `jsonapi:"primary,hyok-customer-key-versions"`
KeyVersion string `jsonapi:"attr,key-version"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Status HYOKKeyVersionStatus `jsonapi:"attr,status"`
WorkspacesSecured int `jsonapi:"attr,workspaces-secured"`
Error string `jsonapi:"attr,error"`
// Relationships
HYOKConfiguration *HYOKConfiguration `jsonapi:"relation,hyok-configuration"`
}
// HYOKKeyVersionStatus represents a key version status.
type HYOKKeyVersionStatus string
// List all available configuration version statuses.
const (
KeyVersionStatusAvailable HYOKKeyVersionStatus = "available"
KeyVersionStatusRevoking HYOKKeyVersionStatus = "revoking"
KeyVersionStatusRevoked HYOKKeyVersionStatus = "revoked"
KeyVersionStatusRevocationFailed HYOKKeyVersionStatus = "revocation_failed"
)
// HYOKCustomerKeyVersionListOptions represents the options for listing hyok customer key versions
type HYOKCustomerKeyVersionListOptions struct {
ListOptions
Refresh bool `url:"refresh,omitempty"`
}
// List all hyok customer key versions.
func (s *hyokCustomerKeyVersions) List(ctx context.Context, hyokConfigurationID string, options *HYOKCustomerKeyVersionListOptions) (*HYOKCustomerKeyVersionList, error) {
if !validStringID(&hyokConfigurationID) {
return nil, ErrInvalidHYOK
}
path := fmt.Sprintf("hyok-configurations/%s/hyok-customer-key-versions", url.PathEscape(hyokConfigurationID))
req, err := s.client.NewRequest("GET", path, options)
if err != nil {
return nil, err
}
kvs := &HYOKCustomerKeyVersionList{}
err = req.Do(ctx, kvs)
if err != nil {
return nil, err
}
return kvs, nil
}
// Read a hyok customer key version by its ID.
func (s *hyokCustomerKeyVersions) Read(ctx context.Context, hyokCustomerKeyVersionID string) (*HYOKCustomerKeyVersion, error) {
if !validStringID(&hyokCustomerKeyVersionID) {
return nil, ErrInvalidHYOKCustomerKeyVersion
}
path := fmt.Sprintf("hyok-customer-key-versions/%s", url.PathEscape(hyokCustomerKeyVersionID))
req, err := s.client.NewRequest("GET", path, nil)
if err != nil {
return nil, err
}
kv := &HYOKCustomerKeyVersion{}
err = req.Do(ctx, kv)
if err != nil {
return nil, err
}
return kv, nil
}
// Revoke a hyok customer key version. This process is asynchronous.
// Returns `error` if there was a problem triggering the revocation. Otherwise revocation has been triggered successfully.
func (s *hyokCustomerKeyVersions) Revoke(ctx context.Context, hyokCustomerKeyVersionID string) error {
if !validStringID(&hyokCustomerKeyVersionID) {
return ErrInvalidHYOKCustomerKeyVersion
}
path := fmt.Sprintf("hyok-customer-key-versions/%s/actions/revoke", url.PathEscape(hyokCustomerKeyVersionID))
req, err := s.client.NewRequest("POST", path, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// Delete a hyok customer key version.
func (s *hyokCustomerKeyVersions) Delete(ctx context.Context, hyokCustomerKeyVersionID string) error {
if !validStringID(&hyokCustomerKeyVersionID) {
return ErrInvalidHYOKCustomerKeyVersion
}
path := fmt.Sprintf("hyok-customer-key-versions/%s", url.PathEscape(hyokCustomerKeyVersionID))
req, err := s.client.NewRequest("DELETE", path, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
================================================
FILE: hyok_customer_key_version_integration_test.go
================================================
package tfe
import (
"context"
"os"
"testing"
"github.com/stretchr/testify/require"
)
// These tests are intended for local execution only, as key versions for HYOK requires specific conditions
// for tests to run successfully. To test locally:
// 1. Follow the instructions outlined in hyok_configuration_integration_test.go.
// 2. Set hyokCustomerKeyVersionID to the ID of an existing HYOK customer key version
func TestHYOKCustomerKeyVersionsList(t *testing.T) {
t.Parallel()
skipHYOKIntegrationTests(t)
client := testClient(t)
ctx := context.Background()
orgTest := testHyokOrganization(t, client)
agentPool, agentPoolCleanup := createAgentPool(t, client, orgTest)
t.Cleanup(agentPoolCleanup)
oidc, oidcCleanup := createGCPOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcCleanup)
hyok, hyokCleanup := oidc.createHYOKConfiguration(t, client, orgTest, agentPool)
t.Cleanup(hyokCleanup)
t.Run("with no list options", func(t *testing.T) {
_, err := client.HYOKCustomerKeyVersions.List(ctx, hyok.ID, nil)
require.NoError(t, err)
})
}
func TestHYOKCustomerKeyVersionsRead(t *testing.T) {
t.Parallel()
skipHYOKIntegrationTests(t)
client := testClient(t)
ctx := context.Background()
t.Run("read an existing key version", func(t *testing.T) {
hyokCustomerKeyVersionID := os.Getenv("HYOK_CUSTOMER_KEY_VERSION_ID")
if hyokCustomerKeyVersionID == "" {
t.Fatal("Export a valid HYOK_CUSTOMER_KEY_VERSION_ID before running this test!")
}
_, err := client.HYOKCustomerKeyVersions.Read(ctx, hyokCustomerKeyVersionID)
require.NoError(t, err)
})
}
================================================
FILE: hyok_encrypted_data_key.go
================================================
package tfe
import (
"context"
"fmt"
"net/url"
"time"
)
var _ HYOKEncryptedDataKeys = (*hyokEncryptedDataKeys)(nil)
// HYOKEncryptedDataKeys describes all the hyok customer key version related methods that the HCP Terraform API supports.
// HCP Terraform API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/hold-your-own-key/encrypted-data-keys
type HYOKEncryptedDataKeys interface {
// Read a HYOK encrypted data key by its ID.
Read(ctx context.Context, hyokEncryptedDataKeyID string) (*HYOKEncryptedDataKey, error)
}
// hyokEncryptedDataKeys implements HYOKEncryptedDataKeys
type hyokEncryptedDataKeys struct {
client *Client
}
// HYOKEncryptedDataKey represents the resource
type HYOKEncryptedDataKey struct {
// Attributes
ID string `jsonapi:"primary,hyok-encrypted-data-keys"`
EncryptedDEK string `jsonapi:"attr,encrypted-dek"`
CustomerKeyName string `jsonapi:"attr,customer-key-name"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
// Relationships
KeyVersion *HYOKCustomerKeyVersion `jsonapi:"relation,hyok-customer-key-versions"`
}
// Read a HYOK encrypted data key by its ID.
func (h hyokEncryptedDataKeys) Read(ctx context.Context, hyokEncryptedDataKeyID string) (*HYOKEncryptedDataKey, error) {
if !validStringID(&hyokEncryptedDataKeyID) {
return nil, ErrInvalidHYOKEncryptedDataKey
}
path := fmt.Sprintf("hyok-encrypted-data-keys/%s", url.PathEscape(hyokEncryptedDataKeyID))
req, err := h.client.NewRequest("GET", path, nil)
if err != nil {
return nil, err
}
dek := &HYOKEncryptedDataKey{}
err = req.Do(ctx, dek)
if err != nil {
return nil, err
}
return dek, nil
}
================================================
FILE: hyok_encrypted_data_key_integration_test.go
================================================
package tfe
import (
"context"
"os"
"testing"
"github.com/stretchr/testify/require"
)
// These tests are intended for local execution only, as data encryption keys for HYOK requires specific conditions
// for tests to run successfully. To test locally:
// 1. Follow the instructions outlined in hyok_configuration_integration_test.go.
// 2. Set hyokEncryptedDataKeyID to the ID of an existing data encryption key
func TestHYOKEncryptedDataKeyRead(t *testing.T) {
t.Parallel()
skipHYOKIntegrationTests(t)
client := testClient(t)
ctx := context.Background()
t.Run("read an existing encrypted data key", func(t *testing.T) {
hyokEncryptedDataKeyID := os.Getenv("HYOK_ENCRYPTED_DATA_KEY_ID")
if hyokEncryptedDataKeyID == "" {
t.Fatal("Export a valid HYOK_ENCRYPTED_DATA_KEY_ID before running this test!")
}
_, err := client.HYOKEncryptedDataKeys.Read(ctx, hyokEncryptedDataKeyID)
require.NoError(t, err)
})
}
================================================
FILE: internal_run_task.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
// A private struct we need for unmarshalling
type internalRunTask struct {
ID string `jsonapi:"primary,tasks"`
Name string `jsonapi:"attr,name"`
URL string `jsonapi:"attr,url"`
Description string `jsonapi:"attr,description"`
Category string `jsonapi:"attr,category"`
HMACKey *string `jsonapi:"attr,hmac-key,omitempty"`
Enabled bool `jsonapi:"attr,enabled"`
RawGlobal map[string]interface{} `jsonapi:"attr,global-configuration,omitempty"`
Organization *Organization `jsonapi:"relation,organization"`
WorkspaceRunTasks []*internalWorkspaceRunTask `jsonapi:"relation,workspace-tasks"`
}
// Due to https://github.com/google/jsonapi/issues/74 we must first unmarshall using map[string]interface{}
// and then perform our own conversion from the map into a GlobalRunTask struct
func (irt internalRunTask) ToRunTask() *RunTask {
obj := RunTask{
ID: irt.ID,
Name: irt.Name,
URL: irt.URL,
Description: irt.Description,
Category: irt.Category,
HMACKey: irt.HMACKey,
Enabled: irt.Enabled,
Organization: irt.Organization,
}
// Convert the WorkspaceRunTasks
workspaceTasks := make([]*WorkspaceRunTask, len(irt.WorkspaceRunTasks))
for idx, rawTask := range irt.WorkspaceRunTasks {
if rawTask != nil {
workspaceTasks[idx] = rawTask.ToWorkspaceRunTask()
}
}
obj.WorkspaceRunTasks = workspaceTasks
var boolVal bool
// Check if the global configuration exists
if val, ok := irt.RawGlobal["enabled"]; !ok {
// The enabled property is required so we can assume now that the
// global configuration was not supplied
return &obj
} else if boolVal, ok = val.(bool); !ok {
// The enabled property exists but it is invalid (Couldn't cast to boolean)
// so assume the global configuration was not supplied
return &obj
}
obj.Global = &GlobalRunTask{
Enabled: boolVal,
}
// Global Enforcement Level
if val, ok := irt.RawGlobal["enforcement-level"]; ok {
if stringVal, ok := val.(string); ok {
obj.Global.EnforcementLevel = TaskEnforcementLevel(stringVal)
}
}
// Global Stages
if val, ok := irt.RawGlobal["stages"]; ok {
if stringsVal, ok := val.([]interface{}); ok {
obj.Global.Stages = make([]Stage, len(stringsVal))
for idx, stageName := range stringsVal {
if stringVal, ok := stageName.(string); ok {
obj.Global.Stages[idx] = Stage(stringVal)
}
}
}
}
return &obj
}
// A private struct we need for unmarshalling
type internalRunTaskList struct {
*Pagination
Items []*internalRunTask
}
// Due to https://github.com/google/jsonapi/issues/74 we must first unmarshall using
// the internal RunTask struct and convert that a RunTask
func (irt internalRunTaskList) ToRunTaskList() *RunTaskList {
obj := RunTaskList{
Pagination: irt.Pagination,
Items: make([]*RunTask, len(irt.Items)),
}
for idx, src := range irt.Items {
if src != nil {
obj.Items[idx] = src.ToRunTask()
}
}
return &obj
}
================================================
FILE: internal_workspace_run_task.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
// A private struct we need for unmarshalling
type internalWorkspaceRunTask struct {
ID string `jsonapi:"primary,workspace-tasks"`
EnforcementLevel TaskEnforcementLevel `jsonapi:"attr,enforcement-level"`
Stage Stage `jsonapi:"attr,stage"`
Stages []string `jsonapi:"attr,stages"`
RunTask *RunTask `jsonapi:"relation,task"`
Workspace *Workspace `jsonapi:"relation,workspace"`
}
// Due to https://github.com/google/jsonapi/issues/74 we must first unmarshall using map[string]interface{}
// and then perform our own conversion for the Stages
func (irt internalWorkspaceRunTask) ToWorkspaceRunTask() *WorkspaceRunTask {
obj := WorkspaceRunTask{
ID: irt.ID,
EnforcementLevel: irt.EnforcementLevel,
Stage: irt.Stage,
Stages: make([]Stage, len(irt.Stages)),
RunTask: irt.RunTask,
Workspace: irt.Workspace,
}
for idx, val := range irt.Stages {
obj.Stages[idx] = Stage(val)
}
return &obj
}
// A private struct we need for unmarshalling
type internalWorkspaceRunTaskList struct {
*Pagination
Items []*internalWorkspaceRunTask
}
// Due to https://github.com/google/jsonapi/issues/74 we must first unmarshall using
// the internal WorkspaceRunTask struct and convert that a WorkspaceRunTask
func (irt internalWorkspaceRunTaskList) ToWorkspaceRunTaskList() *WorkspaceRunTaskList {
obj := WorkspaceRunTaskList{
Pagination: irt.Pagination,
Items: make([]*WorkspaceRunTask, len(irt.Items)),
}
for idx, src := range irt.Items {
if src != nil {
obj.Items[idx] = src.ToWorkspaceRunTask()
}
}
return &obj
}
================================================
FILE: ip_ranges.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
)
// Compile-time proof of interface implementation.
var _ IPRanges = (*ipRanges)(nil)
// IP Ranges provides a list of HCP Terraform or Terraform Enterprise's IP ranges.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/ip-ranges
type IPRanges interface {
// Retrieve HCP Terraform IP ranges. If `modifiedSince` is not an empty string
// then it will only return the IP ranges changes since that date.
// The format for `modifiedSince` can be found here:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
Read(ctx context.Context, modifiedSince string) (*IPRange, error)
}
// ipRanges implements IPRanges interface.
type ipRanges struct {
client *Client
}
// IPRange represents a list of HCP Terraform's IP ranges
type IPRange struct {
// List of IP ranges in CIDR notation used for connections from user site to HCP Terraform APIs
API []string `json:"api"`
// List of IP ranges in CIDR notation used for notifications
Notifications []string `json:"notifications"`
// List of IP ranges in CIDR notation used for outbound requests from Sentinel policies
Sentinel []string `json:"sentinel"`
// List of IP ranges in CIDR notation used for connecting to VCS providers
VCS []string `json:"vcs"`
}
// Read an IPRange that was not modified since the specified date.
func (i *ipRanges) Read(ctx context.Context, modifiedSince string) (*IPRange, error) {
req, err := i.client.NewRequest("GET", "/api/meta/ip-ranges", nil)
if err != nil {
return nil, err
}
if modifiedSince != "" {
req.Header.Add("If-Modified-Since", modifiedSince)
}
ir := &IPRange{}
err = req.DoJSON(ctx, ir)
if err != nil {
return nil, err
}
return ir, nil
}
================================================
FILE: ip_ranges_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIPRangesRead(t *testing.T) {
t.Parallel()
skipIfEnterprise(t)
client := testClient(t)
ctx := context.Background()
t.Run("without modifiedSince", func(t *testing.T) {
r, err := client.Meta.IPRanges.Read(ctx, "")
require.NoError(t, err)
assert.NotEmpty(t, r.API)
assert.NotEmpty(t, r.Notifications)
assert.NotEmpty(t, r.Sentinel)
assert.NotEmpty(t, r.VCS)
})
t.Run("with future modifiedSince", func(t *testing.T) {
ts := time.Now().Add(48 * time.Hour)
modifiedSince := ts.Format("Mon, 02 Jan 2006 00:00:00 GMT")
r, err := client.Meta.IPRanges.Read(ctx, modifiedSince)
require.NoError(t, err)
assert.Empty(t, r.API)
assert.Empty(t, r.Notifications)
assert.Empty(t, r.Sentinel)
assert.Empty(t, r.VCS)
})
}
================================================
FILE: logreader.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"errors"
"fmt"
"io"
"math"
"net/http"
"net/url"
"time"
)
// LogReader implements io.Reader for streaming logs.
type LogReader struct {
client *Client
ctx context.Context
done func() (bool, error)
logURL *url.URL
offset int64
reads int
startOfText bool
endOfText bool
}
func (r *LogReader) Read(l []byte) (int, error) {
if written, err := r.read(l); !errors.Is(err, io.ErrNoProgress) {
return written, err
}
// Loop until we can any data, the context is canceled or the
// run is finsished. If we would return right away without any
// data, we could end up causing a io.ErrNoProgress error.
for r.reads = 1; ; r.reads++ {
select {
case <-r.ctx.Done():
return 0, r.ctx.Err()
case <-time.After(backoff(500, 2000, r.reads)):
if written, err := r.read(l); !errors.Is(err, io.ErrNoProgress) {
return written, err
}
}
}
}
func (r *LogReader) read(l []byte) (int, error) {
// Update the query string.
r.logURL.RawQuery = fmt.Sprintf("limit=%d&offset=%d", len(l), r.offset)
// Create a new request.
req, err := http.NewRequest("GET", r.logURL.String(), nil)
if err != nil {
return 0, err
}
req = req.WithContext(r.ctx)
// Attach the default headers.
for k, v := range r.client.headers {
req.Header[k] = v
}
// Retrieve the next chunk.
resp, err := r.client.http.HTTPClient.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close() //nolint:errcheck
// Basic response checking.
if err := checkResponseCode(resp); err != nil {
return 0, err
}
// Read the retrieved chunk.
written, err := resp.Body.Read(l)
if err != nil && !errors.Is(err, io.EOF) {
// Ignore io.EOF errors returned when reading from the response
// body as this indicates the end of the chunk and not the end
// of the logfile.
return written, err
}
if written > 0 {
// Check for an STX (Start of Text) ASCII control marker.
if !r.startOfText && l[0] == byte(2) {
r.startOfText = true
// Remove the STX marker from the received chunk.
copy(l[:written-1], l[1:])
l[written-1] = byte(0)
r.offset++
written--
// Return early if we only received the STX marker.
if written == 0 {
return 0, io.ErrNoProgress
}
}
// If we found an STX ASCII control character, start looking for
// the ETX (End of Text) control character.
if r.startOfText && l[written-1] == byte(3) {
r.endOfText = true
// Remove the ETX marker from the received chunk.
l[written-1] = byte(0)
r.offset++
written--
}
}
// Check if we need to continue the loop and wait 500 miliseconds
// before checking if there is a new chunk available or that the
// run is finished and we are done reading all chunks.
if written != 0 {
// Update the offset for the next read.
r.offset += int64(written)
return written, nil
}
if (r.startOfText && r.endOfText) || // The logstream finished without issues.
(r.startOfText && r.reads%10 == 0) || // The logstream terminated unexpectedly.
(!r.startOfText && r.reads > 1) { // The logstream doesn't support STX/ETX.
done, err := r.done()
if err != nil {
return 0, err
}
if done {
return 0, io.EOF
}
}
return 0, io.ErrNoProgress
}
// backoff will perform exponential backoff based on the iteration and
// limited by the provided minimum and maximum (in milliseconds) durations.
func backoff(minimum, maximum float64, iter int) time.Duration {
backoff := math.Pow(2, float64(iter)/5) * minimum
if backoff > maximum {
backoff = maximum
}
return time.Duration(backoff) * time.Millisecond
}
================================================
FILE: logreader_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"io"
"net/http"
"net/http/httptest"
"net/url"
"testing"
)
// checkedWrite writes message to w and fails the test if there's an error.
func checkedWrite(t *testing.T, w io.Writer, message []byte) {
_, err := w.Write(message)
if err != nil {
t.Fatalf("error writing response: %s", err)
}
}
func testLogReader(t *testing.T, h http.HandlerFunc) (*httptest.Server, *LogReader) {
ts := httptest.NewServer(h)
cfg := &Config{
Address: ts.URL,
Token: "dummy-token",
HTTPClient: ts.Client(),
}
client, err := NewClient(cfg)
if err != nil {
t.Fatal(err)
}
logURL, err := url.Parse(ts.URL)
if err != nil {
t.Fatal(err)
}
lr := &LogReader{
client: client,
ctx: context.Background(),
logURL: logURL,
}
return ts, lr
}
func TestLogReader_withMarkersSingle(t *testing.T) {
t.Parallel()
logReads := 0
ts, lr := testLogReader(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logReads++
switch logReads {
case 2:
checkedWrite(t, w, []byte("\x02Terraform run started - logs - Terraform run finished\x03"))
}
}))
defer ts.Close()
doneReads := 0
lr.done = func() (bool, error) {
doneReads++
if logReads >= 2 {
return true, nil
}
return false, nil
}
logs, err := io.ReadAll(lr)
if err != nil {
t.Fatal(err)
}
expected := "Terraform run started - logs - Terraform run finished"
if string(logs) != expected {
t.Fatalf("expected %s, got: %s", expected, string(logs))
}
if doneReads != 1 {
t.Fatalf("expected 1 done reads, got %d reads", doneReads)
}
if logReads != 3 {
t.Fatalf("expected 3 log reads, got %d reads", logReads)
}
}
func TestLogReader_withMarkersDouble(t *testing.T) {
t.Parallel()
logReads := 0
ts, lr := testLogReader(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logReads++
switch logReads {
case 2:
checkedWrite(t, w, []byte("\x02Terraform run started"))
case 3:
checkedWrite(t, w, []byte(" - logs - Terraform run finished\x03"))
}
}))
defer ts.Close()
doneReads := 0
lr.done = func() (bool, error) {
doneReads++
if logReads >= 3 {
return true, nil
}
return false, nil
}
logs, err := io.ReadAll(lr)
if err != nil {
t.Fatal(err)
}
expected := "Terraform run started - logs - Terraform run finished"
if string(logs) != expected {
t.Fatalf("expected %s, got: %s", expected, string(logs))
}
if doneReads != 1 {
t.Fatalf("expected 1 done reads, got %d reads", doneReads)
}
if logReads != 4 {
t.Fatalf("expected 4 log reads, got %d reads", logReads)
}
}
func TestLogReader_withMarkersMulti(t *testing.T) {
t.Parallel()
logReads := 0
ts, lr := testLogReader(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logReads++
switch logReads {
case 2:
checkedWrite(t, w, []byte("\x02"))
case 3:
checkedWrite(t, w, []byte("Terraform run started"))
case 16:
checkedWrite(t, w, []byte(" - logs - "))
case 30:
checkedWrite(t, w, []byte("Terraform run finished"))
case 31:
checkedWrite(t, w, []byte("\x03"))
}
}))
defer ts.Close()
doneReads := 0
lr.done = func() (bool, error) {
doneReads++
if logReads >= 31 {
return true, nil
}
return false, nil
}
logs, err := io.ReadAll(lr)
if err != nil {
t.Fatal(err)
}
expected := "Terraform run started - logs - Terraform run finished"
if string(logs) != expected {
t.Fatalf("expected %s, got: %s", expected, string(logs))
}
if doneReads != 3 {
t.Fatalf("expected 3 done reads, got %d reads", doneReads)
}
if logReads != 31 {
t.Fatalf("expected 31 log reads, got %d reads", logReads)
}
}
func TestLogReader_withoutMarkers(t *testing.T) {
t.Parallel()
logReads := 0
ts, lr := testLogReader(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logReads++
switch logReads {
case 2:
checkedWrite(t, w, []byte("Terraform run started"))
case 16:
checkedWrite(t, w, []byte(" - logs - "))
case 31:
checkedWrite(t, w, []byte("Terraform run finished"))
}
}))
defer ts.Close()
doneReads := 0
lr.done = func() (bool, error) {
doneReads++
if logReads >= 31 {
return true, nil
}
return false, nil
}
logs, err := io.ReadAll(lr)
if err != nil {
t.Fatal(err)
}
expected := "Terraform run started - logs - Terraform run finished"
if string(logs) != expected {
t.Fatalf("expected %s, got: %s", expected, string(logs))
}
if doneReads != 25 {
t.Fatalf("expected 14 done reads, got %d reads", doneReads)
}
if logReads != 32 {
t.Fatalf("expected 32 log reads, got %d reads", logReads)
}
}
func TestLogReader_withoutEndOfTextMarker(t *testing.T) {
t.Parallel()
logReads := 0
ts, lr := testLogReader(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logReads++
switch logReads {
case 2:
checkedWrite(t, w, []byte("\x02"))
case 3:
checkedWrite(t, w, []byte("Terraform run started"))
case 16:
checkedWrite(t, w, []byte(" - logs - "))
case 31:
checkedWrite(t, w, []byte("Terraform run finished"))
}
}))
defer ts.Close()
doneReads := 0
lr.done = func() (bool, error) {
doneReads++
if logReads >= 31 {
return true, nil
}
return false, nil
}
logs, err := io.ReadAll(lr)
if err != nil {
t.Fatal(err)
}
expected := "Terraform run started - logs - Terraform run finished"
if string(logs) != expected {
t.Fatalf("expected %s, got: %s", expected, string(logs))
}
if doneReads != 3 {
t.Fatalf("expected 3 done reads, got %d reads", doneReads)
}
if logReads != 42 {
t.Fatalf("expected 42 log reads, got %d reads", logReads)
}
}
================================================
FILE: mocks/admin_opa_version_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: admin_opa_version.go
//
// Generated by this command:
//
// mockgen -source=admin_opa_version.go -destination=mocks/admin_opa_version_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockAdminOPAVersions is a mock of AdminOPAVersions interface.
type MockAdminOPAVersions struct {
ctrl *gomock.Controller
recorder *MockAdminOPAVersionsMockRecorder
}
// MockAdminOPAVersionsMockRecorder is the mock recorder for MockAdminOPAVersions.
type MockAdminOPAVersionsMockRecorder struct {
mock *MockAdminOPAVersions
}
// NewMockAdminOPAVersions creates a new mock instance.
func NewMockAdminOPAVersions(ctrl *gomock.Controller) *MockAdminOPAVersions {
mock := &MockAdminOPAVersions{ctrl: ctrl}
mock.recorder = &MockAdminOPAVersionsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockAdminOPAVersions) EXPECT() *MockAdminOPAVersionsMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockAdminOPAVersions) Create(ctx context.Context, options tfe.AdminOPAVersionCreateOptions) (*tfe.AdminOPAVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, options)
ret0, _ := ret[0].(*tfe.AdminOPAVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockAdminOPAVersionsMockRecorder) Create(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockAdminOPAVersions)(nil).Create), ctx, options)
}
// Delete mocks base method.
func (m *MockAdminOPAVersions) Delete(ctx context.Context, id string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, id)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockAdminOPAVersionsMockRecorder) Delete(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockAdminOPAVersions)(nil).Delete), ctx, id)
}
// List mocks base method.
func (m *MockAdminOPAVersions) List(ctx context.Context, options *tfe.AdminOPAVersionsListOptions) (*tfe.AdminOPAVersionsList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, options)
ret0, _ := ret[0].(*tfe.AdminOPAVersionsList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockAdminOPAVersionsMockRecorder) List(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockAdminOPAVersions)(nil).List), ctx, options)
}
// Read mocks base method.
func (m *MockAdminOPAVersions) Read(ctx context.Context, id string) (*tfe.AdminOPAVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, id)
ret0, _ := ret[0].(*tfe.AdminOPAVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockAdminOPAVersionsMockRecorder) Read(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockAdminOPAVersions)(nil).Read), ctx, id)
}
// Update mocks base method.
func (m *MockAdminOPAVersions) Update(ctx context.Context, id string, options tfe.AdminOPAVersionUpdateOptions) (*tfe.AdminOPAVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, id, options)
ret0, _ := ret[0].(*tfe.AdminOPAVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockAdminOPAVersionsMockRecorder) Update(ctx, id, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockAdminOPAVersions)(nil).Update), ctx, id, options)
}
================================================
FILE: mocks/admin_organization_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: admin_organization.go
//
// Generated by this command:
//
// mockgen -source=admin_organization.go -destination=mocks/admin_organization_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockAdminOrganizations is a mock of AdminOrganizations interface.
type MockAdminOrganizations struct {
ctrl *gomock.Controller
recorder *MockAdminOrganizationsMockRecorder
}
// MockAdminOrganizationsMockRecorder is the mock recorder for MockAdminOrganizations.
type MockAdminOrganizationsMockRecorder struct {
mock *MockAdminOrganizations
}
// NewMockAdminOrganizations creates a new mock instance.
func NewMockAdminOrganizations(ctrl *gomock.Controller) *MockAdminOrganizations {
mock := &MockAdminOrganizations{ctrl: ctrl}
mock.recorder = &MockAdminOrganizationsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockAdminOrganizations) EXPECT() *MockAdminOrganizationsMockRecorder {
return m.recorder
}
// Delete mocks base method.
func (m *MockAdminOrganizations) Delete(ctx context.Context, organization string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, organization)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockAdminOrganizationsMockRecorder) Delete(ctx, organization any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockAdminOrganizations)(nil).Delete), ctx, organization)
}
// List mocks base method.
func (m *MockAdminOrganizations) List(ctx context.Context, options *tfe.AdminOrganizationListOptions) (*tfe.AdminOrganizationList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, options)
ret0, _ := ret[0].(*tfe.AdminOrganizationList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockAdminOrganizationsMockRecorder) List(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockAdminOrganizations)(nil).List), ctx, options)
}
// ListModuleConsumers mocks base method.
func (m *MockAdminOrganizations) ListModuleConsumers(ctx context.Context, organization string, options *tfe.AdminOrganizationListModuleConsumersOptions) (*tfe.AdminOrganizationList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListModuleConsumers", ctx, organization, options)
ret0, _ := ret[0].(*tfe.AdminOrganizationList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListModuleConsumers indicates an expected call of ListModuleConsumers.
func (mr *MockAdminOrganizationsMockRecorder) ListModuleConsumers(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListModuleConsumers", reflect.TypeOf((*MockAdminOrganizations)(nil).ListModuleConsumers), ctx, organization, options)
}
// Read mocks base method.
func (m *MockAdminOrganizations) Read(ctx context.Context, organization string) (*tfe.AdminOrganization, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, organization)
ret0, _ := ret[0].(*tfe.AdminOrganization)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockAdminOrganizationsMockRecorder) Read(ctx, organization any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockAdminOrganizations)(nil).Read), ctx, organization)
}
// Update mocks base method.
func (m *MockAdminOrganizations) Update(ctx context.Context, organization string, options tfe.AdminOrganizationUpdateOptions) (*tfe.AdminOrganization, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, organization, options)
ret0, _ := ret[0].(*tfe.AdminOrganization)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockAdminOrganizationsMockRecorder) Update(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockAdminOrganizations)(nil).Update), ctx, organization, options)
}
// UpdateModuleConsumers mocks base method.
func (m *MockAdminOrganizations) UpdateModuleConsumers(ctx context.Context, organization string, consumerOrganizations []string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateModuleConsumers", ctx, organization, consumerOrganizations)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateModuleConsumers indicates an expected call of UpdateModuleConsumers.
func (mr *MockAdminOrganizationsMockRecorder) UpdateModuleConsumers(ctx, organization, consumerOrganizations any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateModuleConsumers", reflect.TypeOf((*MockAdminOrganizations)(nil).UpdateModuleConsumers), ctx, organization, consumerOrganizations)
}
================================================
FILE: mocks/admin_run_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: admin_run.go
//
// Generated by this command:
//
// mockgen -source=admin_run.go -destination=mocks/admin_run_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockAdminRuns is a mock of AdminRuns interface.
type MockAdminRuns struct {
ctrl *gomock.Controller
recorder *MockAdminRunsMockRecorder
}
// MockAdminRunsMockRecorder is the mock recorder for MockAdminRuns.
type MockAdminRunsMockRecorder struct {
mock *MockAdminRuns
}
// NewMockAdminRuns creates a new mock instance.
func NewMockAdminRuns(ctrl *gomock.Controller) *MockAdminRuns {
mock := &MockAdminRuns{ctrl: ctrl}
mock.recorder = &MockAdminRunsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockAdminRuns) EXPECT() *MockAdminRunsMockRecorder {
return m.recorder
}
// ForceCancel mocks base method.
func (m *MockAdminRuns) ForceCancel(ctx context.Context, runID string, options tfe.AdminRunForceCancelOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ForceCancel", ctx, runID, options)
ret0, _ := ret[0].(error)
return ret0
}
// ForceCancel indicates an expected call of ForceCancel.
func (mr *MockAdminRunsMockRecorder) ForceCancel(ctx, runID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ForceCancel", reflect.TypeOf((*MockAdminRuns)(nil).ForceCancel), ctx, runID, options)
}
// List mocks base method.
func (m *MockAdminRuns) List(ctx context.Context, options *tfe.AdminRunsListOptions) (*tfe.AdminRunsList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, options)
ret0, _ := ret[0].(*tfe.AdminRunsList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockAdminRunsMockRecorder) List(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockAdminRuns)(nil).List), ctx, options)
}
================================================
FILE: mocks/admin_sentinel_version_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: admin_sentinel_version.go
//
// Generated by this command:
//
// mockgen -source=admin_sentinel_version.go -destination=mocks/admin_sentinel_version_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockAdminSentinelVersions is a mock of AdminSentinelVersions interface.
type MockAdminSentinelVersions struct {
ctrl *gomock.Controller
recorder *MockAdminSentinelVersionsMockRecorder
}
// MockAdminSentinelVersionsMockRecorder is the mock recorder for MockAdminSentinelVersions.
type MockAdminSentinelVersionsMockRecorder struct {
mock *MockAdminSentinelVersions
}
// NewMockAdminSentinelVersions creates a new mock instance.
func NewMockAdminSentinelVersions(ctrl *gomock.Controller) *MockAdminSentinelVersions {
mock := &MockAdminSentinelVersions{ctrl: ctrl}
mock.recorder = &MockAdminSentinelVersionsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockAdminSentinelVersions) EXPECT() *MockAdminSentinelVersionsMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockAdminSentinelVersions) Create(ctx context.Context, options tfe.AdminSentinelVersionCreateOptions) (*tfe.AdminSentinelVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, options)
ret0, _ := ret[0].(*tfe.AdminSentinelVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockAdminSentinelVersionsMockRecorder) Create(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockAdminSentinelVersions)(nil).Create), ctx, options)
}
// Delete mocks base method.
func (m *MockAdminSentinelVersions) Delete(ctx context.Context, id string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, id)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockAdminSentinelVersionsMockRecorder) Delete(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockAdminSentinelVersions)(nil).Delete), ctx, id)
}
// List mocks base method.
func (m *MockAdminSentinelVersions) List(ctx context.Context, options *tfe.AdminSentinelVersionsListOptions) (*tfe.AdminSentinelVersionsList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, options)
ret0, _ := ret[0].(*tfe.AdminSentinelVersionsList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockAdminSentinelVersionsMockRecorder) List(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockAdminSentinelVersions)(nil).List), ctx, options)
}
// Read mocks base method.
func (m *MockAdminSentinelVersions) Read(ctx context.Context, id string) (*tfe.AdminSentinelVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, id)
ret0, _ := ret[0].(*tfe.AdminSentinelVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockAdminSentinelVersionsMockRecorder) Read(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockAdminSentinelVersions)(nil).Read), ctx, id)
}
// Update mocks base method.
func (m *MockAdminSentinelVersions) Update(ctx context.Context, id string, options tfe.AdminSentinelVersionUpdateOptions) (*tfe.AdminSentinelVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, id, options)
ret0, _ := ret[0].(*tfe.AdminSentinelVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockAdminSentinelVersionsMockRecorder) Update(ctx, id, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockAdminSentinelVersions)(nil).Update), ctx, id, options)
}
================================================
FILE: mocks/admin_setting_cost_estimation_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: admin_setting_cost_estimation.go
//
// Generated by this command:
//
// mockgen -source=admin_setting_cost_estimation.go -destination=mocks/admin_setting_cost_estimation_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockCostEstimationSettings is a mock of CostEstimationSettings interface.
type MockCostEstimationSettings struct {
ctrl *gomock.Controller
recorder *MockCostEstimationSettingsMockRecorder
}
// MockCostEstimationSettingsMockRecorder is the mock recorder for MockCostEstimationSettings.
type MockCostEstimationSettingsMockRecorder struct {
mock *MockCostEstimationSettings
}
// NewMockCostEstimationSettings creates a new mock instance.
func NewMockCostEstimationSettings(ctrl *gomock.Controller) *MockCostEstimationSettings {
mock := &MockCostEstimationSettings{ctrl: ctrl}
mock.recorder = &MockCostEstimationSettingsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCostEstimationSettings) EXPECT() *MockCostEstimationSettingsMockRecorder {
return m.recorder
}
// Read mocks base method.
func (m *MockCostEstimationSettings) Read(ctx context.Context) (*tfe.AdminCostEstimationSetting, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx)
ret0, _ := ret[0].(*tfe.AdminCostEstimationSetting)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockCostEstimationSettingsMockRecorder) Read(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockCostEstimationSettings)(nil).Read), ctx)
}
// Update mocks base method.
func (m *MockCostEstimationSettings) Update(ctx context.Context, options tfe.AdminCostEstimationSettingOptions) (*tfe.AdminCostEstimationSetting, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, options)
ret0, _ := ret[0].(*tfe.AdminCostEstimationSetting)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockCostEstimationSettingsMockRecorder) Update(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockCostEstimationSettings)(nil).Update), ctx, options)
}
================================================
FILE: mocks/admin_setting_customization_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: admin_setting_customization.go
//
// Generated by this command:
//
// mockgen -source=admin_setting_customization.go -destination=mocks/admin_setting_customization_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockCustomizationSettings is a mock of CustomizationSettings interface.
type MockCustomizationSettings struct {
ctrl *gomock.Controller
recorder *MockCustomizationSettingsMockRecorder
}
// MockCustomizationSettingsMockRecorder is the mock recorder for MockCustomizationSettings.
type MockCustomizationSettingsMockRecorder struct {
mock *MockCustomizationSettings
}
// NewMockCustomizationSettings creates a new mock instance.
func NewMockCustomizationSettings(ctrl *gomock.Controller) *MockCustomizationSettings {
mock := &MockCustomizationSettings{ctrl: ctrl}
mock.recorder = &MockCustomizationSettingsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCustomizationSettings) EXPECT() *MockCustomizationSettingsMockRecorder {
return m.recorder
}
// Read mocks base method.
func (m *MockCustomizationSettings) Read(ctx context.Context) (*tfe.AdminCustomizationSetting, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx)
ret0, _ := ret[0].(*tfe.AdminCustomizationSetting)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockCustomizationSettingsMockRecorder) Read(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockCustomizationSettings)(nil).Read), ctx)
}
// Update mocks base method.
func (m *MockCustomizationSettings) Update(ctx context.Context, options tfe.AdminCustomizationSettingsUpdateOptions) (*tfe.AdminCustomizationSetting, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, options)
ret0, _ := ret[0].(*tfe.AdminCustomizationSetting)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockCustomizationSettingsMockRecorder) Update(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockCustomizationSettings)(nil).Update), ctx, options)
}
================================================
FILE: mocks/admin_setting_general_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: admin_setting_general.go
//
// Generated by this command:
//
// mockgen -source=admin_setting_general.go -destination=mocks/admin_setting_general_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockGeneralSettings is a mock of GeneralSettings interface.
type MockGeneralSettings struct {
ctrl *gomock.Controller
recorder *MockGeneralSettingsMockRecorder
}
// MockGeneralSettingsMockRecorder is the mock recorder for MockGeneralSettings.
type MockGeneralSettingsMockRecorder struct {
mock *MockGeneralSettings
}
// NewMockGeneralSettings creates a new mock instance.
func NewMockGeneralSettings(ctrl *gomock.Controller) *MockGeneralSettings {
mock := &MockGeneralSettings{ctrl: ctrl}
mock.recorder = &MockGeneralSettingsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockGeneralSettings) EXPECT() *MockGeneralSettingsMockRecorder {
return m.recorder
}
// Read mocks base method.
func (m *MockGeneralSettings) Read(ctx context.Context) (*tfe.AdminGeneralSetting, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx)
ret0, _ := ret[0].(*tfe.AdminGeneralSetting)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockGeneralSettingsMockRecorder) Read(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockGeneralSettings)(nil).Read), ctx)
}
// Update mocks base method.
func (m *MockGeneralSettings) Update(ctx context.Context, options tfe.AdminGeneralSettingsUpdateOptions) (*tfe.AdminGeneralSetting, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, options)
ret0, _ := ret[0].(*tfe.AdminGeneralSetting)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockGeneralSettingsMockRecorder) Update(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockGeneralSettings)(nil).Update), ctx, options)
}
================================================
FILE: mocks/admin_setting_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: admin_setting.go
//
// Generated by this command:
//
// mockgen -source=admin_setting.go -destination=mocks/admin_setting_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
================================================
FILE: mocks/admin_setting_oidc_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: admin_setting_oidc.go
//
// Generated by this command:
//
// mockgen -source=admin_setting_oidc.go -destination=mocks/admin_setting_oidc_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockOIDCSettings is a mock of OIDCSettings interface.
type MockOIDCSettings struct {
ctrl *gomock.Controller
recorder *MockOIDCSettingsMockRecorder
}
// MockOIDCSettingsMockRecorder is the mock recorder for MockOIDCSettings.
type MockOIDCSettingsMockRecorder struct {
mock *MockOIDCSettings
}
// NewMockOIDCSettings creates a new mock instance.
func NewMockOIDCSettings(ctrl *gomock.Controller) *MockOIDCSettings {
mock := &MockOIDCSettings{ctrl: ctrl}
mock.recorder = &MockOIDCSettingsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockOIDCSettings) EXPECT() *MockOIDCSettingsMockRecorder {
return m.recorder
}
// RotateKey mocks base method.
func (m *MockOIDCSettings) RotateKey(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RotateKey", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// RotateKey indicates an expected call of RotateKey.
func (mr *MockOIDCSettingsMockRecorder) RotateKey(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RotateKey", reflect.TypeOf((*MockOIDCSettings)(nil).RotateKey), ctx)
}
// TrimKey mocks base method.
func (m *MockOIDCSettings) TrimKey(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TrimKey", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// TrimKey indicates an expected call of TrimKey.
func (mr *MockOIDCSettingsMockRecorder) TrimKey(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TrimKey", reflect.TypeOf((*MockOIDCSettings)(nil).TrimKey), ctx)
}
================================================
FILE: mocks/admin_setting_saml_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: admin_setting_saml.go
//
// Generated by this command:
//
// mockgen -source=admin_setting_saml.go -destination=mocks/admin_setting_saml_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockSAMLSettings is a mock of SAMLSettings interface.
type MockSAMLSettings struct {
ctrl *gomock.Controller
recorder *MockSAMLSettingsMockRecorder
}
// MockSAMLSettingsMockRecorder is the mock recorder for MockSAMLSettings.
type MockSAMLSettingsMockRecorder struct {
mock *MockSAMLSettings
}
// NewMockSAMLSettings creates a new mock instance.
func NewMockSAMLSettings(ctrl *gomock.Controller) *MockSAMLSettings {
mock := &MockSAMLSettings{ctrl: ctrl}
mock.recorder = &MockSAMLSettingsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockSAMLSettings) EXPECT() *MockSAMLSettingsMockRecorder {
return m.recorder
}
// Read mocks base method.
func (m *MockSAMLSettings) Read(ctx context.Context) (*tfe.AdminSAMLSetting, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx)
ret0, _ := ret[0].(*tfe.AdminSAMLSetting)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockSAMLSettingsMockRecorder) Read(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockSAMLSettings)(nil).Read), ctx)
}
// RevokeIdpCert mocks base method.
func (m *MockSAMLSettings) RevokeIdpCert(ctx context.Context) (*tfe.AdminSAMLSetting, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RevokeIdpCert", ctx)
ret0, _ := ret[0].(*tfe.AdminSAMLSetting)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// RevokeIdpCert indicates an expected call of RevokeIdpCert.
func (mr *MockSAMLSettingsMockRecorder) RevokeIdpCert(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeIdpCert", reflect.TypeOf((*MockSAMLSettings)(nil).RevokeIdpCert), ctx)
}
// Update mocks base method.
func (m *MockSAMLSettings) Update(ctx context.Context, options tfe.AdminSAMLSettingsUpdateOptions) (*tfe.AdminSAMLSetting, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, options)
ret0, _ := ret[0].(*tfe.AdminSAMLSetting)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockSAMLSettingsMockRecorder) Update(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockSAMLSettings)(nil).Update), ctx, options)
}
================================================
FILE: mocks/admin_setting_scim_groups_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: admin_setting_scim_groups.go
//
// Generated by this command:
//
// mockgen -source=admin_setting_scim_groups.go -destination=mocks/admin_setting_scim_groups_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockAdminSCIMGroups is a mock of AdminSCIMGroups interface.
type MockAdminSCIMGroups struct {
ctrl *gomock.Controller
recorder *MockAdminSCIMGroupsMockRecorder
}
// MockAdminSCIMGroupsMockRecorder is the mock recorder for MockAdminSCIMGroups.
type MockAdminSCIMGroupsMockRecorder struct {
mock *MockAdminSCIMGroups
}
// NewMockAdminSCIMGroups creates a new mock instance.
func NewMockAdminSCIMGroups(ctrl *gomock.Controller) *MockAdminSCIMGroups {
mock := &MockAdminSCIMGroups{ctrl: ctrl}
mock.recorder = &MockAdminSCIMGroupsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockAdminSCIMGroups) EXPECT() *MockAdminSCIMGroupsMockRecorder {
return m.recorder
}
// List mocks base method.
func (m *MockAdminSCIMGroups) List(ctx context.Context, options *tfe.AdminSCIMGroupListOptions) (*tfe.AdminSCIMGroupList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, options)
ret0, _ := ret[0].(*tfe.AdminSCIMGroupList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockAdminSCIMGroupsMockRecorder) List(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockAdminSCIMGroups)(nil).List), ctx, options)
}
================================================
FILE: mocks/admin_setting_scim_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: admin_setting_scim.go
//
// Generated by this command:
//
// mockgen -source=admin_setting_scim.go -destination=mocks/admin_setting_scim_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockSCIMSettings is a mock of SCIMSettings interface.
type MockSCIMSettings struct {
ctrl *gomock.Controller
recorder *MockSCIMSettingsMockRecorder
}
// MockSCIMSettingsMockRecorder is the mock recorder for MockSCIMSettings.
type MockSCIMSettingsMockRecorder struct {
mock *MockSCIMSettings
}
// NewMockSCIMSettings creates a new mock instance.
func NewMockSCIMSettings(ctrl *gomock.Controller) *MockSCIMSettings {
mock := &MockSCIMSettings{ctrl: ctrl}
mock.recorder = &MockSCIMSettingsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockSCIMSettings) EXPECT() *MockSCIMSettingsMockRecorder {
return m.recorder
}
// Delete mocks base method.
func (m *MockSCIMSettings) Delete(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockSCIMSettingsMockRecorder) Delete(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockSCIMSettings)(nil).Delete), ctx)
}
// Read mocks base method.
func (m *MockSCIMSettings) Read(ctx context.Context) (*tfe.AdminSCIMSetting, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx)
ret0, _ := ret[0].(*tfe.AdminSCIMSetting)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockSCIMSettingsMockRecorder) Read(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockSCIMSettings)(nil).Read), ctx)
}
// Update mocks base method.
func (m *MockSCIMSettings) Update(ctx context.Context, options tfe.AdminSCIMSettingUpdateOptions) (*tfe.AdminSCIMSetting, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, options)
ret0, _ := ret[0].(*tfe.AdminSCIMSetting)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockSCIMSettingsMockRecorder) Update(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockSCIMSettings)(nil).Update), ctx, options)
}
================================================
FILE: mocks/admin_setting_scim_token_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: admin_setting_scim_token.go
//
// Generated by this command:
//
// mockgen -source=admin_setting_scim_token.go -destination=mocks/admin_setting_scim_token_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockAdminSCIMTokens is a mock of AdminSCIMTokens interface.
type MockAdminSCIMTokens struct {
ctrl *gomock.Controller
recorder *MockAdminSCIMTokensMockRecorder
}
// MockAdminSCIMTokensMockRecorder is the mock recorder for MockAdminSCIMTokens.
type MockAdminSCIMTokensMockRecorder struct {
mock *MockAdminSCIMTokens
}
// NewMockAdminSCIMTokens creates a new mock instance.
func NewMockAdminSCIMTokens(ctrl *gomock.Controller) *MockAdminSCIMTokens {
mock := &MockAdminSCIMTokens{ctrl: ctrl}
mock.recorder = &MockAdminSCIMTokensMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockAdminSCIMTokens) EXPECT() *MockAdminSCIMTokensMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockAdminSCIMTokens) Create(ctx context.Context, description string) (*tfe.AdminSCIMToken, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, description)
ret0, _ := ret[0].(*tfe.AdminSCIMToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockAdminSCIMTokensMockRecorder) Create(ctx, description any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockAdminSCIMTokens)(nil).Create), ctx, description)
}
// CreateWithOptions mocks base method.
func (m *MockAdminSCIMTokens) CreateWithOptions(ctx context.Context, options tfe.AdminSCIMTokenCreateOptions) (*tfe.AdminSCIMToken, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateWithOptions", ctx, options)
ret0, _ := ret[0].(*tfe.AdminSCIMToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateWithOptions indicates an expected call of CreateWithOptions.
func (mr *MockAdminSCIMTokensMockRecorder) CreateWithOptions(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateWithOptions", reflect.TypeOf((*MockAdminSCIMTokens)(nil).CreateWithOptions), ctx, options)
}
// Delete mocks base method.
func (m *MockAdminSCIMTokens) Delete(ctx context.Context, scimTokenID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, scimTokenID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockAdminSCIMTokensMockRecorder) Delete(ctx, scimTokenID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockAdminSCIMTokens)(nil).Delete), ctx, scimTokenID)
}
// List mocks base method.
func (m *MockAdminSCIMTokens) List(ctx context.Context) (*tfe.AdminSCIMTokenList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx)
ret0, _ := ret[0].(*tfe.AdminSCIMTokenList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockAdminSCIMTokensMockRecorder) List(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockAdminSCIMTokens)(nil).List), ctx)
}
// Read mocks base method.
func (m *MockAdminSCIMTokens) Read(ctx context.Context, scimTokenID string) (*tfe.AdminSCIMToken, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, scimTokenID)
ret0, _ := ret[0].(*tfe.AdminSCIMToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockAdminSCIMTokensMockRecorder) Read(ctx, scimTokenID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockAdminSCIMTokens)(nil).Read), ctx, scimTokenID)
}
================================================
FILE: mocks/admin_setting_smtp_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: admin_setting_smtp.go
//
// Generated by this command:
//
// mockgen -source=admin_setting_smtp.go -destination=mocks/admin_setting_smtp_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockSMTPSettings is a mock of SMTPSettings interface.
type MockSMTPSettings struct {
ctrl *gomock.Controller
recorder *MockSMTPSettingsMockRecorder
}
// MockSMTPSettingsMockRecorder is the mock recorder for MockSMTPSettings.
type MockSMTPSettingsMockRecorder struct {
mock *MockSMTPSettings
}
// NewMockSMTPSettings creates a new mock instance.
func NewMockSMTPSettings(ctrl *gomock.Controller) *MockSMTPSettings {
mock := &MockSMTPSettings{ctrl: ctrl}
mock.recorder = &MockSMTPSettingsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockSMTPSettings) EXPECT() *MockSMTPSettingsMockRecorder {
return m.recorder
}
// Read mocks base method.
func (m *MockSMTPSettings) Read(ctx context.Context) (*tfe.AdminSMTPSetting, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx)
ret0, _ := ret[0].(*tfe.AdminSMTPSetting)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockSMTPSettingsMockRecorder) Read(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockSMTPSettings)(nil).Read), ctx)
}
// Update mocks base method.
func (m *MockSMTPSettings) Update(ctx context.Context, options tfe.AdminSMTPSettingsUpdateOptions) (*tfe.AdminSMTPSetting, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, options)
ret0, _ := ret[0].(*tfe.AdminSMTPSetting)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockSMTPSettingsMockRecorder) Update(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockSMTPSettings)(nil).Update), ctx, options)
}
================================================
FILE: mocks/admin_setting_twilio_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: admin_setting_twilio.go
//
// Generated by this command:
//
// mockgen -source=admin_setting_twilio.go -destination=mocks/admin_setting_twilio_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockTwilioSettings is a mock of TwilioSettings interface.
type MockTwilioSettings struct {
ctrl *gomock.Controller
recorder *MockTwilioSettingsMockRecorder
}
// MockTwilioSettingsMockRecorder is the mock recorder for MockTwilioSettings.
type MockTwilioSettingsMockRecorder struct {
mock *MockTwilioSettings
}
// NewMockTwilioSettings creates a new mock instance.
func NewMockTwilioSettings(ctrl *gomock.Controller) *MockTwilioSettings {
mock := &MockTwilioSettings{ctrl: ctrl}
mock.recorder = &MockTwilioSettingsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTwilioSettings) EXPECT() *MockTwilioSettingsMockRecorder {
return m.recorder
}
// Read mocks base method.
func (m *MockTwilioSettings) Read(ctx context.Context) (*tfe.AdminTwilioSetting, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx)
ret0, _ := ret[0].(*tfe.AdminTwilioSetting)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockTwilioSettingsMockRecorder) Read(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockTwilioSettings)(nil).Read), ctx)
}
// Update mocks base method.
func (m *MockTwilioSettings) Update(ctx context.Context, options tfe.AdminTwilioSettingsUpdateOptions) (*tfe.AdminTwilioSetting, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, options)
ret0, _ := ret[0].(*tfe.AdminTwilioSetting)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockTwilioSettingsMockRecorder) Update(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockTwilioSettings)(nil).Update), ctx, options)
}
// Verify mocks base method.
func (m *MockTwilioSettings) Verify(ctx context.Context, options tfe.AdminTwilioSettingsVerifyOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Verify", ctx, options)
ret0, _ := ret[0].(error)
return ret0
}
// Verify indicates an expected call of Verify.
func (mr *MockTwilioSettingsMockRecorder) Verify(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockTwilioSettings)(nil).Verify), ctx, options)
}
================================================
FILE: mocks/admin_terraform_version_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: admin_terraform_version.go
//
// Generated by this command:
//
// mockgen -source=admin_terraform_version.go -destination=mocks/admin_terraform_version_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockAdminTerraformVersions is a mock of AdminTerraformVersions interface.
type MockAdminTerraformVersions struct {
ctrl *gomock.Controller
recorder *MockAdminTerraformVersionsMockRecorder
}
// MockAdminTerraformVersionsMockRecorder is the mock recorder for MockAdminTerraformVersions.
type MockAdminTerraformVersionsMockRecorder struct {
mock *MockAdminTerraformVersions
}
// NewMockAdminTerraformVersions creates a new mock instance.
func NewMockAdminTerraformVersions(ctrl *gomock.Controller) *MockAdminTerraformVersions {
mock := &MockAdminTerraformVersions{ctrl: ctrl}
mock.recorder = &MockAdminTerraformVersionsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockAdminTerraformVersions) EXPECT() *MockAdminTerraformVersionsMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockAdminTerraformVersions) Create(ctx context.Context, options tfe.AdminTerraformVersionCreateOptions) (*tfe.AdminTerraformVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, options)
ret0, _ := ret[0].(*tfe.AdminTerraformVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockAdminTerraformVersionsMockRecorder) Create(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockAdminTerraformVersions)(nil).Create), ctx, options)
}
// Delete mocks base method.
func (m *MockAdminTerraformVersions) Delete(ctx context.Context, id string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, id)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockAdminTerraformVersionsMockRecorder) Delete(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockAdminTerraformVersions)(nil).Delete), ctx, id)
}
// List mocks base method.
func (m *MockAdminTerraformVersions) List(ctx context.Context, options *tfe.AdminTerraformVersionsListOptions) (*tfe.AdminTerraformVersionsList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, options)
ret0, _ := ret[0].(*tfe.AdminTerraformVersionsList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockAdminTerraformVersionsMockRecorder) List(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockAdminTerraformVersions)(nil).List), ctx, options)
}
// Read mocks base method.
func (m *MockAdminTerraformVersions) Read(ctx context.Context, id string) (*tfe.AdminTerraformVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, id)
ret0, _ := ret[0].(*tfe.AdminTerraformVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockAdminTerraformVersionsMockRecorder) Read(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockAdminTerraformVersions)(nil).Read), ctx, id)
}
// Update mocks base method.
func (m *MockAdminTerraformVersions) Update(ctx context.Context, id string, options tfe.AdminTerraformVersionUpdateOptions) (*tfe.AdminTerraformVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, id, options)
ret0, _ := ret[0].(*tfe.AdminTerraformVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockAdminTerraformVersionsMockRecorder) Update(ctx, id, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockAdminTerraformVersions)(nil).Update), ctx, id, options)
}
================================================
FILE: mocks/admin_user_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: admin_user.go
//
// Generated by this command:
//
// mockgen -source=admin_user.go -destination=mocks/admin_user_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockAdminUsers is a mock of AdminUsers interface.
type MockAdminUsers struct {
ctrl *gomock.Controller
recorder *MockAdminUsersMockRecorder
}
// MockAdminUsersMockRecorder is the mock recorder for MockAdminUsers.
type MockAdminUsersMockRecorder struct {
mock *MockAdminUsers
}
// NewMockAdminUsers creates a new mock instance.
func NewMockAdminUsers(ctrl *gomock.Controller) *MockAdminUsers {
mock := &MockAdminUsers{ctrl: ctrl}
mock.recorder = &MockAdminUsersMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockAdminUsers) EXPECT() *MockAdminUsersMockRecorder {
return m.recorder
}
// Delete mocks base method.
func (m *MockAdminUsers) Delete(ctx context.Context, userID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, userID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockAdminUsersMockRecorder) Delete(ctx, userID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockAdminUsers)(nil).Delete), ctx, userID)
}
// Disable2FA mocks base method.
func (m *MockAdminUsers) Disable2FA(ctx context.Context, userID string) (*tfe.AdminUser, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Disable2FA", ctx, userID)
ret0, _ := ret[0].(*tfe.AdminUser)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Disable2FA indicates an expected call of Disable2FA.
func (mr *MockAdminUsersMockRecorder) Disable2FA(ctx, userID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Disable2FA", reflect.TypeOf((*MockAdminUsers)(nil).Disable2FA), ctx, userID)
}
// GrantAdmin mocks base method.
func (m *MockAdminUsers) GrantAdmin(ctx context.Context, userID string) (*tfe.AdminUser, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GrantAdmin", ctx, userID)
ret0, _ := ret[0].(*tfe.AdminUser)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GrantAdmin indicates an expected call of GrantAdmin.
func (mr *MockAdminUsersMockRecorder) GrantAdmin(ctx, userID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantAdmin", reflect.TypeOf((*MockAdminUsers)(nil).GrantAdmin), ctx, userID)
}
// List mocks base method.
func (m *MockAdminUsers) List(ctx context.Context, options *tfe.AdminUserListOptions) (*tfe.AdminUserList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, options)
ret0, _ := ret[0].(*tfe.AdminUserList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockAdminUsersMockRecorder) List(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockAdminUsers)(nil).List), ctx, options)
}
// RevokeAdmin mocks base method.
func (m *MockAdminUsers) RevokeAdmin(ctx context.Context, userID string) (*tfe.AdminUser, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RevokeAdmin", ctx, userID)
ret0, _ := ret[0].(*tfe.AdminUser)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// RevokeAdmin indicates an expected call of RevokeAdmin.
func (mr *MockAdminUsersMockRecorder) RevokeAdmin(ctx, userID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeAdmin", reflect.TypeOf((*MockAdminUsers)(nil).RevokeAdmin), ctx, userID)
}
// Suspend mocks base method.
func (m *MockAdminUsers) Suspend(ctx context.Context, userID string) (*tfe.AdminUser, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Suspend", ctx, userID)
ret0, _ := ret[0].(*tfe.AdminUser)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Suspend indicates an expected call of Suspend.
func (mr *MockAdminUsersMockRecorder) Suspend(ctx, userID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Suspend", reflect.TypeOf((*MockAdminUsers)(nil).Suspend), ctx, userID)
}
// Unsuspend mocks base method.
func (m *MockAdminUsers) Unsuspend(ctx context.Context, userID string) (*tfe.AdminUser, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Unsuspend", ctx, userID)
ret0, _ := ret[0].(*tfe.AdminUser)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Unsuspend indicates an expected call of Unsuspend.
func (mr *MockAdminUsersMockRecorder) Unsuspend(ctx, userID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unsuspend", reflect.TypeOf((*MockAdminUsers)(nil).Unsuspend), ctx, userID)
}
================================================
FILE: mocks/admin_workspace_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: admin_workspace.go
//
// Generated by this command:
//
// mockgen -source=admin_workspace.go -destination=mocks/admin_workspace_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockAdminWorkspaces is a mock of AdminWorkspaces interface.
type MockAdminWorkspaces struct {
ctrl *gomock.Controller
recorder *MockAdminWorkspacesMockRecorder
}
// MockAdminWorkspacesMockRecorder is the mock recorder for MockAdminWorkspaces.
type MockAdminWorkspacesMockRecorder struct {
mock *MockAdminWorkspaces
}
// NewMockAdminWorkspaces creates a new mock instance.
func NewMockAdminWorkspaces(ctrl *gomock.Controller) *MockAdminWorkspaces {
mock := &MockAdminWorkspaces{ctrl: ctrl}
mock.recorder = &MockAdminWorkspacesMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockAdminWorkspaces) EXPECT() *MockAdminWorkspacesMockRecorder {
return m.recorder
}
// Delete mocks base method.
func (m *MockAdminWorkspaces) Delete(ctx context.Context, workspaceID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, workspaceID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockAdminWorkspacesMockRecorder) Delete(ctx, workspaceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockAdminWorkspaces)(nil).Delete), ctx, workspaceID)
}
// List mocks base method.
func (m *MockAdminWorkspaces) List(ctx context.Context, options *tfe.AdminWorkspaceListOptions) (*tfe.AdminWorkspaceList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, options)
ret0, _ := ret[0].(*tfe.AdminWorkspaceList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockAdminWorkspacesMockRecorder) List(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockAdminWorkspaces)(nil).List), ctx, options)
}
// Read mocks base method.
func (m *MockAdminWorkspaces) Read(ctx context.Context, workspaceID string) (*tfe.AdminWorkspace, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, workspaceID)
ret0, _ := ret[0].(*tfe.AdminWorkspace)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockAdminWorkspacesMockRecorder) Read(ctx, workspaceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockAdminWorkspaces)(nil).Read), ctx, workspaceID)
}
================================================
FILE: mocks/agent_pool_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: agent_pool.go
//
// Generated by this command:
//
// mockgen -source=agent_pool.go -destination=mocks/agent_pool_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockAgentPools is a mock of AgentPools interface.
type MockAgentPools struct {
ctrl *gomock.Controller
recorder *MockAgentPoolsMockRecorder
}
// MockAgentPoolsMockRecorder is the mock recorder for MockAgentPools.
type MockAgentPoolsMockRecorder struct {
mock *MockAgentPools
}
// NewMockAgentPools creates a new mock instance.
func NewMockAgentPools(ctrl *gomock.Controller) *MockAgentPools {
mock := &MockAgentPools{ctrl: ctrl}
mock.recorder = &MockAgentPoolsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockAgentPools) EXPECT() *MockAgentPoolsMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockAgentPools) Create(ctx context.Context, organization string, options tfe.AgentPoolCreateOptions) (*tfe.AgentPool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, organization, options)
ret0, _ := ret[0].(*tfe.AgentPool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockAgentPoolsMockRecorder) Create(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockAgentPools)(nil).Create), ctx, organization, options)
}
// Delete mocks base method.
func (m *MockAgentPools) Delete(ctx context.Context, agentPoolID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, agentPoolID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockAgentPoolsMockRecorder) Delete(ctx, agentPoolID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockAgentPools)(nil).Delete), ctx, agentPoolID)
}
// List mocks base method.
func (m *MockAgentPools) List(ctx context.Context, organization string, options *tfe.AgentPoolListOptions) (*tfe.AgentPoolList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, organization, options)
ret0, _ := ret[0].(*tfe.AgentPoolList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockAgentPoolsMockRecorder) List(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockAgentPools)(nil).List), ctx, organization, options)
}
// Read mocks base method.
func (m *MockAgentPools) Read(ctx context.Context, agentPoolID string) (*tfe.AgentPool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, agentPoolID)
ret0, _ := ret[0].(*tfe.AgentPool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockAgentPoolsMockRecorder) Read(ctx, agentPoolID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockAgentPools)(nil).Read), ctx, agentPoolID)
}
// ReadWithOptions mocks base method.
func (m *MockAgentPools) ReadWithOptions(ctx context.Context, agentPoolID string, options *tfe.AgentPoolReadOptions) (*tfe.AgentPool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadWithOptions", ctx, agentPoolID, options)
ret0, _ := ret[0].(*tfe.AgentPool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadWithOptions indicates an expected call of ReadWithOptions.
func (mr *MockAgentPoolsMockRecorder) ReadWithOptions(ctx, agentPoolID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadWithOptions", reflect.TypeOf((*MockAgentPools)(nil).ReadWithOptions), ctx, agentPoolID, options)
}
// Update mocks base method.
func (m *MockAgentPools) Update(ctx context.Context, agentPool string, options tfe.AgentPoolUpdateOptions) (*tfe.AgentPool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, agentPool, options)
ret0, _ := ret[0].(*tfe.AgentPool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockAgentPoolsMockRecorder) Update(ctx, agentPool, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockAgentPools)(nil).Update), ctx, agentPool, options)
}
// UpdateAllowedProjects mocks base method.
func (m *MockAgentPools) UpdateAllowedProjects(ctx context.Context, agentPool string, options tfe.AgentPoolAllowedProjectsUpdateOptions) (*tfe.AgentPool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateAllowedProjects", ctx, agentPool, options)
ret0, _ := ret[0].(*tfe.AgentPool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateAllowedProjects indicates an expected call of UpdateAllowedProjects.
func (mr *MockAgentPoolsMockRecorder) UpdateAllowedProjects(ctx, agentPool, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAllowedProjects", reflect.TypeOf((*MockAgentPools)(nil).UpdateAllowedProjects), ctx, agentPool, options)
}
// UpdateAllowedWorkspaces mocks base method.
func (m *MockAgentPools) UpdateAllowedWorkspaces(ctx context.Context, agentPool string, options tfe.AgentPoolAllowedWorkspacesUpdateOptions) (*tfe.AgentPool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateAllowedWorkspaces", ctx, agentPool, options)
ret0, _ := ret[0].(*tfe.AgentPool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateAllowedWorkspaces indicates an expected call of UpdateAllowedWorkspaces.
func (mr *MockAgentPoolsMockRecorder) UpdateAllowedWorkspaces(ctx, agentPool, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAllowedWorkspaces", reflect.TypeOf((*MockAgentPools)(nil).UpdateAllowedWorkspaces), ctx, agentPool, options)
}
// UpdateExcludedWorkspaces mocks base method.
func (m *MockAgentPools) UpdateExcludedWorkspaces(ctx context.Context, agentPool string, options tfe.AgentPoolExcludedWorkspacesUpdateOptions) (*tfe.AgentPool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateExcludedWorkspaces", ctx, agentPool, options)
ret0, _ := ret[0].(*tfe.AgentPool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateExcludedWorkspaces indicates an expected call of UpdateExcludedWorkspaces.
func (mr *MockAgentPoolsMockRecorder) UpdateExcludedWorkspaces(ctx, agentPool, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateExcludedWorkspaces", reflect.TypeOf((*MockAgentPools)(nil).UpdateExcludedWorkspaces), ctx, agentPool, options)
}
================================================
FILE: mocks/agent_token_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: agent_token.go
//
// Generated by this command:
//
// mockgen -source=agent_token.go -destination=mocks/agent_token_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockAgentTokens is a mock of AgentTokens interface.
type MockAgentTokens struct {
ctrl *gomock.Controller
recorder *MockAgentTokensMockRecorder
}
// MockAgentTokensMockRecorder is the mock recorder for MockAgentTokens.
type MockAgentTokensMockRecorder struct {
mock *MockAgentTokens
}
// NewMockAgentTokens creates a new mock instance.
func NewMockAgentTokens(ctrl *gomock.Controller) *MockAgentTokens {
mock := &MockAgentTokens{ctrl: ctrl}
mock.recorder = &MockAgentTokensMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockAgentTokens) EXPECT() *MockAgentTokensMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockAgentTokens) Create(ctx context.Context, agentPoolID string, options tfe.AgentTokenCreateOptions) (*tfe.AgentToken, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, agentPoolID, options)
ret0, _ := ret[0].(*tfe.AgentToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockAgentTokensMockRecorder) Create(ctx, agentPoolID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockAgentTokens)(nil).Create), ctx, agentPoolID, options)
}
// Delete mocks base method.
func (m *MockAgentTokens) Delete(ctx context.Context, agentTokenID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, agentTokenID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockAgentTokensMockRecorder) Delete(ctx, agentTokenID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockAgentTokens)(nil).Delete), ctx, agentTokenID)
}
// List mocks base method.
func (m *MockAgentTokens) List(ctx context.Context, agentPoolID string) (*tfe.AgentTokenList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, agentPoolID)
ret0, _ := ret[0].(*tfe.AgentTokenList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockAgentTokensMockRecorder) List(ctx, agentPoolID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockAgentTokens)(nil).List), ctx, agentPoolID)
}
// Read mocks base method.
func (m *MockAgentTokens) Read(ctx context.Context, agentTokenID string) (*tfe.AgentToken, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, agentTokenID)
ret0, _ := ret[0].(*tfe.AgentToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockAgentTokensMockRecorder) Read(ctx, agentTokenID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockAgentTokens)(nil).Read), ctx, agentTokenID)
}
================================================
FILE: mocks/agents.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: agent.go
//
// Generated by this command:
//
// mockgen -source=agent.go -destination=mocks/agents.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockAgents is a mock of Agents interface.
type MockAgents struct {
ctrl *gomock.Controller
recorder *MockAgentsMockRecorder
}
// MockAgentsMockRecorder is the mock recorder for MockAgents.
type MockAgentsMockRecorder struct {
mock *MockAgents
}
// NewMockAgents creates a new mock instance.
func NewMockAgents(ctrl *gomock.Controller) *MockAgents {
mock := &MockAgents{ctrl: ctrl}
mock.recorder = &MockAgentsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockAgents) EXPECT() *MockAgentsMockRecorder {
return m.recorder
}
// List mocks base method.
func (m *MockAgents) List(ctx context.Context, agentPoolID string, options *tfe.AgentListOptions) (*tfe.AgentList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, agentPoolID, options)
ret0, _ := ret[0].(*tfe.AgentList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockAgentsMockRecorder) List(ctx, agentPoolID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockAgents)(nil).List), ctx, agentPoolID, options)
}
// Read mocks base method.
func (m *MockAgents) Read(ctx context.Context, agentID string) (*tfe.Agent, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, agentID)
ret0, _ := ret[0].(*tfe.Agent)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockAgentsMockRecorder) Read(ctx, agentID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockAgents)(nil).Read), ctx, agentID)
}
================================================
FILE: mocks/apply_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: apply.go
//
// Generated by this command:
//
// mockgen -source=apply.go -destination=mocks/apply_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
io "io"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockApplies is a mock of Applies interface.
type MockApplies struct {
ctrl *gomock.Controller
recorder *MockAppliesMockRecorder
}
// MockAppliesMockRecorder is the mock recorder for MockApplies.
type MockAppliesMockRecorder struct {
mock *MockApplies
}
// NewMockApplies creates a new mock instance.
func NewMockApplies(ctrl *gomock.Controller) *MockApplies {
mock := &MockApplies{ctrl: ctrl}
mock.recorder = &MockAppliesMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockApplies) EXPECT() *MockAppliesMockRecorder {
return m.recorder
}
// Logs mocks base method.
func (m *MockApplies) Logs(ctx context.Context, applyID string) (io.Reader, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Logs", ctx, applyID)
ret0, _ := ret[0].(io.Reader)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Logs indicates an expected call of Logs.
func (mr *MockAppliesMockRecorder) Logs(ctx, applyID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logs", reflect.TypeOf((*MockApplies)(nil).Logs), ctx, applyID)
}
// Read mocks base method.
func (m *MockApplies) Read(ctx context.Context, applyID string) (*tfe.Apply, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, applyID)
ret0, _ := ret[0].(*tfe.Apply)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockAppliesMockRecorder) Read(ctx, applyID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockApplies)(nil).Read), ctx, applyID)
}
================================================
FILE: mocks/audit_trail_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: audit_trail.go
//
// Generated by this command:
//
// mockgen -source=audit_trail.go -destination=mocks/audit_trail_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockAuditTrails is a mock of AuditTrails interface.
type MockAuditTrails struct {
ctrl *gomock.Controller
recorder *MockAuditTrailsMockRecorder
}
// MockAuditTrailsMockRecorder is the mock recorder for MockAuditTrails.
type MockAuditTrailsMockRecorder struct {
mock *MockAuditTrails
}
// NewMockAuditTrails creates a new mock instance.
func NewMockAuditTrails(ctrl *gomock.Controller) *MockAuditTrails {
mock := &MockAuditTrails{ctrl: ctrl}
mock.recorder = &MockAuditTrailsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockAuditTrails) EXPECT() *MockAuditTrailsMockRecorder {
return m.recorder
}
// List mocks base method.
func (m *MockAuditTrails) List(ctx context.Context, options *tfe.AuditTrailListOptions) (*tfe.AuditTrailList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, options)
ret0, _ := ret[0].(*tfe.AuditTrailList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockAuditTrailsMockRecorder) List(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockAuditTrails)(nil).List), ctx, options)
}
================================================
FILE: mocks/comment_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: comment.go
//
// Generated by this command:
//
// mockgen -source=comment.go -destination=mocks/comment_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockComments is a mock of Comments interface.
type MockComments struct {
ctrl *gomock.Controller
recorder *MockCommentsMockRecorder
}
// MockCommentsMockRecorder is the mock recorder for MockComments.
type MockCommentsMockRecorder struct {
mock *MockComments
}
// NewMockComments creates a new mock instance.
func NewMockComments(ctrl *gomock.Controller) *MockComments {
mock := &MockComments{ctrl: ctrl}
mock.recorder = &MockCommentsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockComments) EXPECT() *MockCommentsMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockComments) Create(ctx context.Context, runID string, options tfe.CommentCreateOptions) (*tfe.Comment, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, runID, options)
ret0, _ := ret[0].(*tfe.Comment)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockCommentsMockRecorder) Create(ctx, runID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockComments)(nil).Create), ctx, runID, options)
}
// List mocks base method.
func (m *MockComments) List(ctx context.Context, runID string) (*tfe.CommentList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, runID)
ret0, _ := ret[0].(*tfe.CommentList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockCommentsMockRecorder) List(ctx, runID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockComments)(nil).List), ctx, runID)
}
// Read mocks base method.
func (m *MockComments) Read(ctx context.Context, commentID string) (*tfe.Comment, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, commentID)
ret0, _ := ret[0].(*tfe.Comment)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockCommentsMockRecorder) Read(ctx, commentID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockComments)(nil).Read), ctx, commentID)
}
================================================
FILE: mocks/configuration_version_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: configuration_version.go
//
// Generated by this command:
//
// mockgen -source=configuration_version.go -destination=mocks/configuration_version_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
io "io"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockConfigurationVersions is a mock of ConfigurationVersions interface.
type MockConfigurationVersions struct {
ctrl *gomock.Controller
recorder *MockConfigurationVersionsMockRecorder
}
// MockConfigurationVersionsMockRecorder is the mock recorder for MockConfigurationVersions.
type MockConfigurationVersionsMockRecorder struct {
mock *MockConfigurationVersions
}
// NewMockConfigurationVersions creates a new mock instance.
func NewMockConfigurationVersions(ctrl *gomock.Controller) *MockConfigurationVersions {
mock := &MockConfigurationVersions{ctrl: ctrl}
mock.recorder = &MockConfigurationVersionsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockConfigurationVersions) EXPECT() *MockConfigurationVersionsMockRecorder {
return m.recorder
}
// Archive mocks base method.
func (m *MockConfigurationVersions) Archive(ctx context.Context, cvID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Archive", ctx, cvID)
ret0, _ := ret[0].(error)
return ret0
}
// Archive indicates an expected call of Archive.
func (mr *MockConfigurationVersionsMockRecorder) Archive(ctx, cvID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Archive", reflect.TypeOf((*MockConfigurationVersions)(nil).Archive), ctx, cvID)
}
// Create mocks base method.
func (m *MockConfigurationVersions) Create(ctx context.Context, workspaceID string, options tfe.ConfigurationVersionCreateOptions) (*tfe.ConfigurationVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.ConfigurationVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockConfigurationVersionsMockRecorder) Create(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockConfigurationVersions)(nil).Create), ctx, workspaceID, options)
}
// CreateForRegistryModule mocks base method.
func (m *MockConfigurationVersions) CreateForRegistryModule(ctx context.Context, moduleID tfe.RegistryModuleID) (*tfe.ConfigurationVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateForRegistryModule", ctx, moduleID)
ret0, _ := ret[0].(*tfe.ConfigurationVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateForRegistryModule indicates an expected call of CreateForRegistryModule.
func (mr *MockConfigurationVersionsMockRecorder) CreateForRegistryModule(ctx, moduleID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateForRegistryModule", reflect.TypeOf((*MockConfigurationVersions)(nil).CreateForRegistryModule), ctx, moduleID)
}
// Download mocks base method.
func (m *MockConfigurationVersions) Download(ctx context.Context, cvID string) ([]byte, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Download", ctx, cvID)
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Download indicates an expected call of Download.
func (mr *MockConfigurationVersionsMockRecorder) Download(ctx, cvID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Download", reflect.TypeOf((*MockConfigurationVersions)(nil).Download), ctx, cvID)
}
// List mocks base method.
func (m *MockConfigurationVersions) List(ctx context.Context, workspaceID string, options *tfe.ConfigurationVersionListOptions) (*tfe.ConfigurationVersionList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.ConfigurationVersionList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockConfigurationVersionsMockRecorder) List(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockConfigurationVersions)(nil).List), ctx, workspaceID, options)
}
// PermanentlyDeleteBackingData mocks base method.
func (m *MockConfigurationVersions) PermanentlyDeleteBackingData(ctx context.Context, svID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PermanentlyDeleteBackingData", ctx, svID)
ret0, _ := ret[0].(error)
return ret0
}
// PermanentlyDeleteBackingData indicates an expected call of PermanentlyDeleteBackingData.
func (mr *MockConfigurationVersionsMockRecorder) PermanentlyDeleteBackingData(ctx, svID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PermanentlyDeleteBackingData", reflect.TypeOf((*MockConfigurationVersions)(nil).PermanentlyDeleteBackingData), ctx, svID)
}
// Read mocks base method.
func (m *MockConfigurationVersions) Read(ctx context.Context, cvID string) (*tfe.ConfigurationVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, cvID)
ret0, _ := ret[0].(*tfe.ConfigurationVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockConfigurationVersionsMockRecorder) Read(ctx, cvID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockConfigurationVersions)(nil).Read), ctx, cvID)
}
// ReadWithOptions mocks base method.
func (m *MockConfigurationVersions) ReadWithOptions(ctx context.Context, cvID string, options *tfe.ConfigurationVersionReadOptions) (*tfe.ConfigurationVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadWithOptions", ctx, cvID, options)
ret0, _ := ret[0].(*tfe.ConfigurationVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadWithOptions indicates an expected call of ReadWithOptions.
func (mr *MockConfigurationVersionsMockRecorder) ReadWithOptions(ctx, cvID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadWithOptions", reflect.TypeOf((*MockConfigurationVersions)(nil).ReadWithOptions), ctx, cvID, options)
}
// RestoreBackingData mocks base method.
func (m *MockConfigurationVersions) RestoreBackingData(ctx context.Context, svID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RestoreBackingData", ctx, svID)
ret0, _ := ret[0].(error)
return ret0
}
// RestoreBackingData indicates an expected call of RestoreBackingData.
func (mr *MockConfigurationVersionsMockRecorder) RestoreBackingData(ctx, svID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestoreBackingData", reflect.TypeOf((*MockConfigurationVersions)(nil).RestoreBackingData), ctx, svID)
}
// SoftDeleteBackingData mocks base method.
func (m *MockConfigurationVersions) SoftDeleteBackingData(ctx context.Context, svID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SoftDeleteBackingData", ctx, svID)
ret0, _ := ret[0].(error)
return ret0
}
// SoftDeleteBackingData indicates an expected call of SoftDeleteBackingData.
func (mr *MockConfigurationVersionsMockRecorder) SoftDeleteBackingData(ctx, svID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SoftDeleteBackingData", reflect.TypeOf((*MockConfigurationVersions)(nil).SoftDeleteBackingData), ctx, svID)
}
// Upload mocks base method.
func (m *MockConfigurationVersions) Upload(ctx context.Context, url, path string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Upload", ctx, url, path)
ret0, _ := ret[0].(error)
return ret0
}
// Upload indicates an expected call of Upload.
func (mr *MockConfigurationVersionsMockRecorder) Upload(ctx, url, path any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Upload", reflect.TypeOf((*MockConfigurationVersions)(nil).Upload), ctx, url, path)
}
// UploadTarGzip mocks base method.
func (m *MockConfigurationVersions) UploadTarGzip(ctx context.Context, url string, archive io.Reader) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UploadTarGzip", ctx, url, archive)
ret0, _ := ret[0].(error)
return ret0
}
// UploadTarGzip indicates an expected call of UploadTarGzip.
func (mr *MockConfigurationVersionsMockRecorder) UploadTarGzip(ctx, url, archive any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadTarGzip", reflect.TypeOf((*MockConfigurationVersions)(nil).UploadTarGzip), ctx, url, archive)
}
================================================
FILE: mocks/cost_estimate_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: cost_estimate.go
//
// Generated by this command:
//
// mockgen -source=cost_estimate.go -destination=mocks/cost_estimate_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
io "io"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockCostEstimates is a mock of CostEstimates interface.
type MockCostEstimates struct {
ctrl *gomock.Controller
recorder *MockCostEstimatesMockRecorder
}
// MockCostEstimatesMockRecorder is the mock recorder for MockCostEstimates.
type MockCostEstimatesMockRecorder struct {
mock *MockCostEstimates
}
// NewMockCostEstimates creates a new mock instance.
func NewMockCostEstimates(ctrl *gomock.Controller) *MockCostEstimates {
mock := &MockCostEstimates{ctrl: ctrl}
mock.recorder = &MockCostEstimatesMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCostEstimates) EXPECT() *MockCostEstimatesMockRecorder {
return m.recorder
}
// Logs mocks base method.
func (m *MockCostEstimates) Logs(ctx context.Context, costEstimateID string) (io.Reader, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Logs", ctx, costEstimateID)
ret0, _ := ret[0].(io.Reader)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Logs indicates an expected call of Logs.
func (mr *MockCostEstimatesMockRecorder) Logs(ctx, costEstimateID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logs", reflect.TypeOf((*MockCostEstimates)(nil).Logs), ctx, costEstimateID)
}
// Read mocks base method.
func (m *MockCostEstimates) Read(ctx context.Context, costEstimateID string) (*tfe.CostEstimate, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, costEstimateID)
ret0, _ := ret[0].(*tfe.CostEstimate)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockCostEstimatesMockRecorder) Read(ctx, costEstimateID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockCostEstimates)(nil).Read), ctx, costEstimateID)
}
================================================
FILE: mocks/github_app_installation_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: github_app_installation.go
//
// Generated by this command:
//
// mockgen -source=github_app_installation.go -destination=mocks/github_app_installation_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockGHAInstallations is a mock of GHAInstallations interface.
type MockGHAInstallations struct {
ctrl *gomock.Controller
recorder *MockGHAInstallationsMockRecorder
}
// MockGHAInstallationsMockRecorder is the mock recorder for MockGHAInstallations.
type MockGHAInstallationsMockRecorder struct {
mock *MockGHAInstallations
}
// NewMockGHAInstallations creates a new mock instance.
func NewMockGHAInstallations(ctrl *gomock.Controller) *MockGHAInstallations {
mock := &MockGHAInstallations{ctrl: ctrl}
mock.recorder = &MockGHAInstallationsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockGHAInstallations) EXPECT() *MockGHAInstallationsMockRecorder {
return m.recorder
}
// List mocks base method.
func (m *MockGHAInstallations) List(ctx context.Context, options *tfe.GHAInstallationListOptions) (*tfe.GHAInstallationList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, options)
ret0, _ := ret[0].(*tfe.GHAInstallationList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockGHAInstallationsMockRecorder) List(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockGHAInstallations)(nil).List), ctx, options)
}
// Read mocks base method.
func (m *MockGHAInstallations) Read(ctx context.Context, GHAInstallationID string) (*tfe.GHAInstallation, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, GHAInstallationID)
ret0, _ := ret[0].(*tfe.GHAInstallation)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockGHAInstallationsMockRecorder) Read(ctx, GHAInstallationID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockGHAInstallations)(nil).Read), ctx, GHAInstallationID)
}
================================================
FILE: mocks/gpg_key_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: gpg_key.go
//
// Generated by this command:
//
// mockgen -source=gpg_key.go -destination=mocks/gpg_key_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockGPGKeys is a mock of GPGKeys interface.
type MockGPGKeys struct {
ctrl *gomock.Controller
recorder *MockGPGKeysMockRecorder
}
// MockGPGKeysMockRecorder is the mock recorder for MockGPGKeys.
type MockGPGKeysMockRecorder struct {
mock *MockGPGKeys
}
// NewMockGPGKeys creates a new mock instance.
func NewMockGPGKeys(ctrl *gomock.Controller) *MockGPGKeys {
mock := &MockGPGKeys{ctrl: ctrl}
mock.recorder = &MockGPGKeysMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockGPGKeys) EXPECT() *MockGPGKeysMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockGPGKeys) Create(ctx context.Context, registryName tfe.RegistryName, options tfe.GPGKeyCreateOptions) (*tfe.GPGKey, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, registryName, options)
ret0, _ := ret[0].(*tfe.GPGKey)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockGPGKeysMockRecorder) Create(ctx, registryName, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockGPGKeys)(nil).Create), ctx, registryName, options)
}
// Delete mocks base method.
func (m *MockGPGKeys) Delete(ctx context.Context, keyID tfe.GPGKeyID) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, keyID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockGPGKeysMockRecorder) Delete(ctx, keyID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockGPGKeys)(nil).Delete), ctx, keyID)
}
// ListPrivate mocks base method.
func (m *MockGPGKeys) ListPrivate(ctx context.Context, options tfe.GPGKeyListOptions) (*tfe.GPGKeyList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListPrivate", ctx, options)
ret0, _ := ret[0].(*tfe.GPGKeyList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListPrivate indicates an expected call of ListPrivate.
func (mr *MockGPGKeysMockRecorder) ListPrivate(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPrivate", reflect.TypeOf((*MockGPGKeys)(nil).ListPrivate), ctx, options)
}
// Read mocks base method.
func (m *MockGPGKeys) Read(ctx context.Context, keyID tfe.GPGKeyID) (*tfe.GPGKey, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, keyID)
ret0, _ := ret[0].(*tfe.GPGKey)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockGPGKeysMockRecorder) Read(ctx, keyID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockGPGKeys)(nil).Read), ctx, keyID)
}
// Update mocks base method.
func (m *MockGPGKeys) Update(ctx context.Context, keyID tfe.GPGKeyID, options tfe.GPGKeyUpdateOptions) (*tfe.GPGKey, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, keyID, options)
ret0, _ := ret[0].(*tfe.GPGKey)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockGPGKeysMockRecorder) Update(ctx, keyID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockGPGKeys)(nil).Update), ctx, keyID, options)
}
================================================
FILE: mocks/ip_ranges_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: ip_ranges.go
//
// Generated by this command:
//
// mockgen -source=ip_ranges.go -destination=mocks/ip_ranges_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockIPRanges is a mock of IPRanges interface.
type MockIPRanges struct {
ctrl *gomock.Controller
recorder *MockIPRangesMockRecorder
}
// MockIPRangesMockRecorder is the mock recorder for MockIPRanges.
type MockIPRangesMockRecorder struct {
mock *MockIPRanges
}
// NewMockIPRanges creates a new mock instance.
func NewMockIPRanges(ctrl *gomock.Controller) *MockIPRanges {
mock := &MockIPRanges{ctrl: ctrl}
mock.recorder = &MockIPRangesMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockIPRanges) EXPECT() *MockIPRangesMockRecorder {
return m.recorder
}
// Read mocks base method.
func (m *MockIPRanges) Read(ctx context.Context, modifiedSince string) (*tfe.IPRange, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, modifiedSince)
ret0, _ := ret[0].(*tfe.IPRange)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockIPRangesMockRecorder) Read(ctx, modifiedSince any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockIPRanges)(nil).Read), ctx, modifiedSince)
}
================================================
FILE: mocks/logreader_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: logreader.go
//
// Generated by this command:
//
// mockgen -source=logreader.go -destination=mocks/logreader_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
================================================
FILE: mocks/notification_configuration_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: notification_configuration.go
//
// Generated by this command:
//
// mockgen -source=notification_configuration.go -destination=mocks/notification_configuration_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockNotificationConfigurations is a mock of NotificationConfigurations interface.
type MockNotificationConfigurations struct {
ctrl *gomock.Controller
recorder *MockNotificationConfigurationsMockRecorder
}
// MockNotificationConfigurationsMockRecorder is the mock recorder for MockNotificationConfigurations.
type MockNotificationConfigurationsMockRecorder struct {
mock *MockNotificationConfigurations
}
// NewMockNotificationConfigurations creates a new mock instance.
func NewMockNotificationConfigurations(ctrl *gomock.Controller) *MockNotificationConfigurations {
mock := &MockNotificationConfigurations{ctrl: ctrl}
mock.recorder = &MockNotificationConfigurationsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockNotificationConfigurations) EXPECT() *MockNotificationConfigurationsMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockNotificationConfigurations) Create(ctx context.Context, subscribableID string, options tfe.NotificationConfigurationCreateOptions) (*tfe.NotificationConfiguration, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, subscribableID, options)
ret0, _ := ret[0].(*tfe.NotificationConfiguration)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockNotificationConfigurationsMockRecorder) Create(ctx, subscribableID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockNotificationConfigurations)(nil).Create), ctx, subscribableID, options)
}
// Delete mocks base method.
func (m *MockNotificationConfigurations) Delete(ctx context.Context, notificationConfigurationID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, notificationConfigurationID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockNotificationConfigurationsMockRecorder) Delete(ctx, notificationConfigurationID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockNotificationConfigurations)(nil).Delete), ctx, notificationConfigurationID)
}
// List mocks base method.
func (m *MockNotificationConfigurations) List(ctx context.Context, subscribableID string, options *tfe.NotificationConfigurationListOptions) (*tfe.NotificationConfigurationList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, subscribableID, options)
ret0, _ := ret[0].(*tfe.NotificationConfigurationList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockNotificationConfigurationsMockRecorder) List(ctx, subscribableID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockNotificationConfigurations)(nil).List), ctx, subscribableID, options)
}
// Read mocks base method.
func (m *MockNotificationConfigurations) Read(ctx context.Context, notificationConfigurationID string) (*tfe.NotificationConfiguration, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, notificationConfigurationID)
ret0, _ := ret[0].(*tfe.NotificationConfiguration)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockNotificationConfigurationsMockRecorder) Read(ctx, notificationConfigurationID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockNotificationConfigurations)(nil).Read), ctx, notificationConfigurationID)
}
// Update mocks base method.
func (m *MockNotificationConfigurations) Update(ctx context.Context, notificationConfigurationID string, options tfe.NotificationConfigurationUpdateOptions) (*tfe.NotificationConfiguration, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, notificationConfigurationID, options)
ret0, _ := ret[0].(*tfe.NotificationConfiguration)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockNotificationConfigurationsMockRecorder) Update(ctx, notificationConfigurationID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockNotificationConfigurations)(nil).Update), ctx, notificationConfigurationID, options)
}
// Verify mocks base method.
func (m *MockNotificationConfigurations) Verify(ctx context.Context, notificationConfigurationID string) (*tfe.NotificationConfiguration, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Verify", ctx, notificationConfigurationID)
ret0, _ := ret[0].(*tfe.NotificationConfiguration)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Verify indicates an expected call of Verify.
func (mr *MockNotificationConfigurationsMockRecorder) Verify(ctx, notificationConfigurationID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockNotificationConfigurations)(nil).Verify), ctx, notificationConfigurationID)
}
================================================
FILE: mocks/oauth_client_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: oauth_client.go
//
// Generated by this command:
//
// mockgen -source=oauth_client.go -destination=mocks/oauth_client_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockOAuthClients is a mock of OAuthClients interface.
type MockOAuthClients struct {
ctrl *gomock.Controller
recorder *MockOAuthClientsMockRecorder
}
// MockOAuthClientsMockRecorder is the mock recorder for MockOAuthClients.
type MockOAuthClientsMockRecorder struct {
mock *MockOAuthClients
}
// NewMockOAuthClients creates a new mock instance.
func NewMockOAuthClients(ctrl *gomock.Controller) *MockOAuthClients {
mock := &MockOAuthClients{ctrl: ctrl}
mock.recorder = &MockOAuthClientsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockOAuthClients) EXPECT() *MockOAuthClientsMockRecorder {
return m.recorder
}
// AddProjects mocks base method.
func (m *MockOAuthClients) AddProjects(ctx context.Context, oAuthClientID string, options tfe.OAuthClientAddProjectsOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddProjects", ctx, oAuthClientID, options)
ret0, _ := ret[0].(error)
return ret0
}
// AddProjects indicates an expected call of AddProjects.
func (mr *MockOAuthClientsMockRecorder) AddProjects(ctx, oAuthClientID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddProjects", reflect.TypeOf((*MockOAuthClients)(nil).AddProjects), ctx, oAuthClientID, options)
}
// Create mocks base method.
func (m *MockOAuthClients) Create(ctx context.Context, organization string, options tfe.OAuthClientCreateOptions) (*tfe.OAuthClient, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, organization, options)
ret0, _ := ret[0].(*tfe.OAuthClient)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockOAuthClientsMockRecorder) Create(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockOAuthClients)(nil).Create), ctx, organization, options)
}
// Delete mocks base method.
func (m *MockOAuthClients) Delete(ctx context.Context, oAuthClientID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, oAuthClientID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockOAuthClientsMockRecorder) Delete(ctx, oAuthClientID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockOAuthClients)(nil).Delete), ctx, oAuthClientID)
}
// List mocks base method.
func (m *MockOAuthClients) List(ctx context.Context, organization string, options *tfe.OAuthClientListOptions) (*tfe.OAuthClientList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, organization, options)
ret0, _ := ret[0].(*tfe.OAuthClientList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockOAuthClientsMockRecorder) List(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockOAuthClients)(nil).List), ctx, organization, options)
}
// Read mocks base method.
func (m *MockOAuthClients) Read(ctx context.Context, oAuthClientID string) (*tfe.OAuthClient, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, oAuthClientID)
ret0, _ := ret[0].(*tfe.OAuthClient)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockOAuthClientsMockRecorder) Read(ctx, oAuthClientID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockOAuthClients)(nil).Read), ctx, oAuthClientID)
}
// ReadWithOptions mocks base method.
func (m *MockOAuthClients) ReadWithOptions(ctx context.Context, oAuthClientID string, options *tfe.OAuthClientReadOptions) (*tfe.OAuthClient, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadWithOptions", ctx, oAuthClientID, options)
ret0, _ := ret[0].(*tfe.OAuthClient)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadWithOptions indicates an expected call of ReadWithOptions.
func (mr *MockOAuthClientsMockRecorder) ReadWithOptions(ctx, oAuthClientID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadWithOptions", reflect.TypeOf((*MockOAuthClients)(nil).ReadWithOptions), ctx, oAuthClientID, options)
}
// RemoveProjects mocks base method.
func (m *MockOAuthClients) RemoveProjects(ctx context.Context, oAuthClientID string, options tfe.OAuthClientRemoveProjectsOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveProjects", ctx, oAuthClientID, options)
ret0, _ := ret[0].(error)
return ret0
}
// RemoveProjects indicates an expected call of RemoveProjects.
func (mr *MockOAuthClientsMockRecorder) RemoveProjects(ctx, oAuthClientID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveProjects", reflect.TypeOf((*MockOAuthClients)(nil).RemoveProjects), ctx, oAuthClientID, options)
}
// Update mocks base method.
func (m *MockOAuthClients) Update(ctx context.Context, oAuthClientID string, options tfe.OAuthClientUpdateOptions) (*tfe.OAuthClient, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, oAuthClientID, options)
ret0, _ := ret[0].(*tfe.OAuthClient)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockOAuthClientsMockRecorder) Update(ctx, oAuthClientID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockOAuthClients)(nil).Update), ctx, oAuthClientID, options)
}
================================================
FILE: mocks/oauth_token_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: oauth_token.go
//
// Generated by this command:
//
// mockgen -source=oauth_token.go -destination=mocks/oauth_token_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockOAuthTokens is a mock of OAuthTokens interface.
type MockOAuthTokens struct {
ctrl *gomock.Controller
recorder *MockOAuthTokensMockRecorder
}
// MockOAuthTokensMockRecorder is the mock recorder for MockOAuthTokens.
type MockOAuthTokensMockRecorder struct {
mock *MockOAuthTokens
}
// NewMockOAuthTokens creates a new mock instance.
func NewMockOAuthTokens(ctrl *gomock.Controller) *MockOAuthTokens {
mock := &MockOAuthTokens{ctrl: ctrl}
mock.recorder = &MockOAuthTokensMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockOAuthTokens) EXPECT() *MockOAuthTokensMockRecorder {
return m.recorder
}
// Delete mocks base method.
func (m *MockOAuthTokens) Delete(ctx context.Context, oAuthTokenID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, oAuthTokenID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockOAuthTokensMockRecorder) Delete(ctx, oAuthTokenID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockOAuthTokens)(nil).Delete), ctx, oAuthTokenID)
}
// List mocks base method.
func (m *MockOAuthTokens) List(ctx context.Context, organization string, options *tfe.OAuthTokenListOptions) (*tfe.OAuthTokenList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, organization, options)
ret0, _ := ret[0].(*tfe.OAuthTokenList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockOAuthTokensMockRecorder) List(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockOAuthTokens)(nil).List), ctx, organization, options)
}
// Read mocks base method.
func (m *MockOAuthTokens) Read(ctx context.Context, oAuthTokenID string) (*tfe.OAuthToken, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, oAuthTokenID)
ret0, _ := ret[0].(*tfe.OAuthToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockOAuthTokensMockRecorder) Read(ctx, oAuthTokenID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockOAuthTokens)(nil).Read), ctx, oAuthTokenID)
}
// Update mocks base method.
func (m *MockOAuthTokens) Update(ctx context.Context, oAuthTokenID string, options tfe.OAuthTokenUpdateOptions) (*tfe.OAuthToken, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, oAuthTokenID, options)
ret0, _ := ret[0].(*tfe.OAuthToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockOAuthTokensMockRecorder) Update(ctx, oAuthTokenID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockOAuthTokens)(nil).Update), ctx, oAuthTokenID, options)
}
================================================
FILE: mocks/organization_membership_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: organization_membership.go
//
// Generated by this command:
//
// mockgen -source=organization_membership.go -destination=mocks/organization_membership_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockOrganizationMemberships is a mock of OrganizationMemberships interface.
type MockOrganizationMemberships struct {
ctrl *gomock.Controller
recorder *MockOrganizationMembershipsMockRecorder
}
// MockOrganizationMembershipsMockRecorder is the mock recorder for MockOrganizationMemberships.
type MockOrganizationMembershipsMockRecorder struct {
mock *MockOrganizationMemberships
}
// NewMockOrganizationMemberships creates a new mock instance.
func NewMockOrganizationMemberships(ctrl *gomock.Controller) *MockOrganizationMemberships {
mock := &MockOrganizationMemberships{ctrl: ctrl}
mock.recorder = &MockOrganizationMembershipsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockOrganizationMemberships) EXPECT() *MockOrganizationMembershipsMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockOrganizationMemberships) Create(ctx context.Context, organization string, options tfe.OrganizationMembershipCreateOptions) (*tfe.OrganizationMembership, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, organization, options)
ret0, _ := ret[0].(*tfe.OrganizationMembership)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockOrganizationMembershipsMockRecorder) Create(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockOrganizationMemberships)(nil).Create), ctx, organization, options)
}
// Delete mocks base method.
func (m *MockOrganizationMemberships) Delete(ctx context.Context, organizationMembershipID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, organizationMembershipID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockOrganizationMembershipsMockRecorder) Delete(ctx, organizationMembershipID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockOrganizationMemberships)(nil).Delete), ctx, organizationMembershipID)
}
// List mocks base method.
func (m *MockOrganizationMemberships) List(ctx context.Context, organization string, options *tfe.OrganizationMembershipListOptions) (*tfe.OrganizationMembershipList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, organization, options)
ret0, _ := ret[0].(*tfe.OrganizationMembershipList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockOrganizationMembershipsMockRecorder) List(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockOrganizationMemberships)(nil).List), ctx, organization, options)
}
// Read mocks base method.
func (m *MockOrganizationMemberships) Read(ctx context.Context, organizationMembershipID string) (*tfe.OrganizationMembership, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, organizationMembershipID)
ret0, _ := ret[0].(*tfe.OrganizationMembership)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockOrganizationMembershipsMockRecorder) Read(ctx, organizationMembershipID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockOrganizationMemberships)(nil).Read), ctx, organizationMembershipID)
}
// ReadWithOptions mocks base method.
func (m *MockOrganizationMemberships) ReadWithOptions(ctx context.Context, organizationMembershipID string, options tfe.OrganizationMembershipReadOptions) (*tfe.OrganizationMembership, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadWithOptions", ctx, organizationMembershipID, options)
ret0, _ := ret[0].(*tfe.OrganizationMembership)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadWithOptions indicates an expected call of ReadWithOptions.
func (mr *MockOrganizationMembershipsMockRecorder) ReadWithOptions(ctx, organizationMembershipID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadWithOptions", reflect.TypeOf((*MockOrganizationMemberships)(nil).ReadWithOptions), ctx, organizationMembershipID, options)
}
================================================
FILE: mocks/organization_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: organization.go
//
// Generated by this command:
//
// mockgen -source=organization.go -destination=mocks/organization_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockOrganizations is a mock of Organizations interface.
type MockOrganizations struct {
ctrl *gomock.Controller
recorder *MockOrganizationsMockRecorder
}
// MockOrganizationsMockRecorder is the mock recorder for MockOrganizations.
type MockOrganizationsMockRecorder struct {
mock *MockOrganizations
}
// NewMockOrganizations creates a new mock instance.
func NewMockOrganizations(ctrl *gomock.Controller) *MockOrganizations {
mock := &MockOrganizations{ctrl: ctrl}
mock.recorder = &MockOrganizationsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockOrganizations) EXPECT() *MockOrganizationsMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockOrganizations) Create(ctx context.Context, options tfe.OrganizationCreateOptions) (*tfe.Organization, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, options)
ret0, _ := ret[0].(*tfe.Organization)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockOrganizationsMockRecorder) Create(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockOrganizations)(nil).Create), ctx, options)
}
// Delete mocks base method.
func (m *MockOrganizations) Delete(ctx context.Context, organization string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, organization)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockOrganizationsMockRecorder) Delete(ctx, organization any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockOrganizations)(nil).Delete), ctx, organization)
}
// DeleteDataRetentionPolicy mocks base method.
func (m *MockOrganizations) DeleteDataRetentionPolicy(ctx context.Context, organization string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteDataRetentionPolicy", ctx, organization)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteDataRetentionPolicy indicates an expected call of DeleteDataRetentionPolicy.
func (mr *MockOrganizationsMockRecorder) DeleteDataRetentionPolicy(ctx, organization any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDataRetentionPolicy", reflect.TypeOf((*MockOrganizations)(nil).DeleteDataRetentionPolicy), ctx, organization)
}
// List mocks base method.
func (m *MockOrganizations) List(ctx context.Context, options *tfe.OrganizationListOptions) (*tfe.OrganizationList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, options)
ret0, _ := ret[0].(*tfe.OrganizationList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockOrganizationsMockRecorder) List(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockOrganizations)(nil).List), ctx, options)
}
// Read mocks base method.
func (m *MockOrganizations) Read(ctx context.Context, organization string) (*tfe.Organization, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, organization)
ret0, _ := ret[0].(*tfe.Organization)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockOrganizationsMockRecorder) Read(ctx, organization any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockOrganizations)(nil).Read), ctx, organization)
}
// ReadCapacity mocks base method.
func (m *MockOrganizations) ReadCapacity(ctx context.Context, organization string) (*tfe.Capacity, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadCapacity", ctx, organization)
ret0, _ := ret[0].(*tfe.Capacity)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadCapacity indicates an expected call of ReadCapacity.
func (mr *MockOrganizationsMockRecorder) ReadCapacity(ctx, organization any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadCapacity", reflect.TypeOf((*MockOrganizations)(nil).ReadCapacity), ctx, organization)
}
// ReadDataRetentionPolicy mocks base method.
func (m *MockOrganizations) ReadDataRetentionPolicy(ctx context.Context, organization string) (*tfe.DataRetentionPolicy, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadDataRetentionPolicy", ctx, organization)
ret0, _ := ret[0].(*tfe.DataRetentionPolicy)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadDataRetentionPolicy indicates an expected call of ReadDataRetentionPolicy.
func (mr *MockOrganizationsMockRecorder) ReadDataRetentionPolicy(ctx, organization any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadDataRetentionPolicy", reflect.TypeOf((*MockOrganizations)(nil).ReadDataRetentionPolicy), ctx, organization)
}
// ReadDataRetentionPolicyChoice mocks base method.
func (m *MockOrganizations) ReadDataRetentionPolicyChoice(ctx context.Context, organization string) (*tfe.DataRetentionPolicyChoice, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadDataRetentionPolicyChoice", ctx, organization)
ret0, _ := ret[0].(*tfe.DataRetentionPolicyChoice)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadDataRetentionPolicyChoice indicates an expected call of ReadDataRetentionPolicyChoice.
func (mr *MockOrganizationsMockRecorder) ReadDataRetentionPolicyChoice(ctx, organization any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadDataRetentionPolicyChoice", reflect.TypeOf((*MockOrganizations)(nil).ReadDataRetentionPolicyChoice), ctx, organization)
}
// ReadEntitlements mocks base method.
func (m *MockOrganizations) ReadEntitlements(ctx context.Context, organization string) (*tfe.Entitlements, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadEntitlements", ctx, organization)
ret0, _ := ret[0].(*tfe.Entitlements)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadEntitlements indicates an expected call of ReadEntitlements.
func (mr *MockOrganizationsMockRecorder) ReadEntitlements(ctx, organization any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadEntitlements", reflect.TypeOf((*MockOrganizations)(nil).ReadEntitlements), ctx, organization)
}
// ReadRunQueue mocks base method.
func (m *MockOrganizations) ReadRunQueue(ctx context.Context, organization string, options tfe.ReadRunQueueOptions) (*tfe.RunQueue, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadRunQueue", ctx, organization, options)
ret0, _ := ret[0].(*tfe.RunQueue)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadRunQueue indicates an expected call of ReadRunQueue.
func (mr *MockOrganizationsMockRecorder) ReadRunQueue(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadRunQueue", reflect.TypeOf((*MockOrganizations)(nil).ReadRunQueue), ctx, organization, options)
}
// ReadWithOptions mocks base method.
func (m *MockOrganizations) ReadWithOptions(ctx context.Context, organization string, options tfe.OrganizationReadOptions) (*tfe.Organization, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadWithOptions", ctx, organization, options)
ret0, _ := ret[0].(*tfe.Organization)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadWithOptions indicates an expected call of ReadWithOptions.
func (mr *MockOrganizationsMockRecorder) ReadWithOptions(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadWithOptions", reflect.TypeOf((*MockOrganizations)(nil).ReadWithOptions), ctx, organization, options)
}
// SetDataRetentionPolicy mocks base method.
func (m *MockOrganizations) SetDataRetentionPolicy(ctx context.Context, organization string, options tfe.DataRetentionPolicySetOptions) (*tfe.DataRetentionPolicy, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetDataRetentionPolicy", ctx, organization, options)
ret0, _ := ret[0].(*tfe.DataRetentionPolicy)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SetDataRetentionPolicy indicates an expected call of SetDataRetentionPolicy.
func (mr *MockOrganizationsMockRecorder) SetDataRetentionPolicy(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDataRetentionPolicy", reflect.TypeOf((*MockOrganizations)(nil).SetDataRetentionPolicy), ctx, organization, options)
}
// SetDataRetentionPolicyDeleteOlder mocks base method.
func (m *MockOrganizations) SetDataRetentionPolicyDeleteOlder(ctx context.Context, organization string, options tfe.DataRetentionPolicyDeleteOlderSetOptions) (*tfe.DataRetentionPolicyDeleteOlder, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetDataRetentionPolicyDeleteOlder", ctx, organization, options)
ret0, _ := ret[0].(*tfe.DataRetentionPolicyDeleteOlder)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SetDataRetentionPolicyDeleteOlder indicates an expected call of SetDataRetentionPolicyDeleteOlder.
func (mr *MockOrganizationsMockRecorder) SetDataRetentionPolicyDeleteOlder(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDataRetentionPolicyDeleteOlder", reflect.TypeOf((*MockOrganizations)(nil).SetDataRetentionPolicyDeleteOlder), ctx, organization, options)
}
// SetDataRetentionPolicyDontDelete mocks base method.
func (m *MockOrganizations) SetDataRetentionPolicyDontDelete(ctx context.Context, organization string, options tfe.DataRetentionPolicyDontDeleteSetOptions) (*tfe.DataRetentionPolicyDontDelete, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetDataRetentionPolicyDontDelete", ctx, organization, options)
ret0, _ := ret[0].(*tfe.DataRetentionPolicyDontDelete)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SetDataRetentionPolicyDontDelete indicates an expected call of SetDataRetentionPolicyDontDelete.
func (mr *MockOrganizationsMockRecorder) SetDataRetentionPolicyDontDelete(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDataRetentionPolicyDontDelete", reflect.TypeOf((*MockOrganizations)(nil).SetDataRetentionPolicyDontDelete), ctx, organization, options)
}
// Update mocks base method.
func (m *MockOrganizations) Update(ctx context.Context, organization string, options tfe.OrganizationUpdateOptions) (*tfe.Organization, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, organization, options)
ret0, _ := ret[0].(*tfe.Organization)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockOrganizationsMockRecorder) Update(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockOrganizations)(nil).Update), ctx, organization, options)
}
================================================
FILE: mocks/organization_token_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: organization_token.go
//
// Generated by this command:
//
// mockgen -source=organization_token.go -destination=mocks/organization_token_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockOrganizationTokens is a mock of OrganizationTokens interface.
type MockOrganizationTokens struct {
ctrl *gomock.Controller
recorder *MockOrganizationTokensMockRecorder
}
// MockOrganizationTokensMockRecorder is the mock recorder for MockOrganizationTokens.
type MockOrganizationTokensMockRecorder struct {
mock *MockOrganizationTokens
}
// NewMockOrganizationTokens creates a new mock instance.
func NewMockOrganizationTokens(ctrl *gomock.Controller) *MockOrganizationTokens {
mock := &MockOrganizationTokens{ctrl: ctrl}
mock.recorder = &MockOrganizationTokensMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockOrganizationTokens) EXPECT() *MockOrganizationTokensMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockOrganizationTokens) Create(ctx context.Context, organization string) (*tfe.OrganizationToken, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, organization)
ret0, _ := ret[0].(*tfe.OrganizationToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockOrganizationTokensMockRecorder) Create(ctx, organization any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockOrganizationTokens)(nil).Create), ctx, organization)
}
// CreateWithOptions mocks base method.
func (m *MockOrganizationTokens) CreateWithOptions(ctx context.Context, organization string, options tfe.OrganizationTokenCreateOptions) (*tfe.OrganizationToken, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateWithOptions", ctx, organization, options)
ret0, _ := ret[0].(*tfe.OrganizationToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateWithOptions indicates an expected call of CreateWithOptions.
func (mr *MockOrganizationTokensMockRecorder) CreateWithOptions(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateWithOptions", reflect.TypeOf((*MockOrganizationTokens)(nil).CreateWithOptions), ctx, organization, options)
}
// Delete mocks base method.
func (m *MockOrganizationTokens) Delete(ctx context.Context, organization string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, organization)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockOrganizationTokensMockRecorder) Delete(ctx, organization any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockOrganizationTokens)(nil).Delete), ctx, organization)
}
// DeleteWithOptions mocks base method.
func (m *MockOrganizationTokens) DeleteWithOptions(ctx context.Context, organization string, options tfe.OrganizationTokenDeleteOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteWithOptions", ctx, organization, options)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteWithOptions indicates an expected call of DeleteWithOptions.
func (mr *MockOrganizationTokensMockRecorder) DeleteWithOptions(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWithOptions", reflect.TypeOf((*MockOrganizationTokens)(nil).DeleteWithOptions), ctx, organization, options)
}
// Read mocks base method.
func (m *MockOrganizationTokens) Read(ctx context.Context, organization string) (*tfe.OrganizationToken, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, organization)
ret0, _ := ret[0].(*tfe.OrganizationToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockOrganizationTokensMockRecorder) Read(ctx, organization any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockOrganizationTokens)(nil).Read), ctx, organization)
}
// ReadWithOptions mocks base method.
func (m *MockOrganizationTokens) ReadWithOptions(ctx context.Context, organization string, options tfe.OrganizationTokenReadOptions) (*tfe.OrganizationToken, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadWithOptions", ctx, organization, options)
ret0, _ := ret[0].(*tfe.OrganizationToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadWithOptions indicates an expected call of ReadWithOptions.
func (mr *MockOrganizationTokensMockRecorder) ReadWithOptions(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadWithOptions", reflect.TypeOf((*MockOrganizationTokens)(nil).ReadWithOptions), ctx, organization, options)
}
================================================
FILE: mocks/plan_export_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: plan_export.go
//
// Generated by this command:
//
// mockgen -source=plan_export.go -destination=mocks/plan_export_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockPlanExports is a mock of PlanExports interface.
type MockPlanExports struct {
ctrl *gomock.Controller
recorder *MockPlanExportsMockRecorder
}
// MockPlanExportsMockRecorder is the mock recorder for MockPlanExports.
type MockPlanExportsMockRecorder struct {
mock *MockPlanExports
}
// NewMockPlanExports creates a new mock instance.
func NewMockPlanExports(ctrl *gomock.Controller) *MockPlanExports {
mock := &MockPlanExports{ctrl: ctrl}
mock.recorder = &MockPlanExportsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPlanExports) EXPECT() *MockPlanExportsMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockPlanExports) Create(ctx context.Context, options tfe.PlanExportCreateOptions) (*tfe.PlanExport, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, options)
ret0, _ := ret[0].(*tfe.PlanExport)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockPlanExportsMockRecorder) Create(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockPlanExports)(nil).Create), ctx, options)
}
// Delete mocks base method.
func (m *MockPlanExports) Delete(ctx context.Context, planExportID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, planExportID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockPlanExportsMockRecorder) Delete(ctx, planExportID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockPlanExports)(nil).Delete), ctx, planExportID)
}
// Download mocks base method.
func (m *MockPlanExports) Download(ctx context.Context, planExportID string) ([]byte, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Download", ctx, planExportID)
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Download indicates an expected call of Download.
func (mr *MockPlanExportsMockRecorder) Download(ctx, planExportID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Download", reflect.TypeOf((*MockPlanExports)(nil).Download), ctx, planExportID)
}
// Read mocks base method.
func (m *MockPlanExports) Read(ctx context.Context, planExportID string) (*tfe.PlanExport, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, planExportID)
ret0, _ := ret[0].(*tfe.PlanExport)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockPlanExportsMockRecorder) Read(ctx, planExportID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockPlanExports)(nil).Read), ctx, planExportID)
}
================================================
FILE: mocks/plan_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: plan.go
//
// Generated by this command:
//
// mockgen -source=plan.go -destination=mocks/plan_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
io "io"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockPlans is a mock of Plans interface.
type MockPlans struct {
ctrl *gomock.Controller
recorder *MockPlansMockRecorder
}
// MockPlansMockRecorder is the mock recorder for MockPlans.
type MockPlansMockRecorder struct {
mock *MockPlans
}
// NewMockPlans creates a new mock instance.
func NewMockPlans(ctrl *gomock.Controller) *MockPlans {
mock := &MockPlans{ctrl: ctrl}
mock.recorder = &MockPlansMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPlans) EXPECT() *MockPlansMockRecorder {
return m.recorder
}
// Logs mocks base method.
func (m *MockPlans) Logs(ctx context.Context, planID string) (io.Reader, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Logs", ctx, planID)
ret0, _ := ret[0].(io.Reader)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Logs indicates an expected call of Logs.
func (mr *MockPlansMockRecorder) Logs(ctx, planID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logs", reflect.TypeOf((*MockPlans)(nil).Logs), ctx, planID)
}
// Read mocks base method.
func (m *MockPlans) Read(ctx context.Context, planID string) (*tfe.Plan, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, planID)
ret0, _ := ret[0].(*tfe.Plan)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockPlansMockRecorder) Read(ctx, planID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockPlans)(nil).Read), ctx, planID)
}
// ReadJSONOutput mocks base method.
func (m *MockPlans) ReadJSONOutput(ctx context.Context, planID string) ([]byte, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadJSONOutput", ctx, planID)
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadJSONOutput indicates an expected call of ReadJSONOutput.
func (mr *MockPlansMockRecorder) ReadJSONOutput(ctx, planID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadJSONOutput", reflect.TypeOf((*MockPlans)(nil).ReadJSONOutput), ctx, planID)
}
================================================
FILE: mocks/policy_check_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: policy_check.go
//
// Generated by this command:
//
// mockgen -source=policy_check.go -destination=mocks/policy_check_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
io "io"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockPolicyChecks is a mock of PolicyChecks interface.
type MockPolicyChecks struct {
ctrl *gomock.Controller
recorder *MockPolicyChecksMockRecorder
}
// MockPolicyChecksMockRecorder is the mock recorder for MockPolicyChecks.
type MockPolicyChecksMockRecorder struct {
mock *MockPolicyChecks
}
// NewMockPolicyChecks creates a new mock instance.
func NewMockPolicyChecks(ctrl *gomock.Controller) *MockPolicyChecks {
mock := &MockPolicyChecks{ctrl: ctrl}
mock.recorder = &MockPolicyChecksMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPolicyChecks) EXPECT() *MockPolicyChecksMockRecorder {
return m.recorder
}
// List mocks base method.
func (m *MockPolicyChecks) List(ctx context.Context, runID string, options *tfe.PolicyCheckListOptions) (*tfe.PolicyCheckList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, runID, options)
ret0, _ := ret[0].(*tfe.PolicyCheckList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockPolicyChecksMockRecorder) List(ctx, runID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockPolicyChecks)(nil).List), ctx, runID, options)
}
// Logs mocks base method.
func (m *MockPolicyChecks) Logs(ctx context.Context, policyCheckID string) (io.Reader, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Logs", ctx, policyCheckID)
ret0, _ := ret[0].(io.Reader)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Logs indicates an expected call of Logs.
func (mr *MockPolicyChecksMockRecorder) Logs(ctx, policyCheckID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logs", reflect.TypeOf((*MockPolicyChecks)(nil).Logs), ctx, policyCheckID)
}
// Override mocks base method.
func (m *MockPolicyChecks) Override(ctx context.Context, policyCheckID string) (*tfe.PolicyCheck, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Override", ctx, policyCheckID)
ret0, _ := ret[0].(*tfe.PolicyCheck)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Override indicates an expected call of Override.
func (mr *MockPolicyChecksMockRecorder) Override(ctx, policyCheckID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Override", reflect.TypeOf((*MockPolicyChecks)(nil).Override), ctx, policyCheckID)
}
// Read mocks base method.
func (m *MockPolicyChecks) Read(ctx context.Context, policyCheckID string) (*tfe.PolicyCheck, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, policyCheckID)
ret0, _ := ret[0].(*tfe.PolicyCheck)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockPolicyChecksMockRecorder) Read(ctx, policyCheckID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockPolicyChecks)(nil).Read), ctx, policyCheckID)
}
================================================
FILE: mocks/policy_evaluation.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: policy_evaluation.go
//
// Generated by this command:
//
// mockgen -source=policy_evaluation.go -destination=mocks/policy_evaluation.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockPolicyEvaluations is a mock of PolicyEvaluations interface.
type MockPolicyEvaluations struct {
ctrl *gomock.Controller
recorder *MockPolicyEvaluationsMockRecorder
}
// MockPolicyEvaluationsMockRecorder is the mock recorder for MockPolicyEvaluations.
type MockPolicyEvaluationsMockRecorder struct {
mock *MockPolicyEvaluations
}
// NewMockPolicyEvaluations creates a new mock instance.
func NewMockPolicyEvaluations(ctrl *gomock.Controller) *MockPolicyEvaluations {
mock := &MockPolicyEvaluations{ctrl: ctrl}
mock.recorder = &MockPolicyEvaluationsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPolicyEvaluations) EXPECT() *MockPolicyEvaluationsMockRecorder {
return m.recorder
}
// List mocks base method.
func (m *MockPolicyEvaluations) List(ctx context.Context, taskStageID string, options *tfe.PolicyEvaluationListOptions) (*tfe.PolicyEvaluationList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, taskStageID, options)
ret0, _ := ret[0].(*tfe.PolicyEvaluationList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockPolicyEvaluationsMockRecorder) List(ctx, taskStageID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockPolicyEvaluations)(nil).List), ctx, taskStageID, options)
}
// MockPolicySetOutcomes is a mock of PolicySetOutcomes interface.
type MockPolicySetOutcomes struct {
ctrl *gomock.Controller
recorder *MockPolicySetOutcomesMockRecorder
}
// MockPolicySetOutcomesMockRecorder is the mock recorder for MockPolicySetOutcomes.
type MockPolicySetOutcomesMockRecorder struct {
mock *MockPolicySetOutcomes
}
// NewMockPolicySetOutcomes creates a new mock instance.
func NewMockPolicySetOutcomes(ctrl *gomock.Controller) *MockPolicySetOutcomes {
mock := &MockPolicySetOutcomes{ctrl: ctrl}
mock.recorder = &MockPolicySetOutcomesMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPolicySetOutcomes) EXPECT() *MockPolicySetOutcomesMockRecorder {
return m.recorder
}
// List mocks base method.
func (m *MockPolicySetOutcomes) List(ctx context.Context, policyEvaluationID string, options *tfe.PolicySetOutcomeListOptions) (*tfe.PolicySetOutcomeList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, policyEvaluationID, options)
ret0, _ := ret[0].(*tfe.PolicySetOutcomeList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockPolicySetOutcomesMockRecorder) List(ctx, policyEvaluationID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockPolicySetOutcomes)(nil).List), ctx, policyEvaluationID, options)
}
// Read mocks base method.
func (m *MockPolicySetOutcomes) Read(ctx context.Context, policySetOutcomeID string) (*tfe.PolicySetOutcome, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, policySetOutcomeID)
ret0, _ := ret[0].(*tfe.PolicySetOutcome)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockPolicySetOutcomesMockRecorder) Read(ctx, policySetOutcomeID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockPolicySetOutcomes)(nil).Read), ctx, policySetOutcomeID)
}
================================================
FILE: mocks/policy_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: policy.go
//
// Generated by this command:
//
// mockgen -source=policy.go -destination=mocks/policy_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockPolicies is a mock of Policies interface.
type MockPolicies struct {
ctrl *gomock.Controller
recorder *MockPoliciesMockRecorder
}
// MockPoliciesMockRecorder is the mock recorder for MockPolicies.
type MockPoliciesMockRecorder struct {
mock *MockPolicies
}
// NewMockPolicies creates a new mock instance.
func NewMockPolicies(ctrl *gomock.Controller) *MockPolicies {
mock := &MockPolicies{ctrl: ctrl}
mock.recorder = &MockPoliciesMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPolicies) EXPECT() *MockPoliciesMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockPolicies) Create(ctx context.Context, organization string, options tfe.PolicyCreateOptions) (*tfe.Policy, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, organization, options)
ret0, _ := ret[0].(*tfe.Policy)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockPoliciesMockRecorder) Create(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockPolicies)(nil).Create), ctx, organization, options)
}
// Delete mocks base method.
func (m *MockPolicies) Delete(ctx context.Context, policyID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, policyID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockPoliciesMockRecorder) Delete(ctx, policyID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockPolicies)(nil).Delete), ctx, policyID)
}
// Download mocks base method.
func (m *MockPolicies) Download(ctx context.Context, policyID string) ([]byte, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Download", ctx, policyID)
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Download indicates an expected call of Download.
func (mr *MockPoliciesMockRecorder) Download(ctx, policyID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Download", reflect.TypeOf((*MockPolicies)(nil).Download), ctx, policyID)
}
// List mocks base method.
func (m *MockPolicies) List(ctx context.Context, organization string, options *tfe.PolicyListOptions) (*tfe.PolicyList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, organization, options)
ret0, _ := ret[0].(*tfe.PolicyList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockPoliciesMockRecorder) List(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockPolicies)(nil).List), ctx, organization, options)
}
// Read mocks base method.
func (m *MockPolicies) Read(ctx context.Context, policyID string) (*tfe.Policy, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, policyID)
ret0, _ := ret[0].(*tfe.Policy)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockPoliciesMockRecorder) Read(ctx, policyID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockPolicies)(nil).Read), ctx, policyID)
}
// Update mocks base method.
func (m *MockPolicies) Update(ctx context.Context, policyID string, options tfe.PolicyUpdateOptions) (*tfe.Policy, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, policyID, options)
ret0, _ := ret[0].(*tfe.Policy)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockPoliciesMockRecorder) Update(ctx, policyID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockPolicies)(nil).Update), ctx, policyID, options)
}
// Upload mocks base method.
func (m *MockPolicies) Upload(ctx context.Context, policyID string, content []byte) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Upload", ctx, policyID, content)
ret0, _ := ret[0].(error)
return ret0
}
// Upload indicates an expected call of Upload.
func (mr *MockPoliciesMockRecorder) Upload(ctx, policyID, content any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Upload", reflect.TypeOf((*MockPolicies)(nil).Upload), ctx, policyID, content)
}
================================================
FILE: mocks/policy_set_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: policy_set.go
//
// Generated by this command:
//
// mockgen -source=policy_set.go -destination=mocks/policy_set_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockPolicySets is a mock of PolicySets interface.
type MockPolicySets struct {
ctrl *gomock.Controller
recorder *MockPolicySetsMockRecorder
}
// MockPolicySetsMockRecorder is the mock recorder for MockPolicySets.
type MockPolicySetsMockRecorder struct {
mock *MockPolicySets
}
// NewMockPolicySets creates a new mock instance.
func NewMockPolicySets(ctrl *gomock.Controller) *MockPolicySets {
mock := &MockPolicySets{ctrl: ctrl}
mock.recorder = &MockPolicySetsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPolicySets) EXPECT() *MockPolicySetsMockRecorder {
return m.recorder
}
// AddPolicies mocks base method.
func (m *MockPolicySets) AddPolicies(ctx context.Context, policySetID string, options tfe.PolicySetAddPoliciesOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddPolicies", ctx, policySetID, options)
ret0, _ := ret[0].(error)
return ret0
}
// AddPolicies indicates an expected call of AddPolicies.
func (mr *MockPolicySetsMockRecorder) AddPolicies(ctx, policySetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPolicies", reflect.TypeOf((*MockPolicySets)(nil).AddPolicies), ctx, policySetID, options)
}
// AddProjectExclusions mocks base method.
func (m *MockPolicySets) AddProjectExclusions(ctx context.Context, policySetID string, options tfe.PolicySetAddProjectExclusionsOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddProjectExclusions", ctx, policySetID, options)
ret0, _ := ret[0].(error)
return ret0
}
// AddProjectExclusions indicates an expected call of AddProjectExclusions.
func (mr *MockPolicySetsMockRecorder) AddProjectExclusions(ctx, policySetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddProjectExclusions", reflect.TypeOf((*MockPolicySets)(nil).AddProjectExclusions), ctx, policySetID, options)
}
// AddProjects mocks base method.
func (m *MockPolicySets) AddProjects(ctx context.Context, policySetID string, options tfe.PolicySetAddProjectsOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddProjects", ctx, policySetID, options)
ret0, _ := ret[0].(error)
return ret0
}
// AddProjects indicates an expected call of AddProjects.
func (mr *MockPolicySetsMockRecorder) AddProjects(ctx, policySetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddProjects", reflect.TypeOf((*MockPolicySets)(nil).AddProjects), ctx, policySetID, options)
}
// AddWorkspaceExclusions mocks base method.
func (m *MockPolicySets) AddWorkspaceExclusions(ctx context.Context, policySetID string, options tfe.PolicySetAddWorkspaceExclusionsOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddWorkspaceExclusions", ctx, policySetID, options)
ret0, _ := ret[0].(error)
return ret0
}
// AddWorkspaceExclusions indicates an expected call of AddWorkspaceExclusions.
func (mr *MockPolicySetsMockRecorder) AddWorkspaceExclusions(ctx, policySetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddWorkspaceExclusions", reflect.TypeOf((*MockPolicySets)(nil).AddWorkspaceExclusions), ctx, policySetID, options)
}
// AddWorkspaces mocks base method.
func (m *MockPolicySets) AddWorkspaces(ctx context.Context, policySetID string, options tfe.PolicySetAddWorkspacesOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddWorkspaces", ctx, policySetID, options)
ret0, _ := ret[0].(error)
return ret0
}
// AddWorkspaces indicates an expected call of AddWorkspaces.
func (mr *MockPolicySetsMockRecorder) AddWorkspaces(ctx, policySetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddWorkspaces", reflect.TypeOf((*MockPolicySets)(nil).AddWorkspaces), ctx, policySetID, options)
}
// Create mocks base method.
func (m *MockPolicySets) Create(ctx context.Context, organization string, options tfe.PolicySetCreateOptions) (*tfe.PolicySet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, organization, options)
ret0, _ := ret[0].(*tfe.PolicySet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockPolicySetsMockRecorder) Create(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockPolicySets)(nil).Create), ctx, organization, options)
}
// Delete mocks base method.
func (m *MockPolicySets) Delete(ctx context.Context, policyID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, policyID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockPolicySetsMockRecorder) Delete(ctx, policyID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockPolicySets)(nil).Delete), ctx, policyID)
}
// List mocks base method.
func (m *MockPolicySets) List(ctx context.Context, organization string, options *tfe.PolicySetListOptions) (*tfe.PolicySetList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, organization, options)
ret0, _ := ret[0].(*tfe.PolicySetList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockPolicySetsMockRecorder) List(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockPolicySets)(nil).List), ctx, organization, options)
}
// Read mocks base method.
func (m *MockPolicySets) Read(ctx context.Context, policySetID string) (*tfe.PolicySet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, policySetID)
ret0, _ := ret[0].(*tfe.PolicySet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockPolicySetsMockRecorder) Read(ctx, policySetID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockPolicySets)(nil).Read), ctx, policySetID)
}
// ReadWithOptions mocks base method.
func (m *MockPolicySets) ReadWithOptions(ctx context.Context, policySetID string, options *tfe.PolicySetReadOptions) (*tfe.PolicySet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadWithOptions", ctx, policySetID, options)
ret0, _ := ret[0].(*tfe.PolicySet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadWithOptions indicates an expected call of ReadWithOptions.
func (mr *MockPolicySetsMockRecorder) ReadWithOptions(ctx, policySetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadWithOptions", reflect.TypeOf((*MockPolicySets)(nil).ReadWithOptions), ctx, policySetID, options)
}
// RemovePolicies mocks base method.
func (m *MockPolicySets) RemovePolicies(ctx context.Context, policySetID string, options tfe.PolicySetRemovePoliciesOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemovePolicies", ctx, policySetID, options)
ret0, _ := ret[0].(error)
return ret0
}
// RemovePolicies indicates an expected call of RemovePolicies.
func (mr *MockPolicySetsMockRecorder) RemovePolicies(ctx, policySetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemovePolicies", reflect.TypeOf((*MockPolicySets)(nil).RemovePolicies), ctx, policySetID, options)
}
// RemoveProjectExclusions mocks base method.
func (m *MockPolicySets) RemoveProjectExclusions(ctx context.Context, policySetID string, options tfe.PolicySetRemoveProjectExclusionsOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveProjectExclusions", ctx, policySetID, options)
ret0, _ := ret[0].(error)
return ret0
}
// RemoveProjectExclusions indicates an expected call of RemoveProjectExclusions.
func (mr *MockPolicySetsMockRecorder) RemoveProjectExclusions(ctx, policySetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveProjectExclusions", reflect.TypeOf((*MockPolicySets)(nil).RemoveProjectExclusions), ctx, policySetID, options)
}
// RemoveProjects mocks base method.
func (m *MockPolicySets) RemoveProjects(ctx context.Context, policySetID string, options tfe.PolicySetRemoveProjectsOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveProjects", ctx, policySetID, options)
ret0, _ := ret[0].(error)
return ret0
}
// RemoveProjects indicates an expected call of RemoveProjects.
func (mr *MockPolicySetsMockRecorder) RemoveProjects(ctx, policySetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveProjects", reflect.TypeOf((*MockPolicySets)(nil).RemoveProjects), ctx, policySetID, options)
}
// RemoveWorkspaceExclusions mocks base method.
func (m *MockPolicySets) RemoveWorkspaceExclusions(ctx context.Context, policySetID string, options tfe.PolicySetRemoveWorkspaceExclusionsOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveWorkspaceExclusions", ctx, policySetID, options)
ret0, _ := ret[0].(error)
return ret0
}
// RemoveWorkspaceExclusions indicates an expected call of RemoveWorkspaceExclusions.
func (mr *MockPolicySetsMockRecorder) RemoveWorkspaceExclusions(ctx, policySetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveWorkspaceExclusions", reflect.TypeOf((*MockPolicySets)(nil).RemoveWorkspaceExclusions), ctx, policySetID, options)
}
// RemoveWorkspaces mocks base method.
func (m *MockPolicySets) RemoveWorkspaces(ctx context.Context, policySetID string, options tfe.PolicySetRemoveWorkspacesOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveWorkspaces", ctx, policySetID, options)
ret0, _ := ret[0].(error)
return ret0
}
// RemoveWorkspaces indicates an expected call of RemoveWorkspaces.
func (mr *MockPolicySetsMockRecorder) RemoveWorkspaces(ctx, policySetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveWorkspaces", reflect.TypeOf((*MockPolicySets)(nil).RemoveWorkspaces), ctx, policySetID, options)
}
// Update mocks base method.
func (m *MockPolicySets) Update(ctx context.Context, policySetID string, options tfe.PolicySetUpdateOptions) (*tfe.PolicySet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, policySetID, options)
ret0, _ := ret[0].(*tfe.PolicySet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockPolicySetsMockRecorder) Update(ctx, policySetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockPolicySets)(nil).Update), ctx, policySetID, options)
}
================================================
FILE: mocks/policy_set_parameter_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: policy_set_parameter.go
//
// Generated by this command:
//
// mockgen -source=policy_set_parameter.go -destination=mocks/policy_set_parameter_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockPolicySetParameters is a mock of PolicySetParameters interface.
type MockPolicySetParameters struct {
ctrl *gomock.Controller
recorder *MockPolicySetParametersMockRecorder
}
// MockPolicySetParametersMockRecorder is the mock recorder for MockPolicySetParameters.
type MockPolicySetParametersMockRecorder struct {
mock *MockPolicySetParameters
}
// NewMockPolicySetParameters creates a new mock instance.
func NewMockPolicySetParameters(ctrl *gomock.Controller) *MockPolicySetParameters {
mock := &MockPolicySetParameters{ctrl: ctrl}
mock.recorder = &MockPolicySetParametersMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPolicySetParameters) EXPECT() *MockPolicySetParametersMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockPolicySetParameters) Create(ctx context.Context, policySetID string, options tfe.PolicySetParameterCreateOptions) (*tfe.PolicySetParameter, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, policySetID, options)
ret0, _ := ret[0].(*tfe.PolicySetParameter)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockPolicySetParametersMockRecorder) Create(ctx, policySetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockPolicySetParameters)(nil).Create), ctx, policySetID, options)
}
// Delete mocks base method.
func (m *MockPolicySetParameters) Delete(ctx context.Context, policySetID, parameterID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, policySetID, parameterID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockPolicySetParametersMockRecorder) Delete(ctx, policySetID, parameterID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockPolicySetParameters)(nil).Delete), ctx, policySetID, parameterID)
}
// List mocks base method.
func (m *MockPolicySetParameters) List(ctx context.Context, policySetID string, options *tfe.PolicySetParameterListOptions) (*tfe.PolicySetParameterList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, policySetID, options)
ret0, _ := ret[0].(*tfe.PolicySetParameterList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockPolicySetParametersMockRecorder) List(ctx, policySetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockPolicySetParameters)(nil).List), ctx, policySetID, options)
}
// Read mocks base method.
func (m *MockPolicySetParameters) Read(ctx context.Context, policySetID, parameterID string) (*tfe.PolicySetParameter, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, policySetID, parameterID)
ret0, _ := ret[0].(*tfe.PolicySetParameter)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockPolicySetParametersMockRecorder) Read(ctx, policySetID, parameterID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockPolicySetParameters)(nil).Read), ctx, policySetID, parameterID)
}
// Update mocks base method.
func (m *MockPolicySetParameters) Update(ctx context.Context, policySetID, parameterID string, options tfe.PolicySetParameterUpdateOptions) (*tfe.PolicySetParameter, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, policySetID, parameterID, options)
ret0, _ := ret[0].(*tfe.PolicySetParameter)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockPolicySetParametersMockRecorder) Update(ctx, policySetID, parameterID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockPolicySetParameters)(nil).Update), ctx, policySetID, parameterID, options)
}
================================================
FILE: mocks/policy_set_version_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: policy_set_version.go
//
// Generated by this command:
//
// mockgen -source=policy_set_version.go -destination=mocks/policy_set_version_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockPolicySetVersions is a mock of PolicySetVersions interface.
type MockPolicySetVersions struct {
ctrl *gomock.Controller
recorder *MockPolicySetVersionsMockRecorder
}
// MockPolicySetVersionsMockRecorder is the mock recorder for MockPolicySetVersions.
type MockPolicySetVersionsMockRecorder struct {
mock *MockPolicySetVersions
}
// NewMockPolicySetVersions creates a new mock instance.
func NewMockPolicySetVersions(ctrl *gomock.Controller) *MockPolicySetVersions {
mock := &MockPolicySetVersions{ctrl: ctrl}
mock.recorder = &MockPolicySetVersionsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPolicySetVersions) EXPECT() *MockPolicySetVersionsMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockPolicySetVersions) Create(ctx context.Context, policySetID string) (*tfe.PolicySetVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, policySetID)
ret0, _ := ret[0].(*tfe.PolicySetVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockPolicySetVersionsMockRecorder) Create(ctx, policySetID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockPolicySetVersions)(nil).Create), ctx, policySetID)
}
// Read mocks base method.
func (m *MockPolicySetVersions) Read(ctx context.Context, policySetVersionID string) (*tfe.PolicySetVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, policySetVersionID)
ret0, _ := ret[0].(*tfe.PolicySetVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockPolicySetVersionsMockRecorder) Read(ctx, policySetVersionID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockPolicySetVersions)(nil).Read), ctx, policySetVersionID)
}
// Upload mocks base method.
func (m *MockPolicySetVersions) Upload(ctx context.Context, psv tfe.PolicySetVersion, path string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Upload", ctx, psv, path)
ret0, _ := ret[0].(error)
return ret0
}
// Upload indicates an expected call of Upload.
func (mr *MockPolicySetVersionsMockRecorder) Upload(ctx, psv, path any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Upload", reflect.TypeOf((*MockPolicySetVersions)(nil).Upload), ctx, psv, path)
}
================================================
FILE: mocks/project_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: project.go
//
// Generated by this command:
//
// mockgen -source=project.go -destination=mocks/project_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockProjects is a mock of Projects interface.
type MockProjects struct {
ctrl *gomock.Controller
recorder *MockProjectsMockRecorder
}
// MockProjectsMockRecorder is the mock recorder for MockProjects.
type MockProjectsMockRecorder struct {
mock *MockProjects
}
// NewMockProjects creates a new mock instance.
func NewMockProjects(ctrl *gomock.Controller) *MockProjects {
mock := &MockProjects{ctrl: ctrl}
mock.recorder = &MockProjectsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockProjects) EXPECT() *MockProjectsMockRecorder {
return m.recorder
}
// AddTagBindings mocks base method.
func (m *MockProjects) AddTagBindings(ctx context.Context, projectID string, options tfe.ProjectAddTagBindingsOptions) ([]*tfe.TagBinding, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddTagBindings", ctx, projectID, options)
ret0, _ := ret[0].([]*tfe.TagBinding)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AddTagBindings indicates an expected call of AddTagBindings.
func (mr *MockProjectsMockRecorder) AddTagBindings(ctx, projectID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddTagBindings", reflect.TypeOf((*MockProjects)(nil).AddTagBindings), ctx, projectID, options)
}
// Create mocks base method.
func (m *MockProjects) Create(ctx context.Context, organization string, options tfe.ProjectCreateOptions) (*tfe.Project, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, organization, options)
ret0, _ := ret[0].(*tfe.Project)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockProjectsMockRecorder) Create(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockProjects)(nil).Create), ctx, organization, options)
}
// Delete mocks base method.
func (m *MockProjects) Delete(ctx context.Context, projectID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, projectID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockProjectsMockRecorder) Delete(ctx, projectID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockProjects)(nil).Delete), ctx, projectID)
}
// DeleteAllTagBindings mocks base method.
func (m *MockProjects) DeleteAllTagBindings(ctx context.Context, projectID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteAllTagBindings", ctx, projectID)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteAllTagBindings indicates an expected call of DeleteAllTagBindings.
func (mr *MockProjectsMockRecorder) DeleteAllTagBindings(ctx, projectID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAllTagBindings", reflect.TypeOf((*MockProjects)(nil).DeleteAllTagBindings), ctx, projectID)
}
// List mocks base method.
func (m *MockProjects) List(ctx context.Context, organization string, options *tfe.ProjectListOptions) (*tfe.ProjectList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, organization, options)
ret0, _ := ret[0].(*tfe.ProjectList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockProjectsMockRecorder) List(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockProjects)(nil).List), ctx, organization, options)
}
// ListEffectiveTagBindings mocks base method.
func (m *MockProjects) ListEffectiveTagBindings(ctx context.Context, workspaceID string) ([]*tfe.EffectiveTagBinding, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListEffectiveTagBindings", ctx, workspaceID)
ret0, _ := ret[0].([]*tfe.EffectiveTagBinding)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListEffectiveTagBindings indicates an expected call of ListEffectiveTagBindings.
func (mr *MockProjectsMockRecorder) ListEffectiveTagBindings(ctx, workspaceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListEffectiveTagBindings", reflect.TypeOf((*MockProjects)(nil).ListEffectiveTagBindings), ctx, workspaceID)
}
// ListTagBindings mocks base method.
func (m *MockProjects) ListTagBindings(ctx context.Context, projectID string) ([]*tfe.TagBinding, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListTagBindings", ctx, projectID)
ret0, _ := ret[0].([]*tfe.TagBinding)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListTagBindings indicates an expected call of ListTagBindings.
func (mr *MockProjectsMockRecorder) ListTagBindings(ctx, projectID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTagBindings", reflect.TypeOf((*MockProjects)(nil).ListTagBindings), ctx, projectID)
}
// Read mocks base method.
func (m *MockProjects) Read(ctx context.Context, projectID string) (*tfe.Project, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, projectID)
ret0, _ := ret[0].(*tfe.Project)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockProjectsMockRecorder) Read(ctx, projectID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockProjects)(nil).Read), ctx, projectID)
}
// ReadWithOptions mocks base method.
func (m *MockProjects) ReadWithOptions(ctx context.Context, projectID string, options tfe.ProjectReadOptions) (*tfe.Project, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadWithOptions", ctx, projectID, options)
ret0, _ := ret[0].(*tfe.Project)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadWithOptions indicates an expected call of ReadWithOptions.
func (mr *MockProjectsMockRecorder) ReadWithOptions(ctx, projectID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadWithOptions", reflect.TypeOf((*MockProjects)(nil).ReadWithOptions), ctx, projectID, options)
}
// Update mocks base method.
func (m *MockProjects) Update(ctx context.Context, projectID string, options tfe.ProjectUpdateOptions) (*tfe.Project, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, projectID, options)
ret0, _ := ret[0].(*tfe.Project)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockProjectsMockRecorder) Update(ctx, projectID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockProjects)(nil).Update), ctx, projectID, options)
}
================================================
FILE: mocks/query_runs_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: query_runs.go
//
// Generated by this command:
//
// mockgen -source=query_runs.go -destination=mocks/query_runs_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
io "io"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockQueryRuns is a mock of QueryRuns interface.
type MockQueryRuns struct {
ctrl *gomock.Controller
recorder *MockQueryRunsMockRecorder
}
// MockQueryRunsMockRecorder is the mock recorder for MockQueryRuns.
type MockQueryRunsMockRecorder struct {
mock *MockQueryRuns
}
// NewMockQueryRuns creates a new mock instance.
func NewMockQueryRuns(ctrl *gomock.Controller) *MockQueryRuns {
mock := &MockQueryRuns{ctrl: ctrl}
mock.recorder = &MockQueryRunsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockQueryRuns) EXPECT() *MockQueryRunsMockRecorder {
return m.recorder
}
// Cancel mocks base method.
func (m *MockQueryRuns) Cancel(ctx context.Context, runID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Cancel", ctx, runID)
ret0, _ := ret[0].(error)
return ret0
}
// Cancel indicates an expected call of Cancel.
func (mr *MockQueryRunsMockRecorder) Cancel(ctx, runID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cancel", reflect.TypeOf((*MockQueryRuns)(nil).Cancel), ctx, runID)
}
// Create mocks base method.
func (m *MockQueryRuns) Create(ctx context.Context, options tfe.QueryRunCreateOptions) (*tfe.QueryRun, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, options)
ret0, _ := ret[0].(*tfe.QueryRun)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockQueryRunsMockRecorder) Create(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockQueryRuns)(nil).Create), ctx, options)
}
// ForceCancel mocks base method.
func (m *MockQueryRuns) ForceCancel(ctx context.Context, runID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ForceCancel", ctx, runID)
ret0, _ := ret[0].(error)
return ret0
}
// ForceCancel indicates an expected call of ForceCancel.
func (mr *MockQueryRunsMockRecorder) ForceCancel(ctx, runID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ForceCancel", reflect.TypeOf((*MockQueryRuns)(nil).ForceCancel), ctx, runID)
}
// List mocks base method.
func (m *MockQueryRuns) List(ctx context.Context, workspaceID string, options *tfe.QueryRunListOptions) (*tfe.QueryRunList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.QueryRunList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockQueryRunsMockRecorder) List(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockQueryRuns)(nil).List), ctx, workspaceID, options)
}
// Logs mocks base method.
func (m *MockQueryRuns) Logs(ctx context.Context, queryRunID string) (io.Reader, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Logs", ctx, queryRunID)
ret0, _ := ret[0].(io.Reader)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Logs indicates an expected call of Logs.
func (mr *MockQueryRunsMockRecorder) Logs(ctx, queryRunID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logs", reflect.TypeOf((*MockQueryRuns)(nil).Logs), ctx, queryRunID)
}
// Read mocks base method.
func (m *MockQueryRuns) Read(ctx context.Context, queryRunID string) (*tfe.QueryRun, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, queryRunID)
ret0, _ := ret[0].(*tfe.QueryRun)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockQueryRunsMockRecorder) Read(ctx, queryRunID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockQueryRuns)(nil).Read), ctx, queryRunID)
}
// ReadWithOptions mocks base method.
func (m *MockQueryRuns) ReadWithOptions(ctx context.Context, queryRunID string, options *tfe.QueryRunReadOptions) (*tfe.QueryRun, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadWithOptions", ctx, queryRunID, options)
ret0, _ := ret[0].(*tfe.QueryRun)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadWithOptions indicates an expected call of ReadWithOptions.
func (mr *MockQueryRunsMockRecorder) ReadWithOptions(ctx, queryRunID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadWithOptions", reflect.TypeOf((*MockQueryRuns)(nil).ReadWithOptions), ctx, queryRunID, options)
}
================================================
FILE: mocks/registry_module_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: registry_module.go
//
// Generated by this command:
//
// mockgen -source=registry_module.go -destination=mocks/registry_module_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
io "io"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockRegistryModules is a mock of RegistryModules interface.
type MockRegistryModules struct {
ctrl *gomock.Controller
recorder *MockRegistryModulesMockRecorder
}
// MockRegistryModulesMockRecorder is the mock recorder for MockRegistryModules.
type MockRegistryModulesMockRecorder struct {
mock *MockRegistryModules
}
// NewMockRegistryModules creates a new mock instance.
func NewMockRegistryModules(ctrl *gomock.Controller) *MockRegistryModules {
mock := &MockRegistryModules{ctrl: ctrl}
mock.recorder = &MockRegistryModulesMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockRegistryModules) EXPECT() *MockRegistryModulesMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockRegistryModules) Create(ctx context.Context, organization string, options tfe.RegistryModuleCreateOptions) (*tfe.RegistryModule, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, organization, options)
ret0, _ := ret[0].(*tfe.RegistryModule)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockRegistryModulesMockRecorder) Create(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRegistryModules)(nil).Create), ctx, organization, options)
}
// CreateVersion mocks base method.
func (m *MockRegistryModules) CreateVersion(ctx context.Context, moduleID tfe.RegistryModuleID, options tfe.RegistryModuleCreateVersionOptions) (*tfe.RegistryModuleVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateVersion", ctx, moduleID, options)
ret0, _ := ret[0].(*tfe.RegistryModuleVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateVersion indicates an expected call of CreateVersion.
func (mr *MockRegistryModulesMockRecorder) CreateVersion(ctx, moduleID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateVersion", reflect.TypeOf((*MockRegistryModules)(nil).CreateVersion), ctx, moduleID, options)
}
// CreateWithVCSConnection mocks base method.
func (m *MockRegistryModules) CreateWithVCSConnection(ctx context.Context, options tfe.RegistryModuleCreateWithVCSConnectionOptions) (*tfe.RegistryModule, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateWithVCSConnection", ctx, options)
ret0, _ := ret[0].(*tfe.RegistryModule)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateWithVCSConnection indicates an expected call of CreateWithVCSConnection.
func (mr *MockRegistryModulesMockRecorder) CreateWithVCSConnection(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateWithVCSConnection", reflect.TypeOf((*MockRegistryModules)(nil).CreateWithVCSConnection), ctx, options)
}
// Delete mocks base method.
func (m *MockRegistryModules) Delete(ctx context.Context, organization, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, organization, name)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockRegistryModulesMockRecorder) Delete(ctx, organization, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRegistryModules)(nil).Delete), ctx, organization, name)
}
// DeleteByName mocks base method.
func (m *MockRegistryModules) DeleteByName(ctx context.Context, module tfe.RegistryModuleID) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteByName", ctx, module)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteByName indicates an expected call of DeleteByName.
func (mr *MockRegistryModulesMockRecorder) DeleteByName(ctx, module any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteByName", reflect.TypeOf((*MockRegistryModules)(nil).DeleteByName), ctx, module)
}
// DeleteProvider mocks base method.
func (m *MockRegistryModules) DeleteProvider(ctx context.Context, moduleID tfe.RegistryModuleID) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteProvider", ctx, moduleID)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteProvider indicates an expected call of DeleteProvider.
func (mr *MockRegistryModulesMockRecorder) DeleteProvider(ctx, moduleID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteProvider", reflect.TypeOf((*MockRegistryModules)(nil).DeleteProvider), ctx, moduleID)
}
// DeleteVersion mocks base method.
func (m *MockRegistryModules) DeleteVersion(ctx context.Context, moduleID tfe.RegistryModuleID, version string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteVersion", ctx, moduleID, version)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteVersion indicates an expected call of DeleteVersion.
func (mr *MockRegistryModulesMockRecorder) DeleteVersion(ctx, moduleID, version any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteVersion", reflect.TypeOf((*MockRegistryModules)(nil).DeleteVersion), ctx, moduleID, version)
}
// List mocks base method.
func (m *MockRegistryModules) List(ctx context.Context, organization string, options *tfe.RegistryModuleListOptions) (*tfe.RegistryModuleList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, organization, options)
ret0, _ := ret[0].(*tfe.RegistryModuleList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockRegistryModulesMockRecorder) List(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockRegistryModules)(nil).List), ctx, organization, options)
}
// ListCommits mocks base method.
func (m *MockRegistryModules) ListCommits(ctx context.Context, moduleID tfe.RegistryModuleID) (*tfe.CommitList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListCommits", ctx, moduleID)
ret0, _ := ret[0].(*tfe.CommitList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListCommits indicates an expected call of ListCommits.
func (mr *MockRegistryModulesMockRecorder) ListCommits(ctx, moduleID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCommits", reflect.TypeOf((*MockRegistryModules)(nil).ListCommits), ctx, moduleID)
}
// Read mocks base method.
func (m *MockRegistryModules) Read(ctx context.Context, moduleID tfe.RegistryModuleID) (*tfe.RegistryModule, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, moduleID)
ret0, _ := ret[0].(*tfe.RegistryModule)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockRegistryModulesMockRecorder) Read(ctx, moduleID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockRegistryModules)(nil).Read), ctx, moduleID)
}
// ReadTerraformRegistryModule mocks base method.
func (m *MockRegistryModules) ReadTerraformRegistryModule(ctx context.Context, moduleID tfe.RegistryModuleID, version string) (*tfe.TerraformRegistryModule, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadTerraformRegistryModule", ctx, moduleID, version)
ret0, _ := ret[0].(*tfe.TerraformRegistryModule)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadTerraformRegistryModule indicates an expected call of ReadTerraformRegistryModule.
func (mr *MockRegistryModulesMockRecorder) ReadTerraformRegistryModule(ctx, moduleID, version any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadTerraformRegistryModule", reflect.TypeOf((*MockRegistryModules)(nil).ReadTerraformRegistryModule), ctx, moduleID, version)
}
// ReadVersion mocks base method.
func (m *MockRegistryModules) ReadVersion(ctx context.Context, moduleID tfe.RegistryModuleID, version string) (*tfe.RegistryModuleVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadVersion", ctx, moduleID, version)
ret0, _ := ret[0].(*tfe.RegistryModuleVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadVersion indicates an expected call of ReadVersion.
func (mr *MockRegistryModulesMockRecorder) ReadVersion(ctx, moduleID, version any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadVersion", reflect.TypeOf((*MockRegistryModules)(nil).ReadVersion), ctx, moduleID, version)
}
// Update mocks base method.
func (m *MockRegistryModules) Update(ctx context.Context, moduleID tfe.RegistryModuleID, options tfe.RegistryModuleUpdateOptions) (*tfe.RegistryModule, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, moduleID, options)
ret0, _ := ret[0].(*tfe.RegistryModule)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockRegistryModulesMockRecorder) Update(ctx, moduleID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRegistryModules)(nil).Update), ctx, moduleID, options)
}
// Upload mocks base method.
func (m *MockRegistryModules) Upload(ctx context.Context, rmv tfe.RegistryModuleVersion, path string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Upload", ctx, rmv, path)
ret0, _ := ret[0].(error)
return ret0
}
// Upload indicates an expected call of Upload.
func (mr *MockRegistryModulesMockRecorder) Upload(ctx, rmv, path any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Upload", reflect.TypeOf((*MockRegistryModules)(nil).Upload), ctx, rmv, path)
}
// UploadTarGzip mocks base method.
func (m *MockRegistryModules) UploadTarGzip(ctx context.Context, url string, r io.Reader) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UploadTarGzip", ctx, url, r)
ret0, _ := ret[0].(error)
return ret0
}
// UploadTarGzip indicates an expected call of UploadTarGzip.
func (mr *MockRegistryModulesMockRecorder) UploadTarGzip(ctx, url, r any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadTarGzip", reflect.TypeOf((*MockRegistryModules)(nil).UploadTarGzip), ctx, url, r)
}
================================================
FILE: mocks/registry_no_code_module_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: registry_no_code_module.go
//
// Generated by this command:
//
// mockgen -source=registry_no_code_module.go -destination=mocks/registry_no_code_module_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockRegistryNoCodeModules is a mock of RegistryNoCodeModules interface.
type MockRegistryNoCodeModules struct {
ctrl *gomock.Controller
recorder *MockRegistryNoCodeModulesMockRecorder
}
// MockRegistryNoCodeModulesMockRecorder is the mock recorder for MockRegistryNoCodeModules.
type MockRegistryNoCodeModulesMockRecorder struct {
mock *MockRegistryNoCodeModules
}
// NewMockRegistryNoCodeModules creates a new mock instance.
func NewMockRegistryNoCodeModules(ctrl *gomock.Controller) *MockRegistryNoCodeModules {
mock := &MockRegistryNoCodeModules{ctrl: ctrl}
mock.recorder = &MockRegistryNoCodeModulesMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockRegistryNoCodeModules) EXPECT() *MockRegistryNoCodeModulesMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockRegistryNoCodeModules) Create(ctx context.Context, organization string, options tfe.RegistryNoCodeModuleCreateOptions) (*tfe.RegistryNoCodeModule, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, organization, options)
ret0, _ := ret[0].(*tfe.RegistryNoCodeModule)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockRegistryNoCodeModulesMockRecorder) Create(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRegistryNoCodeModules)(nil).Create), ctx, organization, options)
}
// CreateWorkspace mocks base method.
func (m *MockRegistryNoCodeModules) CreateWorkspace(ctx context.Context, noCodeModuleID string, options *tfe.RegistryNoCodeModuleCreateWorkspaceOptions) (*tfe.Workspace, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateWorkspace", ctx, noCodeModuleID, options)
ret0, _ := ret[0].(*tfe.Workspace)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateWorkspace indicates an expected call of CreateWorkspace.
func (mr *MockRegistryNoCodeModulesMockRecorder) CreateWorkspace(ctx, noCodeModuleID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateWorkspace", reflect.TypeOf((*MockRegistryNoCodeModules)(nil).CreateWorkspace), ctx, noCodeModuleID, options)
}
// Delete mocks base method.
func (m *MockRegistryNoCodeModules) Delete(ctx context.Context, ID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, ID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockRegistryNoCodeModulesMockRecorder) Delete(ctx, ID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRegistryNoCodeModules)(nil).Delete), ctx, ID)
}
// Read mocks base method.
func (m *MockRegistryNoCodeModules) Read(ctx context.Context, noCodeModuleID string, options *tfe.RegistryNoCodeModuleReadOptions) (*tfe.RegistryNoCodeModule, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, noCodeModuleID, options)
ret0, _ := ret[0].(*tfe.RegistryNoCodeModule)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockRegistryNoCodeModulesMockRecorder) Read(ctx, noCodeModuleID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockRegistryNoCodeModules)(nil).Read), ctx, noCodeModuleID, options)
}
// ReadVariables mocks base method.
func (m *MockRegistryNoCodeModules) ReadVariables(ctx context.Context, noCodeModuleID, noCodeModuleVersion string, options *tfe.RegistryNoCodeModuleReadVariablesOptions) (*tfe.RegistryModuleVariableList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadVariables", ctx, noCodeModuleID, noCodeModuleVersion, options)
ret0, _ := ret[0].(*tfe.RegistryModuleVariableList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadVariables indicates an expected call of ReadVariables.
func (mr *MockRegistryNoCodeModulesMockRecorder) ReadVariables(ctx, noCodeModuleID, noCodeModuleVersion, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadVariables", reflect.TypeOf((*MockRegistryNoCodeModules)(nil).ReadVariables), ctx, noCodeModuleID, noCodeModuleVersion, options)
}
// Update mocks base method.
func (m *MockRegistryNoCodeModules) Update(ctx context.Context, noCodeModuleID string, options tfe.RegistryNoCodeModuleUpdateOptions) (*tfe.RegistryNoCodeModule, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, noCodeModuleID, options)
ret0, _ := ret[0].(*tfe.RegistryNoCodeModule)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockRegistryNoCodeModulesMockRecorder) Update(ctx, noCodeModuleID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRegistryNoCodeModules)(nil).Update), ctx, noCodeModuleID, options)
}
// UpgradeWorkspace mocks base method.
func (m *MockRegistryNoCodeModules) UpgradeWorkspace(ctx context.Context, noCodeModuleID, workspaceID string, options *tfe.RegistryNoCodeModuleUpgradeWorkspaceOptions) (*tfe.WorkspaceUpgrade, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpgradeWorkspace", ctx, noCodeModuleID, workspaceID, options)
ret0, _ := ret[0].(*tfe.WorkspaceUpgrade)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpgradeWorkspace indicates an expected call of UpgradeWorkspace.
func (mr *MockRegistryNoCodeModulesMockRecorder) UpgradeWorkspace(ctx, noCodeModuleID, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpgradeWorkspace", reflect.TypeOf((*MockRegistryNoCodeModules)(nil).UpgradeWorkspace), ctx, noCodeModuleID, workspaceID, options)
}
================================================
FILE: mocks/registry_provider_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: registry_provider.go
//
// Generated by this command:
//
// mockgen -source=registry_provider.go -destination=mocks/registry_provider_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockRegistryProviders is a mock of RegistryProviders interface.
type MockRegistryProviders struct {
ctrl *gomock.Controller
recorder *MockRegistryProvidersMockRecorder
}
// MockRegistryProvidersMockRecorder is the mock recorder for MockRegistryProviders.
type MockRegistryProvidersMockRecorder struct {
mock *MockRegistryProviders
}
// NewMockRegistryProviders creates a new mock instance.
func NewMockRegistryProviders(ctrl *gomock.Controller) *MockRegistryProviders {
mock := &MockRegistryProviders{ctrl: ctrl}
mock.recorder = &MockRegistryProvidersMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockRegistryProviders) EXPECT() *MockRegistryProvidersMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockRegistryProviders) Create(ctx context.Context, organization string, options tfe.RegistryProviderCreateOptions) (*tfe.RegistryProvider, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, organization, options)
ret0, _ := ret[0].(*tfe.RegistryProvider)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockRegistryProvidersMockRecorder) Create(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRegistryProviders)(nil).Create), ctx, organization, options)
}
// Delete mocks base method.
func (m *MockRegistryProviders) Delete(ctx context.Context, providerID tfe.RegistryProviderID) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, providerID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockRegistryProvidersMockRecorder) Delete(ctx, providerID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRegistryProviders)(nil).Delete), ctx, providerID)
}
// List mocks base method.
func (m *MockRegistryProviders) List(ctx context.Context, organization string, options *tfe.RegistryProviderListOptions) (*tfe.RegistryProviderList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, organization, options)
ret0, _ := ret[0].(*tfe.RegistryProviderList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockRegistryProvidersMockRecorder) List(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockRegistryProviders)(nil).List), ctx, organization, options)
}
// Read mocks base method.
func (m *MockRegistryProviders) Read(ctx context.Context, providerID tfe.RegistryProviderID, options *tfe.RegistryProviderReadOptions) (*tfe.RegistryProvider, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, providerID, options)
ret0, _ := ret[0].(*tfe.RegistryProvider)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockRegistryProvidersMockRecorder) Read(ctx, providerID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockRegistryProviders)(nil).Read), ctx, providerID, options)
}
================================================
FILE: mocks/registry_provider_platform_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: registry_provider_platform.go
//
// Generated by this command:
//
// mockgen -source=registry_provider_platform.go -destination=mocks/registry_provider_platform_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockRegistryProviderPlatforms is a mock of RegistryProviderPlatforms interface.
type MockRegistryProviderPlatforms struct {
ctrl *gomock.Controller
recorder *MockRegistryProviderPlatformsMockRecorder
}
// MockRegistryProviderPlatformsMockRecorder is the mock recorder for MockRegistryProviderPlatforms.
type MockRegistryProviderPlatformsMockRecorder struct {
mock *MockRegistryProviderPlatforms
}
// NewMockRegistryProviderPlatforms creates a new mock instance.
func NewMockRegistryProviderPlatforms(ctrl *gomock.Controller) *MockRegistryProviderPlatforms {
mock := &MockRegistryProviderPlatforms{ctrl: ctrl}
mock.recorder = &MockRegistryProviderPlatformsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockRegistryProviderPlatforms) EXPECT() *MockRegistryProviderPlatformsMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockRegistryProviderPlatforms) Create(ctx context.Context, versionID tfe.RegistryProviderVersionID, options tfe.RegistryProviderPlatformCreateOptions) (*tfe.RegistryProviderPlatform, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, versionID, options)
ret0, _ := ret[0].(*tfe.RegistryProviderPlatform)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockRegistryProviderPlatformsMockRecorder) Create(ctx, versionID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRegistryProviderPlatforms)(nil).Create), ctx, versionID, options)
}
// Delete mocks base method.
func (m *MockRegistryProviderPlatforms) Delete(ctx context.Context, platformID tfe.RegistryProviderPlatformID) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, platformID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockRegistryProviderPlatformsMockRecorder) Delete(ctx, platformID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRegistryProviderPlatforms)(nil).Delete), ctx, platformID)
}
// List mocks base method.
func (m *MockRegistryProviderPlatforms) List(ctx context.Context, versionID tfe.RegistryProviderVersionID, options *tfe.RegistryProviderPlatformListOptions) (*tfe.RegistryProviderPlatformList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, versionID, options)
ret0, _ := ret[0].(*tfe.RegistryProviderPlatformList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockRegistryProviderPlatformsMockRecorder) List(ctx, versionID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockRegistryProviderPlatforms)(nil).List), ctx, versionID, options)
}
// Read mocks base method.
func (m *MockRegistryProviderPlatforms) Read(ctx context.Context, platformID tfe.RegistryProviderPlatformID) (*tfe.RegistryProviderPlatform, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, platformID)
ret0, _ := ret[0].(*tfe.RegistryProviderPlatform)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockRegistryProviderPlatformsMockRecorder) Read(ctx, platformID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockRegistryProviderPlatforms)(nil).Read), ctx, platformID)
}
================================================
FILE: mocks/registry_provider_version_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: registry_provider_version.go
//
// Generated by this command:
//
// mockgen -source=registry_provider_version.go -destination=mocks/registry_provider_version_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockRegistryProviderVersions is a mock of RegistryProviderVersions interface.
type MockRegistryProviderVersions struct {
ctrl *gomock.Controller
recorder *MockRegistryProviderVersionsMockRecorder
}
// MockRegistryProviderVersionsMockRecorder is the mock recorder for MockRegistryProviderVersions.
type MockRegistryProviderVersionsMockRecorder struct {
mock *MockRegistryProviderVersions
}
// NewMockRegistryProviderVersions creates a new mock instance.
func NewMockRegistryProviderVersions(ctrl *gomock.Controller) *MockRegistryProviderVersions {
mock := &MockRegistryProviderVersions{ctrl: ctrl}
mock.recorder = &MockRegistryProviderVersionsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockRegistryProviderVersions) EXPECT() *MockRegistryProviderVersionsMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockRegistryProviderVersions) Create(ctx context.Context, providerID tfe.RegistryProviderID, options tfe.RegistryProviderVersionCreateOptions) (*tfe.RegistryProviderVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, providerID, options)
ret0, _ := ret[0].(*tfe.RegistryProviderVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockRegistryProviderVersionsMockRecorder) Create(ctx, providerID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRegistryProviderVersions)(nil).Create), ctx, providerID, options)
}
// Delete mocks base method.
func (m *MockRegistryProviderVersions) Delete(ctx context.Context, versionID tfe.RegistryProviderVersionID) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, versionID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockRegistryProviderVersionsMockRecorder) Delete(ctx, versionID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRegistryProviderVersions)(nil).Delete), ctx, versionID)
}
// List mocks base method.
func (m *MockRegistryProviderVersions) List(ctx context.Context, providerID tfe.RegistryProviderID, options *tfe.RegistryProviderVersionListOptions) (*tfe.RegistryProviderVersionList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, providerID, options)
ret0, _ := ret[0].(*tfe.RegistryProviderVersionList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockRegistryProviderVersionsMockRecorder) List(ctx, providerID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockRegistryProviderVersions)(nil).List), ctx, providerID, options)
}
// Read mocks base method.
func (m *MockRegistryProviderVersions) Read(ctx context.Context, versionID tfe.RegistryProviderVersionID) (*tfe.RegistryProviderVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, versionID)
ret0, _ := ret[0].(*tfe.RegistryProviderVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockRegistryProviderVersionsMockRecorder) Read(ctx, versionID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockRegistryProviderVersions)(nil).Read), ctx, versionID)
}
================================================
FILE: mocks/run_events_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: run_event.go
//
// Generated by this command:
//
// mockgen -source=run_event.go -destination=mocks/run_events_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockRunEvents is a mock of RunEvents interface.
type MockRunEvents struct {
ctrl *gomock.Controller
recorder *MockRunEventsMockRecorder
}
// MockRunEventsMockRecorder is the mock recorder for MockRunEvents.
type MockRunEventsMockRecorder struct {
mock *MockRunEvents
}
// NewMockRunEvents creates a new mock instance.
func NewMockRunEvents(ctrl *gomock.Controller) *MockRunEvents {
mock := &MockRunEvents{ctrl: ctrl}
mock.recorder = &MockRunEventsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockRunEvents) EXPECT() *MockRunEventsMockRecorder {
return m.recorder
}
// List mocks base method.
func (m *MockRunEvents) List(ctx context.Context, runID string, options *tfe.RunEventListOptions) (*tfe.RunEventList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, runID, options)
ret0, _ := ret[0].(*tfe.RunEventList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockRunEventsMockRecorder) List(ctx, runID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockRunEvents)(nil).List), ctx, runID, options)
}
// Read mocks base method.
func (m *MockRunEvents) Read(ctx context.Context, runEventID string) (*tfe.RunEvent, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, runEventID)
ret0, _ := ret[0].(*tfe.RunEvent)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockRunEventsMockRecorder) Read(ctx, runEventID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockRunEvents)(nil).Read), ctx, runEventID)
}
// ReadWithOptions mocks base method.
func (m *MockRunEvents) ReadWithOptions(ctx context.Context, runEventID string, options *tfe.RunEventReadOptions) (*tfe.RunEvent, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadWithOptions", ctx, runEventID, options)
ret0, _ := ret[0].(*tfe.RunEvent)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadWithOptions indicates an expected call of ReadWithOptions.
func (mr *MockRunEventsMockRecorder) ReadWithOptions(ctx, runEventID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadWithOptions", reflect.TypeOf((*MockRunEvents)(nil).ReadWithOptions), ctx, runEventID, options)
}
================================================
FILE: mocks/run_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: run.go
//
// Generated by this command:
//
// mockgen -source=run.go -destination=mocks/run_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockRuns is a mock of Runs interface.
type MockRuns struct {
ctrl *gomock.Controller
recorder *MockRunsMockRecorder
}
// MockRunsMockRecorder is the mock recorder for MockRuns.
type MockRunsMockRecorder struct {
mock *MockRuns
}
// NewMockRuns creates a new mock instance.
func NewMockRuns(ctrl *gomock.Controller) *MockRuns {
mock := &MockRuns{ctrl: ctrl}
mock.recorder = &MockRunsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockRuns) EXPECT() *MockRunsMockRecorder {
return m.recorder
}
// Apply mocks base method.
func (m *MockRuns) Apply(ctx context.Context, runID string, options tfe.RunApplyOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Apply", ctx, runID, options)
ret0, _ := ret[0].(error)
return ret0
}
// Apply indicates an expected call of Apply.
func (mr *MockRunsMockRecorder) Apply(ctx, runID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Apply", reflect.TypeOf((*MockRuns)(nil).Apply), ctx, runID, options)
}
// Cancel mocks base method.
func (m *MockRuns) Cancel(ctx context.Context, runID string, options tfe.RunCancelOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Cancel", ctx, runID, options)
ret0, _ := ret[0].(error)
return ret0
}
// Cancel indicates an expected call of Cancel.
func (mr *MockRunsMockRecorder) Cancel(ctx, runID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cancel", reflect.TypeOf((*MockRuns)(nil).Cancel), ctx, runID, options)
}
// Create mocks base method.
func (m *MockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*tfe.Run, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, options)
ret0, _ := ret[0].(*tfe.Run)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockRunsMockRecorder) Create(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRuns)(nil).Create), ctx, options)
}
// Discard mocks base method.
func (m *MockRuns) Discard(ctx context.Context, runID string, options tfe.RunDiscardOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Discard", ctx, runID, options)
ret0, _ := ret[0].(error)
return ret0
}
// Discard indicates an expected call of Discard.
func (mr *MockRunsMockRecorder) Discard(ctx, runID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Discard", reflect.TypeOf((*MockRuns)(nil).Discard), ctx, runID, options)
}
// ForceCancel mocks base method.
func (m *MockRuns) ForceCancel(ctx context.Context, runID string, options tfe.RunForceCancelOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ForceCancel", ctx, runID, options)
ret0, _ := ret[0].(error)
return ret0
}
// ForceCancel indicates an expected call of ForceCancel.
func (mr *MockRunsMockRecorder) ForceCancel(ctx, runID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ForceCancel", reflect.TypeOf((*MockRuns)(nil).ForceCancel), ctx, runID, options)
}
// ForceExecute mocks base method.
func (m *MockRuns) ForceExecute(ctx context.Context, runID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ForceExecute", ctx, runID)
ret0, _ := ret[0].(error)
return ret0
}
// ForceExecute indicates an expected call of ForceExecute.
func (mr *MockRunsMockRecorder) ForceExecute(ctx, runID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ForceExecute", reflect.TypeOf((*MockRuns)(nil).ForceExecute), ctx, runID)
}
// List mocks base method.
func (m *MockRuns) List(ctx context.Context, workspaceID string, options *tfe.RunListOptions) (*tfe.RunList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.RunList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockRunsMockRecorder) List(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockRuns)(nil).List), ctx, workspaceID, options)
}
// ListForOrganization mocks base method.
func (m *MockRuns) ListForOrganization(ctx context.Context, organization string, options *tfe.RunListForOrganizationOptions) (*tfe.OrganizationRunList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListForOrganization", ctx, organization, options)
ret0, _ := ret[0].(*tfe.OrganizationRunList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListForOrganization indicates an expected call of ListForOrganization.
func (mr *MockRunsMockRecorder) ListForOrganization(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListForOrganization", reflect.TypeOf((*MockRuns)(nil).ListForOrganization), ctx, organization, options)
}
// Read mocks base method.
func (m *MockRuns) Read(ctx context.Context, runID string) (*tfe.Run, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, runID)
ret0, _ := ret[0].(*tfe.Run)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockRunsMockRecorder) Read(ctx, runID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockRuns)(nil).Read), ctx, runID)
}
// ReadWithOptions mocks base method.
func (m *MockRuns) ReadWithOptions(ctx context.Context, runID string, options *tfe.RunReadOptions) (*tfe.Run, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadWithOptions", ctx, runID, options)
ret0, _ := ret[0].(*tfe.Run)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadWithOptions indicates an expected call of ReadWithOptions.
func (mr *MockRunsMockRecorder) ReadWithOptions(ctx, runID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadWithOptions", reflect.TypeOf((*MockRuns)(nil).ReadWithOptions), ctx, runID, options)
}
================================================
FILE: mocks/run_tasks_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: run_task.go
//
// Generated by this command:
//
// mockgen -source=run_task.go -destination=mocks/run_tasks_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockRunTasks is a mock of RunTasks interface.
type MockRunTasks struct {
ctrl *gomock.Controller
recorder *MockRunTasksMockRecorder
}
// MockRunTasksMockRecorder is the mock recorder for MockRunTasks.
type MockRunTasksMockRecorder struct {
mock *MockRunTasks
}
// NewMockRunTasks creates a new mock instance.
func NewMockRunTasks(ctrl *gomock.Controller) *MockRunTasks {
mock := &MockRunTasks{ctrl: ctrl}
mock.recorder = &MockRunTasksMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockRunTasks) EXPECT() *MockRunTasksMockRecorder {
return m.recorder
}
// AttachToWorkspace mocks base method.
func (m *MockRunTasks) AttachToWorkspace(ctx context.Context, workspaceID, runTaskID string, enforcementLevel tfe.TaskEnforcementLevel) (*tfe.WorkspaceRunTask, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AttachToWorkspace", ctx, workspaceID, runTaskID, enforcementLevel)
ret0, _ := ret[0].(*tfe.WorkspaceRunTask)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AttachToWorkspace indicates an expected call of AttachToWorkspace.
func (mr *MockRunTasksMockRecorder) AttachToWorkspace(ctx, workspaceID, runTaskID, enforcementLevel any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AttachToWorkspace", reflect.TypeOf((*MockRunTasks)(nil).AttachToWorkspace), ctx, workspaceID, runTaskID, enforcementLevel)
}
// Create mocks base method.
func (m *MockRunTasks) Create(ctx context.Context, organization string, options tfe.RunTaskCreateOptions) (*tfe.RunTask, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, organization, options)
ret0, _ := ret[0].(*tfe.RunTask)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockRunTasksMockRecorder) Create(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRunTasks)(nil).Create), ctx, organization, options)
}
// Delete mocks base method.
func (m *MockRunTasks) Delete(ctx context.Context, runTaskID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, runTaskID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockRunTasksMockRecorder) Delete(ctx, runTaskID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRunTasks)(nil).Delete), ctx, runTaskID)
}
// List mocks base method.
func (m *MockRunTasks) List(ctx context.Context, organization string, options *tfe.RunTaskListOptions) (*tfe.RunTaskList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, organization, options)
ret0, _ := ret[0].(*tfe.RunTaskList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockRunTasksMockRecorder) List(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockRunTasks)(nil).List), ctx, organization, options)
}
// Read mocks base method.
func (m *MockRunTasks) Read(ctx context.Context, runTaskID string) (*tfe.RunTask, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, runTaskID)
ret0, _ := ret[0].(*tfe.RunTask)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockRunTasksMockRecorder) Read(ctx, runTaskID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockRunTasks)(nil).Read), ctx, runTaskID)
}
// ReadWithOptions mocks base method.
func (m *MockRunTasks) ReadWithOptions(ctx context.Context, runTaskID string, options *tfe.RunTaskReadOptions) (*tfe.RunTask, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadWithOptions", ctx, runTaskID, options)
ret0, _ := ret[0].(*tfe.RunTask)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadWithOptions indicates an expected call of ReadWithOptions.
func (mr *MockRunTasksMockRecorder) ReadWithOptions(ctx, runTaskID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadWithOptions", reflect.TypeOf((*MockRunTasks)(nil).ReadWithOptions), ctx, runTaskID, options)
}
// Update mocks base method.
func (m *MockRunTasks) Update(ctx context.Context, runTaskID string, options tfe.RunTaskUpdateOptions) (*tfe.RunTask, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, runTaskID, options)
ret0, _ := ret[0].(*tfe.RunTask)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockRunTasksMockRecorder) Update(ctx, runTaskID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRunTasks)(nil).Update), ctx, runTaskID, options)
}
================================================
FILE: mocks/run_trigger_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: run_trigger.go
//
// Generated by this command:
//
// mockgen -source=run_trigger.go -destination=mocks/run_trigger_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockRunTriggers is a mock of RunTriggers interface.
type MockRunTriggers struct {
ctrl *gomock.Controller
recorder *MockRunTriggersMockRecorder
}
// MockRunTriggersMockRecorder is the mock recorder for MockRunTriggers.
type MockRunTriggersMockRecorder struct {
mock *MockRunTriggers
}
// NewMockRunTriggers creates a new mock instance.
func NewMockRunTriggers(ctrl *gomock.Controller) *MockRunTriggers {
mock := &MockRunTriggers{ctrl: ctrl}
mock.recorder = &MockRunTriggersMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockRunTriggers) EXPECT() *MockRunTriggersMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockRunTriggers) Create(ctx context.Context, workspaceID string, options tfe.RunTriggerCreateOptions) (*tfe.RunTrigger, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.RunTrigger)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockRunTriggersMockRecorder) Create(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRunTriggers)(nil).Create), ctx, workspaceID, options)
}
// Delete mocks base method.
func (m *MockRunTriggers) Delete(ctx context.Context, RunTriggerID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, RunTriggerID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockRunTriggersMockRecorder) Delete(ctx, RunTriggerID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRunTriggers)(nil).Delete), ctx, RunTriggerID)
}
// List mocks base method.
func (m *MockRunTriggers) List(ctx context.Context, workspaceID string, options *tfe.RunTriggerListOptions) (*tfe.RunTriggerList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.RunTriggerList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockRunTriggersMockRecorder) List(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockRunTriggers)(nil).List), ctx, workspaceID, options)
}
// Read mocks base method.
func (m *MockRunTriggers) Read(ctx context.Context, RunTriggerID string) (*tfe.RunTrigger, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, RunTriggerID)
ret0, _ := ret[0].(*tfe.RunTrigger)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockRunTriggersMockRecorder) Read(ctx, RunTriggerID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockRunTriggers)(nil).Read), ctx, RunTriggerID)
}
// ReadWithOptions mocks base method.
func (m *MockRunTriggers) ReadWithOptions(ctx context.Context, runID string, options *tfe.RunTriggerReadOptions) (*tfe.RunTrigger, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadWithOptions", ctx, runID, options)
ret0, _ := ret[0].(*tfe.RunTrigger)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadWithOptions indicates an expected call of ReadWithOptions.
func (mr *MockRunTriggersMockRecorder) ReadWithOptions(ctx, runID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadWithOptions", reflect.TypeOf((*MockRunTriggers)(nil).ReadWithOptions), ctx, runID, options)
}
================================================
FILE: mocks/ssh_key_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: ssh_key.go
//
// Generated by this command:
//
// mockgen -source=ssh_key.go -destination=mocks/ssh_key_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockSSHKeys is a mock of SSHKeys interface.
type MockSSHKeys struct {
ctrl *gomock.Controller
recorder *MockSSHKeysMockRecorder
}
// MockSSHKeysMockRecorder is the mock recorder for MockSSHKeys.
type MockSSHKeysMockRecorder struct {
mock *MockSSHKeys
}
// NewMockSSHKeys creates a new mock instance.
func NewMockSSHKeys(ctrl *gomock.Controller) *MockSSHKeys {
mock := &MockSSHKeys{ctrl: ctrl}
mock.recorder = &MockSSHKeysMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockSSHKeys) EXPECT() *MockSSHKeysMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockSSHKeys) Create(ctx context.Context, organization string, options tfe.SSHKeyCreateOptions) (*tfe.SSHKey, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, organization, options)
ret0, _ := ret[0].(*tfe.SSHKey)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockSSHKeysMockRecorder) Create(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockSSHKeys)(nil).Create), ctx, organization, options)
}
// Delete mocks base method.
func (m *MockSSHKeys) Delete(ctx context.Context, sshKeyID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, sshKeyID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockSSHKeysMockRecorder) Delete(ctx, sshKeyID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockSSHKeys)(nil).Delete), ctx, sshKeyID)
}
// List mocks base method.
func (m *MockSSHKeys) List(ctx context.Context, organization string, options *tfe.SSHKeyListOptions) (*tfe.SSHKeyList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, organization, options)
ret0, _ := ret[0].(*tfe.SSHKeyList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockSSHKeysMockRecorder) List(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockSSHKeys)(nil).List), ctx, organization, options)
}
// Read mocks base method.
func (m *MockSSHKeys) Read(ctx context.Context, sshKeyID string) (*tfe.SSHKey, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, sshKeyID)
ret0, _ := ret[0].(*tfe.SSHKey)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockSSHKeysMockRecorder) Read(ctx, sshKeyID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockSSHKeys)(nil).Read), ctx, sshKeyID)
}
// Update mocks base method.
func (m *MockSSHKeys) Update(ctx context.Context, sshKeyID string, options tfe.SSHKeyUpdateOptions) (*tfe.SSHKey, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, sshKeyID, options)
ret0, _ := ret[0].(*tfe.SSHKey)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockSSHKeysMockRecorder) Update(ctx, sshKeyID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockSSHKeys)(nil).Update), ctx, sshKeyID, options)
}
================================================
FILE: mocks/state_version_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: state_version.go
//
// Generated by this command:
//
// mockgen -source=state_version.go -destination=mocks/state_version_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockStateVersions is a mock of StateVersions interface.
type MockStateVersions struct {
ctrl *gomock.Controller
recorder *MockStateVersionsMockRecorder
}
// MockStateVersionsMockRecorder is the mock recorder for MockStateVersions.
type MockStateVersionsMockRecorder struct {
mock *MockStateVersions
}
// NewMockStateVersions creates a new mock instance.
func NewMockStateVersions(ctrl *gomock.Controller) *MockStateVersions {
mock := &MockStateVersions{ctrl: ctrl}
mock.recorder = &MockStateVersionsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockStateVersions) EXPECT() *MockStateVersionsMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockStateVersions) Create(ctx context.Context, workspaceID string, options tfe.StateVersionCreateOptions) (*tfe.StateVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.StateVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockStateVersionsMockRecorder) Create(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockStateVersions)(nil).Create), ctx, workspaceID, options)
}
// Download mocks base method.
func (m *MockStateVersions) Download(ctx context.Context, url string) ([]byte, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Download", ctx, url)
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Download indicates an expected call of Download.
func (mr *MockStateVersionsMockRecorder) Download(ctx, url any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Download", reflect.TypeOf((*MockStateVersions)(nil).Download), ctx, url)
}
// List mocks base method.
func (m *MockStateVersions) List(ctx context.Context, options *tfe.StateVersionListOptions) (*tfe.StateVersionList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, options)
ret0, _ := ret[0].(*tfe.StateVersionList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockStateVersionsMockRecorder) List(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockStateVersions)(nil).List), ctx, options)
}
// ListOutputs mocks base method.
func (m *MockStateVersions) ListOutputs(ctx context.Context, svID string, options *tfe.StateVersionOutputsListOptions) (*tfe.StateVersionOutputsList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListOutputs", ctx, svID, options)
ret0, _ := ret[0].(*tfe.StateVersionOutputsList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListOutputs indicates an expected call of ListOutputs.
func (mr *MockStateVersionsMockRecorder) ListOutputs(ctx, svID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListOutputs", reflect.TypeOf((*MockStateVersions)(nil).ListOutputs), ctx, svID, options)
}
// PermanentlyDeleteBackingData mocks base method.
func (m *MockStateVersions) PermanentlyDeleteBackingData(ctx context.Context, svID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PermanentlyDeleteBackingData", ctx, svID)
ret0, _ := ret[0].(error)
return ret0
}
// PermanentlyDeleteBackingData indicates an expected call of PermanentlyDeleteBackingData.
func (mr *MockStateVersionsMockRecorder) PermanentlyDeleteBackingData(ctx, svID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PermanentlyDeleteBackingData", reflect.TypeOf((*MockStateVersions)(nil).PermanentlyDeleteBackingData), ctx, svID)
}
// Read mocks base method.
func (m *MockStateVersions) Read(ctx context.Context, svID string) (*tfe.StateVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, svID)
ret0, _ := ret[0].(*tfe.StateVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockStateVersionsMockRecorder) Read(ctx, svID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockStateVersions)(nil).Read), ctx, svID)
}
// ReadCurrent mocks base method.
func (m *MockStateVersions) ReadCurrent(ctx context.Context, workspaceID string) (*tfe.StateVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadCurrent", ctx, workspaceID)
ret0, _ := ret[0].(*tfe.StateVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadCurrent indicates an expected call of ReadCurrent.
func (mr *MockStateVersionsMockRecorder) ReadCurrent(ctx, workspaceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadCurrent", reflect.TypeOf((*MockStateVersions)(nil).ReadCurrent), ctx, workspaceID)
}
// ReadCurrentWithOptions mocks base method.
func (m *MockStateVersions) ReadCurrentWithOptions(ctx context.Context, workspaceID string, options *tfe.StateVersionCurrentOptions) (*tfe.StateVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadCurrentWithOptions", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.StateVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadCurrentWithOptions indicates an expected call of ReadCurrentWithOptions.
func (mr *MockStateVersionsMockRecorder) ReadCurrentWithOptions(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadCurrentWithOptions", reflect.TypeOf((*MockStateVersions)(nil).ReadCurrentWithOptions), ctx, workspaceID, options)
}
// ReadWithOptions mocks base method.
func (m *MockStateVersions) ReadWithOptions(ctx context.Context, svID string, options *tfe.StateVersionReadOptions) (*tfe.StateVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadWithOptions", ctx, svID, options)
ret0, _ := ret[0].(*tfe.StateVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadWithOptions indicates an expected call of ReadWithOptions.
func (mr *MockStateVersionsMockRecorder) ReadWithOptions(ctx, svID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadWithOptions", reflect.TypeOf((*MockStateVersions)(nil).ReadWithOptions), ctx, svID, options)
}
// RestoreBackingData mocks base method.
func (m *MockStateVersions) RestoreBackingData(ctx context.Context, svID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RestoreBackingData", ctx, svID)
ret0, _ := ret[0].(error)
return ret0
}
// RestoreBackingData indicates an expected call of RestoreBackingData.
func (mr *MockStateVersionsMockRecorder) RestoreBackingData(ctx, svID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestoreBackingData", reflect.TypeOf((*MockStateVersions)(nil).RestoreBackingData), ctx, svID)
}
// SoftDeleteBackingData mocks base method.
func (m *MockStateVersions) SoftDeleteBackingData(ctx context.Context, svID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SoftDeleteBackingData", ctx, svID)
ret0, _ := ret[0].(error)
return ret0
}
// SoftDeleteBackingData indicates an expected call of SoftDeleteBackingData.
func (mr *MockStateVersionsMockRecorder) SoftDeleteBackingData(ctx, svID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SoftDeleteBackingData", reflect.TypeOf((*MockStateVersions)(nil).SoftDeleteBackingData), ctx, svID)
}
// Upload mocks base method.
func (m *MockStateVersions) Upload(ctx context.Context, workspaceID string, options tfe.StateVersionUploadOptions) (*tfe.StateVersion, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Upload", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.StateVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Upload indicates an expected call of Upload.
func (mr *MockStateVersionsMockRecorder) Upload(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Upload", reflect.TypeOf((*MockStateVersions)(nil).Upload), ctx, workspaceID, options)
}
// UploadSanitizedState mocks base method.
func (m *MockStateVersions) UploadSanitizedState(ctx context.Context, sanitizedStateUploadURL *string, sanitizedState []byte) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UploadSanitizedState", ctx, sanitizedStateUploadURL, sanitizedState)
ret0, _ := ret[0].(error)
return ret0
}
// UploadSanitizedState indicates an expected call of UploadSanitizedState.
func (mr *MockStateVersionsMockRecorder) UploadSanitizedState(ctx, sanitizedStateUploadURL, sanitizedState any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadSanitizedState", reflect.TypeOf((*MockStateVersions)(nil).UploadSanitizedState), ctx, sanitizedStateUploadURL, sanitizedState)
}
================================================
FILE: mocks/state_version_output_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: state_version_output.go
//
// Generated by this command:
//
// mockgen -source=state_version_output.go -destination=mocks/state_version_output_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockStateVersionOutputs is a mock of StateVersionOutputs interface.
type MockStateVersionOutputs struct {
ctrl *gomock.Controller
recorder *MockStateVersionOutputsMockRecorder
}
// MockStateVersionOutputsMockRecorder is the mock recorder for MockStateVersionOutputs.
type MockStateVersionOutputsMockRecorder struct {
mock *MockStateVersionOutputs
}
// NewMockStateVersionOutputs creates a new mock instance.
func NewMockStateVersionOutputs(ctrl *gomock.Controller) *MockStateVersionOutputs {
mock := &MockStateVersionOutputs{ctrl: ctrl}
mock.recorder = &MockStateVersionOutputsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockStateVersionOutputs) EXPECT() *MockStateVersionOutputsMockRecorder {
return m.recorder
}
// Read mocks base method.
func (m *MockStateVersionOutputs) Read(ctx context.Context, outputID string) (*tfe.StateVersionOutput, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, outputID)
ret0, _ := ret[0].(*tfe.StateVersionOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockStateVersionOutputsMockRecorder) Read(ctx, outputID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockStateVersionOutputs)(nil).Read), ctx, outputID)
}
// ReadCurrent mocks base method.
func (m *MockStateVersionOutputs) ReadCurrent(ctx context.Context, workspaceID string) (*tfe.StateVersionOutputsList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadCurrent", ctx, workspaceID)
ret0, _ := ret[0].(*tfe.StateVersionOutputsList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadCurrent indicates an expected call of ReadCurrent.
func (mr *MockStateVersionOutputsMockRecorder) ReadCurrent(ctx, workspaceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadCurrent", reflect.TypeOf((*MockStateVersionOutputs)(nil).ReadCurrent), ctx, workspaceID)
}
================================================
FILE: mocks/tag_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: tag.go
//
// Generated by this command:
//
// mockgen -source=tag.go -destination=mocks/tag_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
================================================
FILE: mocks/task_result_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: task_result.go
//
// Generated by this command:
//
// mockgen -source=task_result.go -destination=mocks/task_result_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockTaskResults is a mock of TaskResults interface.
type MockTaskResults struct {
ctrl *gomock.Controller
recorder *MockTaskResultsMockRecorder
}
// MockTaskResultsMockRecorder is the mock recorder for MockTaskResults.
type MockTaskResultsMockRecorder struct {
mock *MockTaskResults
}
// NewMockTaskResults creates a new mock instance.
func NewMockTaskResults(ctrl *gomock.Controller) *MockTaskResults {
mock := &MockTaskResults{ctrl: ctrl}
mock.recorder = &MockTaskResultsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTaskResults) EXPECT() *MockTaskResultsMockRecorder {
return m.recorder
}
// Read mocks base method.
func (m *MockTaskResults) Read(ctx context.Context, taskResultID string) (*tfe.TaskResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, taskResultID)
ret0, _ := ret[0].(*tfe.TaskResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockTaskResultsMockRecorder) Read(ctx, taskResultID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockTaskResults)(nil).Read), ctx, taskResultID)
}
================================================
FILE: mocks/task_stages_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: task_stages.go
//
// Generated by this command:
//
// mockgen -source=task_stages.go -destination=mocks/task_stages_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockTaskStages is a mock of TaskStages interface.
type MockTaskStages struct {
ctrl *gomock.Controller
recorder *MockTaskStagesMockRecorder
}
// MockTaskStagesMockRecorder is the mock recorder for MockTaskStages.
type MockTaskStagesMockRecorder struct {
mock *MockTaskStages
}
// NewMockTaskStages creates a new mock instance.
func NewMockTaskStages(ctrl *gomock.Controller) *MockTaskStages {
mock := &MockTaskStages{ctrl: ctrl}
mock.recorder = &MockTaskStagesMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTaskStages) EXPECT() *MockTaskStagesMockRecorder {
return m.recorder
}
// List mocks base method.
func (m *MockTaskStages) List(ctx context.Context, runID string, options *tfe.TaskStageListOptions) (*tfe.TaskStageList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, runID, options)
ret0, _ := ret[0].(*tfe.TaskStageList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockTaskStagesMockRecorder) List(ctx, runID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockTaskStages)(nil).List), ctx, runID, options)
}
// Override mocks base method.
func (m *MockTaskStages) Override(ctx context.Context, taskStageID string, options tfe.TaskStageOverrideOptions) (*tfe.TaskStage, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Override", ctx, taskStageID, options)
ret0, _ := ret[0].(*tfe.TaskStage)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Override indicates an expected call of Override.
func (mr *MockTaskStagesMockRecorder) Override(ctx, taskStageID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Override", reflect.TypeOf((*MockTaskStages)(nil).Override), ctx, taskStageID, options)
}
// Read mocks base method.
func (m *MockTaskStages) Read(ctx context.Context, taskStageID string, options *tfe.TaskStageReadOptions) (*tfe.TaskStage, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, taskStageID, options)
ret0, _ := ret[0].(*tfe.TaskStage)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockTaskStagesMockRecorder) Read(ctx, taskStageID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockTaskStages)(nil).Read), ctx, taskStageID, options)
}
================================================
FILE: mocks/team_access_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: team_access.go
//
// Generated by this command:
//
// mockgen -source=team_access.go -destination=mocks/team_access_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockTeamAccesses is a mock of TeamAccesses interface.
type MockTeamAccesses struct {
ctrl *gomock.Controller
recorder *MockTeamAccessesMockRecorder
}
// MockTeamAccessesMockRecorder is the mock recorder for MockTeamAccesses.
type MockTeamAccessesMockRecorder struct {
mock *MockTeamAccesses
}
// NewMockTeamAccesses creates a new mock instance.
func NewMockTeamAccesses(ctrl *gomock.Controller) *MockTeamAccesses {
mock := &MockTeamAccesses{ctrl: ctrl}
mock.recorder = &MockTeamAccessesMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTeamAccesses) EXPECT() *MockTeamAccessesMockRecorder {
return m.recorder
}
// Add mocks base method.
func (m *MockTeamAccesses) Add(ctx context.Context, options tfe.TeamAccessAddOptions) (*tfe.TeamAccess, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Add", ctx, options)
ret0, _ := ret[0].(*tfe.TeamAccess)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Add indicates an expected call of Add.
func (mr *MockTeamAccessesMockRecorder) Add(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockTeamAccesses)(nil).Add), ctx, options)
}
// List mocks base method.
func (m *MockTeamAccesses) List(ctx context.Context, options *tfe.TeamAccessListOptions) (*tfe.TeamAccessList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, options)
ret0, _ := ret[0].(*tfe.TeamAccessList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockTeamAccessesMockRecorder) List(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockTeamAccesses)(nil).List), ctx, options)
}
// Read mocks base method.
func (m *MockTeamAccesses) Read(ctx context.Context, teamAccessID string) (*tfe.TeamAccess, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, teamAccessID)
ret0, _ := ret[0].(*tfe.TeamAccess)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockTeamAccessesMockRecorder) Read(ctx, teamAccessID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockTeamAccesses)(nil).Read), ctx, teamAccessID)
}
// Remove mocks base method.
func (m *MockTeamAccesses) Remove(ctx context.Context, teamAccessID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Remove", ctx, teamAccessID)
ret0, _ := ret[0].(error)
return ret0
}
// Remove indicates an expected call of Remove.
func (mr *MockTeamAccessesMockRecorder) Remove(ctx, teamAccessID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockTeamAccesses)(nil).Remove), ctx, teamAccessID)
}
// Update mocks base method.
func (m *MockTeamAccesses) Update(ctx context.Context, teamAccessID string, options tfe.TeamAccessUpdateOptions) (*tfe.TeamAccess, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, teamAccessID, options)
ret0, _ := ret[0].(*tfe.TeamAccess)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockTeamAccessesMockRecorder) Update(ctx, teamAccessID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockTeamAccesses)(nil).Update), ctx, teamAccessID, options)
}
================================================
FILE: mocks/team_member_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: team_member.go
//
// Generated by this command:
//
// mockgen -source=team_member.go -destination=mocks/team_member_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockTeamMembers is a mock of TeamMembers interface.
type MockTeamMembers struct {
ctrl *gomock.Controller
recorder *MockTeamMembersMockRecorder
}
// MockTeamMembersMockRecorder is the mock recorder for MockTeamMembers.
type MockTeamMembersMockRecorder struct {
mock *MockTeamMembers
}
// NewMockTeamMembers creates a new mock instance.
func NewMockTeamMembers(ctrl *gomock.Controller) *MockTeamMembers {
mock := &MockTeamMembers{ctrl: ctrl}
mock.recorder = &MockTeamMembersMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTeamMembers) EXPECT() *MockTeamMembersMockRecorder {
return m.recorder
}
// Add mocks base method.
func (m *MockTeamMembers) Add(ctx context.Context, teamID string, options tfe.TeamMemberAddOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Add", ctx, teamID, options)
ret0, _ := ret[0].(error)
return ret0
}
// Add indicates an expected call of Add.
func (mr *MockTeamMembersMockRecorder) Add(ctx, teamID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockTeamMembers)(nil).Add), ctx, teamID, options)
}
// List mocks base method.
func (m *MockTeamMembers) List(ctx context.Context, teamID string) ([]*tfe.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, teamID)
ret0, _ := ret[0].([]*tfe.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockTeamMembersMockRecorder) List(ctx, teamID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockTeamMembers)(nil).List), ctx, teamID)
}
// ListOrganizationMemberships mocks base method.
func (m *MockTeamMembers) ListOrganizationMemberships(ctx context.Context, teamID string) ([]*tfe.OrganizationMembership, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListOrganizationMemberships", ctx, teamID)
ret0, _ := ret[0].([]*tfe.OrganizationMembership)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListOrganizationMemberships indicates an expected call of ListOrganizationMemberships.
func (mr *MockTeamMembersMockRecorder) ListOrganizationMemberships(ctx, teamID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListOrganizationMemberships", reflect.TypeOf((*MockTeamMembers)(nil).ListOrganizationMemberships), ctx, teamID)
}
// ListUsers mocks base method.
func (m *MockTeamMembers) ListUsers(ctx context.Context, teamID string) ([]*tfe.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListUsers", ctx, teamID)
ret0, _ := ret[0].([]*tfe.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListUsers indicates an expected call of ListUsers.
func (mr *MockTeamMembersMockRecorder) ListUsers(ctx, teamID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListUsers", reflect.TypeOf((*MockTeamMembers)(nil).ListUsers), ctx, teamID)
}
// Remove mocks base method.
func (m *MockTeamMembers) Remove(ctx context.Context, teamID string, options tfe.TeamMemberRemoveOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Remove", ctx, teamID, options)
ret0, _ := ret[0].(error)
return ret0
}
// Remove indicates an expected call of Remove.
func (mr *MockTeamMembersMockRecorder) Remove(ctx, teamID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockTeamMembers)(nil).Remove), ctx, teamID, options)
}
================================================
FILE: mocks/team_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: team.go
//
// Generated by this command:
//
// mockgen -source=team.go -destination=mocks/team_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockTeams is a mock of Teams interface.
type MockTeams struct {
ctrl *gomock.Controller
recorder *MockTeamsMockRecorder
}
// MockTeamsMockRecorder is the mock recorder for MockTeams.
type MockTeamsMockRecorder struct {
mock *MockTeams
}
// NewMockTeams creates a new mock instance.
func NewMockTeams(ctrl *gomock.Controller) *MockTeams {
mock := &MockTeams{ctrl: ctrl}
mock.recorder = &MockTeamsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTeams) EXPECT() *MockTeamsMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockTeams) Create(ctx context.Context, organization string, options tfe.TeamCreateOptions) (*tfe.Team, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, organization, options)
ret0, _ := ret[0].(*tfe.Team)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockTeamsMockRecorder) Create(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockTeams)(nil).Create), ctx, organization, options)
}
// Delete mocks base method.
func (m *MockTeams) Delete(ctx context.Context, teamID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, teamID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockTeamsMockRecorder) Delete(ctx, teamID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockTeams)(nil).Delete), ctx, teamID)
}
// List mocks base method.
func (m *MockTeams) List(ctx context.Context, organization string, options *tfe.TeamListOptions) (*tfe.TeamList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, organization, options)
ret0, _ := ret[0].(*tfe.TeamList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockTeamsMockRecorder) List(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockTeams)(nil).List), ctx, organization, options)
}
// Read mocks base method.
func (m *MockTeams) Read(ctx context.Context, teamID string) (*tfe.Team, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, teamID)
ret0, _ := ret[0].(*tfe.Team)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockTeamsMockRecorder) Read(ctx, teamID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockTeams)(nil).Read), ctx, teamID)
}
// Update mocks base method.
func (m *MockTeams) Update(ctx context.Context, teamID string, options tfe.TeamUpdateOptions) (*tfe.Team, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, teamID, options)
ret0, _ := ret[0].(*tfe.Team)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockTeamsMockRecorder) Update(ctx, teamID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockTeams)(nil).Update), ctx, teamID, options)
}
================================================
FILE: mocks/team_project_access_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: team_project_access.go
//
// Generated by this command:
//
// mockgen -source=team_project_access.go -destination=mocks/team_project_access_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockTeamProjectAccesses is a mock of TeamProjectAccesses interface.
type MockTeamProjectAccesses struct {
ctrl *gomock.Controller
recorder *MockTeamProjectAccessesMockRecorder
}
// MockTeamProjectAccessesMockRecorder is the mock recorder for MockTeamProjectAccesses.
type MockTeamProjectAccessesMockRecorder struct {
mock *MockTeamProjectAccesses
}
// NewMockTeamProjectAccesses creates a new mock instance.
func NewMockTeamProjectAccesses(ctrl *gomock.Controller) *MockTeamProjectAccesses {
mock := &MockTeamProjectAccesses{ctrl: ctrl}
mock.recorder = &MockTeamProjectAccessesMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTeamProjectAccesses) EXPECT() *MockTeamProjectAccessesMockRecorder {
return m.recorder
}
// Add mocks base method.
func (m *MockTeamProjectAccesses) Add(ctx context.Context, options tfe.TeamProjectAccessAddOptions) (*tfe.TeamProjectAccess, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Add", ctx, options)
ret0, _ := ret[0].(*tfe.TeamProjectAccess)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Add indicates an expected call of Add.
func (mr *MockTeamProjectAccessesMockRecorder) Add(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockTeamProjectAccesses)(nil).Add), ctx, options)
}
// List mocks base method.
func (m *MockTeamProjectAccesses) List(ctx context.Context, options tfe.TeamProjectAccessListOptions) (*tfe.TeamProjectAccessList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, options)
ret0, _ := ret[0].(*tfe.TeamProjectAccessList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockTeamProjectAccessesMockRecorder) List(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockTeamProjectAccesses)(nil).List), ctx, options)
}
// Read mocks base method.
func (m *MockTeamProjectAccesses) Read(ctx context.Context, teamProjectAccessID string) (*tfe.TeamProjectAccess, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, teamProjectAccessID)
ret0, _ := ret[0].(*tfe.TeamProjectAccess)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockTeamProjectAccessesMockRecorder) Read(ctx, teamProjectAccessID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockTeamProjectAccesses)(nil).Read), ctx, teamProjectAccessID)
}
// Remove mocks base method.
func (m *MockTeamProjectAccesses) Remove(ctx context.Context, teamProjectAccessID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Remove", ctx, teamProjectAccessID)
ret0, _ := ret[0].(error)
return ret0
}
// Remove indicates an expected call of Remove.
func (mr *MockTeamProjectAccessesMockRecorder) Remove(ctx, teamProjectAccessID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockTeamProjectAccesses)(nil).Remove), ctx, teamProjectAccessID)
}
// Update mocks base method.
func (m *MockTeamProjectAccesses) Update(ctx context.Context, teamProjectAccessID string, options tfe.TeamProjectAccessUpdateOptions) (*tfe.TeamProjectAccess, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, teamProjectAccessID, options)
ret0, _ := ret[0].(*tfe.TeamProjectAccess)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockTeamProjectAccessesMockRecorder) Update(ctx, teamProjectAccessID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockTeamProjectAccesses)(nil).Update), ctx, teamProjectAccessID, options)
}
================================================
FILE: mocks/team_token_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: team_token.go
//
// Generated by this command:
//
// mockgen -source=team_token.go -destination=mocks/team_token_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockTeamTokens is a mock of TeamTokens interface.
type MockTeamTokens struct {
ctrl *gomock.Controller
recorder *MockTeamTokensMockRecorder
}
// MockTeamTokensMockRecorder is the mock recorder for MockTeamTokens.
type MockTeamTokensMockRecorder struct {
mock *MockTeamTokens
}
// NewMockTeamTokens creates a new mock instance.
func NewMockTeamTokens(ctrl *gomock.Controller) *MockTeamTokens {
mock := &MockTeamTokens{ctrl: ctrl}
mock.recorder = &MockTeamTokensMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTeamTokens) EXPECT() *MockTeamTokensMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockTeamTokens) Create(ctx context.Context, teamID string) (*tfe.TeamToken, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, teamID)
ret0, _ := ret[0].(*tfe.TeamToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockTeamTokensMockRecorder) Create(ctx, teamID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockTeamTokens)(nil).Create), ctx, teamID)
}
// CreateWithOptions mocks base method.
func (m *MockTeamTokens) CreateWithOptions(ctx context.Context, teamID string, options tfe.TeamTokenCreateOptions) (*tfe.TeamToken, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateWithOptions", ctx, teamID, options)
ret0, _ := ret[0].(*tfe.TeamToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateWithOptions indicates an expected call of CreateWithOptions.
func (mr *MockTeamTokensMockRecorder) CreateWithOptions(ctx, teamID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateWithOptions", reflect.TypeOf((*MockTeamTokens)(nil).CreateWithOptions), ctx, teamID, options)
}
// Delete mocks base method.
func (m *MockTeamTokens) Delete(ctx context.Context, teamID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, teamID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockTeamTokensMockRecorder) Delete(ctx, teamID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockTeamTokens)(nil).Delete), ctx, teamID)
}
// DeleteByID mocks base method.
func (m *MockTeamTokens) DeleteByID(ctx context.Context, tokenID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteByID", ctx, tokenID)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteByID indicates an expected call of DeleteByID.
func (mr *MockTeamTokensMockRecorder) DeleteByID(ctx, tokenID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteByID", reflect.TypeOf((*MockTeamTokens)(nil).DeleteByID), ctx, tokenID)
}
// List mocks base method.
func (m *MockTeamTokens) List(ctx context.Context, organizationID string, options *tfe.TeamTokenListOptions) (*tfe.TeamTokenList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, organizationID, options)
ret0, _ := ret[0].(*tfe.TeamTokenList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockTeamTokensMockRecorder) List(ctx, organizationID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockTeamTokens)(nil).List), ctx, organizationID, options)
}
// Read mocks base method.
func (m *MockTeamTokens) Read(ctx context.Context, teamID string) (*tfe.TeamToken, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, teamID)
ret0, _ := ret[0].(*tfe.TeamToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockTeamTokensMockRecorder) Read(ctx, teamID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockTeamTokens)(nil).Read), ctx, teamID)
}
// ReadByID mocks base method.
func (m *MockTeamTokens) ReadByID(ctx context.Context, teamID string) (*tfe.TeamToken, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadByID", ctx, teamID)
ret0, _ := ret[0].(*tfe.TeamToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadByID indicates an expected call of ReadByID.
func (mr *MockTeamTokensMockRecorder) ReadByID(ctx, teamID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadByID", reflect.TypeOf((*MockTeamTokens)(nil).ReadByID), ctx, teamID)
}
================================================
FILE: mocks/test_run_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: test_run.go
//
// Generated by this command:
//
// mockgen -source=test_run.go -destination=mocks/test_run_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
io "io"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockTestRuns is a mock of TestRuns interface.
type MockTestRuns struct {
ctrl *gomock.Controller
recorder *MockTestRunsMockRecorder
}
// MockTestRunsMockRecorder is the mock recorder for MockTestRuns.
type MockTestRunsMockRecorder struct {
mock *MockTestRuns
}
// NewMockTestRuns creates a new mock instance.
func NewMockTestRuns(ctrl *gomock.Controller) *MockTestRuns {
mock := &MockTestRuns{ctrl: ctrl}
mock.recorder = &MockTestRunsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTestRuns) EXPECT() *MockTestRunsMockRecorder {
return m.recorder
}
// Cancel mocks base method.
func (m *MockTestRuns) Cancel(ctx context.Context, moduleID tfe.RegistryModuleID, testRunID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Cancel", ctx, moduleID, testRunID)
ret0, _ := ret[0].(error)
return ret0
}
// Cancel indicates an expected call of Cancel.
func (mr *MockTestRunsMockRecorder) Cancel(ctx, moduleID, testRunID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cancel", reflect.TypeOf((*MockTestRuns)(nil).Cancel), ctx, moduleID, testRunID)
}
// Create mocks base method.
func (m *MockTestRuns) Create(ctx context.Context, options tfe.TestRunCreateOptions) (*tfe.TestRun, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, options)
ret0, _ := ret[0].(*tfe.TestRun)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockTestRunsMockRecorder) Create(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockTestRuns)(nil).Create), ctx, options)
}
// ForceCancel mocks base method.
func (m *MockTestRuns) ForceCancel(ctx context.Context, moduleID tfe.RegistryModuleID, testRunID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ForceCancel", ctx, moduleID, testRunID)
ret0, _ := ret[0].(error)
return ret0
}
// ForceCancel indicates an expected call of ForceCancel.
func (mr *MockTestRunsMockRecorder) ForceCancel(ctx, moduleID, testRunID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ForceCancel", reflect.TypeOf((*MockTestRuns)(nil).ForceCancel), ctx, moduleID, testRunID)
}
// List mocks base method.
func (m *MockTestRuns) List(ctx context.Context, moduleID tfe.RegistryModuleID, options *tfe.TestRunListOptions) (*tfe.TestRunList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, moduleID, options)
ret0, _ := ret[0].(*tfe.TestRunList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockTestRunsMockRecorder) List(ctx, moduleID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockTestRuns)(nil).List), ctx, moduleID, options)
}
// Logs mocks base method.
func (m *MockTestRuns) Logs(ctx context.Context, moduleID tfe.RegistryModuleID, testRunID string) (io.Reader, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Logs", ctx, moduleID, testRunID)
ret0, _ := ret[0].(io.Reader)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Logs indicates an expected call of Logs.
func (mr *MockTestRunsMockRecorder) Logs(ctx, moduleID, testRunID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logs", reflect.TypeOf((*MockTestRuns)(nil).Logs), ctx, moduleID, testRunID)
}
// Read mocks base method.
func (m *MockTestRuns) Read(ctx context.Context, moduleID tfe.RegistryModuleID, testRunID string) (*tfe.TestRun, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, moduleID, testRunID)
ret0, _ := ret[0].(*tfe.TestRun)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockTestRunsMockRecorder) Read(ctx, moduleID, testRunID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockTestRuns)(nil).Read), ctx, moduleID, testRunID)
}
================================================
FILE: mocks/test_variables_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: test_variables.go
//
// Generated by this command:
//
// mockgen -source=test_variables.go -destination=mocks/test_variables_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockTestVariables is a mock of TestVariables interface.
type MockTestVariables struct {
ctrl *gomock.Controller
recorder *MockTestVariablesMockRecorder
}
// MockTestVariablesMockRecorder is the mock recorder for MockTestVariables.
type MockTestVariablesMockRecorder struct {
mock *MockTestVariables
}
// NewMockTestVariables creates a new mock instance.
func NewMockTestVariables(ctrl *gomock.Controller) *MockTestVariables {
mock := &MockTestVariables{ctrl: ctrl}
mock.recorder = &MockTestVariablesMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTestVariables) EXPECT() *MockTestVariablesMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockTestVariables) Create(ctx context.Context, moduleID tfe.RegistryModuleID, options tfe.VariableCreateOptions) (*tfe.Variable, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, moduleID, options)
ret0, _ := ret[0].(*tfe.Variable)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockTestVariablesMockRecorder) Create(ctx, moduleID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockTestVariables)(nil).Create), ctx, moduleID, options)
}
// Delete mocks base method.
func (m *MockTestVariables) Delete(ctx context.Context, moduleID tfe.RegistryModuleID, variableID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, moduleID, variableID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockTestVariablesMockRecorder) Delete(ctx, moduleID, variableID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockTestVariables)(nil).Delete), ctx, moduleID, variableID)
}
// List mocks base method.
func (m *MockTestVariables) List(ctx context.Context, moduleID tfe.RegistryModuleID, options *tfe.VariableListOptions) (*tfe.VariableList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, moduleID, options)
ret0, _ := ret[0].(*tfe.VariableList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockTestVariablesMockRecorder) List(ctx, moduleID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockTestVariables)(nil).List), ctx, moduleID, options)
}
// Read mocks base method.
func (m *MockTestVariables) Read(ctx context.Context, moduleID tfe.RegistryModuleID, variableID string) (*tfe.Variable, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, moduleID, variableID)
ret0, _ := ret[0].(*tfe.Variable)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockTestVariablesMockRecorder) Read(ctx, moduleID, variableID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockTestVariables)(nil).Read), ctx, moduleID, variableID)
}
// Update mocks base method.
func (m *MockTestVariables) Update(ctx context.Context, moduleID tfe.RegistryModuleID, variableID string, options tfe.VariableUpdateOptions) (*tfe.Variable, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, moduleID, variableID, options)
ret0, _ := ret[0].(*tfe.Variable)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockTestVariablesMockRecorder) Update(ctx, moduleID, variableID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockTestVariables)(nil).Update), ctx, moduleID, variableID, options)
}
================================================
FILE: mocks/user_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: user.go
//
// Generated by this command:
//
// mockgen -source=user.go -destination=mocks/user_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockUsers is a mock of Users interface.
type MockUsers struct {
ctrl *gomock.Controller
recorder *MockUsersMockRecorder
}
// MockUsersMockRecorder is the mock recorder for MockUsers.
type MockUsersMockRecorder struct {
mock *MockUsers
}
// NewMockUsers creates a new mock instance.
func NewMockUsers(ctrl *gomock.Controller) *MockUsers {
mock := &MockUsers{ctrl: ctrl}
mock.recorder = &MockUsersMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockUsers) EXPECT() *MockUsersMockRecorder {
return m.recorder
}
// ReadCurrent mocks base method.
func (m *MockUsers) ReadCurrent(ctx context.Context) (*tfe.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadCurrent", ctx)
ret0, _ := ret[0].(*tfe.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadCurrent indicates an expected call of ReadCurrent.
func (mr *MockUsersMockRecorder) ReadCurrent(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadCurrent", reflect.TypeOf((*MockUsers)(nil).ReadCurrent), ctx)
}
// UpdateCurrent mocks base method.
func (m *MockUsers) UpdateCurrent(ctx context.Context, options tfe.UserUpdateOptions) (*tfe.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateCurrent", ctx, options)
ret0, _ := ret[0].(*tfe.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateCurrent indicates an expected call of UpdateCurrent.
func (mr *MockUsersMockRecorder) UpdateCurrent(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateCurrent", reflect.TypeOf((*MockUsers)(nil).UpdateCurrent), ctx, options)
}
================================================
FILE: mocks/user_token_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: user_token.go
//
// Generated by this command:
//
// mockgen -source=user_token.go -destination=mocks/user_token_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockUserTokens is a mock of UserTokens interface.
type MockUserTokens struct {
ctrl *gomock.Controller
recorder *MockUserTokensMockRecorder
}
// MockUserTokensMockRecorder is the mock recorder for MockUserTokens.
type MockUserTokensMockRecorder struct {
mock *MockUserTokens
}
// NewMockUserTokens creates a new mock instance.
func NewMockUserTokens(ctrl *gomock.Controller) *MockUserTokens {
mock := &MockUserTokens{ctrl: ctrl}
mock.recorder = &MockUserTokensMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockUserTokens) EXPECT() *MockUserTokensMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockUserTokens) Create(ctx context.Context, userID string, options tfe.UserTokenCreateOptions) (*tfe.UserToken, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, userID, options)
ret0, _ := ret[0].(*tfe.UserToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockUserTokensMockRecorder) Create(ctx, userID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockUserTokens)(nil).Create), ctx, userID, options)
}
// Delete mocks base method.
func (m *MockUserTokens) Delete(ctx context.Context, tokenID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, tokenID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockUserTokensMockRecorder) Delete(ctx, tokenID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockUserTokens)(nil).Delete), ctx, tokenID)
}
// List mocks base method.
func (m *MockUserTokens) List(ctx context.Context, userID string) (*tfe.UserTokenList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, userID)
ret0, _ := ret[0].(*tfe.UserTokenList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockUserTokensMockRecorder) List(ctx, userID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockUserTokens)(nil).List), ctx, userID)
}
// Read mocks base method.
func (m *MockUserTokens) Read(ctx context.Context, tokenID string) (*tfe.UserToken, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, tokenID)
ret0, _ := ret[0].(*tfe.UserToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockUserTokensMockRecorder) Read(ctx, tokenID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockUserTokens)(nil).Read), ctx, tokenID)
}
================================================
FILE: mocks/variable_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: variable.go
//
// Generated by this command:
//
// mockgen -source=variable.go -destination=mocks/variable_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockVariables is a mock of Variables interface.
type MockVariables struct {
ctrl *gomock.Controller
recorder *MockVariablesMockRecorder
}
// MockVariablesMockRecorder is the mock recorder for MockVariables.
type MockVariablesMockRecorder struct {
mock *MockVariables
}
// NewMockVariables creates a new mock instance.
func NewMockVariables(ctrl *gomock.Controller) *MockVariables {
mock := &MockVariables{ctrl: ctrl}
mock.recorder = &MockVariablesMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockVariables) EXPECT() *MockVariablesMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockVariables) Create(ctx context.Context, workspaceID string, options tfe.VariableCreateOptions) (*tfe.Variable, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.Variable)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockVariablesMockRecorder) Create(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockVariables)(nil).Create), ctx, workspaceID, options)
}
// Delete mocks base method.
func (m *MockVariables) Delete(ctx context.Context, workspaceID, variableID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, workspaceID, variableID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockVariablesMockRecorder) Delete(ctx, workspaceID, variableID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockVariables)(nil).Delete), ctx, workspaceID, variableID)
}
// List mocks base method.
func (m *MockVariables) List(ctx context.Context, workspaceID string, options *tfe.VariableListOptions) (*tfe.VariableList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.VariableList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockVariablesMockRecorder) List(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockVariables)(nil).List), ctx, workspaceID, options)
}
// ListAll mocks base method.
func (m *MockVariables) ListAll(ctx context.Context, workspaceID string, options *tfe.VariableListOptions) (*tfe.VariableList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListAll", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.VariableList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListAll indicates an expected call of ListAll.
func (mr *MockVariablesMockRecorder) ListAll(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAll", reflect.TypeOf((*MockVariables)(nil).ListAll), ctx, workspaceID, options)
}
// Read mocks base method.
func (m *MockVariables) Read(ctx context.Context, workspaceID, variableID string) (*tfe.Variable, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, workspaceID, variableID)
ret0, _ := ret[0].(*tfe.Variable)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockVariablesMockRecorder) Read(ctx, workspaceID, variableID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockVariables)(nil).Read), ctx, workspaceID, variableID)
}
// Update mocks base method.
func (m *MockVariables) Update(ctx context.Context, workspaceID, variableID string, options tfe.VariableUpdateOptions) (*tfe.Variable, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, workspaceID, variableID, options)
ret0, _ := ret[0].(*tfe.Variable)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockVariablesMockRecorder) Update(ctx, workspaceID, variableID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockVariables)(nil).Update), ctx, workspaceID, variableID, options)
}
================================================
FILE: mocks/variable_set_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: variable_set.go
//
// Generated by this command:
//
// mockgen -source=variable_set.go -destination=mocks/variable_set_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockVariableSets is a mock of VariableSets interface.
type MockVariableSets struct {
ctrl *gomock.Controller
recorder *MockVariableSetsMockRecorder
}
// MockVariableSetsMockRecorder is the mock recorder for MockVariableSets.
type MockVariableSetsMockRecorder struct {
mock *MockVariableSets
}
// NewMockVariableSets creates a new mock instance.
func NewMockVariableSets(ctrl *gomock.Controller) *MockVariableSets {
mock := &MockVariableSets{ctrl: ctrl}
mock.recorder = &MockVariableSetsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockVariableSets) EXPECT() *MockVariableSetsMockRecorder {
return m.recorder
}
// ApplyToProjects mocks base method.
func (m *MockVariableSets) ApplyToProjects(ctx context.Context, variableSetID string, options tfe.VariableSetApplyToProjectsOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ApplyToProjects", ctx, variableSetID, options)
ret0, _ := ret[0].(error)
return ret0
}
// ApplyToProjects indicates an expected call of ApplyToProjects.
func (mr *MockVariableSetsMockRecorder) ApplyToProjects(ctx, variableSetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplyToProjects", reflect.TypeOf((*MockVariableSets)(nil).ApplyToProjects), ctx, variableSetID, options)
}
// ApplyToStacks mocks base method.
func (m *MockVariableSets) ApplyToStacks(ctx context.Context, variableSetID string, options *tfe.VariableSetApplyToStacksOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ApplyToStacks", ctx, variableSetID, options)
ret0, _ := ret[0].(error)
return ret0
}
// ApplyToStacks indicates an expected call of ApplyToStacks.
func (mr *MockVariableSetsMockRecorder) ApplyToStacks(ctx, variableSetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplyToStacks", reflect.TypeOf((*MockVariableSets)(nil).ApplyToStacks), ctx, variableSetID, options)
}
// ApplyToWorkspaces mocks base method.
func (m *MockVariableSets) ApplyToWorkspaces(ctx context.Context, variableSetID string, options *tfe.VariableSetApplyToWorkspacesOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ApplyToWorkspaces", ctx, variableSetID, options)
ret0, _ := ret[0].(error)
return ret0
}
// ApplyToWorkspaces indicates an expected call of ApplyToWorkspaces.
func (mr *MockVariableSetsMockRecorder) ApplyToWorkspaces(ctx, variableSetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplyToWorkspaces", reflect.TypeOf((*MockVariableSets)(nil).ApplyToWorkspaces), ctx, variableSetID, options)
}
// Create mocks base method.
func (m *MockVariableSets) Create(ctx context.Context, organization string, options *tfe.VariableSetCreateOptions) (*tfe.VariableSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, organization, options)
ret0, _ := ret[0].(*tfe.VariableSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockVariableSetsMockRecorder) Create(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockVariableSets)(nil).Create), ctx, organization, options)
}
// Delete mocks base method.
func (m *MockVariableSets) Delete(ctx context.Context, variableSetID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, variableSetID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockVariableSetsMockRecorder) Delete(ctx, variableSetID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockVariableSets)(nil).Delete), ctx, variableSetID)
}
// List mocks base method.
func (m *MockVariableSets) List(ctx context.Context, organization string, options *tfe.VariableSetListOptions) (*tfe.VariableSetList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, organization, options)
ret0, _ := ret[0].(*tfe.VariableSetList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockVariableSetsMockRecorder) List(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockVariableSets)(nil).List), ctx, organization, options)
}
// ListForProject mocks base method.
func (m *MockVariableSets) ListForProject(ctx context.Context, projectID string, options *tfe.VariableSetListOptions) (*tfe.VariableSetList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListForProject", ctx, projectID, options)
ret0, _ := ret[0].(*tfe.VariableSetList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListForProject indicates an expected call of ListForProject.
func (mr *MockVariableSetsMockRecorder) ListForProject(ctx, projectID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListForProject", reflect.TypeOf((*MockVariableSets)(nil).ListForProject), ctx, projectID, options)
}
// ListForWorkspace mocks base method.
func (m *MockVariableSets) ListForWorkspace(ctx context.Context, workspaceID string, options *tfe.VariableSetListOptions) (*tfe.VariableSetList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListForWorkspace", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.VariableSetList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListForWorkspace indicates an expected call of ListForWorkspace.
func (mr *MockVariableSetsMockRecorder) ListForWorkspace(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListForWorkspace", reflect.TypeOf((*MockVariableSets)(nil).ListForWorkspace), ctx, workspaceID, options)
}
// Read mocks base method.
func (m *MockVariableSets) Read(ctx context.Context, variableSetID string, options *tfe.VariableSetReadOptions) (*tfe.VariableSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, variableSetID, options)
ret0, _ := ret[0].(*tfe.VariableSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockVariableSetsMockRecorder) Read(ctx, variableSetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockVariableSets)(nil).Read), ctx, variableSetID, options)
}
// RemoveFromProjects mocks base method.
func (m *MockVariableSets) RemoveFromProjects(ctx context.Context, variableSetID string, options tfe.VariableSetRemoveFromProjectsOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveFromProjects", ctx, variableSetID, options)
ret0, _ := ret[0].(error)
return ret0
}
// RemoveFromProjects indicates an expected call of RemoveFromProjects.
func (mr *MockVariableSetsMockRecorder) RemoveFromProjects(ctx, variableSetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveFromProjects", reflect.TypeOf((*MockVariableSets)(nil).RemoveFromProjects), ctx, variableSetID, options)
}
// RemoveFromStacks mocks base method.
func (m *MockVariableSets) RemoveFromStacks(ctx context.Context, variableSetID string, options *tfe.VariableSetRemoveFromStacksOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveFromStacks", ctx, variableSetID, options)
ret0, _ := ret[0].(error)
return ret0
}
// RemoveFromStacks indicates an expected call of RemoveFromStacks.
func (mr *MockVariableSetsMockRecorder) RemoveFromStacks(ctx, variableSetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveFromStacks", reflect.TypeOf((*MockVariableSets)(nil).RemoveFromStacks), ctx, variableSetID, options)
}
// RemoveFromWorkspaces mocks base method.
func (m *MockVariableSets) RemoveFromWorkspaces(ctx context.Context, variableSetID string, options *tfe.VariableSetRemoveFromWorkspacesOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveFromWorkspaces", ctx, variableSetID, options)
ret0, _ := ret[0].(error)
return ret0
}
// RemoveFromWorkspaces indicates an expected call of RemoveFromWorkspaces.
func (mr *MockVariableSetsMockRecorder) RemoveFromWorkspaces(ctx, variableSetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveFromWorkspaces", reflect.TypeOf((*MockVariableSets)(nil).RemoveFromWorkspaces), ctx, variableSetID, options)
}
// Update mocks base method.
func (m *MockVariableSets) Update(ctx context.Context, variableSetID string, options *tfe.VariableSetUpdateOptions) (*tfe.VariableSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, variableSetID, options)
ret0, _ := ret[0].(*tfe.VariableSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockVariableSetsMockRecorder) Update(ctx, variableSetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockVariableSets)(nil).Update), ctx, variableSetID, options)
}
// UpdateStacks mocks base method.
func (m *MockVariableSets) UpdateStacks(ctx context.Context, variableSetID string, options *tfe.VariableSetUpdateStacksOptions) (*tfe.VariableSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateStacks", ctx, variableSetID, options)
ret0, _ := ret[0].(*tfe.VariableSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateStacks indicates an expected call of UpdateStacks.
func (mr *MockVariableSetsMockRecorder) UpdateStacks(ctx, variableSetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStacks", reflect.TypeOf((*MockVariableSets)(nil).UpdateStacks), ctx, variableSetID, options)
}
// UpdateWorkspaces mocks base method.
func (m *MockVariableSets) UpdateWorkspaces(ctx context.Context, variableSetID string, options *tfe.VariableSetUpdateWorkspacesOptions) (*tfe.VariableSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateWorkspaces", ctx, variableSetID, options)
ret0, _ := ret[0].(*tfe.VariableSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateWorkspaces indicates an expected call of UpdateWorkspaces.
func (mr *MockVariableSetsMockRecorder) UpdateWorkspaces(ctx, variableSetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaces", reflect.TypeOf((*MockVariableSets)(nil).UpdateWorkspaces), ctx, variableSetID, options)
}
================================================
FILE: mocks/variable_set_variable_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: variable_set_variable.go
//
// Generated by this command:
//
// mockgen -source=variable_set_variable.go -destination=mocks/variable_set_variable_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockVariableSetVariables is a mock of VariableSetVariables interface.
type MockVariableSetVariables struct {
ctrl *gomock.Controller
recorder *MockVariableSetVariablesMockRecorder
}
// MockVariableSetVariablesMockRecorder is the mock recorder for MockVariableSetVariables.
type MockVariableSetVariablesMockRecorder struct {
mock *MockVariableSetVariables
}
// NewMockVariableSetVariables creates a new mock instance.
func NewMockVariableSetVariables(ctrl *gomock.Controller) *MockVariableSetVariables {
mock := &MockVariableSetVariables{ctrl: ctrl}
mock.recorder = &MockVariableSetVariablesMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockVariableSetVariables) EXPECT() *MockVariableSetVariablesMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockVariableSetVariables) Create(ctx context.Context, variableSetID string, options *tfe.VariableSetVariableCreateOptions) (*tfe.VariableSetVariable, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, variableSetID, options)
ret0, _ := ret[0].(*tfe.VariableSetVariable)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockVariableSetVariablesMockRecorder) Create(ctx, variableSetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockVariableSetVariables)(nil).Create), ctx, variableSetID, options)
}
// Delete mocks base method.
func (m *MockVariableSetVariables) Delete(ctx context.Context, variableSetID, variableID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, variableSetID, variableID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockVariableSetVariablesMockRecorder) Delete(ctx, variableSetID, variableID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockVariableSetVariables)(nil).Delete), ctx, variableSetID, variableID)
}
// List mocks base method.
func (m *MockVariableSetVariables) List(ctx context.Context, variableSetID string, options *tfe.VariableSetVariableListOptions) (*tfe.VariableSetVariableList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, variableSetID, options)
ret0, _ := ret[0].(*tfe.VariableSetVariableList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockVariableSetVariablesMockRecorder) List(ctx, variableSetID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockVariableSetVariables)(nil).List), ctx, variableSetID, options)
}
// Read mocks base method.
func (m *MockVariableSetVariables) Read(ctx context.Context, variableSetID, variableID string) (*tfe.VariableSetVariable, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, variableSetID, variableID)
ret0, _ := ret[0].(*tfe.VariableSetVariable)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockVariableSetVariablesMockRecorder) Read(ctx, variableSetID, variableID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockVariableSetVariables)(nil).Read), ctx, variableSetID, variableID)
}
// Update mocks base method.
func (m *MockVariableSetVariables) Update(ctx context.Context, variableSetID, variableID string, options *tfe.VariableSetVariableUpdateOptions) (*tfe.VariableSetVariable, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, variableSetID, variableID, options)
ret0, _ := ret[0].(*tfe.VariableSetVariable)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockVariableSetVariablesMockRecorder) Update(ctx, variableSetID, variableID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockVariableSetVariables)(nil).Update), ctx, variableSetID, variableID, options)
}
================================================
FILE: mocks/workspace_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: workspace.go
//
// Generated by this command:
//
// mockgen -source=workspace.go -destination=mocks/workspace_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
io "io"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockWorkspaces is a mock of Workspaces interface.
type MockWorkspaces struct {
ctrl *gomock.Controller
recorder *MockWorkspacesMockRecorder
}
// MockWorkspacesMockRecorder is the mock recorder for MockWorkspaces.
type MockWorkspacesMockRecorder struct {
mock *MockWorkspaces
}
// NewMockWorkspaces creates a new mock instance.
func NewMockWorkspaces(ctrl *gomock.Controller) *MockWorkspaces {
mock := &MockWorkspaces{ctrl: ctrl}
mock.recorder = &MockWorkspacesMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockWorkspaces) EXPECT() *MockWorkspacesMockRecorder {
return m.recorder
}
// AddRemoteStateConsumers mocks base method.
func (m *MockWorkspaces) AddRemoteStateConsumers(ctx context.Context, workspaceID string, options tfe.WorkspaceAddRemoteStateConsumersOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddRemoteStateConsumers", ctx, workspaceID, options)
ret0, _ := ret[0].(error)
return ret0
}
// AddRemoteStateConsumers indicates an expected call of AddRemoteStateConsumers.
func (mr *MockWorkspacesMockRecorder) AddRemoteStateConsumers(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddRemoteStateConsumers", reflect.TypeOf((*MockWorkspaces)(nil).AddRemoteStateConsumers), ctx, workspaceID, options)
}
// AddTagBindings mocks base method.
func (m *MockWorkspaces) AddTagBindings(ctx context.Context, workspaceID string, options tfe.WorkspaceAddTagBindingsOptions) ([]*tfe.TagBinding, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddTagBindings", ctx, workspaceID, options)
ret0, _ := ret[0].([]*tfe.TagBinding)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AddTagBindings indicates an expected call of AddTagBindings.
func (mr *MockWorkspacesMockRecorder) AddTagBindings(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddTagBindings", reflect.TypeOf((*MockWorkspaces)(nil).AddTagBindings), ctx, workspaceID, options)
}
// AddTags mocks base method.
func (m *MockWorkspaces) AddTags(ctx context.Context, workspaceID string, options tfe.WorkspaceAddTagsOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddTags", ctx, workspaceID, options)
ret0, _ := ret[0].(error)
return ret0
}
// AddTags indicates an expected call of AddTags.
func (mr *MockWorkspacesMockRecorder) AddTags(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddTags", reflect.TypeOf((*MockWorkspaces)(nil).AddTags), ctx, workspaceID, options)
}
// AssignSSHKey mocks base method.
func (m *MockWorkspaces) AssignSSHKey(ctx context.Context, workspaceID string, options tfe.WorkspaceAssignSSHKeyOptions) (*tfe.Workspace, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AssignSSHKey", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.Workspace)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AssignSSHKey indicates an expected call of AssignSSHKey.
func (mr *MockWorkspacesMockRecorder) AssignSSHKey(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssignSSHKey", reflect.TypeOf((*MockWorkspaces)(nil).AssignSSHKey), ctx, workspaceID, options)
}
// Create mocks base method.
func (m *MockWorkspaces) Create(ctx context.Context, organization string, options tfe.WorkspaceCreateOptions) (*tfe.Workspace, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, organization, options)
ret0, _ := ret[0].(*tfe.Workspace)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockWorkspacesMockRecorder) Create(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockWorkspaces)(nil).Create), ctx, organization, options)
}
// Delete mocks base method.
func (m *MockWorkspaces) Delete(ctx context.Context, organization, workspace string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, organization, workspace)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockWorkspacesMockRecorder) Delete(ctx, organization, workspace any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockWorkspaces)(nil).Delete), ctx, organization, workspace)
}
// DeleteAllTagBindings mocks base method.
func (m *MockWorkspaces) DeleteAllTagBindings(ctx context.Context, workspaceID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteAllTagBindings", ctx, workspaceID)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteAllTagBindings indicates an expected call of DeleteAllTagBindings.
func (mr *MockWorkspacesMockRecorder) DeleteAllTagBindings(ctx, workspaceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAllTagBindings", reflect.TypeOf((*MockWorkspaces)(nil).DeleteAllTagBindings), ctx, workspaceID)
}
// DeleteByID mocks base method.
func (m *MockWorkspaces) DeleteByID(ctx context.Context, workspaceID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteByID", ctx, workspaceID)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteByID indicates an expected call of DeleteByID.
func (mr *MockWorkspacesMockRecorder) DeleteByID(ctx, workspaceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteByID", reflect.TypeOf((*MockWorkspaces)(nil).DeleteByID), ctx, workspaceID)
}
// DeleteDataRetentionPolicy mocks base method.
func (m *MockWorkspaces) DeleteDataRetentionPolicy(ctx context.Context, workspaceID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteDataRetentionPolicy", ctx, workspaceID)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteDataRetentionPolicy indicates an expected call of DeleteDataRetentionPolicy.
func (mr *MockWorkspacesMockRecorder) DeleteDataRetentionPolicy(ctx, workspaceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDataRetentionPolicy", reflect.TypeOf((*MockWorkspaces)(nil).DeleteDataRetentionPolicy), ctx, workspaceID)
}
// ForceUnlock mocks base method.
func (m *MockWorkspaces) ForceUnlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ForceUnlock", ctx, workspaceID)
ret0, _ := ret[0].(*tfe.Workspace)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ForceUnlock indicates an expected call of ForceUnlock.
func (mr *MockWorkspacesMockRecorder) ForceUnlock(ctx, workspaceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ForceUnlock", reflect.TypeOf((*MockWorkspaces)(nil).ForceUnlock), ctx, workspaceID)
}
// List mocks base method.
func (m *MockWorkspaces) List(ctx context.Context, organization string, options *tfe.WorkspaceListOptions) (*tfe.WorkspaceList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, organization, options)
ret0, _ := ret[0].(*tfe.WorkspaceList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockWorkspacesMockRecorder) List(ctx, organization, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockWorkspaces)(nil).List), ctx, organization, options)
}
// ListEffectiveTagBindings mocks base method.
func (m *MockWorkspaces) ListEffectiveTagBindings(ctx context.Context, workspaceID string) ([]*tfe.EffectiveTagBinding, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListEffectiveTagBindings", ctx, workspaceID)
ret0, _ := ret[0].([]*tfe.EffectiveTagBinding)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListEffectiveTagBindings indicates an expected call of ListEffectiveTagBindings.
func (mr *MockWorkspacesMockRecorder) ListEffectiveTagBindings(ctx, workspaceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListEffectiveTagBindings", reflect.TypeOf((*MockWorkspaces)(nil).ListEffectiveTagBindings), ctx, workspaceID)
}
// ListRemoteStateConsumers mocks base method.
func (m *MockWorkspaces) ListRemoteStateConsumers(ctx context.Context, workspaceID string, options *tfe.RemoteStateConsumersListOptions) (*tfe.WorkspaceList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListRemoteStateConsumers", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.WorkspaceList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListRemoteStateConsumers indicates an expected call of ListRemoteStateConsumers.
func (mr *MockWorkspacesMockRecorder) ListRemoteStateConsumers(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRemoteStateConsumers", reflect.TypeOf((*MockWorkspaces)(nil).ListRemoteStateConsumers), ctx, workspaceID, options)
}
// ListTagBindings mocks base method.
func (m *MockWorkspaces) ListTagBindings(ctx context.Context, workspaceID string) ([]*tfe.TagBinding, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListTagBindings", ctx, workspaceID)
ret0, _ := ret[0].([]*tfe.TagBinding)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListTagBindings indicates an expected call of ListTagBindings.
func (mr *MockWorkspacesMockRecorder) ListTagBindings(ctx, workspaceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTagBindings", reflect.TypeOf((*MockWorkspaces)(nil).ListTagBindings), ctx, workspaceID)
}
// ListTags mocks base method.
func (m *MockWorkspaces) ListTags(ctx context.Context, workspaceID string, options *tfe.WorkspaceTagListOptions) (*tfe.TagList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListTags", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.TagList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListTags indicates an expected call of ListTags.
func (mr *MockWorkspacesMockRecorder) ListTags(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTags", reflect.TypeOf((*MockWorkspaces)(nil).ListTags), ctx, workspaceID, options)
}
// Lock mocks base method.
func (m *MockWorkspaces) Lock(ctx context.Context, workspaceID string, options tfe.WorkspaceLockOptions) (*tfe.Workspace, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Lock", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.Workspace)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Lock indicates an expected call of Lock.
func (mr *MockWorkspacesMockRecorder) Lock(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Lock", reflect.TypeOf((*MockWorkspaces)(nil).Lock), ctx, workspaceID, options)
}
// Read mocks base method.
func (m *MockWorkspaces) Read(ctx context.Context, organization, workspace string) (*tfe.Workspace, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, organization, workspace)
ret0, _ := ret[0].(*tfe.Workspace)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockWorkspacesMockRecorder) Read(ctx, organization, workspace any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockWorkspaces)(nil).Read), ctx, organization, workspace)
}
// ReadByID mocks base method.
func (m *MockWorkspaces) ReadByID(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadByID", ctx, workspaceID)
ret0, _ := ret[0].(*tfe.Workspace)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadByID indicates an expected call of ReadByID.
func (mr *MockWorkspacesMockRecorder) ReadByID(ctx, workspaceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadByID", reflect.TypeOf((*MockWorkspaces)(nil).ReadByID), ctx, workspaceID)
}
// ReadByIDWithOptions mocks base method.
func (m *MockWorkspaces) ReadByIDWithOptions(ctx context.Context, workspaceID string, options *tfe.WorkspaceReadOptions) (*tfe.Workspace, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadByIDWithOptions", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.Workspace)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadByIDWithOptions indicates an expected call of ReadByIDWithOptions.
func (mr *MockWorkspacesMockRecorder) ReadByIDWithOptions(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadByIDWithOptions", reflect.TypeOf((*MockWorkspaces)(nil).ReadByIDWithOptions), ctx, workspaceID, options)
}
// ReadDataRetentionPolicy mocks base method.
func (m *MockWorkspaces) ReadDataRetentionPolicy(ctx context.Context, workspaceID string) (*tfe.DataRetentionPolicy, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadDataRetentionPolicy", ctx, workspaceID)
ret0, _ := ret[0].(*tfe.DataRetentionPolicy)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadDataRetentionPolicy indicates an expected call of ReadDataRetentionPolicy.
func (mr *MockWorkspacesMockRecorder) ReadDataRetentionPolicy(ctx, workspaceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadDataRetentionPolicy", reflect.TypeOf((*MockWorkspaces)(nil).ReadDataRetentionPolicy), ctx, workspaceID)
}
// ReadDataRetentionPolicyChoice mocks base method.
func (m *MockWorkspaces) ReadDataRetentionPolicyChoice(ctx context.Context, workspaceID string) (*tfe.DataRetentionPolicyChoice, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadDataRetentionPolicyChoice", ctx, workspaceID)
ret0, _ := ret[0].(*tfe.DataRetentionPolicyChoice)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadDataRetentionPolicyChoice indicates an expected call of ReadDataRetentionPolicyChoice.
func (mr *MockWorkspacesMockRecorder) ReadDataRetentionPolicyChoice(ctx, workspaceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadDataRetentionPolicyChoice", reflect.TypeOf((*MockWorkspaces)(nil).ReadDataRetentionPolicyChoice), ctx, workspaceID)
}
// ReadWithOptions mocks base method.
func (m *MockWorkspaces) ReadWithOptions(ctx context.Context, organization, workspace string, options *tfe.WorkspaceReadOptions) (*tfe.Workspace, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadWithOptions", ctx, organization, workspace, options)
ret0, _ := ret[0].(*tfe.Workspace)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadWithOptions indicates an expected call of ReadWithOptions.
func (mr *MockWorkspacesMockRecorder) ReadWithOptions(ctx, organization, workspace, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadWithOptions", reflect.TypeOf((*MockWorkspaces)(nil).ReadWithOptions), ctx, organization, workspace, options)
}
// Readme mocks base method.
func (m *MockWorkspaces) Readme(ctx context.Context, workspaceID string) (io.Reader, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Readme", ctx, workspaceID)
ret0, _ := ret[0].(io.Reader)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Readme indicates an expected call of Readme.
func (mr *MockWorkspacesMockRecorder) Readme(ctx, workspaceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Readme", reflect.TypeOf((*MockWorkspaces)(nil).Readme), ctx, workspaceID)
}
// RemoveRemoteStateConsumers mocks base method.
func (m *MockWorkspaces) RemoveRemoteStateConsumers(ctx context.Context, workspaceID string, options tfe.WorkspaceRemoveRemoteStateConsumersOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveRemoteStateConsumers", ctx, workspaceID, options)
ret0, _ := ret[0].(error)
return ret0
}
// RemoveRemoteStateConsumers indicates an expected call of RemoveRemoteStateConsumers.
func (mr *MockWorkspacesMockRecorder) RemoveRemoteStateConsumers(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveRemoteStateConsumers", reflect.TypeOf((*MockWorkspaces)(nil).RemoveRemoteStateConsumers), ctx, workspaceID, options)
}
// RemoveTags mocks base method.
func (m *MockWorkspaces) RemoveTags(ctx context.Context, workspaceID string, options tfe.WorkspaceRemoveTagsOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveTags", ctx, workspaceID, options)
ret0, _ := ret[0].(error)
return ret0
}
// RemoveTags indicates an expected call of RemoveTags.
func (mr *MockWorkspacesMockRecorder) RemoveTags(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveTags", reflect.TypeOf((*MockWorkspaces)(nil).RemoveTags), ctx, workspaceID, options)
}
// RemoveVCSConnection mocks base method.
func (m *MockWorkspaces) RemoveVCSConnection(ctx context.Context, organization, workspace string) (*tfe.Workspace, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveVCSConnection", ctx, organization, workspace)
ret0, _ := ret[0].(*tfe.Workspace)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// RemoveVCSConnection indicates an expected call of RemoveVCSConnection.
func (mr *MockWorkspacesMockRecorder) RemoveVCSConnection(ctx, organization, workspace any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveVCSConnection", reflect.TypeOf((*MockWorkspaces)(nil).RemoveVCSConnection), ctx, organization, workspace)
}
// RemoveVCSConnectionByID mocks base method.
func (m *MockWorkspaces) RemoveVCSConnectionByID(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveVCSConnectionByID", ctx, workspaceID)
ret0, _ := ret[0].(*tfe.Workspace)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// RemoveVCSConnectionByID indicates an expected call of RemoveVCSConnectionByID.
func (mr *MockWorkspacesMockRecorder) RemoveVCSConnectionByID(ctx, workspaceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveVCSConnectionByID", reflect.TypeOf((*MockWorkspaces)(nil).RemoveVCSConnectionByID), ctx, workspaceID)
}
// SafeDelete mocks base method.
func (m *MockWorkspaces) SafeDelete(ctx context.Context, organization, workspace string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SafeDelete", ctx, organization, workspace)
ret0, _ := ret[0].(error)
return ret0
}
// SafeDelete indicates an expected call of SafeDelete.
func (mr *MockWorkspacesMockRecorder) SafeDelete(ctx, organization, workspace any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SafeDelete", reflect.TypeOf((*MockWorkspaces)(nil).SafeDelete), ctx, organization, workspace)
}
// SafeDeleteByID mocks base method.
func (m *MockWorkspaces) SafeDeleteByID(ctx context.Context, workspaceID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SafeDeleteByID", ctx, workspaceID)
ret0, _ := ret[0].(error)
return ret0
}
// SafeDeleteByID indicates an expected call of SafeDeleteByID.
func (mr *MockWorkspacesMockRecorder) SafeDeleteByID(ctx, workspaceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SafeDeleteByID", reflect.TypeOf((*MockWorkspaces)(nil).SafeDeleteByID), ctx, workspaceID)
}
// SetDataRetentionPolicy mocks base method.
func (m *MockWorkspaces) SetDataRetentionPolicy(ctx context.Context, workspaceID string, options tfe.DataRetentionPolicySetOptions) (*tfe.DataRetentionPolicy, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetDataRetentionPolicy", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.DataRetentionPolicy)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SetDataRetentionPolicy indicates an expected call of SetDataRetentionPolicy.
func (mr *MockWorkspacesMockRecorder) SetDataRetentionPolicy(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDataRetentionPolicy", reflect.TypeOf((*MockWorkspaces)(nil).SetDataRetentionPolicy), ctx, workspaceID, options)
}
// SetDataRetentionPolicyDeleteOlder mocks base method.
func (m *MockWorkspaces) SetDataRetentionPolicyDeleteOlder(ctx context.Context, workspaceID string, options tfe.DataRetentionPolicyDeleteOlderSetOptions) (*tfe.DataRetentionPolicyDeleteOlder, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetDataRetentionPolicyDeleteOlder", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.DataRetentionPolicyDeleteOlder)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SetDataRetentionPolicyDeleteOlder indicates an expected call of SetDataRetentionPolicyDeleteOlder.
func (mr *MockWorkspacesMockRecorder) SetDataRetentionPolicyDeleteOlder(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDataRetentionPolicyDeleteOlder", reflect.TypeOf((*MockWorkspaces)(nil).SetDataRetentionPolicyDeleteOlder), ctx, workspaceID, options)
}
// SetDataRetentionPolicyDontDelete mocks base method.
func (m *MockWorkspaces) SetDataRetentionPolicyDontDelete(ctx context.Context, workspaceID string, options tfe.DataRetentionPolicyDontDeleteSetOptions) (*tfe.DataRetentionPolicyDontDelete, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetDataRetentionPolicyDontDelete", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.DataRetentionPolicyDontDelete)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SetDataRetentionPolicyDontDelete indicates an expected call of SetDataRetentionPolicyDontDelete.
func (mr *MockWorkspacesMockRecorder) SetDataRetentionPolicyDontDelete(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDataRetentionPolicyDontDelete", reflect.TypeOf((*MockWorkspaces)(nil).SetDataRetentionPolicyDontDelete), ctx, workspaceID, options)
}
// UnassignSSHKey mocks base method.
func (m *MockWorkspaces) UnassignSSHKey(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UnassignSSHKey", ctx, workspaceID)
ret0, _ := ret[0].(*tfe.Workspace)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UnassignSSHKey indicates an expected call of UnassignSSHKey.
func (mr *MockWorkspacesMockRecorder) UnassignSSHKey(ctx, workspaceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnassignSSHKey", reflect.TypeOf((*MockWorkspaces)(nil).UnassignSSHKey), ctx, workspaceID)
}
// Unlock mocks base method.
func (m *MockWorkspaces) Unlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Unlock", ctx, workspaceID)
ret0, _ := ret[0].(*tfe.Workspace)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Unlock indicates an expected call of Unlock.
func (mr *MockWorkspacesMockRecorder) Unlock(ctx, workspaceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unlock", reflect.TypeOf((*MockWorkspaces)(nil).Unlock), ctx, workspaceID)
}
// Update mocks base method.
func (m *MockWorkspaces) Update(ctx context.Context, organization, workspace string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, organization, workspace, options)
ret0, _ := ret[0].(*tfe.Workspace)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockWorkspacesMockRecorder) Update(ctx, organization, workspace, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockWorkspaces)(nil).Update), ctx, organization, workspace, options)
}
// UpdateByID mocks base method.
func (m *MockWorkspaces) UpdateByID(ctx context.Context, workspaceID string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateByID", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.Workspace)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateByID indicates an expected call of UpdateByID.
func (mr *MockWorkspacesMockRecorder) UpdateByID(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateByID", reflect.TypeOf((*MockWorkspaces)(nil).UpdateByID), ctx, workspaceID, options)
}
// UpdateRemoteStateConsumers mocks base method.
func (m *MockWorkspaces) UpdateRemoteStateConsumers(ctx context.Context, workspaceID string, options tfe.WorkspaceUpdateRemoteStateConsumersOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateRemoteStateConsumers", ctx, workspaceID, options)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateRemoteStateConsumers indicates an expected call of UpdateRemoteStateConsumers.
func (mr *MockWorkspacesMockRecorder) UpdateRemoteStateConsumers(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateRemoteStateConsumers", reflect.TypeOf((*MockWorkspaces)(nil).UpdateRemoteStateConsumers), ctx, workspaceID, options)
}
================================================
FILE: mocks/workspace_resources.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: workspace_resources.go
//
// Generated by this command:
//
// mockgen -source=workspace_resources.go -destination=mocks/workspace_resources.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockWorkspaceResources is a mock of WorkspaceResources interface.
type MockWorkspaceResources struct {
ctrl *gomock.Controller
recorder *MockWorkspaceResourcesMockRecorder
}
// MockWorkspaceResourcesMockRecorder is the mock recorder for MockWorkspaceResources.
type MockWorkspaceResourcesMockRecorder struct {
mock *MockWorkspaceResources
}
// NewMockWorkspaceResources creates a new mock instance.
func NewMockWorkspaceResources(ctrl *gomock.Controller) *MockWorkspaceResources {
mock := &MockWorkspaceResources{ctrl: ctrl}
mock.recorder = &MockWorkspaceResourcesMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockWorkspaceResources) EXPECT() *MockWorkspaceResourcesMockRecorder {
return m.recorder
}
// List mocks base method.
func (m *MockWorkspaceResources) List(ctx context.Context, workspaceID string, options *tfe.WorkspaceResourceListOptions) (*tfe.WorkspaceResourcesList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.WorkspaceResourcesList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockWorkspaceResourcesMockRecorder) List(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockWorkspaceResources)(nil).List), ctx, workspaceID, options)
}
================================================
FILE: mocks/workspace_run_tasks_mocks.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: workspace_run_task.go
//
// Generated by this command:
//
// mockgen -source=workspace_run_task.go -destination=mocks/workspace_run_tasks_mocks.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
tfe "github.com/hashicorp/go-tfe"
gomock "go.uber.org/mock/gomock"
)
// MockWorkspaceRunTasks is a mock of WorkspaceRunTasks interface.
type MockWorkspaceRunTasks struct {
ctrl *gomock.Controller
recorder *MockWorkspaceRunTasksMockRecorder
}
// MockWorkspaceRunTasksMockRecorder is the mock recorder for MockWorkspaceRunTasks.
type MockWorkspaceRunTasksMockRecorder struct {
mock *MockWorkspaceRunTasks
}
// NewMockWorkspaceRunTasks creates a new mock instance.
func NewMockWorkspaceRunTasks(ctrl *gomock.Controller) *MockWorkspaceRunTasks {
mock := &MockWorkspaceRunTasks{ctrl: ctrl}
mock.recorder = &MockWorkspaceRunTasksMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockWorkspaceRunTasks) EXPECT() *MockWorkspaceRunTasksMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockWorkspaceRunTasks) Create(ctx context.Context, workspaceID string, options tfe.WorkspaceRunTaskCreateOptions) (*tfe.WorkspaceRunTask, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.WorkspaceRunTask)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockWorkspaceRunTasksMockRecorder) Create(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockWorkspaceRunTasks)(nil).Create), ctx, workspaceID, options)
}
// Delete mocks base method.
func (m *MockWorkspaceRunTasks) Delete(ctx context.Context, workspaceID, workspaceTaskID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, workspaceID, workspaceTaskID)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockWorkspaceRunTasksMockRecorder) Delete(ctx, workspaceID, workspaceTaskID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockWorkspaceRunTasks)(nil).Delete), ctx, workspaceID, workspaceTaskID)
}
// List mocks base method.
func (m *MockWorkspaceRunTasks) List(ctx context.Context, workspaceID string, options *tfe.WorkspaceRunTaskListOptions) (*tfe.WorkspaceRunTaskList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, workspaceID, options)
ret0, _ := ret[0].(*tfe.WorkspaceRunTaskList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockWorkspaceRunTasksMockRecorder) List(ctx, workspaceID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockWorkspaceRunTasks)(nil).List), ctx, workspaceID, options)
}
// Read mocks base method.
func (m *MockWorkspaceRunTasks) Read(ctx context.Context, workspaceID, workspaceTaskID string) (*tfe.WorkspaceRunTask, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", ctx, workspaceID, workspaceTaskID)
ret0, _ := ret[0].(*tfe.WorkspaceRunTask)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockWorkspaceRunTasksMockRecorder) Read(ctx, workspaceID, workspaceTaskID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockWorkspaceRunTasks)(nil).Read), ctx, workspaceID, workspaceTaskID)
}
// Update mocks base method.
func (m *MockWorkspaceRunTasks) Update(ctx context.Context, workspaceID, workspaceTaskID string, options tfe.WorkspaceRunTaskUpdateOptions) (*tfe.WorkspaceRunTask, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, workspaceID, workspaceTaskID, options)
ret0, _ := ret[0].(*tfe.WorkspaceRunTask)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockWorkspaceRunTasksMockRecorder) Update(ctx, workspaceID, workspaceTaskID, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockWorkspaceRunTasks)(nil).Update), ctx, workspaceID, workspaceTaskID, options)
}
================================================
FILE: notification_configuration.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ NotificationConfigurations = (*notificationConfigurations)(nil)
// NotificationConfigurations describes all the Notification Configuration
// related methods that the Terraform Enterprise API supports.
//
// TFE API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/notification-configurations
type NotificationConfigurations interface {
// List all the notification configurations within a workspace.
List(ctx context.Context, subscribableID string, options *NotificationConfigurationListOptions) (*NotificationConfigurationList, error)
// Create a new notification configuration with the given options.
Create(ctx context.Context, subscribableID string, options NotificationConfigurationCreateOptions) (*NotificationConfiguration, error)
// Read a notification configuration by its ID.
Read(ctx context.Context, notificationConfigurationID string) (*NotificationConfiguration, error)
// Update an existing notification configuration.
Update(ctx context.Context, notificationConfigurationID string, options NotificationConfigurationUpdateOptions) (*NotificationConfiguration, error)
// Delete a notification configuration by its ID.
Delete(ctx context.Context, notificationConfigurationID string) error
// Verify a notification configuration by its ID.
Verify(ctx context.Context, notificationConfigurationID string) (*NotificationConfiguration, error)
}
// notificationConfigurations implements NotificationConfigurations.
type notificationConfigurations struct {
client *Client
}
// NotificationTriggerType represents the different TFE notifications that can be sent
// as a run's progress transitions between different states
type NotificationTriggerType string
const (
NotificationTriggerCreated NotificationTriggerType = "run:created"
NotificationTriggerPlanning NotificationTriggerType = "run:planning"
NotificationTriggerNeedsAttention NotificationTriggerType = "run:needs_attention"
NotificationTriggerApplying NotificationTriggerType = "run:applying"
NotificationTriggerCompleted NotificationTriggerType = "run:completed"
NotificationTriggerErrored NotificationTriggerType = "run:errored"
NotificationTriggerAssessmentDrifted NotificationTriggerType = "assessment:drifted"
NotificationTriggerAssessmentFailed NotificationTriggerType = "assessment:failed"
NotificationTriggerAssessmentCheckFailed NotificationTriggerType = "assessment:check_failure"
NotificationTriggerWorkspaceAutoDestroyReminder NotificationTriggerType = "workspace:auto_destroy_reminder"
NotificationTriggerWorkspaceAutoDestroyRunResults NotificationTriggerType = "workspace:auto_destroy_run_results"
NotificationTriggerChangeRequestCreated NotificationTriggerType = "change_request:created"
)
// NotificationDestinationType represents the destination type of the
// notification configuration.
type NotificationDestinationType string
// List of available notification destination types.
const (
NotificationDestinationTypeEmail NotificationDestinationType = "email"
NotificationDestinationTypeGeneric NotificationDestinationType = "generic"
NotificationDestinationTypeSlack NotificationDestinationType = "slack"
NotificationDestinationTypeMicrosoftTeams NotificationDestinationType = "microsoft-teams"
)
// NotificationConfigurationList represents a list of Notification
// Configurations.
type NotificationConfigurationList struct {
*Pagination
Items []*NotificationConfiguration
}
// NotificationConfigurationSubscribableChoice is a choice type struct that represents the possible values
// within a polymorphic relation. If a value is available, exactly one field
// will be non-nil.
type NotificationConfigurationSubscribableChoice struct {
Team *Team
Workspace *Workspace
}
// NotificationConfiguration represents a Notification Configuration.
type NotificationConfiguration struct {
ID string `jsonapi:"primary,notification-configurations"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
DeliveryResponses []*DeliveryResponse `jsonapi:"attr,delivery-responses"`
DestinationType NotificationDestinationType `jsonapi:"attr,destination-type"`
Enabled bool `jsonapi:"attr,enabled"`
Name string `jsonapi:"attr,name"`
Token string `jsonapi:"attr,token"`
Triggers []string `jsonapi:"attr,triggers"`
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
URL string `jsonapi:"attr,url"`
// EmailAddresses is only available for TFE users. It is not available in HCP Terraform.
EmailAddresses []string `jsonapi:"attr,email-addresses"`
// Relations
// DEPRECATED. The subscribable field is polymorphic. Use NotificationConfigurationSubscribableChoice instead.
Subscribable *Workspace `jsonapi:"relation,subscribable,omitempty"`
SubscribableChoice *NotificationConfigurationSubscribableChoice `jsonapi:"polyrelation,subscribable"`
EmailUsers []*User `jsonapi:"relation,users"`
}
// DeliveryResponse represents a notification configuration delivery response.
type DeliveryResponse struct {
Body string `jsonapi:"attr,body"`
Code string `jsonapi:"attr,code"`
Headers map[string][]string `jsonapi:"attr,headers"`
SentAt time.Time `jsonapi:"attr,sent-at,rfc3339"`
Successful string `jsonapi:"attr,successful"`
URL string `jsonapi:"attr,url"`
}
// NotificationConfigurationListOptions represents the options for listing
// notification configurations.
type NotificationConfigurationListOptions struct {
ListOptions
SubscribableChoice *NotificationConfigurationSubscribableChoice
}
// NotificationConfigurationCreateOptions represents the options for
// creating a new notification configuration.
type NotificationConfigurationCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,notification-configurations"`
// Required: The destination type of the notification configuration
DestinationType *NotificationDestinationType `jsonapi:"attr,destination-type"`
// Required: Whether the notification configuration should be enabled or not
Enabled *bool `jsonapi:"attr,enabled"`
// Required: The name of the notification configuration
Name *string `jsonapi:"attr,name"`
// Optional: The token of the notification configuration
Token *string `jsonapi:"attr,token,omitempty"`
// Optional: The list of run events that will trigger notifications.
Triggers []NotificationTriggerType `jsonapi:"attr,triggers,omitempty"`
// Optional: The url of the notification configuration
URL *string `jsonapi:"attr,url,omitempty"`
// Optional: The list of email addresses that will receive notification emails.
// EmailAddresses is only available for TFE users. It is not available in HCP Terraform.
EmailAddresses []string `jsonapi:"attr,email-addresses,omitempty"`
// Optional: The list of users belonging to the organization that will receive notification emails.
EmailUsers []*User `jsonapi:"relation,users,omitempty"`
// Required: The workspace or team that the notification configuration is associated with.
SubscribableChoice *NotificationConfigurationSubscribableChoice `jsonapi:"polyrelation,subscribable,omitempty"`
}
// NotificationConfigurationUpdateOptions represents the options for
// updating a existing notification configuration.
type NotificationConfigurationUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,notification-configurations"`
// Optional: Whether the notification configuration should be enabled or not
Enabled *bool `jsonapi:"attr,enabled,omitempty"`
// Optional: The name of the notification configuration
Name *string `jsonapi:"attr,name,omitempty"`
// Optional: The token of the notification configuration
Token *string `jsonapi:"attr,token,omitempty"`
// Optional: The list of run events that will trigger notifications.
Triggers []NotificationTriggerType `jsonapi:"attr,triggers,omitempty"`
// Optional: The url of the notification configuration
URL *string `jsonapi:"attr,url,omitempty"`
// Optional: The list of email addresses that will receive notification emails.
// EmailAddresses is only available for TFE users. It is not available in HCP Terraform.
EmailAddresses []string `jsonapi:"attr,email-addresses,omitempty"`
// Optional: The list of users belonging to the organization that will receive notification emails.
EmailUsers []*User `jsonapi:"relation,users,omitempty"`
}
// List all the notification configurations associated with a workspace.
func (s *notificationConfigurations) List(ctx context.Context, subscribableID string, options *NotificationConfigurationListOptions) (*NotificationConfigurationList, error) {
var u string
if options == nil {
options = &NotificationConfigurationListOptions{
SubscribableChoice: &NotificationConfigurationSubscribableChoice{
Workspace: &Workspace{ID: subscribableID},
},
}
} else if options.SubscribableChoice == nil {
options.SubscribableChoice = &NotificationConfigurationSubscribableChoice{
Workspace: &Workspace{ID: subscribableID},
}
}
if options.SubscribableChoice.Team != nil {
if !validStringID(&subscribableID) {
return nil, ErrInvalidTeamID
}
u = fmt.Sprintf("teams/%s/notification-configurations", url.PathEscape(subscribableID))
} else {
if !validStringID(&subscribableID) {
return nil, ErrInvalidWorkspaceID
}
u = fmt.Sprintf("workspaces/%s/notification-configurations", url.PathEscape(subscribableID))
}
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
ncl := &NotificationConfigurationList{}
err = req.Do(ctx, ncl)
if err != nil {
return nil, err
}
for i := range ncl.Items {
backfillDeprecatedSubscribable(ncl.Items[i])
}
return ncl, nil
}
// Create a notification configuration with the given options.
func (s *notificationConfigurations) Create(ctx context.Context, subscribableID string, options NotificationConfigurationCreateOptions) (*NotificationConfiguration, error) {
var u string
var subscribableChoice *NotificationConfigurationSubscribableChoice
if options.SubscribableChoice == nil || options.SubscribableChoice.Team == nil {
u = fmt.Sprintf("workspaces/%s/notification-configurations", url.PathEscape(subscribableID))
options.SubscribableChoice = &NotificationConfigurationSubscribableChoice{Workspace: &Workspace{ID: subscribableID}}
} else {
u = fmt.Sprintf("teams/%s/notification-configurations", url.PathEscape(subscribableID))
options.SubscribableChoice = &NotificationConfigurationSubscribableChoice{Team: &Team{ID: subscribableID}}
}
if err := options.valid(); err != nil {
return nil, err
}
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
nc := &NotificationConfiguration{SubscribableChoice: subscribableChoice}
err = req.Do(ctx, nc)
if err != nil {
return nil, err
}
backfillDeprecatedSubscribable(nc)
return nc, nil
}
// Read a notification configuration by its ID.
func (s *notificationConfigurations) Read(ctx context.Context, notificationConfigurationID string) (*NotificationConfiguration, error) {
if !validStringID(¬ificationConfigurationID) {
return nil, ErrInvalidNotificationConfigID
}
u := fmt.Sprintf("notification-configurations/%s", url.PathEscape(notificationConfigurationID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
nc := &NotificationConfiguration{}
err = req.Do(ctx, nc)
if err != nil {
return nil, err
}
backfillDeprecatedSubscribable(nc)
return nc, nil
}
// Updates a notification configuration with the given options.
func (s *notificationConfigurations) Update(ctx context.Context, notificationConfigurationID string, options NotificationConfigurationUpdateOptions) (*NotificationConfiguration, error) {
if !validStringID(¬ificationConfigurationID) {
return nil, ErrInvalidNotificationConfigID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("notification-configurations/%s", url.PathEscape(notificationConfigurationID))
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
nc := &NotificationConfiguration{}
err = req.Do(ctx, nc)
if err != nil {
return nil, err
}
backfillDeprecatedSubscribable(nc)
return nc, nil
}
// Delete a notifications configuration by its ID.
func (s *notificationConfigurations) Delete(ctx context.Context, notificationConfigurationID string) error {
if !validStringID(¬ificationConfigurationID) {
return ErrInvalidNotificationConfigID
}
u := fmt.Sprintf("notification-configurations/%s", url.PathEscape(notificationConfigurationID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// Verify a notification configuration by delivering a verification
// payload to the configured url.
func (s *notificationConfigurations) Verify(ctx context.Context, notificationConfigurationID string) (*NotificationConfiguration, error) {
if !validStringID(¬ificationConfigurationID) {
return nil, ErrInvalidNotificationConfigID
}
u := fmt.Sprintf(
"notification-configurations/%s/actions/verify", url.PathEscape(notificationConfigurationID))
req, err := s.client.NewRequest("POST", u, nil)
if err != nil {
return nil, err
}
nc := &NotificationConfiguration{}
err = req.Do(ctx, nc)
if err != nil {
return nil, err
}
return nc, nil
}
func (o NotificationConfigurationCreateOptions) valid() error {
if o.SubscribableChoice == nil || o.SubscribableChoice.Workspace != nil {
if !validStringID(&o.SubscribableChoice.Workspace.ID) {
return ErrInvalidWorkspaceID
}
} else {
if !validStringID(&o.SubscribableChoice.Team.ID) {
return ErrInvalidTeamID
}
}
if o.DestinationType == nil {
return ErrRequiredDestinationType
}
if o.Enabled == nil {
return ErrRequiredEnabled
}
if !validString(o.Name) {
return ErrRequiredName
}
if !validNotificationTriggerType(o.Triggers) {
return ErrInvalidNotificationTrigger
}
if *o.DestinationType == NotificationDestinationTypeGeneric ||
*o.DestinationType == NotificationDestinationTypeSlack ||
*o.DestinationType == NotificationDestinationTypeMicrosoftTeams {
if o.URL == nil {
return ErrRequiredURL
}
}
return nil
}
func (o NotificationConfigurationUpdateOptions) valid() error {
if o.Name != nil && !validString(o.Name) {
return ErrRequiredName
}
if !validNotificationTriggerType(o.Triggers) {
return ErrInvalidNotificationTrigger
}
return nil
}
func backfillDeprecatedSubscribable(notification *NotificationConfiguration) {
if notification.Subscribable != nil || notification.SubscribableChoice == nil {
return
}
if notification.SubscribableChoice.Workspace != nil {
notification.Subscribable = notification.SubscribableChoice.Workspace
}
}
func validNotificationTriggerType(triggers []NotificationTriggerType) bool {
for _, t := range triggers {
switch t {
case NotificationTriggerApplying,
NotificationTriggerNeedsAttention,
NotificationTriggerCompleted,
NotificationTriggerCreated,
NotificationTriggerErrored,
NotificationTriggerPlanning,
NotificationTriggerAssessmentDrifted,
NotificationTriggerAssessmentFailed,
NotificationTriggerWorkspaceAutoDestroyReminder,
NotificationTriggerWorkspaceAutoDestroyRunResults,
NotificationTriggerChangeRequestCreated,
NotificationTriggerAssessmentCheckFailed:
continue
default:
return false
}
}
return true
}
================================================
FILE: notification_configuration_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNotificationConfigurationList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
wTest, wTestCleanup := createWorkspace(t, client, nil)
defer wTestCleanup()
ncTest1, ncTestCleanup1 := createNotificationConfiguration(t, client, wTest, nil)
defer ncTestCleanup1()
ncTest2, ncTestCleanup2 := createNotificationConfiguration(t, client, wTest, nil)
defer ncTestCleanup2()
t.Run("with a valid workspace", func(t *testing.T) {
ncl, err := client.NotificationConfigurations.List(
ctx,
wTest.ID,
nil,
)
require.NoError(t, err)
assert.Contains(t, ncl.Items, ncTest1)
assert.Contains(t, ncl.Items, ncTest2)
assert.Equal(t, 0, ncl.CurrentPage)
assert.Equal(t, 0, ncl.TotalCount)
assert.NotNil(t, ncl.Items[0].Subscribable)
assert.NotEmpty(t, ncl.Items[0].Subscribable)
assert.NotNil(t, ncl.Items[0].SubscribableChoice.Workspace)
assert.NotEmpty(t, ncl.Items[0].SubscribableChoice.Workspace)
})
t.Run("with list options", func(t *testing.T) {
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
ncl, err := client.NotificationConfigurations.List(
ctx,
wTest.ID,
&NotificationConfigurationListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
},
)
require.NoError(t, err)
assert.Empty(t, ncl.Items)
assert.Equal(t, 999, ncl.CurrentPage)
assert.Equal(t, 2, ncl.TotalCount)
})
t.Run("without a valid workspace", func(t *testing.T) {
ncl, err := client.NotificationConfigurations.List(
ctx,
badIdentifier,
nil,
)
assert.Nil(t, ncl)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
}
func TestNotificationConfigurationList_forTeams(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
newSubscriptionUpdater(orgTest).WithStandardEntitlementPlan().Update(t)
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
t.Cleanup(tmTestCleanup)
require.NotNil(t, tmTest)
ncTest1, ncTestCleanup1 := createTeamNotificationConfiguration(t, client, tmTest, nil)
t.Cleanup(ncTestCleanup1)
ncTest2, ncTestCleanup2 := createTeamNotificationConfiguration(t, client, tmTest, nil)
t.Cleanup(ncTestCleanup2)
t.Run("with a valid team", func(t *testing.T) {
ncl, err := client.NotificationConfigurations.List(
ctx,
tmTest.ID,
&NotificationConfigurationListOptions{
SubscribableChoice: &NotificationConfigurationSubscribableChoice{
Team: tmTest,
},
},
)
require.NoError(t, err)
assert.Contains(t, ncl.Items, ncTest1)
assert.Contains(t, ncl.Items, ncTest2)
})
t.Run("without a valid team", func(t *testing.T) {
ncl, err := client.NotificationConfigurations.List(
ctx,
badIdentifier,
&NotificationConfigurationListOptions{
SubscribableChoice: &NotificationConfigurationSubscribableChoice{
Team: tmTest,
},
},
)
assert.Nil(t, ncl)
assert.EqualError(t, err, ErrInvalidTeamID.Error())
})
}
func TestNotificationConfigurationCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
// Create user to use when testing email destination type
orgMemberTest, orgMemberTestCleanup := createOrganizationMembership(t, client, orgTest)
defer orgMemberTestCleanup()
orgMemberTest.User = &User{ID: orgMemberTest.User.ID}
t.Run("with all required values", func(t *testing.T) {
options := NotificationConfigurationCreateOptions{
DestinationType: NotificationDestination(NotificationDestinationTypeGeneric),
Enabled: Bool(false),
Name: String(randomString(t)),
Token: String(randomString(t)),
URL: String("http://example.com"),
Triggers: []NotificationTriggerType{NotificationTriggerCreated},
}
_, err := client.NotificationConfigurations.Create(ctx, wTest.ID, options)
require.NoError(t, err)
})
t.Run("without a required value", func(t *testing.T) {
options := NotificationConfigurationCreateOptions{
DestinationType: NotificationDestination(NotificationDestinationTypeGeneric),
Enabled: Bool(false),
Token: String(randomString(t)),
URL: String("http://example.com"),
Triggers: []NotificationTriggerType{NotificationTriggerCreated},
}
nc, err := client.NotificationConfigurations.Create(ctx, wTest.ID, options)
assert.Nil(t, nc)
assert.EqualError(t, err, ErrRequiredName.Error())
})
t.Run("without a required value URL when destination type is generic", func(t *testing.T) {
options := NotificationConfigurationCreateOptions{
DestinationType: NotificationDestination(NotificationDestinationTypeGeneric),
Enabled: Bool(false),
Name: String(randomString(t)),
Token: String(randomString(t)),
Triggers: []NotificationTriggerType{NotificationTriggerCreated},
}
nc, err := client.NotificationConfigurations.Create(ctx, wTest.ID, options)
assert.Nil(t, nc)
assert.Equal(t, err, ErrRequiredURL)
})
t.Run("without a required value URL when destination type is slack", func(t *testing.T) {
options := NotificationConfigurationCreateOptions{
DestinationType: NotificationDestination(NotificationDestinationTypeSlack),
Enabled: Bool(false),
Name: String(randomString(t)),
Triggers: []NotificationTriggerType{NotificationTriggerCreated},
}
nc, err := client.NotificationConfigurations.Create(ctx, wTest.ID, options)
assert.Nil(t, nc)
assert.Equal(t, err, ErrRequiredURL)
})
t.Run("without a required value URL when destination type is MS Teams", func(t *testing.T) {
options := NotificationConfigurationCreateOptions{
DestinationType: NotificationDestination(NotificationDestinationTypeMicrosoftTeams),
Enabled: Bool(false),
Name: String(randomString(t)),
Triggers: []NotificationTriggerType{NotificationTriggerCreated},
}
nc, err := client.NotificationConfigurations.Create(ctx, wTest.ID, options)
assert.Nil(t, nc)
assert.Equal(t, err, ErrRequiredURL)
})
t.Run("without a valid workspace", func(t *testing.T) {
nc, err := client.NotificationConfigurations.Create(ctx, badIdentifier, NotificationConfigurationCreateOptions{})
assert.Nil(t, nc)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
t.Run("with an invalid notification trigger", func(t *testing.T) {
options := NotificationConfigurationCreateOptions{
DestinationType: NotificationDestination(NotificationDestinationTypeGeneric),
Enabled: Bool(false),
Name: String(randomString(t)),
Token: String(randomString(t)),
URL: String("http://example.com"),
Triggers: []NotificationTriggerType{"the beacons of gondor are lit"},
}
nc, err := client.NotificationConfigurations.Create(ctx, wTest.ID, options)
assert.Nil(t, nc)
assert.EqualError(t, err, ErrInvalidNotificationTrigger.Error())
})
t.Run("with email users when destination type is email", func(t *testing.T) {
options := NotificationConfigurationCreateOptions{
DestinationType: NotificationDestination(NotificationDestinationTypeEmail),
Enabled: Bool(false),
Name: String(randomString(t)),
EmailUsers: []*User{orgMemberTest.User},
}
_, err := client.NotificationConfigurations.Create(ctx, wTest.ID, options)
require.NoError(t, err)
})
t.Run("without email users when destination type is email", func(t *testing.T) {
options := NotificationConfigurationCreateOptions{
DestinationType: NotificationDestination(NotificationDestinationTypeEmail),
Enabled: Bool(false),
Name: String(randomString(t)),
}
_, err := client.NotificationConfigurations.Create(ctx, wTest.ID, options)
require.NoError(t, err)
})
}
func TestNotificationConfigurationsCreate_byType(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
upgradeOrganizationSubscription(t, client, orgTest)
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup)
// Create user to use when testing email destination type
orgMemberTest, orgMemberTestCleanup := createOrganizationMembership(t, client, orgTest)
t.Cleanup(orgMemberTestCleanup)
orgMemberTest.User = &User{ID: orgMemberTest.User.ID}
testCases := []NotificationTriggerType{
NotificationTriggerCreated,
NotificationTriggerPlanning,
NotificationTriggerNeedsAttention,
NotificationTriggerApplying,
NotificationTriggerCompleted,
NotificationTriggerErrored,
NotificationTriggerAssessmentDrifted,
NotificationTriggerAssessmentFailed,
NotificationTriggerAssessmentCheckFailed,
NotificationTriggerWorkspaceAutoDestroyReminder,
NotificationTriggerWorkspaceAutoDestroyRunResults,
}
for _, trigger := range testCases {
message := fmt.Sprintf("with trigger %s and all required values", trigger)
t.Run(message, func(t *testing.T) {
t.Parallel()
options := NotificationConfigurationCreateOptions{
DestinationType: NotificationDestination(NotificationDestinationTypeGeneric),
Enabled: Bool(false),
Name: String(randomString(t)),
Token: String(randomString(t)),
URL: String("http://example.com"),
Triggers: []NotificationTriggerType{trigger},
}
_, err := client.NotificationConfigurations.Create(ctx, wTest.ID, options)
require.NoError(t, err)
})
}
}
func TestNotificationConfigurationCreate_forTeams(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
newSubscriptionUpdater(orgTest).WithStandardEntitlementPlan().Update(t)
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
t.Cleanup(tmTestCleanup)
// Create user to use when testing email destination type
orgMemberTest, orgMemberTestCleanup := createOrganizationMembership(t, client, orgTest)
t.Cleanup(orgMemberTestCleanup)
// Add user to team
options := TeamMemberAddOptions{
OrganizationMembershipIDs: []string{orgMemberTest.ID},
}
err := client.TeamMembers.Add(ctx, tmTest.ID, options)
require.NoError(t, err)
t.Run("with all required values", func(t *testing.T) {
options := NotificationConfigurationCreateOptions{
DestinationType: NotificationDestination(NotificationDestinationTypeGeneric),
Enabled: Bool(false),
Name: String(randomString(t)),
Token: String(randomString(t)),
URL: String("http://example.com"),
Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated},
SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest},
}
nc, err := client.NotificationConfigurations.Create(ctx, tmTest.ID, options)
require.NoError(t, err)
require.NotNil(t, nc)
})
t.Run("without a required value", func(t *testing.T) {
options := NotificationConfigurationCreateOptions{
DestinationType: NotificationDestination(NotificationDestinationTypeGeneric),
Enabled: Bool(false),
Token: String(randomString(t)),
URL: String("http://example.com"),
Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated},
SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest},
}
nc, err := client.NotificationConfigurations.Create(ctx, tmTest.ID, options)
assert.Nil(t, nc)
assert.EqualError(t, err, ErrRequiredName.Error())
})
t.Run("without a required value URL when destination type is generic", func(t *testing.T) {
options := NotificationConfigurationCreateOptions{
DestinationType: NotificationDestination(NotificationDestinationTypeGeneric),
Enabled: Bool(false),
Name: String(randomString(t)),
Token: String(randomString(t)),
Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated},
SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest},
}
nc, err := client.NotificationConfigurations.Create(ctx, tmTest.ID, options)
assert.Nil(t, nc)
assert.Equal(t, err, ErrRequiredURL)
})
t.Run("without a required value URL when destination type is slack", func(t *testing.T) {
options := NotificationConfigurationCreateOptions{
DestinationType: NotificationDestination(NotificationDestinationTypeSlack),
Enabled: Bool(false),
Name: String(randomString(t)),
Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated},
SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest},
}
nc, err := client.NotificationConfigurations.Create(ctx, tmTest.ID, options)
assert.Nil(t, nc)
assert.Equal(t, err, ErrRequiredURL)
})
t.Run("without a required value URL when destination type is MS Teams", func(t *testing.T) {
options := NotificationConfigurationCreateOptions{
DestinationType: NotificationDestination(NotificationDestinationTypeMicrosoftTeams),
Enabled: Bool(false),
Name: String(randomString(t)),
Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated},
SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest},
}
nc, err := client.NotificationConfigurations.Create(ctx, tmTest.ID, options)
assert.Nil(t, nc)
assert.Equal(t, err, ErrRequiredURL)
})
t.Run("without a valid team", func(t *testing.T) {
nc, err := client.NotificationConfigurations.Create(ctx, badIdentifier, NotificationConfigurationCreateOptions{
SubscribableChoice: &NotificationConfigurationSubscribableChoice{
Team: tmTest,
},
})
assert.Nil(t, nc)
assert.EqualError(t, err, ErrInvalidTeamID.Error())
})
t.Run("with an invalid notification trigger", func(t *testing.T) {
options := NotificationConfigurationCreateOptions{
DestinationType: NotificationDestination(NotificationDestinationTypeGeneric),
Enabled: Bool(false),
Name: String(randomString(t)),
Token: String(randomString(t)),
URL: String("http://example.com"),
Triggers: []NotificationTriggerType{"the beacons of gondor are lit"},
SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest},
}
nc, err := client.NotificationConfigurations.Create(ctx, tmTest.ID, options)
assert.Nil(t, nc)
assert.EqualError(t, err, ErrInvalidNotificationTrigger.Error())
})
t.Run("with email users when destination type is email", func(t *testing.T) {
options := NotificationConfigurationCreateOptions{
DestinationType: NotificationDestination(NotificationDestinationTypeEmail),
Enabled: Bool(false),
Name: String(randomString(t)),
EmailUsers: []*User{orgMemberTest.User},
SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest},
}
_, err := client.NotificationConfigurations.Create(ctx, tmTest.ID, options)
require.NoError(t, err)
})
t.Run("without email users when destination type is email", func(t *testing.T) {
options := NotificationConfigurationCreateOptions{
DestinationType: NotificationDestination(NotificationDestinationTypeEmail),
Enabled: Bool(false),
Name: String(randomString(t)),
SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest},
}
_, err := client.NotificationConfigurations.Create(ctx, tmTest.ID, options)
require.NoError(t, err)
})
}
func TestNotificationConfigurationRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
ncTest, ncTestCleanup := createNotificationConfiguration(t, client, nil, nil)
defer ncTestCleanup()
t.Run("with a valid ID", func(t *testing.T) {
nc, err := client.NotificationConfigurations.Read(ctx, ncTest.ID)
require.NoError(t, err)
assert.Equal(t, ncTest.ID, nc.ID)
})
t.Run("when the notification configuration does not exist", func(t *testing.T) {
_, err := client.NotificationConfigurations.Read(ctx, "nonexisting")
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the notification configuration ID is invalid", func(t *testing.T) {
_, err := client.NotificationConfigurations.Read(ctx, badIdentifier)
assert.Equal(t, err, ErrInvalidNotificationConfigID)
})
}
func TestNotificationConfigurationRead_forTeams(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
newSubscriptionUpdater(orgTest).WithStandardEntitlementPlan().Update(t)
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
t.Cleanup(tmTestCleanup)
ncTest, ncTestCleanup := createTeamNotificationConfiguration(t, client, tmTest, nil)
t.Cleanup(ncTestCleanup)
t.Run("with a valid ID", func(t *testing.T) {
nc, err := client.NotificationConfigurations.Read(ctx, ncTest.ID)
require.NoError(t, err)
assert.Equal(t, ncTest.ID, nc.ID)
})
t.Run("when the notification configuration does not exist", func(t *testing.T) {
_, err := client.NotificationConfigurations.Read(ctx, "nonexisting")
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the notification configuration ID is invalid", func(t *testing.T) {
_, err := client.NotificationConfigurations.Read(ctx, badIdentifier)
assert.Equal(t, err, ErrInvalidNotificationConfigID)
})
}
func TestNotificationConfigurationUpdate_forTeams(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
newSubscriptionUpdater(orgTest).WithStandardEntitlementPlan().Update(t)
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
t.Cleanup(tmTestCleanup)
ncTest, ncTestCleanup := createTeamNotificationConfiguration(t, client, tmTest, nil)
t.Cleanup(ncTestCleanup)
// Create users to use when testing email destination type
orgMemberTest1, orgMemberTest1Cleanup := createOrganizationMembership(t, client, orgTest)
defer orgMemberTest1Cleanup()
orgMemberTest2, orgMemberTest2Cleanup := createOrganizationMembership(t, client, orgTest)
defer orgMemberTest2Cleanup()
orgMemberTest1.User = &User{ID: orgMemberTest1.User.ID}
orgMemberTest2.User = &User{ID: orgMemberTest2.User.ID}
// Add users to team
for _, orgMember := range []*OrganizationMembership{orgMemberTest1, orgMemberTest2} {
options := TeamMemberAddOptions{
OrganizationMembershipIDs: []string{orgMember.ID},
}
err := client.TeamMembers.Add(ctx, tmTest.ID, options)
require.NoError(t, err)
}
options := &NotificationConfigurationCreateOptions{
DestinationType: NotificationDestination(NotificationDestinationTypeEmail),
Enabled: Bool(false),
Name: String(randomString(t)),
EmailUsers: []*User{orgMemberTest1.User},
SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest},
}
ncEmailTest, ncEmailTestCleanup := createTeamNotificationConfiguration(t, client, tmTest, options)
t.Cleanup(ncEmailTestCleanup)
t.Run("with options", func(t *testing.T) {
options := NotificationConfigurationUpdateOptions{
Enabled: Bool(true),
Name: String("newName"),
}
nc, err := client.NotificationConfigurations.Update(ctx, ncTest.ID, options)
require.NoError(t, err)
assert.Equal(t, nc.Enabled, true)
assert.Equal(t, nc.Name, "newName")
})
t.Run("with invalid notification trigger", func(t *testing.T) {
options := NotificationConfigurationUpdateOptions{
Triggers: []NotificationTriggerType{"fly you fools!"},
}
nc, err := client.NotificationConfigurations.Update(ctx, ncTest.ID, options)
assert.Nil(t, nc)
assert.EqualError(t, err, ErrInvalidNotificationTrigger.Error())
})
t.Run("with email users when destination type is email", func(t *testing.T) {
options := NotificationConfigurationUpdateOptions{
Enabled: Bool(true),
Name: String("newName"),
EmailUsers: []*User{orgMemberTest1.User, orgMemberTest2.User},
}
nc, err := client.NotificationConfigurations.Update(ctx, ncEmailTest.ID, options)
require.NoError(t, err)
assert.Equal(t, nc.Enabled, true)
assert.Equal(t, nc.Name, "newName")
assert.Contains(t, nc.EmailUsers, orgMemberTest1.User)
assert.Contains(t, nc.EmailUsers, orgMemberTest2.User)
})
t.Run("without email users when destination type is email", func(t *testing.T) {
options := NotificationConfigurationUpdateOptions{
Enabled: Bool(true),
Name: String("newName"),
}
nc, err := client.NotificationConfigurations.Update(ctx, ncEmailTest.ID, options)
require.NoError(t, err)
assert.Equal(t, nc.Enabled, true)
assert.Equal(t, nc.Name, "newName")
assert.Empty(t, nc.EmailUsers)
})
t.Run("without options", func(t *testing.T) {
_, err := client.NotificationConfigurations.Update(ctx, ncTest.ID, NotificationConfigurationUpdateOptions{})
require.NoError(t, err)
})
t.Run("when the notification configuration does not exist", func(t *testing.T) {
_, err := client.NotificationConfigurations.Update(ctx, "nonexisting", NotificationConfigurationUpdateOptions{})
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the notification configuration ID is invalid", func(t *testing.T) {
_, err := client.NotificationConfigurations.Update(ctx, badIdentifier, NotificationConfigurationUpdateOptions{})
assert.Equal(t, err, ErrInvalidNotificationConfigID)
})
}
func TestNotificationConfigurationUpdate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
ncTest, ncTestCleanup := createNotificationConfiguration(t, client, wTest, nil)
defer ncTestCleanup()
// Create users to use when testing email destination type
orgMemberTest1, orgMemberTest1Cleanup := createOrganizationMembership(t, client, orgTest)
defer orgMemberTest1Cleanup()
orgMemberTest2, orgMemberTest2Cleanup := createOrganizationMembership(t, client, orgTest)
defer orgMemberTest2Cleanup()
orgMemberTest1.User = &User{ID: orgMemberTest1.User.ID}
orgMemberTest2.User = &User{ID: orgMemberTest2.User.ID}
options := &NotificationConfigurationCreateOptions{
DestinationType: NotificationDestination(NotificationDestinationTypeEmail),
Enabled: Bool(false),
Name: String(randomString(t)),
EmailUsers: []*User{orgMemberTest1.User},
}
ncEmailTest, ncEmailTestCleanup := createNotificationConfiguration(t, client, wTest, options)
defer ncEmailTestCleanup()
t.Run("with options", func(t *testing.T) {
options := NotificationConfigurationUpdateOptions{
Enabled: Bool(true),
Name: String("newName"),
}
nc, err := client.NotificationConfigurations.Update(ctx, ncTest.ID, options)
require.NoError(t, err)
assert.Equal(t, nc.Enabled, true)
assert.Equal(t, nc.Name, "newName")
})
t.Run("with invalid notification trigger", func(t *testing.T) {
options := NotificationConfigurationUpdateOptions{
Triggers: []NotificationTriggerType{"fly you fools!"},
}
nc, err := client.NotificationConfigurations.Update(ctx, ncTest.ID, options)
assert.Nil(t, nc)
assert.EqualError(t, err, ErrInvalidNotificationTrigger.Error())
})
t.Run("with email users when destination type is email", func(t *testing.T) {
options := NotificationConfigurationUpdateOptions{
Enabled: Bool(true),
Name: String("newName"),
EmailUsers: []*User{orgMemberTest1.User, orgMemberTest2.User},
}
nc, err := client.NotificationConfigurations.Update(ctx, ncEmailTest.ID, options)
require.NoError(t, err)
assert.Equal(t, nc.Enabled, true)
assert.Equal(t, nc.Name, "newName")
assert.Contains(t, nc.EmailUsers, orgMemberTest1.User)
assert.Contains(t, nc.EmailUsers, orgMemberTest2.User)
})
t.Run("without email users when destination type is email", func(t *testing.T) {
options := NotificationConfigurationUpdateOptions{
Enabled: Bool(true),
Name: String("newName"),
}
nc, err := client.NotificationConfigurations.Update(ctx, ncEmailTest.ID, options)
require.NoError(t, err)
assert.Equal(t, nc.Enabled, true)
assert.Equal(t, nc.Name, "newName")
assert.Empty(t, nc.EmailUsers)
})
t.Run("without options", func(t *testing.T) {
_, err := client.NotificationConfigurations.Update(ctx, ncTest.ID, NotificationConfigurationUpdateOptions{})
require.NoError(t, err)
})
t.Run("when the notification configuration does not exist", func(t *testing.T) {
_, err := client.NotificationConfigurations.Update(ctx, "nonexisting", NotificationConfigurationUpdateOptions{})
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the notification configuration ID is invalid", func(t *testing.T) {
_, err := client.NotificationConfigurations.Update(ctx, badIdentifier, NotificationConfigurationUpdateOptions{})
assert.Equal(t, err, ErrInvalidNotificationConfigID)
})
}
func TestNotificationConfigurationDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
wTest, wTestCleanup := createWorkspace(t, client, nil)
defer wTestCleanup()
ncTest, _ := createNotificationConfiguration(t, client, wTest, nil)
t.Run("with a valid ID", func(t *testing.T) {
err := client.NotificationConfigurations.Delete(ctx, ncTest.ID)
require.NoError(t, err)
_, err = client.NotificationConfigurations.Read(ctx, ncTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the notification configuration does not exist", func(t *testing.T) {
err := client.NotificationConfigurations.Delete(ctx, "nonexisting")
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the notification configuration ID is invalid", func(t *testing.T) {
err := client.NotificationConfigurations.Delete(ctx, badIdentifier)
assert.Equal(t, err, ErrInvalidNotificationConfigID)
})
}
func TestNotificationConfigurationDelete_forTeams(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
newSubscriptionUpdater(orgTest).WithStandardEntitlementPlan().Update(t)
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
t.Cleanup(tmTestCleanup)
ncTest, _ := createTeamNotificationConfiguration(t, client, tmTest, nil)
t.Run("with a valid ID", func(t *testing.T) {
err := client.NotificationConfigurations.Delete(ctx, ncTest.ID)
require.NoError(t, err)
_, err = client.NotificationConfigurations.Read(ctx, ncTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the notification configuration does not exist", func(t *testing.T) {
err := client.NotificationConfigurations.Delete(ctx, "nonexisting")
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the notification configuration ID is invalid", func(t *testing.T) {
err := client.NotificationConfigurations.Delete(ctx, badIdentifier)
assert.Equal(t, err, ErrInvalidNotificationConfigID)
})
}
func TestNotificationConfigurationVerify(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
ncTest, ncTestCleanup := createNotificationConfiguration(t, client, nil, nil)
defer ncTestCleanup()
t.Run("with a valid ID", func(t *testing.T) {
_, err := client.NotificationConfigurations.Verify(ctx, ncTest.ID)
require.NoError(t, err)
})
t.Run("when the notification configuration does not exists", func(t *testing.T) {
_, err := client.NotificationConfigurations.Verify(ctx, "nonexisting")
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the notification configuration ID is invalid", func(t *testing.T) {
_, err := client.NotificationConfigurations.Verify(ctx, badIdentifier)
assert.Equal(t, err, ErrInvalidNotificationConfigID)
})
}
func TestNotificationConfigurationVerify_forTeams(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
newSubscriptionUpdater(orgTest).WithStandardEntitlementPlan().Update(t)
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
t.Cleanup(tmTestCleanup)
ncTest, ncTestCleanup := createTeamNotificationConfiguration(t, client, tmTest, nil)
t.Cleanup(ncTestCleanup)
t.Run("with a valid ID", func(t *testing.T) {
_, err := client.NotificationConfigurations.Verify(ctx, ncTest.ID)
require.NoError(t, err)
})
t.Run("when the notification configuration does not exists", func(t *testing.T) {
_, err := client.NotificationConfigurations.Verify(ctx, "nonexisting")
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the notification configuration ID is invalid", func(t *testing.T) {
_, err := client.NotificationConfigurations.Verify(ctx, badIdentifier)
assert.Equal(t, err, ErrInvalidNotificationConfigID)
})
}
================================================
FILE: oauth_client.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ OAuthClients = (*oAuthClients)(nil)
// OAuthClients describes all the OAuth client related methods that the
// Terraform Enterprise API supports.
//
// TFE API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/oauth-clients
type OAuthClients interface {
// List all the OAuth clients for a given organization.
List(ctx context.Context, organization string, options *OAuthClientListOptions) (*OAuthClientList, error)
// Create an OAuth client to connect an organization and a VCS provider.
Create(ctx context.Context, organization string, options OAuthClientCreateOptions) (*OAuthClient, error)
// Read an OAuth client by its ID.
Read(ctx context.Context, oAuthClientID string) (*OAuthClient, error)
// ReadWithOptions reads an oauth client by its ID using the options supplied.
ReadWithOptions(ctx context.Context, oAuthClientID string, options *OAuthClientReadOptions) (*OAuthClient, error)
// Update an existing OAuth client by its ID.
Update(ctx context.Context, oAuthClientID string, options OAuthClientUpdateOptions) (*OAuthClient, error)
// Delete an OAuth client by its ID.
Delete(ctx context.Context, oAuthClientID string) error
// AddProjects add projects to an oauth client.
AddProjects(ctx context.Context, oAuthClientID string, options OAuthClientAddProjectsOptions) error
// RemoveProjects remove projects from an oauth client.
RemoveProjects(ctx context.Context, oAuthClientID string, options OAuthClientRemoveProjectsOptions) error
}
// oAuthClients implements OAuthClients.
type oAuthClients struct {
client *Client
}
// ServiceProviderType represents a VCS type.
type ServiceProviderType string
// List of available VCS types.
const (
ServiceProviderAzureDevOpsServer ServiceProviderType = "ado_server"
ServiceProviderAzureDevOpsServices ServiceProviderType = "ado_services"
ServiceProviderBitbucketDataCenter ServiceProviderType = "bitbucket_data_center"
ServiceProviderBitbucket ServiceProviderType = "bitbucket_hosted"
// Bitbucket Server v5.4.0 and above
ServiceProviderBitbucketServer ServiceProviderType = "bitbucket_server"
// Bitbucket Server v5.3.0 and below
ServiceProviderBitbucketServerLegacy ServiceProviderType = "bitbucket_server_legacy"
ServiceProviderGithub ServiceProviderType = "github"
ServiceProviderGithubEE ServiceProviderType = "github_enterprise"
ServiceProviderGitlab ServiceProviderType = "gitlab_hosted"
ServiceProviderGitlabCE ServiceProviderType = "gitlab_community_edition"
ServiceProviderGitlabEE ServiceProviderType = "gitlab_enterprise_edition"
)
// OAuthClientList represents a list of OAuth clients.
type OAuthClientList struct {
*Pagination
Items []*OAuthClient
}
// OAuthClient represents a connection between an organization and a VCS
// provider.
type OAuthClient struct {
ID string `jsonapi:"primary,oauth-clients"`
APIURL string `jsonapi:"attr,api-url"`
CallbackURL string `jsonapi:"attr,callback-url"`
ConnectPath string `jsonapi:"attr,connect-path"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
HTTPURL string `jsonapi:"attr,http-url"`
Key string `jsonapi:"attr,key"`
RSAPublicKey string `jsonapi:"attr,rsa-public-key"`
Name *string `jsonapi:"attr,name"`
Secret string `jsonapi:"attr,secret"`
ServiceProvider ServiceProviderType `jsonapi:"attr,service-provider"`
ServiceProviderName string `jsonapi:"attr,service-provider-display-name"`
OrganizationScoped *bool `jsonapi:"attr,organization-scoped"`
// Relations
Organization *Organization `jsonapi:"relation,organization"`
OAuthTokens []*OAuthToken `jsonapi:"relation,oauth-tokens"`
AgentPool *AgentPool `jsonapi:"relation,agent-pool"`
// The projects to which the oauth client applies.
Projects []*Project `jsonapi:"relation,projects"`
}
// A list of relations to include
type OAuthClientIncludeOpt string
const (
OauthClientOauthTokens OAuthClientIncludeOpt = "oauth_tokens"
OauthClientProjects OAuthClientIncludeOpt = "projects"
)
// OAuthClientListOptions represents the options for listing
// OAuth clients.
type OAuthClientListOptions struct {
ListOptions
Include []OAuthClientIncludeOpt `url:"include,omitempty"`
}
// OAuthClientReadOptions are read options.
// For a full list of relations, please see:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/oauth-clients#relationships
type OAuthClientReadOptions struct {
// Optional: A list of relations to include. See available resources
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/oauth-clients#available-related-resources
Include []OAuthClientIncludeOpt `url:"include,omitempty"`
}
// OAuthClientCreateOptions represents the options for creating an OAuth client.
type OAuthClientCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,oauth-clients"`
// A display name for the OAuth Client.
Name *string `jsonapi:"attr,name"`
// Required: The base URL of your VCS provider's API.
APIURL *string `jsonapi:"attr,api-url"`
// Required: The homepage of your VCS provider.
HTTPURL *string `jsonapi:"attr,http-url"`
// Optional: The OAuth Client key.
Key *string `jsonapi:"attr,key,omitempty"`
// Optional: The token string you were given by your VCS provider.
OAuthToken *string `jsonapi:"attr,oauth-token-string,omitempty"`
// Optional: The initial list of projects for which the oauth client should be associated with.
Projects []*Project `jsonapi:"relation,projects,omitempty"`
// Optional: Private key associated with this vcs provider - only available for ado_server
PrivateKey *string `jsonapi:"attr,private-key,omitempty"`
// Optional: Secret key associated with this vcs provider - only available for ado_server
Secret *string `jsonapi:"attr,secret,omitempty"`
// Optional: RSAPublicKey the text of the SSH public key associated with your
// BitBucket Data Center Application Link.
RSAPublicKey *string `jsonapi:"attr,rsa-public-key,omitempty"`
// Required: The VCS provider being connected with.
ServiceProvider *ServiceProviderType `jsonapi:"attr,service-provider"`
// Optional: AgentPool to associate the VCS Provider with, for PrivateVCS support
AgentPool *AgentPool `jsonapi:"relation,agent-pool,omitempty"`
// Optional: Whether the OAuthClient is available to all workspaces in the organization.
// True if the oauth client is organization scoped, false otherwise.
OrganizationScoped *bool `jsonapi:"attr,organization-scoped,omitempty"`
}
// OAuthClientUpdateOptions represents the options for updating an OAuth client.
type OAuthClientUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,oauth-clients"`
// Optional: A display name for the OAuth Client.
Name *string `jsonapi:"attr,name,omitempty"`
// Optional: The OAuth Client key.
Key *string `jsonapi:"attr,key,omitempty"`
// Optional: Secret key associated with this vcs provider - only available for ado_server
Secret *string `jsonapi:"attr,secret,omitempty"`
// Optional: RSAPublicKey the text of the SSH public key associated with your BitBucket
// Server Application Link.
RSAPublicKey *string `jsonapi:"attr,rsa-public-key,omitempty"`
// Optional: The token string you were given by your VCS provider.
OAuthToken *string `jsonapi:"attr,oauth-token-string,omitempty"`
// Optional: AgentPool to associate the VCS Provider with, for PrivateVCS support
AgentPool *AgentPool `jsonapi:"relation,agent-pool,omitempty"`
// Optional: Whether the OAuthClient is available to all workspaces in the organization.
// True if the oauth client is organization scoped, false otherwise.
OrganizationScoped *bool `jsonapi:"attr,organization-scoped,omitempty"`
}
// OAuthClientAddProjectsOptions represents the options for adding projects
// to an oauth client.
type OAuthClientAddProjectsOptions struct {
// The projects to add to an oauth client.
Projects []*Project
}
// OAuthClientRemoveProjectsOptions represents the options for removing
// projects from an oauth client.
type OAuthClientRemoveProjectsOptions struct {
// The projects to remove from an oauth client.
Projects []*Project
}
// List all the OAuth clients for a given organization.
func (s *oAuthClients) List(ctx context.Context, organization string, options *OAuthClientListOptions) (*OAuthClientList, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("organizations/%s/oauth-clients", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
ocl := &OAuthClientList{}
err = req.Do(ctx, ocl)
if err != nil {
return nil, err
}
return ocl, nil
}
// Create an OAuth client to connect an organization and a VCS provider.
func (s *oAuthClients) Create(ctx context.Context, organization string, options OAuthClientCreateOptions) (*OAuthClient, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("organizations/%s/oauth-clients", url.PathEscape(organization))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
oc := &OAuthClient{}
err = req.Do(ctx, oc)
if err != nil {
return nil, err
}
return oc, nil
}
// Read an OAuth client by its ID.
func (s *oAuthClients) Read(ctx context.Context, oAuthClientID string) (*OAuthClient, error) {
return s.ReadWithOptions(ctx, oAuthClientID, nil)
}
func (s *oAuthClients) ReadWithOptions(ctx context.Context, oAuthClientID string, options *OAuthClientReadOptions) (*OAuthClient, error) {
if !validStringID(&oAuthClientID) {
return nil, ErrInvalidOauthClientID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("oauth-clients/%s", url.PathEscape(oAuthClientID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
oc := &OAuthClient{}
err = req.Do(ctx, oc)
if err != nil {
return nil, err
}
return oc, err
}
// Update an OAuth client by its ID.
func (s *oAuthClients) Update(ctx context.Context, oAuthClientID string, options OAuthClientUpdateOptions) (*OAuthClient, error) {
if !validStringID(&oAuthClientID) {
return nil, ErrInvalidOauthClientID
}
u := fmt.Sprintf("oauth-clients/%s", url.PathEscape(oAuthClientID))
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
oc := &OAuthClient{}
err = req.Do(ctx, oc)
if err != nil {
return nil, err
}
return oc, err
}
// Delete an OAuth client by its ID.
func (s *oAuthClients) Delete(ctx context.Context, oAuthClientID string) error {
if !validStringID(&oAuthClientID) {
return ErrInvalidOauthClientID
}
u := fmt.Sprintf("oauth-clients/%s", url.PathEscape(oAuthClientID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o OAuthClientCreateOptions) valid() error {
if !validString(o.APIURL) {
return ErrRequiredAPIURL
}
if !validString(o.HTTPURL) {
return ErrRequiredHTTPURL
}
if o.ServiceProvider == nil {
return ErrRequiredServiceProvider
}
if !validString(o.OAuthToken) &&
*o.ServiceProvider != *ServiceProvider(ServiceProviderBitbucketServer) &&
*o.ServiceProvider != *ServiceProvider(ServiceProviderBitbucketDataCenter) {
return ErrRequiredOauthToken
}
if validString(o.PrivateKey) && *o.ServiceProvider != *ServiceProvider(ServiceProviderAzureDevOpsServer) {
return ErrUnsupportedPrivateKey
}
return nil
}
func (o *OAuthClientListOptions) valid() error {
return nil
}
// AddProjects adds projects to a given oauth client.
func (s *oAuthClients) AddProjects(ctx context.Context, oAuthClientID string, options OAuthClientAddProjectsOptions) error {
if !validStringID(&oAuthClientID) {
return ErrInvalidOauthClientID
}
if err := options.valid(); err != nil {
return err
}
u := fmt.Sprintf("oauth-clients/%s/relationships/projects", url.PathEscape(oAuthClientID))
req, err := s.client.NewRequest("POST", u, options.Projects)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// RemoveProjects removes projects from an oauth client.
func (s *oAuthClients) RemoveProjects(ctx context.Context, oAuthClientID string, options OAuthClientRemoveProjectsOptions) error {
if !validStringID(&oAuthClientID) {
return ErrInvalidOauthClientID
}
if err := options.valid(); err != nil {
return err
}
u := fmt.Sprintf("oauth-clients/%s/relationships/projects", url.PathEscape(oAuthClientID))
req, err := s.client.NewRequest("DELETE", u, options.Projects)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o OAuthClientAddProjectsOptions) valid() error {
if o.Projects == nil {
return ErrRequiredProject
}
if len(o.Projects) == 0 {
return ErrProjectMinLimit
}
return nil
}
func (o OAuthClientRemoveProjectsOptions) valid() error {
if o.Projects == nil {
return ErrRequiredProject
}
if len(o.Projects) == 0 {
return ErrProjectMinLimit
}
return nil
}
func (o *OAuthClientReadOptions) valid() error {
return nil
}
================================================
FILE: oauth_client_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestOAuthClientsList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
ocTest1, ocTestCleanup1 := createOAuthClient(t, client, orgTest, nil)
defer ocTestCleanup1()
ocTest2, ocTestCleanup2 := createOAuthClient(t, client, orgTest, nil)
defer ocTestCleanup2()
t.Run("without list options", func(t *testing.T) {
ocl, err := client.OAuthClients.List(ctx, orgTest.Name, nil)
require.NoError(t, err)
t.Run("the OAuth tokens relationship is decoded correcly", func(t *testing.T) {
for _, oc := range ocl.Items {
assert.Equal(t, 1, len(oc.OAuthTokens))
}
})
// We need to strip some fields before the next test.
for _, oc := range append(ocl.Items, ocTest1, ocTest2) {
oc.OAuthTokens = nil
oc.Organization = nil
}
assert.Contains(t, ocl.Items, ocTest1)
assert.Contains(t, ocl.Items, ocTest2)
t.Skip("paging not supported yet in API")
assert.Equal(t, 1, ocl.CurrentPage)
assert.Equal(t, 2, ocl.TotalCount)
})
t.Run("with list options", func(t *testing.T) {
t.Skip("paging not supported yet in API")
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
options := &OAuthClientListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
}
ocl, err := client.OAuthClients.List(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.Empty(t, ocl.Items)
assert.Equal(t, 999, ocl.CurrentPage)
assert.Equal(t, 2, ocl.TotalCount)
})
t.Run("with Include options", func(t *testing.T) {
ocl, err := client.OAuthClients.List(ctx, orgTest.Name, &OAuthClientListOptions{
Include: []OAuthClientIncludeOpt{OauthClientOauthTokens},
})
require.NoError(t, err)
require.NotEmpty(t, ocl.Items)
require.NotNil(t, ocl.Items[0])
require.NotEmpty(t, ocl.Items[0].OAuthTokens)
assert.NotEmpty(t, ocl.Items[0].OAuthTokens[0].ID)
})
t.Run("without a valid organization", func(t *testing.T) {
ocl, err := client.OAuthClients.List(ctx, badIdentifier, nil)
assert.Nil(t, ocl)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
func TestOAuthClientsCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
githubToken := os.Getenv("OAUTH_CLIENT_GITHUB_TOKEN")
if githubToken == "" {
t.Skip("Export a valid OAUTH_CLIENT_GITHUB_TOKEN before running this test!")
}
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
t.Run("with valid options", func(t *testing.T) {
options := OAuthClientCreateOptions{
APIURL: String("https://api.github.com"),
HTTPURL: String("https://github.com"),
OAuthToken: String(githubToken),
ServiceProvider: ServiceProvider(ServiceProviderGithub),
}
oc, err := client.OAuthClients.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.NotEmpty(t, oc.ID)
assert.Nil(t, oc.Name)
assert.Equal(t, "https://api.github.com", oc.APIURL)
assert.Equal(t, "https://github.com", oc.HTTPURL)
assert.Equal(t, 1, len(oc.OAuthTokens))
assert.Equal(t, ServiceProviderGithub, oc.ServiceProvider)
t.Run("the organization relationship is decoded correctly", func(t *testing.T) {
assert.NotEmpty(t, oc.Organization)
})
})
t.Run("without an valid organization", func(t *testing.T) {
options := OAuthClientCreateOptions{
APIURL: String("https://api.github.com"),
HTTPURL: String("https://github.com"),
OAuthToken: String(githubToken),
ServiceProvider: ServiceProvider(ServiceProviderGithub),
}
_, err := client.OAuthClients.Create(ctx, badIdentifier, options)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("without an API URL", func(t *testing.T) {
options := OAuthClientCreateOptions{
HTTPURL: String("https://github.com"),
OAuthToken: String(githubToken),
ServiceProvider: ServiceProvider(ServiceProviderGithub),
}
_, err := client.OAuthClients.Create(ctx, orgTest.Name, options)
assert.Equal(t, err, ErrRequiredAPIURL)
})
t.Run("without a HTTP URL", func(t *testing.T) {
options := OAuthClientCreateOptions{
APIURL: String("https://api.github.com"),
OAuthToken: String(githubToken),
ServiceProvider: ServiceProvider(ServiceProviderGithub),
}
_, err := client.OAuthClients.Create(ctx, orgTest.Name, options)
assert.Equal(t, err, ErrRequiredHTTPURL)
})
t.Run("without an OAuth token", func(t *testing.T) {
options := OAuthClientCreateOptions{
APIURL: String("https://api.github.com"),
HTTPURL: String("https://github.com"),
ServiceProvider: ServiceProvider(ServiceProviderGithub),
}
_, err := client.OAuthClients.Create(ctx, orgTest.Name, options)
assert.Equal(t, err, ErrRequiredOauthToken)
})
t.Run("without a service provider", func(t *testing.T) {
options := OAuthClientCreateOptions{
APIURL: String("https://api.github.com"),
HTTPURL: String("https://github.com"),
OAuthToken: String(githubToken),
}
_, err := client.OAuthClients.Create(ctx, orgTest.Name, options)
assert.Equal(t, err, ErrRequiredServiceProvider)
})
}
func TestOAuthClientsCreate_rsaKeyPair(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
t.Run("with key, rsa public/private key options", func(t *testing.T) {
key := randomString(t)
options := OAuthClientCreateOptions{
APIURL: String("https://bbdc.com"),
HTTPURL: String("https://bbdc.com"),
ServiceProvider: ServiceProvider(ServiceProviderBitbucketDataCenter),
Key: String(key),
Secret: String(privateKey),
RSAPublicKey: String(publicKey),
}
oc, err := client.OAuthClients.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.NotEmpty(t, oc.ID)
assert.Equal(t, "https://bbdc.com", oc.APIURL)
assert.Equal(t, "https://bbdc.com", oc.HTTPURL)
assert.Equal(t, ServiceProviderBitbucketDataCenter, oc.ServiceProvider)
assert.Equal(t, publicKey, oc.RSAPublicKey)
assert.Equal(t, key, oc.Key)
})
}
func TestOAuthClientsCreate_agentPool(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
githubToken := os.Getenv("OAUTH_CLIENT_GITHUB_TOKEN")
if githubToken == "" {
t.Skip("Export a valid OAUTH_CLIENT_GITHUB_TOKEN before running this test!")
}
t.Run("with valid agent pool external id", func(t *testing.T) {
// This requires access to Private VCS feature and tfc-agent running locally
t.Skip()
orgTestRead, errOrg := client.Organizations.Read(ctx, "xxxxx")
require.NoError(t, errOrg)
agentPoolTestRead, errAgentPool := client.AgentPools.Read(ctx, "xxxxx")
require.NoError(t, errAgentPool)
options := OAuthClientCreateOptions{
APIURL: String("https://githubenterprise.xxxxx"),
HTTPURL: String("https://githubenterprise.xxxxx"),
OAuthToken: String(githubToken),
ServiceProvider: ServiceProvider(ServiceProviderGithubEE),
AgentPool: agentPoolTestRead,
}
oc, errCreate := client.OAuthClients.Create(ctx, orgTestRead.Name, options)
require.NoError(t, errCreate)
assert.NotEmpty(t, oc.ID)
assert.Equal(t, "https://githubenterprise.xxxxx", oc.APIURL)
assert.Equal(t, "https://githubenterprise.xxxxx", oc.HTTPURL)
assert.Equal(t, 1, len(oc.OAuthTokens))
assert.Equal(t, ServiceProviderGithubEE, oc.ServiceProvider)
assert.Equal(t, agentPoolTestRead.ID, oc.AgentPool.ID)
})
t.Run("with an invalid agent pool", func(t *testing.T) {
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
agentPoolTest, agentPoolCleanup := createAgentPool(t, client, orgTest)
defer agentPoolCleanup()
agentPoolID := agentPoolTest.ID
agentPoolTest.ID = badIdentifier
options := OAuthClientCreateOptions{
APIURL: String("https://githubenterprise.xxxxx"),
HTTPURL: String("https://githubenterprise.xxxxx"),
OAuthToken: String(githubToken),
ServiceProvider: ServiceProvider(ServiceProviderGithubEE),
AgentPool: agentPoolTest,
}
_, errCreate := client.OAuthClients.Create(ctx, orgTest.Name, options)
require.Error(t, errCreate)
assert.Contains(t, errCreate.Error(), "the provided agent pool does not exist or you are not authorized to use it")
agentPoolTest.ID = agentPoolID
})
t.Run("with no agents connected", func(t *testing.T) {
t.Skip("Skipping due to persistent failures - see TF-31172")
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
agentPoolTest, agentPoolCleanup := createAgentPool(t, client, orgTest)
defer agentPoolCleanup()
options := OAuthClientCreateOptions{
APIURL: String("https://githubenterprise.xxxxx"),
HTTPURL: String("https://githubenterprise.xxxxx"),
OAuthToken: String(githubToken),
ServiceProvider: ServiceProvider(ServiceProviderGithubEE),
AgentPool: agentPoolTest,
}
_, errCreate := client.OAuthClients.Create(ctx, orgTest.Name, options)
assert.Contains(t, errCreate.Error(), "the organization does not have private VCS enabled")
require.Error(t, errCreate)
})
}
func TestOAuthClientsRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
ocTest, ocTestCleanup := createOAuthClient(t, client, nil, nil)
defer ocTestCleanup()
t.Run("when the OAuth client exists", func(t *testing.T) {
oc, err := client.OAuthClients.Read(ctx, ocTest.ID)
require.NoError(t, err)
assert.Equal(t, ocTest.ID, oc.ID)
assert.Equal(t, ocTest.APIURL, oc.APIURL)
assert.Equal(t, ocTest.CallbackURL, oc.CallbackURL)
assert.Equal(t, ocTest.ConnectPath, oc.ConnectPath)
assert.Equal(t, ocTest.HTTPURL, oc.HTTPURL)
assert.Equal(t, ocTest.ServiceProvider, oc.ServiceProvider)
assert.Equal(t, ocTest.ServiceProviderName, oc.ServiceProviderName)
assert.Equal(t, ocTest.OAuthTokens, oc.OAuthTokens)
assert.Equal(t, ocTest.OrganizationScoped, oc.OrganizationScoped)
})
t.Run("when the OAuth client does not exist", func(t *testing.T) {
oc, err := client.OAuthClients.Read(ctx, "nonexisting")
assert.Nil(t, oc)
assert.Equal(t, ErrResourceNotFound, err)
})
t.Run("without a valid OAuth client ID", func(t *testing.T) {
oc, err := client.OAuthClients.Read(ctx, badIdentifier)
assert.Nil(t, oc)
assert.Equal(t, err, ErrInvalidOauthClientID)
})
}
func TestOAuthClientsReadWithOptions(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
pj, pjCleanup := createProject(t, client, orgTest)
defer pjCleanup()
ocTest, ocTestCleanup := createOAuthClient(t, client, nil, []*Project{pj})
defer ocTestCleanup()
opts := &OAuthClientReadOptions{
Include: []OAuthClientIncludeOpt{OauthClientProjects},
}
t.Run("when the OAuth client exists", func(t *testing.T) {
ocWithOptions, err := client.OAuthClients.ReadWithOptions(ctx, ocTest.ID, opts)
require.NoError(t, err)
assert.Equal(t, ocTest.Projects, ocWithOptions.Projects)
})
}
func TestOAuthClientsDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
ocTest, _ := createOAuthClient(t, client, orgTest, nil)
t.Run("with valid options", func(t *testing.T) {
err := client.OAuthClients.Delete(ctx, ocTest.ID)
require.NoError(t, err)
_, err = retry(func() (interface{}, error) {
c, err := client.OAuthClients.Read(ctx, ocTest.ID)
if err != ErrResourceNotFound {
return nil, fmt.Errorf("expected %s, but err was %s", ErrResourceNotFound, err)
}
return c, err
})
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the OAuth client does not exist", func(t *testing.T) {
err := client.OAuthClients.Delete(ctx, ocTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the OAuth client ID is invalid", func(t *testing.T) {
err := client.OAuthClients.Delete(ctx, badIdentifier)
assert.Equal(t, err, ErrInvalidOauthClientID)
})
}
func TestOAuthClientsCreateOptionsValid(t *testing.T) {
t.Parallel()
t.Run("with valid options", func(t *testing.T) {
options := OAuthClientCreateOptions{
APIURL: String("https://api.github.com"),
HTTPURL: String("https://github.com"),
OAuthToken: String("NOTHING"),
ServiceProvider: ServiceProvider(ServiceProviderGithub),
}
err := options.valid()
assert.Nil(t, err)
})
t.Run("without an API URL", func(t *testing.T) {
options := OAuthClientCreateOptions{
HTTPURL: String("https://github.com"),
OAuthToken: String("NOTHING"),
ServiceProvider: ServiceProvider(ServiceProviderGithub),
}
err := options.valid()
assert.Equal(t, err, ErrRequiredAPIURL)
})
t.Run("without a HTTP URL", func(t *testing.T) {
options := OAuthClientCreateOptions{
APIURL: String("https://api.github.com"),
OAuthToken: String("NOTHING"),
ServiceProvider: ServiceProvider(ServiceProviderGithub),
}
err := options.valid()
assert.Equal(t, err, ErrRequiredHTTPURL)
})
t.Run("without an OAuth token", func(t *testing.T) {
options := OAuthClientCreateOptions{
APIURL: String("https://api.github.com"),
HTTPURL: String("https://github.com"),
ServiceProvider: ServiceProvider(ServiceProviderGithub),
}
err := options.valid()
assert.Equal(t, err, ErrRequiredOauthToken)
})
t.Run("without a service provider", func(t *testing.T) {
options := OAuthClientCreateOptions{
APIURL: String("https://api.github.com"),
HTTPURL: String("https://github.com"),
OAuthToken: String("NOTHING"),
}
err := options.valid()
assert.Equal(t, err, ErrRequiredServiceProvider)
})
t.Run("without private key and not ado_server options", func(t *testing.T) {
options := OAuthClientCreateOptions{
APIURL: String("https://api.github.com"),
HTTPURL: String("https://github.com"),
OAuthToken: String("NOTHING"),
ServiceProvider: ServiceProvider(ServiceProviderGitlabEE),
}
err := options.valid()
assert.Nil(t, err)
})
t.Run("with empty private key and not ado_server options", func(t *testing.T) {
options := OAuthClientCreateOptions{
APIURL: String("https://api.github.com"),
HTTPURL: String("https://github.com"),
OAuthToken: String("NOTHING"),
ServiceProvider: ServiceProvider(ServiceProviderGitlabEE),
PrivateKey: String(""),
}
err := options.valid()
assert.Nil(t, err)
})
t.Run("with private key and not ado_server options", func(t *testing.T) {
options := OAuthClientCreateOptions{
APIURL: String("https://api.github.com"),
HTTPURL: String("https://github.com"),
OAuthToken: String("NOTHING"),
ServiceProvider: ServiceProvider(ServiceProviderGithub),
PrivateKey: String("NOTHING"),
}
err := options.valid()
assert.Equal(t, err, ErrUnsupportedPrivateKey)
})
t.Run("with valid options including private key", func(t *testing.T) {
options := OAuthClientCreateOptions{
APIURL: String("https://ado.example.com"),
HTTPURL: String("https://ado.example.com"),
OAuthToken: String("NOTHING"),
ServiceProvider: ServiceProvider(ServiceProviderAzureDevOpsServer),
PrivateKey: String("NOTHING"),
}
err := options.valid()
assert.Nil(t, err)
})
}
func TestOAuthClientsAddProjects(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
pTest1, pTestCleanup1 := createProject(t, client, orgTest)
defer pTestCleanup1()
pTest2, pTestCleanup2 := createProject(t, client, orgTest)
defer pTestCleanup2()
psTest, psTestCleanup := createOAuthClient(t, client, orgTest, nil)
defer psTestCleanup()
t.Run("with projects provided", func(t *testing.T) {
err := client.OAuthClients.AddProjects(
ctx,
psTest.ID,
OAuthClientAddProjectsOptions{
Projects: []*Project{pTest1, pTest2},
},
)
require.NoError(t, err)
ps, err := client.OAuthClients.Read(ctx, psTest.ID)
require.NoError(t, err)
assert.Equal(t, 2, len(ps.Projects))
var ids []string
for _, pj := range ps.Projects {
ids = append(ids, pj.ID)
}
assert.Contains(t, ids, pTest1.ID)
assert.Contains(t, ids, pTest2.ID)
})
t.Run("without projects provided", func(t *testing.T) {
err := client.OAuthClients.AddProjects(
ctx,
psTest.ID,
OAuthClientAddProjectsOptions{},
)
assert.Equal(t, err, ErrRequiredProject)
})
t.Run("with empty projects slice", func(t *testing.T) {
err := client.OAuthClients.AddProjects(
ctx,
psTest.ID,
OAuthClientAddProjectsOptions{Projects: []*Project{}},
)
assert.Equal(t, err, ErrProjectMinLimit)
})
t.Run("without a valid ID", func(t *testing.T) {
err := client.OAuthClients.AddProjects(
ctx,
badIdentifier,
OAuthClientAddProjectsOptions{
Projects: []*Project{pTest1, pTest2},
},
)
assert.Equal(t, err, ErrInvalidOauthClientID)
})
}
func TestOAuthClientsRemoveProjects(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
pTest1, pTestCleanup1 := createProject(t, client, orgTest)
defer pTestCleanup1()
pTest2, pTestCleanup2 := createProject(t, client, orgTest)
defer pTestCleanup2()
psTest, psTestCleanup := createOAuthClient(t, client, orgTest, []*Project{pTest1, pTest2})
defer psTestCleanup()
t.Run("with projects provided", func(t *testing.T) {
err := client.OAuthClients.RemoveProjects(
ctx,
psTest.ID,
OAuthClientRemoveProjectsOptions{
Projects: []*Project{pTest1, pTest2},
},
)
require.NoError(t, err)
ps, err := client.OAuthClients.Read(ctx, psTest.ID)
require.NoError(t, err)
assert.Equal(t, 0, len(ps.Projects))
assert.Empty(t, ps.Projects)
})
t.Run("without projects provided", func(t *testing.T) {
err := client.OAuthClients.RemoveProjects(
ctx,
psTest.ID,
OAuthClientRemoveProjectsOptions{},
)
assert.Equal(t, err, ErrRequiredProject)
})
t.Run("with empty projects slice", func(t *testing.T) {
err := client.OAuthClients.RemoveProjects(
ctx,
psTest.ID,
OAuthClientRemoveProjectsOptions{Projects: []*Project{}},
)
assert.Equal(t, err, ErrProjectMinLimit)
})
t.Run("without a valid ID", func(t *testing.T) {
err := client.OAuthClients.RemoveProjects(
ctx,
badIdentifier,
OAuthClientRemoveProjectsOptions{
Projects: []*Project{pTest1, pTest2},
},
)
assert.Equal(t, err, ErrInvalidOauthClientID)
})
}
func TestOAuthClientsUpdate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
t.Run("updates organization scoped", func(t *testing.T) {
organizationScoped := false
organizationScopedTrue := true
options := OAuthClientCreateOptions{
APIURL: String("https://bbdc.com"),
HTTPURL: String("https://bbdc.com"),
ServiceProvider: ServiceProvider(ServiceProviderBitbucketDataCenter),
OrganizationScoped: &organizationScopedTrue,
}
origOC, err := client.OAuthClients.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.NotEmpty(t, origOC.ID)
updateOpts := OAuthClientUpdateOptions{
OrganizationScoped: &organizationScoped,
}
oc, err := client.OAuthClients.Update(ctx, origOC.ID, updateOpts)
require.NoError(t, err)
assert.NotEmpty(t, oc.ID)
assert.NotEqual(t, origOC.OrganizationScoped, oc.OrganizationScoped)
})
t.Run("updates agent pool", func(t *testing.T) {
t.Skip("Skipping due to persistent failures - see TF-31172")
testAgentPool1, agentPoolCleanup := createAgentPool(t, client, orgTest)
defer agentPoolCleanup()
testAgentPool2, agentPoolCleanup2 := createAgentPool(t, client, orgTest)
defer agentPoolCleanup2()
organizationScopedTrue := true
options := OAuthClientCreateOptions{
APIURL: String("https://bbdc.com"),
HTTPURL: String("https://bbdc.com"),
ServiceProvider: ServiceProvider(ServiceProviderBitbucketDataCenter),
OrganizationScoped: &organizationScopedTrue,
AgentPool: testAgentPool1,
}
origOC, err := client.OAuthClients.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.NotEmpty(t, origOC.ID)
updateOpts := OAuthClientUpdateOptions{
AgentPool: testAgentPool2,
}
oc, err := client.OAuthClients.Update(ctx, origOC.ID, updateOpts)
require.NoError(t, err)
assert.NotEmpty(t, oc.ID)
assert.Equal(t, oc.AgentPool.ID, testAgentPool2.ID)
assert.NotEqual(t, origOC.AgentPool.ID, oc.AgentPool.ID)
})
}
const publicKey = `
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoKizy4xbN6qZFAwIJV24
-----END PUBLIC KEY-----
`
const privateKey = `
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAoKizy4xbN6qZFAwIJV24liz/vYBSvR3SjEiUzhpp0uMAmICN
-----END RSA PRIVATE KEY-----
`
func TestOAuthClientsUpdate_rsaKeyPair(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
t.Run("updates a new key", func(t *testing.T) {
originalKey := randomString(t)
options := OAuthClientCreateOptions{
APIURL: String("https://bbdc.com"),
HTTPURL: String("https://bbdc.com"),
ServiceProvider: ServiceProvider(ServiceProviderBitbucketDataCenter),
Key: String(originalKey),
Secret: String(privateKey),
RSAPublicKey: String(publicKey),
}
origOC, err := client.OAuthClients.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.NotEmpty(t, origOC.ID)
newKey := randomString(t)
updateOpts := OAuthClientUpdateOptions{
Key: String(newKey),
}
oc, err := client.OAuthClients.Update(ctx, origOC.ID, updateOpts)
require.NoError(t, err)
assert.NotEmpty(t, oc.ID)
assert.Equal(t, ServiceProviderBitbucketDataCenter, oc.ServiceProvider)
assert.Equal(t, oc.RSAPublicKey, origOC.RSAPublicKey)
assert.Equal(t, newKey, oc.Key)
})
t.Run("errors when missing key", func(t *testing.T) {
originalKey := randomString(t)
options := OAuthClientCreateOptions{
APIURL: String("https://bbdc.com"),
HTTPURL: String("https://bbdc.com"),
ServiceProvider: ServiceProvider(ServiceProviderBitbucketDataCenter),
Key: String(originalKey),
Secret: String(privateKey),
RSAPublicKey: String(publicKey),
}
origOC, err := client.OAuthClients.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.NotEmpty(t, origOC.ID)
updateOpts := OAuthClientUpdateOptions{
Key: String(""),
}
_, err = client.OAuthClients.Update(ctx, origOC.ID, updateOpts)
assert.Error(t, err, "The Consumer Key for Bitbucket Data Center must be present. Please add a value for `key`.")
})
}
================================================
FILE: oauth_token.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ OAuthTokens = (*oAuthTokens)(nil)
// OAuthTokens describes all the OAuth token related methods that the
// Terraform Enterprise API supports.
//
// TFE API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/oauth-tokens
type OAuthTokens interface {
// List all the OAuth tokens for a given organization.
List(ctx context.Context, organization string, options *OAuthTokenListOptions) (*OAuthTokenList, error)
// Read a OAuth token by its ID.
Read(ctx context.Context, oAuthTokenID string) (*OAuthToken, error)
// Update an existing OAuth token.
Update(ctx context.Context, oAuthTokenID string, options OAuthTokenUpdateOptions) (*OAuthToken, error)
// Delete a OAuth token by its ID.
Delete(ctx context.Context, oAuthTokenID string) error
}
// oAuthTokens implements OAuthTokens.
type oAuthTokens struct {
client *Client
}
// OAuthTokenList represents a list of OAuth tokens.
type OAuthTokenList struct {
*Pagination
Items []*OAuthToken
}
// OAuthToken represents a VCS configuration including the associated
// OAuth token
type OAuthToken struct {
ID string `jsonapi:"primary,oauth-tokens"`
UID string `jsonapi:"attr,uid"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
HasSSHKey bool `jsonapi:"attr,has-ssh-key"`
ServiceProviderUser string `jsonapi:"attr,service-provider-user"`
// Relations
OAuthClient *OAuthClient `jsonapi:"relation,oauth-client"`
}
// OAuthTokenListOptions represents the options for listing
// OAuth tokens.
type OAuthTokenListOptions struct {
ListOptions
}
// OAuthTokenUpdateOptions represents the options for updating an OAuth token.
type OAuthTokenUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,oauth-tokens"`
// Optional: A private SSH key to be used for git clone operations.
PrivateSSHKey *string `jsonapi:"attr,ssh-key,omitempty"`
}
// List all the OAuth tokens for a given organization.
func (s *oAuthTokens) List(ctx context.Context, organization string, options *OAuthTokenListOptions) (*OAuthTokenList, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf("organizations/%s/oauth-tokens", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
otl := &OAuthTokenList{}
err = req.Do(ctx, otl)
if err != nil {
return nil, err
}
return otl, nil
}
// Read an OAuth token by its ID.
func (s *oAuthTokens) Read(ctx context.Context, oAuthTokenID string) (*OAuthToken, error) {
if !validStringID(&oAuthTokenID) {
return nil, ErrInvalidOauthTokenID
}
u := fmt.Sprintf("oauth-tokens/%s", url.PathEscape(oAuthTokenID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
ot := &OAuthToken{}
err = req.Do(ctx, ot)
if err != nil {
return nil, err
}
return ot, err
}
// Update an existing OAuth token.
func (s *oAuthTokens) Update(ctx context.Context, oAuthTokenID string, options OAuthTokenUpdateOptions) (*OAuthToken, error) {
if !validStringID(&oAuthTokenID) {
return nil, ErrInvalidOauthTokenID
}
u := fmt.Sprintf("oauth-tokens/%s", url.PathEscape(oAuthTokenID))
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
ot := &OAuthToken{}
err = req.Do(ctx, ot)
if err != nil {
return nil, err
}
return ot, err
}
// Delete an OAuth token by its ID.
func (s *oAuthTokens) Delete(ctx context.Context, oAuthTokenID string) error {
if !validStringID(&oAuthTokenID) {
return ErrInvalidOauthTokenID
}
u := fmt.Sprintf("oauth-tokens/%s", url.PathEscape(oAuthTokenID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
================================================
FILE: oauth_token_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestOAuthTokensList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
otTest1, otTest1Cleanup := createOAuthToken(t, client, orgTest)
defer otTest1Cleanup()
otTest2, otTest2Cleanup := createOAuthToken(t, client, orgTest)
defer otTest2Cleanup()
t.Run("without list options", func(t *testing.T) {
otl, err := client.OAuthTokens.List(ctx, orgTest.Name, nil)
require.NoError(t, err)
t.Run("the OAuth client relationship is decoded correcly", func(t *testing.T) {
for _, ot := range otl.Items {
assert.NotEmpty(t, ot.OAuthClient)
}
})
// We need to strip some fields before the next test.
for _, ot := range otl.Items {
ot.CreatedAt = time.Time{}
ot.ServiceProviderUser = ""
ot.OAuthClient = nil
}
assert.Contains(t, otl.Items, otTest1)
assert.Contains(t, otl.Items, otTest2)
t.Skip("paging not supported yet in API")
assert.Equal(t, 1, otl.CurrentPage)
assert.Equal(t, 2, otl.TotalCount)
})
t.Run("with list options", func(t *testing.T) {
t.Skip("paging not supported yet in API")
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
options := &OAuthTokenListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
}
otl, err := client.OAuthTokens.List(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.Empty(t, otl.Items)
assert.Equal(t, 999, otl.CurrentPage)
assert.Equal(t, 2, otl.TotalCount)
})
t.Run("without a valid organization", func(t *testing.T) {
otl, err := client.OAuthTokens.List(ctx, badIdentifier, nil)
assert.Nil(t, otl)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
func TestOAuthTokensRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
otTest, otTestCleanup := createOAuthToken(t, client, nil)
defer otTestCleanup()
t.Run("when the OAuth token exists", func(t *testing.T) {
ot, err := client.OAuthTokens.Read(ctx, otTest.ID)
require.NoError(t, err)
assert.Equal(t, otTest.ID, ot.ID)
assert.NotEmpty(t, ot.OAuthClient)
})
t.Run("when the OAuth token does not exist", func(t *testing.T) {
ot, err := client.OAuthTokens.Read(ctx, "nonexisting")
assert.Nil(t, ot)
assert.Equal(t, ErrResourceNotFound, err)
})
t.Run("without a valid OAuth token ID", func(t *testing.T) {
ot, err := client.OAuthTokens.Read(ctx, badIdentifier)
assert.Nil(t, ot)
assert.Equal(t, err, ErrInvalidOauthTokenID)
})
}
func TestOAuthTokensUpdate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
otTest, otTestCleanup := createOAuthToken(t, client, nil)
defer otTestCleanup()
t.Run("before updating with an SSH key", func(t *testing.T) {
assert.False(t, otTest.HasSSHKey)
})
t.Run("without options", func(t *testing.T) {
ot, err := client.OAuthTokens.Update(ctx, otTest.ID, OAuthTokenUpdateOptions{})
require.NoError(t, err)
assert.False(t, ot.HasSSHKey)
})
t.Run("when updating with a valid SSH key", func(t *testing.T) {
dummyPrivateSSHKey := `-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQDIF0s2yX7dSQQL1grdTbai1Mb7sEco6RIOz8iqrHTGqmESpu5n
d8imMkV5KadgVBJ/UvHsWpg446O3DAMYn0Y6f8dDlK7pmCEtiGVKTR1PaVRMpF8R
5Guvrmlru8Kex5ozh0pPMB15aGsIzSezCKgSs74Od9YL4smdgKyYwqsu3wIDAQAB
AoGBAKCs6+4j4icqYgBrMjBCHp4lRWCJTqtQdfrE6jv73o5F9Uu4FwupScwD5HwG
cezNtkjeP3zvxvsv+aCdGcNk60vSz4n9Nt6gEJveWFSpePYXKZ9cz/IjFLI7nSzc
1msLyE3DfUqB91s/A/aT5p0LiVDc8i4mCGDOga2OINIwqDGZAkEA/Vz8dkcqsAVW
CL1F000hWTrM6tu0V+x8Nm8CRx7wM/Gy/19PbV0t26wCVG0GXyLWsV2//huY7w5b
3AcSl5pfJQJBAMosYQXk5L4S+qivz2zmZdtyz+Ik6IZ3PwZoED32PxGSdW5rG8iP
V+iSJek5ESkS1zeXwDMnF4LeoBY9H07DiLMCQQCrHm1o2SIMpl34IxWQ4+wdHuid
yuuf4pn2Db2lGVE0VA8ICXBUtfUuA5vDN6tw/8+vFVmBn1QISVNjZOd6uwl9AkA+
jIRoAm0SsWSDlAEkvBN/VYIjgS+/il0haki8ItdYZGuYgeLSpiaYeb7o7RL2FjIn
rPd12/5WKvJ0buykvbIpAkEA5Uy3T8xQJkDGbp0+xA0yThoOYiB09lAok8I7Sv/5
dpIe8YOINN27XaojJvVpT5uBVCcZLF+G7kaMjSwCTlDx3Q==
-----END RSA PRIVATE KEY-----`
ot, err := client.OAuthTokens.Update(ctx, otTest.ID, OAuthTokenUpdateOptions{
PrivateSSHKey: String(dummyPrivateSSHKey),
})
require.NoError(t, err)
assert.Equal(t, otTest.ID, ot.ID)
assert.True(t, ot.HasSSHKey)
})
t.Run("when updating with an invalid SSH key", func(t *testing.T) {
ot, err := client.OAuthTokens.Update(ctx, otTest.ID, OAuthTokenUpdateOptions{
PrivateSSHKey: String(randomString(t)),
})
assert.Nil(t, ot)
assert.Contains(t, err.Error(), "Ssh key is invalid")
})
t.Run("without a valid policy ID", func(t *testing.T) {
ot, err := client.OAuthTokens.Update(ctx, badIdentifier, OAuthTokenUpdateOptions{})
assert.Nil(t, ot)
assert.Equal(t, err, ErrInvalidOauthTokenID)
})
}
func TestOAuthTokensDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
otTest, otCleanup := createOAuthToken(t, client, orgTest)
defer otCleanup()
t.Run("with valid options", func(t *testing.T) {
err := client.OAuthTokens.Delete(ctx, otTest.ID)
require.NoError(t, err)
// Try loading the OAuth token - it should fail.
_, err = client.OAuthTokens.Read(ctx, otTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the OAuth token does not exist", func(t *testing.T) {
err := client.OAuthTokens.Delete(ctx, otTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the OAuth token ID is invalid", func(t *testing.T) {
err := client.OAuthTokens.Delete(ctx, badIdentifier)
assert.Equal(t, err, ErrInvalidOauthTokenID)
})
}
================================================
FILE: organization.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ Organizations = (*organizations)(nil)
// Organizations describes all the organization related methods that the
// Terraform Enterprise API supports.
//
// TFE API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/organizations
type Organizations interface {
// List all the organizations visible to the current user.
List(ctx context.Context, options *OrganizationListOptions) (*OrganizationList, error)
// Create a new organization with the given options.
Create(ctx context.Context, options OrganizationCreateOptions) (*Organization, error)
// Read an organization by its name.
Read(ctx context.Context, organization string) (*Organization, error)
// Read an organization by its name with options
ReadWithOptions(ctx context.Context, organization string, options OrganizationReadOptions) (*Organization, error)
// Update attributes of an existing organization.
Update(ctx context.Context, organization string, options OrganizationUpdateOptions) (*Organization, error)
// Delete an organization by its name.
Delete(ctx context.Context, organization string) error
// ReadCapacity shows the current run capacity of an organization.
ReadCapacity(ctx context.Context, organization string) (*Capacity, error)
// ReadEntitlements shows the entitlements of an organization.
ReadEntitlements(ctx context.Context, organization string) (*Entitlements, error)
// ReadRunQueue shows the current run queue of an organization.
ReadRunQueue(ctx context.Context, organization string, options ReadRunQueueOptions) (*RunQueue, error)
// ReadDataRetentionPolicy reads an organization's data retention policy
// **Note: This functionality is only available in Terraform Enterprise versions v202311-1 and v202312-1.**
//
// Deprecated: Use ReadDataRetentionPolicyChoice instead.
ReadDataRetentionPolicy(ctx context.Context, organization string) (*DataRetentionPolicy, error)
// ReadDataRetentionPolicyChoice reads an organization's data retention policy
// **Note: This functionality is only available in Terraform Enterprise.**
ReadDataRetentionPolicyChoice(ctx context.Context, organization string) (*DataRetentionPolicyChoice, error)
// SetDataRetentionPolicy sets an organization's data retention policy
// **Note: This functionality is only available in Terraform Enterprise versions v202311-1 and v202312-1.**
//
// Deprecated: Use SetDataRetentionPolicyDeleteOlder instead
SetDataRetentionPolicy(ctx context.Context, organization string, options DataRetentionPolicySetOptions) (*DataRetentionPolicy, error)
// SetDataRetentionPolicyDeleteOlder sets an organization's data retention policy to delete data older than a certain number of days
// **Note: This functionality is only available in Terraform Enterprise.**
SetDataRetentionPolicyDeleteOlder(ctx context.Context, organization string, options DataRetentionPolicyDeleteOlderSetOptions) (*DataRetentionPolicyDeleteOlder, error)
// SetDataRetentionPolicyDontDelete sets an organization's data retention policy to explicitly not delete data
// **Note: This functionality is only available in Terraform Enterprise.**
SetDataRetentionPolicyDontDelete(ctx context.Context, organization string, options DataRetentionPolicyDontDeleteSetOptions) (*DataRetentionPolicyDontDelete, error)
// DeleteDataRetentionPolicy deletes an organization's data retention policy
// **Note: This functionality is only available in Terraform Enterprise.**
DeleteDataRetentionPolicy(ctx context.Context, organization string) error
}
// organizations implements Organizations.
type organizations struct {
client *Client
}
// AuthPolicyType represents an authentication policy type.
type AuthPolicyType string
// List of available authentication policies.
const (
AuthPolicyPassword AuthPolicyType = "password"
AuthPolicyTwoFactor AuthPolicyType = "two_factor_mandatory"
)
// OrganizationList represents a list of organizations.
type OrganizationList struct {
*Pagination
Items []*Organization
}
// Organization represents a Terraform Enterprise organization.
type Organization struct {
Name string `jsonapi:"primary,organizations"`
AssessmentsEnforced bool `jsonapi:"attr,assessments-enforced"`
CollaboratorAuthPolicy AuthPolicyType `jsonapi:"attr,collaborator-auth-policy"`
CostEstimationEnabled bool `jsonapi:"attr,cost-estimation-enabled"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
DefaultExecutionMode string `jsonapi:"attr,default-execution-mode"`
Email string `jsonapi:"attr,email"`
ExternalID string `jsonapi:"attr,external-id"`
IsUnified bool `jsonapi:"attr,is-unified"`
OwnersTeamSAMLRoleID string `jsonapi:"attr,owners-team-saml-role-id"`
Permissions *OrganizationPermissions `jsonapi:"attr,permissions"`
SAMLEnabled bool `jsonapi:"attr,saml-enabled"`
StacksEnabled bool `jsonapi:"attr,stacks-enabled"`
SessionRemember int `jsonapi:"attr,session-remember"`
SessionTimeout int `jsonapi:"attr,session-timeout"`
TrialExpiresAt time.Time `jsonapi:"attr,trial-expires-at,iso8601"`
TwoFactorConformant bool `jsonapi:"attr,two-factor-conformant"`
SendPassingStatusesForUntriggeredSpeculativePlans bool `jsonapi:"attr,send-passing-statuses-for-untriggered-speculative-plans"`
RemainingTestableCount int `jsonapi:"attr,remaining-testable-count"`
SpeculativePlanManagementEnabled bool `jsonapi:"attr,speculative-plan-management-enabled"`
EnforceHYOK bool `jsonapi:"attr,enforce-hyok"`
UserTokensEnabled *bool `jsonapi:"attr,user-tokens-enabled"`
// Optional: If enabled, SendPassingStatusesForUntriggeredSpeculativePlans needs to be false.
AggregatedCommitStatusEnabled bool `jsonapi:"attr,aggregated-commit-status-enabled,omitempty"`
// Note: This will be false for TFE versions older than v202211, where the setting was introduced.
// On those TFE versions, safe delete does not exist, so ALL deletes will be force deletes.
AllowForceDeleteWorkspaces bool `jsonapi:"attr,allow-force-delete-workspaces"`
// Relations
DefaultProject *Project `jsonapi:"relation,default-project"`
DefaultAgentPool *AgentPool `jsonapi:"relation,default-agent-pool"`
PrimaryHYOKConfiguration *HYOKConfiguration `jsonapi:"relation,primary-hyok-configuration,omitempty"`
// Deprecated: Use DataRetentionPolicyChoice instead.
DataRetentionPolicy *DataRetentionPolicy
// **Note: This functionality is only available in Terraform Enterprise.**
DataRetentionPolicyChoice *DataRetentionPolicyChoice `jsonapi:"polyrelation,data-retention-policy"`
}
// OrganizationIncludeOpt represents the available options for include query params.
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/organizations#available-related-resources
type OrganizationIncludeOpt string
const (
// **Note: This include option is still in BETA and subject to change.**
OrganizationDefaultProject OrganizationIncludeOpt = "default-project"
)
// OrganizationReadOptions represents the options for reading organizations.
type OrganizationReadOptions struct {
// Optional: A list of relations to include. See available resources
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/organizations#available-related-resources
Include []OrganizationIncludeOpt `url:"include,omitempty"`
}
// Capacity represents the current run capacity of an organization.
type Capacity struct {
Organization string `jsonapi:"primary,organization-capacity"`
Pending int `jsonapi:"attr,pending"`
Running int `jsonapi:"attr,running"`
}
// Entitlements represents the entitlements of an organization.
type Entitlements struct {
ID string `jsonapi:"primary,entitlement-sets"`
Agents bool `jsonapi:"attr,agents"`
AuditLogging bool `jsonapi:"attr,audit-logging"`
CostEstimation bool `jsonapi:"attr,cost-estimation"`
GlobalRunTasks bool `jsonapi:"attr,global-run-tasks"`
Operations bool `jsonapi:"attr,operations"`
PrivateModuleRegistry bool `jsonapi:"attr,private-module-registry"`
PrivateRunTasks bool `jsonapi:"attr,private-run-tasks"`
RunTasks bool `jsonapi:"attr,run-tasks"`
SSO bool `jsonapi:"attr,sso"`
Sentinel bool `jsonapi:"attr,sentinel"`
StateStorage bool `jsonapi:"attr,state-storage"`
Teams bool `jsonapi:"attr,teams"`
VCSIntegrations bool `jsonapi:"attr,vcs-integrations"`
WaypointActions bool `jsonapi:"attr,waypoint-actions"`
WaypointTemplatesAndAddons bool `jsonapi:"attr,waypoint-templates-and-addons"`
}
// RunQueue represents the current run queue of an organization.
type RunQueue struct {
*Pagination
Items []*Run
}
// OrganizationPermissions represents the organization permissions.
type OrganizationPermissions struct {
CanCreateTeam bool `jsonapi:"attr,can-create-team"`
CanCreateWorkspace bool `jsonapi:"attr,can-create-workspace"`
CanCreateWorkspaceMigration bool `jsonapi:"attr,can-create-workspace-migration"`
CanDeployNoCodeModules bool `jsonapi:"attr,can-deploy-no-code-modules"`
CanDestroy bool `jsonapi:"attr,can-destroy"`
CanManageAuditing bool `jsonapi:"attr,can-manage-auditing"`
CanManageNoCodeModules bool `jsonapi:"attr,can-manage-no-code-modules"`
CanManageRunTasks bool `jsonapi:"attr,can-manage-run-tasks"`
CanTraverse bool `jsonapi:"attr,can-traverse"`
CanUpdate bool `jsonapi:"attr,can-update"`
CanUpdateAPIToken bool `jsonapi:"attr,can-update-api-token"`
CanUpdateOAuth bool `jsonapi:"attr,can-update-oauth"`
CanUpdateSentinel bool `jsonapi:"attr,can-update-sentinel"`
CanUpdateHYOKConfiguration bool `jsonapi:"attr,can-update-hyok-configuration"`
CanViewHYOKFeatureInfo bool `jsonapi:"attr,can-view-hyok-feature-info"`
CanEnableStacks bool `jsonapi:"attr,can-enable-stacks"`
CanCreateProject bool `jsonapi:"attr,can-create-project"`
}
// OrganizationListOptions represents the options for listing organizations.
type OrganizationListOptions struct {
ListOptions
// Optional: A query string used to filter organizations.
// Organizations with a name or email partially matching this value will be returned.
Query string `url:"q,omitempty"`
}
// OrganizationCreateOptions represents the options for creating an organization.
type OrganizationCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,organizations"`
// Required: Name of the organization.
Name *string `jsonapi:"attr,name"`
// Optional: AssessmentsEnforced toggles whether health assessment enablement is enforced across all assessable workspaces (those with a minimum terraform version of 0.15.4 and not running in local execution mode) or if the decision to enabled health assessments is delegated to the workspace setting AssessmentsEnabled.
AssessmentsEnforced *bool `jsonapi:"attr,assessments-enforced,omitempty"`
// Required: Admin email address.
Email *string `jsonapi:"attr,email"`
// Optional: Session expiration (minutes).
SessionRemember *int `jsonapi:"attr,session-remember,omitempty"`
// Optional: Session timeout after inactivity (minutes).
SessionTimeout *int `jsonapi:"attr,session-timeout,omitempty"`
// Optional: Authentication policy.
CollaboratorAuthPolicy *AuthPolicyType `jsonapi:"attr,collaborator-auth-policy,omitempty"`
// Optional: Enable Cost Estimation
CostEstimationEnabled *bool `jsonapi:"attr,cost-estimation-enabled,omitempty"`
// Optional: The name of the "owners" team
OwnersTeamSAMLRoleID *string `jsonapi:"attr,owners-team-saml-role-id,omitempty"`
// Optional: SendPassingStatusesForUntriggeredSpeculativePlans toggles behavior of untriggered speculative plans to send status updates to version control systems like GitHub.
SendPassingStatusesForUntriggeredSpeculativePlans *bool `jsonapi:"attr,send-passing-statuses-for-untriggered-speculative-plans,omitempty"`
// Optional: If enabled, SendPassingStatusesForUntriggeredSpeculativePlans needs to be false.
AggregatedCommitStatusEnabled *bool `jsonapi:"attr,aggregated-commit-status-enabled,omitempty"`
// Optional: SpeculativePlanManagementEnabled toggles whether pending speculative plans from outdated commits will be cancelled if a newer commit is pushed to the same branch.
SpeculativePlanManagementEnabled *bool `jsonapi:"attr,speculative-plan-management-enabled,omitempty"`
// Optional: AllowForceDeleteWorkspaces toggles behavior of allowing workspace admins to delete workspaces with resources under management.
AllowForceDeleteWorkspaces *bool `jsonapi:"attr,allow-force-delete-workspaces,omitempty"`
// Optional: DefaultExecutionMode the default execution mode for workspaces
DefaultExecutionMode *string `jsonapi:"attr,default-execution-mode,omitempty"`
// Optional: EnforceHYOK if HYOK is enforced for the organization.
EnforceHYOK *bool `jsonapi:"attr,enforce-hyok,omitempty"`
// Optional: StacksEnabled toggles whether stacks are enabled for the organization. This setting
// is considered BETA, SUBJECT TO CHANGE, and likely unavailable to most users.
StacksEnabled *bool `jsonapi:"attr,stacks-enabled,omitempty"`
// Optional: RegistryMonorepoSupportEnabled toggles whether monorepo support is enabled for the organization
RegistryMonorepoSupportEnabled *bool `jsonapi:"attr,registry-monorepo-support-enabled,omitempty"`
// Optional: UserTokensEnabled toggles whether user tokens may be used to access resources in this organization.
UserTokensEnabled *bool `jsonapi:"attr,user-tokens-enabled,omitempty"`
}
// OrganizationUpdateOptions represents the options for updating an organization.
type OrganizationUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,organizations"`
// New name for the organization.
Name *string `jsonapi:"attr,name,omitempty"`
// Optional: AssessmentsEnforced toggles whether health assessment enablement is enforced across all assessable workspaces (those with a minimum terraform version of 0.15.4 and not running in local execution mode) or if the decision to enabled health assessments is delegated to the workspace setting AssessmentsEnabled.
AssessmentsEnforced *bool `jsonapi:"attr,assessments-enforced,omitempty"`
// New admin email address.
Email *string `jsonapi:"attr,email,omitempty"`
// Session expiration (minutes).
SessionRemember *int `jsonapi:"attr,session-remember,omitempty"`
// Session timeout after inactivity (minutes).
SessionTimeout *int `jsonapi:"attr,session-timeout,omitempty"`
// Authentication policy.
CollaboratorAuthPolicy *AuthPolicyType `jsonapi:"attr,collaborator-auth-policy,omitempty"`
// Enable Cost Estimation
CostEstimationEnabled *bool `jsonapi:"attr,cost-estimation-enabled,omitempty"`
// The name of the "owners" team
OwnersTeamSAMLRoleID *string `jsonapi:"attr,owners-team-saml-role-id,omitempty"`
// SendPassingStatusesForUntriggeredSpeculativePlans toggles behavior of untriggered speculative plans to send status updates to version control systems like GitHub.
SendPassingStatusesForUntriggeredSpeculativePlans *bool `jsonapi:"attr,send-passing-statuses-for-untriggered-speculative-plans,omitempty"`
// Optional: If enabled, SendPassingStatusesForUntriggeredSpeculativePlans needs to be false.
AggregatedCommitStatusEnabled *bool `jsonapi:"attr,aggregated-commit-status-enabled,omitempty"`
// Optional: SpeculativePlanManagementEnabled toggles whether pending speculative plans from outdated commits will be cancelled if a newer commit is pushed to the same branch.
SpeculativePlanManagementEnabled *bool `jsonapi:"attr,speculative-plan-management-enabled,omitempty"`
// Optional: AllowForceDeleteWorkspaces toggles behavior of allowing workspace admins to delete workspaces with resources under management.
AllowForceDeleteWorkspaces *bool `jsonapi:"attr,allow-force-delete-workspaces,omitempty"`
// Optional: DefaultExecutionMode the default execution mode for workspaces
DefaultExecutionMode *string `jsonapi:"attr,default-execution-mode,omitempty"`
// Optional: DefaultAgentPoolId default agent pool for workspaces, requires DefaultExecutionMode to be set to `agent`
DefaultAgentPool *AgentPool `jsonapi:"relation,default-agent-pool,omitempty"`
// Optional: EnforceHYOK if HYOK is enforced for the organization.
EnforceHYOK *bool `jsonapi:"attr,enforce-hyok,omitempty"`
// Optional: StacksEnabled toggles whether stacks are enabled for the organization. This setting
// is considered BETA, SUBJECT TO CHANGE, and likely unavailable to most users.
StacksEnabled *bool `jsonapi:"attr,stacks-enabled,omitempty"`
// Optional: RegistryMonorepoSupportEnabled toggles whether monorepo support is enabled for the organization
RegistryMonorepoSupportEnabled *bool `jsonapi:"attr,registry-monorepo-support-enabled,omitempty"`
// Optional: UserTokensEnabled toggles whether user tokens may be used to access resources in this organization.
UserTokensEnabled *bool `jsonapi:"attr,user-tokens-enabled,omitempty"`
}
// ReadRunQueueOptions represents the options for showing the queue.
type ReadRunQueueOptions struct {
ListOptions
}
// List all the organizations visible to the current user.
func (s *organizations) List(ctx context.Context, options *OrganizationListOptions) (*OrganizationList, error) {
req, err := s.client.NewRequest("GET", "organizations", options)
if err != nil {
return nil, err
}
orgl := &OrganizationList{}
err = req.Do(ctx, orgl)
if err != nil {
return nil, err
}
return orgl, nil
}
// Create a new organization with the given options.
func (s *organizations) Create(ctx context.Context, options OrganizationCreateOptions) (*Organization, error) {
if err := options.valid(); err != nil {
return nil, err
}
req, err := s.client.NewRequest("POST", "organizations", &options)
if err != nil {
return nil, err
}
org := &Organization{}
err = req.Do(ctx, org)
if err != nil {
return nil, err
}
return org, nil
}
// Read an organization by its name.
func (s *organizations) Read(ctx context.Context, organization string) (*Organization, error) {
return s.ReadWithOptions(ctx, organization, OrganizationReadOptions{})
}
// Read an organization by its name with options
func (s *organizations) ReadWithOptions(ctx context.Context, organization string, options OrganizationReadOptions) (*Organization, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf("organizations/%s", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, &options)
if err != nil {
return nil, err
}
org := &Organization{}
err = req.Do(ctx, org)
if err != nil {
return nil, err
}
// Manually populate the deprecated DataRetentionPolicy field
org.DataRetentionPolicy = org.DataRetentionPolicyChoice.ConvertToLegacyStruct()
return org, nil
}
// Update attributes of an existing organization.
func (s *organizations) Update(ctx context.Context, organization string, options OrganizationUpdateOptions) (*Organization, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf("organizations/%s", url.PathEscape(organization))
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
org := &Organization{}
err = req.Do(ctx, org)
if err != nil {
return nil, err
}
return org, nil
}
// Delete an organization by its name.
func (s *organizations) Delete(ctx context.Context, organization string) error {
if !validStringID(&organization) {
return ErrInvalidOrg
}
u := fmt.Sprintf("organizations/%s", url.PathEscape(organization))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// ReadCapacity shows the currently used capacity of an organization.
func (s *organizations) ReadCapacity(ctx context.Context, organization string) (*Capacity, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf("organizations/%s/capacity", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
c := &Capacity{}
err = req.Do(ctx, c)
if err != nil {
return nil, err
}
return c, nil
}
// ReadEntitlements shows the entitlements of an organization.
func (s *organizations) ReadEntitlements(ctx context.Context, organization string) (*Entitlements, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf("organizations/%s/entitlement-set", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
e := &Entitlements{}
err = req.Do(ctx, e)
if err != nil {
return nil, err
}
return e, nil
}
// ReadRunQueue shows the current run queue of an organization.
func (s *organizations) ReadRunQueue(ctx context.Context, organization string, options ReadRunQueueOptions) (*RunQueue, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf("organizations/%s/runs/queue", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, &options)
if err != nil {
return nil, err
}
rq := &RunQueue{}
err = req.Do(ctx, rq)
if err != nil {
return nil, err
}
return rq, nil
}
func (s *organizations) ReadDataRetentionPolicy(ctx context.Context, organization string) (*DataRetentionPolicy, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf("organizations/%s/relationships/data-retention-policy", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
dataRetentionPolicy := &DataRetentionPolicy{}
err = req.Do(ctx, dataRetentionPolicy)
if err != nil {
// try to detect known issue where this function is used with TFE >= 202401,
// and direct user towards the V2 function
if drpUnmarshalEr.MatchString(err.Error()) {
return nil, fmt.Errorf("error reading deprecated DataRetentionPolicy, use ReadDataRetentionPolicyChoice instead")
}
return nil, err
}
return dataRetentionPolicy, nil
}
func (s *organizations) ReadDataRetentionPolicyChoice(ctx context.Context, organization string) (*DataRetentionPolicyChoice, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
// The API to read the drp is org//relationships/data-retention-policy
// However, this API can return multiple "types" (e.g. data-retention-policy-delete-olders, or data-retention-policy-dont-deletes)
// Ideally we would deserialize this directly into the choice type (DataRetentionPolicyChoice)...however, there isn't a way to
// tell the current jsonapi implementation that the direct result of an endpoint could be different types. Relationships can be polymorphic,
// but the direct result of an endpoint can't be (as far as the jsonapi implementation is concerned)
// Instead, we need to figure out the type of the data retention policy first, and deserialize it into the matching model. We
// can then create a choice type manually
org, err := s.Read(ctx, organization)
if err != nil {
return nil, err
}
// there is no drp (of a known type)
if org.DataRetentionPolicyChoice == nil || !org.DataRetentionPolicyChoice.IsPopulated() {
return org.DataRetentionPolicyChoice, nil
}
u := s.dataRetentionPolicyLink(organization)
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
dataRetentionPolicy := &DataRetentionPolicyChoice{}
// if reading the org told us it was a "delete older policy" deserialize into the DeleteOlder portion of the choice model
if org.DataRetentionPolicyChoice.DataRetentionPolicyDeleteOlder != nil {
deleteOlder := &DataRetentionPolicyDeleteOlder{}
err = req.Do(ctx, deleteOlder)
dataRetentionPolicy.DataRetentionPolicyDeleteOlder = deleteOlder
// if reading the org told us it was a "delete older policy" deserialize into the DeleteOlder portion of the choice model
} else if org.DataRetentionPolicyChoice.DataRetentionPolicyDontDelete != nil {
dontDelete := &DataRetentionPolicyDontDelete{}
err = req.Do(ctx, dontDelete)
dataRetentionPolicy.DataRetentionPolicyDontDelete = dontDelete
} else if org.DataRetentionPolicyChoice.DataRetentionPolicy != nil {
legacyDrp := &DataRetentionPolicy{}
err = req.Do(ctx, legacyDrp)
dataRetentionPolicy.DataRetentionPolicy = legacyDrp
}
if err != nil {
return nil, err
}
return dataRetentionPolicy, nil
}
// Deprecated: Use SetDataRetentionPolicyDeleteOlder instead
// **Note: This functionality is only available in Terraform Enterprise versions v202311-1 and v202312-1.**
func (s *organizations) SetDataRetentionPolicy(ctx context.Context, organization string, options DataRetentionPolicySetOptions) (*DataRetentionPolicy, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := s.dataRetentionPolicyLink(organization)
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
dataRetentionPolicy := &DataRetentionPolicy{}
err = req.Do(ctx, dataRetentionPolicy)
if err != nil {
return nil, err
}
return dataRetentionPolicy, nil
}
func (s *organizations) SetDataRetentionPolicyDeleteOlder(ctx context.Context, organization string, options DataRetentionPolicyDeleteOlderSetOptions) (*DataRetentionPolicyDeleteOlder, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := s.dataRetentionPolicyLink(organization)
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
dataRetentionPolicy := &DataRetentionPolicyDeleteOlder{}
err = req.Do(ctx, dataRetentionPolicy)
if err != nil {
return nil, err
}
return dataRetentionPolicy, nil
}
func (s *organizations) SetDataRetentionPolicyDontDelete(ctx context.Context, organization string, options DataRetentionPolicyDontDeleteSetOptions) (*DataRetentionPolicyDontDelete, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := s.dataRetentionPolicyLink(organization)
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
dataRetentionPolicy := &DataRetentionPolicyDontDelete{}
err = req.Do(ctx, dataRetentionPolicy)
if err != nil {
return nil, err
}
return dataRetentionPolicy, nil
}
func (s *organizations) DeleteDataRetentionPolicy(ctx context.Context, organization string) error {
if !validStringID(&organization) {
return ErrInvalidOrg
}
u := s.dataRetentionPolicyLink(organization)
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o OrganizationCreateOptions) valid() error {
if !validString(o.Name) {
return ErrRequiredName
}
if !validStringID(o.Name) {
return ErrInvalidName
}
if !validString(o.Email) {
return ErrRequiredEmail
}
return nil
}
func (s *organizations) dataRetentionPolicyLink(name string) string {
return fmt.Sprintf("organizations/%s/relationships/data-retention-policy", url.PathEscape(name))
}
================================================
FILE: organization_audit_configuration.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"time"
)
var _ OrganizationAuditConfigurations = (*organizationAuditConfigurations)(nil)
// OrganizationAuditConfigurations describes the configuration for auditing events for the organization.
type OrganizationAuditConfigurations interface {
// Read the audit configuration of an organization by its name.
Read(ctx context.Context, organization string) (*OrganizationAuditConfiguration, error)
// Send a test audit event for an organization by its name.
Test(ctx context.Context, organization string) (*OrganizationAuditConfigurationTest, error)
// Update the audit configuration of an organization by its name.
Update(ctx context.Context, organization string, options OrganizationAuditConfigurationOptions) (*OrganizationAuditConfiguration, error)
}
// OrganizationAuditConfiguration represents the auditing configuration for a HCP Terraform Organization.
type OrganizationAuditConfiguration struct {
AuditTrails *OrganizationAuditConfigAuditTrails `jsonapi:"attr,audit-trails,omitempty"`
HCPAuditLogStreaming *OrganizationAuditConfigAuditStreaming `jsonapi:"attr,hcp-audit-log-streaming,omitempty"`
ID string `jsonapi:"primary,audit-configurations"`
Permissions *OrganizationAuditConfigPermissions `jsonapi:"attr,permissions,omitempty"`
Timestamps *OrganizationAuditConfigTimestamps `jsonapi:"attr,timestamps,omitempty"`
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
Organization *Organization `jsonapi:"relation,organization"`
}
type OrganizationAuditConfigAuditTrails struct {
Enabled bool `jsonapi:"attr,enabled"`
}
type OrganizationAuditConfigAuditStreaming struct {
Enabled bool `jsonapi:"attr,enabled"`
OrganizationID string `jsonapi:"attr,organization-id"`
UseDefaultOrganization bool `jsonapi:"attr,use-default-organization"`
}
type OrganizationAuditConfigPermissions struct {
CanEnableHCPAuditLogStreaming bool `jsonapi:"attr,can-enable-hcp-audit-log-streaming"`
CanSetHCPAuditLogStreamingOrganization bool `jsonapi:"attr,can-set-hcp-audit-log-streaming-organization-id"`
CanUseDefaultAuditLogStreamingOrganization bool `jsonapi:"attr,can-use-default-audit-log-streaming-organization"`
}
type OrganizationAuditConfigTimestamps struct {
AuditTrailsDisabledAt *time.Time `jsonapi:"attr,audit-trails-disabled-at,iso8601,omitempty"`
AuditTrailsEnabledAt *time.Time `jsonapi:"attr,audit-trails-enabled-at,iso8601,omitempty"`
AuditTrailsLastFailure *time.Time `jsonapi:"attr,audit-trails-last-failure,iso8601,omitempty"`
AuditTrailsLastSuccess *time.Time `jsonapi:"attr,audit-trails-last-success,iso8601,omitempty"`
HCPAuditLogStreamingDisabledAt *time.Time `jsonapi:"attr,hcp-audit-log-streaming-disabled-at,iso8601,omitempty"`
HCPAuditLogStreamingEnabledAt *time.Time `jsonapi:"attr,hcp-audit-log-streaming-enabled-at,iso8601,omitempty"`
HCPAuditLogStreamingLastFailure *time.Time `jsonapi:"attr,hcp-audit-log-streaming-last-failure,iso8601,omitempty"`
HCPAuditLogStreamingLastSuccess *time.Time `jsonapi:"attr,hcp-audit-log-streaming-last-success,iso8601,omitempty"`
}
type OrganizationAuditConfigurationTest struct {
RequestID *string `json:"request-id,omitempty"`
}
type OrganizationAuditConfigurationOptions struct {
AuditTrails *OrganizationAuditConfigAuditTrails `jsonapi:"attr,audit-trails,omitempty"`
HCPAuditLogStreaming *OrganizationAuditConfigAuditStreaming `jsonapi:"attr,hcp-audit-log-streaming,omitempty"`
}
type organizationAuditConfigurations struct {
client *Client
}
// Read the audit configuration of an organization by its name.
func (s *organizationAuditConfigurations) Read(ctx context.Context, organization string) (*OrganizationAuditConfiguration, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf("organizations/%s/audit-configuration", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
ac := &OrganizationAuditConfiguration{}
err = req.Do(ctx, ac)
if err != nil {
return nil, err
}
return ac, err
}
// Send a test audit event for an organization by its name.
func (s *organizationAuditConfigurations) Test(ctx context.Context, organization string) (*OrganizationAuditConfigurationTest, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf("organizations/%s/audit-configuration/test", url.PathEscape(organization))
req, err := s.client.NewRequest("POST", u, nil)
if err != nil {
return nil, err
}
result := &OrganizationAuditConfigurationTest{}
err = req.DoJSON(ctx, result)
if err != nil {
return nil, err
}
return result, err
}
// Update the audit configuration of an organization by its name.
func (s *organizationAuditConfigurations) Update(ctx context.Context, organization string, options OrganizationAuditConfigurationOptions) (*OrganizationAuditConfiguration, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf("organizations/%s/audit-configuration", url.PathEscape(organization))
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
ac := &OrganizationAuditConfiguration{}
err = req.Do(ctx, ac)
if err != nil {
return nil, err
}
return ac, err
}
================================================
FILE: organization_audit_configuration_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestOrganizationAuditConfigurationRead(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t)
if v, err := hasAuditLogging(client, orgTest.Name); err != nil {
t.Fatalf("Could not retrieve the entitlements for the test organization.: %s", err)
} else if !v {
t.Fatal("The test organization requires the audit-logging entitlement but is not entitled.")
return
}
ac, err := client.OrganizationAuditConfigurations.Read(ctx, orgTest.Name)
require.NoError(t, err)
// By default audit trails is enabled
assert.Equal(t, ac.AuditTrails.Enabled, true)
assert.NotNil(t, ac.Organization)
assert.Equal(t, orgTest.Name, ac.Organization.Name)
}
func TestOrganizationAuditConfigurationTest(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t)
if v, err := hasAuditLogging(client, orgTest.Name); err != nil {
t.Fatalf("Could not retrieve the entitlements for the test organization.: %s", err)
} else if !v {
t.Fatal("The test organization requires the audit-logging entitlement but is not entitled.")
return
}
result, err := client.OrganizationAuditConfigurations.Test(ctx, orgTest.Name)
require.NoError(t, err)
// Expect a Request ID is returned
assert.NotNil(t, result.RequestID)
}
func TestOrganizationAuditConfigurationUpdate(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t)
if v, err := hasAuditLogging(client, orgTest.Name); err != nil {
t.Fatalf("Could not retrieve the entitlements for the test organization.: %s", err)
} else if !v {
t.Fatal("The test organization requires the audit-logging entitlement but is not entitled.")
return
}
ac, err := client.OrganizationAuditConfigurations.Read(ctx, orgTest.Name)
require.NoError(t, err)
// Unfortunately we can't really test the HCP Log Streaming because it requires either an integrated HCP organization,
// or a valid HCP login session. Neither of which are setup for the test organization. Instead we just "update" the settings
// with the existing ones. This doesn't prove that the endpoint behaves properly, but just tests that we can at least send
// a payload to the expected API route.
newCfg, err := client.OrganizationAuditConfigurations.Update(ctx, orgTest.Name, OrganizationAuditConfigurationOptions{
AuditTrails: &OrganizationAuditConfigAuditTrails{
Enabled: ac.AuditTrails.Enabled,
},
HCPAuditLogStreaming: &OrganizationAuditConfigAuditStreaming{
Enabled: ac.HCPAuditLogStreaming.Enabled,
},
})
require.NoError(t, err)
assert.Equal(t, ac.AuditTrails.Enabled, newCfg.AuditTrails.Enabled)
assert.Equal(t, ac.HCPAuditLogStreaming.Enabled, newCfg.HCPAuditLogStreaming.Enabled)
}
================================================
FILE: organization_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"encoding/json"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestOrganizationsList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest1, orgTest1Cleanup := createOrganization(t, client)
t.Cleanup(orgTest1Cleanup)
orgTest2, orgTest2Cleanup := createOrganization(t, client)
t.Cleanup(orgTest2Cleanup)
t.Run("with no list options", func(t *testing.T) {
orgl, err := client.Organizations.List(ctx, nil)
require.NoError(t, err)
assert.Contains(t, orgl.Items, orgTest1)
assert.Contains(t, orgl.Items, orgTest2)
t.Skip("paging not supported yet in API")
assert.Equal(t, 1, orgl.CurrentPage)
assert.Equal(t, 2, orgl.TotalCount)
})
t.Run("with list options", func(t *testing.T) {
t.Skip("paging not supported yet in API")
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
orgl, err := client.Organizations.List(ctx, &OrganizationListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, orgl)
assert.Equal(t, 999, orgl.CurrentPage)
assert.Equal(t, 2, orgl.TotalCount)
})
t.Run("when querying on a valid org name", func(t *testing.T) {
org, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
orgList, err := client.Organizations.List(ctx, &OrganizationListOptions{
Query: org.Name,
})
require.NoError(t, err)
assert.Equal(t, true, orgItemsContainsName(orgList.Items, org.Name))
})
t.Run("when querying on a valid email", func(t *testing.T) {
org, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
orgList, err := client.Organizations.List(ctx, &OrganizationListOptions{
Query: org.Email,
})
require.NoError(t, err)
assert.Equal(t, true, orgItemsContainsEmail(orgList.Items, org.Email))
})
t.Run("with invalid query name", func(t *testing.T) {
org, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
orgList, err := client.Organizations.List(ctx, &OrganizationListOptions{
Query: org.Name,
})
require.NoError(t, err)
assert.NotEqual(t, orgList.Items, orgTest1.Name)
})
t.Run("with invalid query email", func(t *testing.T) {
org, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
orgList, err := client.Organizations.List(ctx, &OrganizationListOptions{
Query: org.Email,
})
require.NoError(t, err)
assert.NotEqual(t, orgList.Items, orgTest1.Email)
})
}
func TestOrganizationsCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
t.Run("with valid options", func(t *testing.T) {
options := OrganizationCreateOptions{
Name: String(randomString(t)),
Email: String(randomString(t) + "@tfe.local"),
}
org, err := client.Organizations.Create(ctx, options)
require.NoError(t, err)
t.Cleanup(func() {
err := client.Organizations.Delete(ctx, org.Name)
if err != nil {
t.Logf("error deleting organization (%s): %s", org.Name, err)
}
})
assert.Equal(t, *options.Name, org.Name)
assert.Equal(t, *options.Email, org.Email)
assert.Equal(t, "remote", org.DefaultExecutionMode)
assert.Nil(t, org.DefaultAgentPool)
})
t.Run("when no email is provided", func(t *testing.T) {
org, err := client.Organizations.Create(ctx, OrganizationCreateOptions{
Name: String("foo"),
})
assert.Nil(t, org)
assert.Equal(t, err, ErrRequiredEmail)
})
t.Run("when no name is provided", func(t *testing.T) {
_, err := client.Organizations.Create(ctx, OrganizationCreateOptions{
Email: String("foo@bar.com"),
})
assert.EqualError(t, err, ErrRequiredName.Error())
})
t.Run("with invalid name", func(t *testing.T) {
org, err := client.Organizations.Create(ctx, OrganizationCreateOptions{
Name: String(badIdentifier),
Email: String("foo@bar.com"),
})
assert.Nil(t, org)
assert.EqualError(t, err, ErrInvalidName.Error())
})
}
func TestOrganizationsReadWithBusiness(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
// With Business
newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t)
t.Run("when the org exists", func(t *testing.T) {
org, err := client.Organizations.Read(ctx, orgTest.Name)
require.NoError(t, err)
assert.Equal(t, orgTest.Name, org.Name)
assert.Equal(t, orgTest.ExternalID, org.ExternalID)
assert.NotEmpty(t, org.Permissions)
t.Run("permissions are properly decoded", func(t *testing.T) {
assert.True(t, org.Permissions.CanDestroy)
assert.True(t, org.Permissions.CanDeployNoCodeModules)
assert.True(t, org.Permissions.CanManageNoCodeModules)
})
})
}
func TestOrganizationsRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
t.Run("when the org exists", func(t *testing.T) {
org, err := client.Organizations.Read(ctx, orgTest.Name)
require.NoError(t, err)
assert.Equal(t, orgTest, org)
assert.NotEmpty(t, org.Permissions)
t.Run("permissions are properly decoded", func(t *testing.T) {
assert.True(t, org.Permissions.CanDestroy)
})
t.Run("timestamps are populated", func(t *testing.T) {
assert.NotEmpty(t, org.CreatedAt)
// By default accounts are in the free tier and are not in a trial
assert.Empty(t, org.TrialExpiresAt)
assert.Greater(t, org.RemainingTestableCount, 1)
})
})
t.Run("with invalid name", func(t *testing.T) {
org, err := client.Organizations.Read(ctx, badIdentifier)
assert.Nil(t, org)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("when the org does not exist", func(t *testing.T) {
_, err := client.Organizations.Read(ctx, randomString(t))
assert.Error(t, err)
})
t.Run("reads default project", func(t *testing.T) {
org, err := client.Organizations.ReadWithOptions(ctx, orgTest.Name, OrganizationReadOptions{Include: []OrganizationIncludeOpt{OrganizationDefaultProject}})
require.NoError(t, err)
assert.Equal(t, orgTest.Name, org.Name)
require.NotNil(t, org.DefaultProject)
assert.NotNil(t, org.DefaultProject.Name)
})
t.Run("with default execution mode of 'agent'", func(t *testing.T) {
orgAgentTest, orgAgentTestCleanup := createOrganizationWithDefaultAgentPool(t, client)
org, err := client.Organizations.Read(ctx, orgAgentTest.Name)
t.Cleanup(orgAgentTestCleanup)
require.NoError(t, err)
t.Run("execution mode and agent pool are properly decoded", func(t *testing.T) {
assert.Equal(t, "agent", org.DefaultExecutionMode)
assert.NotNil(t, org.DefaultAgentPool)
assert.Equal(t, org.DefaultAgentPool.ID, orgAgentTest.DefaultAgentPool.ID)
})
})
t.Run("read primary hyok configuration of an organization", func(t *testing.T) {
skipHYOKIntegrationTests(t)
// replace the environment variable with a valid organization name that has primary hyok configuration
hyokOrganizationName := os.Getenv("HYOK_ORGANIZATION_NAME")
if hyokOrganizationName == "" {
t.Fatal("Export a valid HYOK_ORGANIZATION_NAME before running this test!")
}
org, err := client.Organizations.Read(ctx, hyokOrganizationName)
require.NoError(t, err)
assert.NotEmpty(t, org.PrimaryHYOKConfiguration)
})
t.Run("read enforce hyok of an organization", func(t *testing.T) {
skipHYOKIntegrationTests(t)
// replace the environment variable with a valid organization name that has enforce hyok set to true or false
hyokOrganizationName := os.Getenv("HYOK_ORGANIZATION_NAME")
if hyokOrganizationName == "" {
t.Fatal("Export a valid HYOK_ORGANIZATION_NAME before running this test!")
}
org, err := client.Organizations.Read(ctx, hyokOrganizationName)
require.NoError(t, err)
assert.True(t, org.EnforceHYOK || !org.EnforceHYOK)
})
}
func TestOrganizationsUpdate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
t.Run("with HCP Terraform-only options", func(t *testing.T) {
skipIfEnterprise(t)
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
options := OrganizationUpdateOptions{
SendPassingStatusesForUntriggeredSpeculativePlans: Bool(false),
}
org, err := client.Organizations.Update(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, false, org.SendPassingStatusesForUntriggeredSpeculativePlans)
})
t.Run("with new AggregatedCommitStatusEnabled option", func(t *testing.T) {
skipIfEnterprise(t)
for _, testCase := range []bool{true, false} {
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
options := OrganizationUpdateOptions{
AggregatedCommitStatusEnabled: Bool(testCase),
}
org, err := client.Organizations.Update(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, testCase, org.AggregatedCommitStatusEnabled)
}
})
t.Run("with new SpeculativePlanManagementEnabled option", func(t *testing.T) {
skipIfEnterprise(t)
for _, testCase := range []bool{true, false} {
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
options := OrganizationUpdateOptions{
SpeculativePlanManagementEnabled: Bool(testCase),
}
org, err := client.Organizations.Update(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, testCase, org.SpeculativePlanManagementEnabled)
}
})
t.Run("with new UserTokensEnabled option", func(t *testing.T) {
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
assert.True(t, *orgTest.UserTokensEnabled, "user tokens enabled by default")
// we need to switch to an owner's team token, otherwise the client (which auths with a user token)
// wont be able to delete the org after we disable user tokens
teamList, err := client.Teams.List(ctx, orgTest.Name, &TeamListOptions{
Names: []string{"owners"},
})
require.NoError(t, err)
// it should be the only team, we just created the org...
require.Len(t, teamList.Items, 1)
ownersTeam := teamList.Items[0]
ownerToken, ownerTokenCleanup := createTeamToken(t, client, ownersTeam)
t.Cleanup(ownerTokenCleanup)
ownerClient := testClient(t)
ownerClient.token = ownerToken.Token
// disable user tokens for the organization
options := OrganizationUpdateOptions{
UserTokensEnabled: Bool(false),
}
org, err := ownerClient.Organizations.Update(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.False(t, *org.UserTokensEnabled, "user tokens disabled")
// try reading something with the user token client and verify that it fails, where the team token client
// succeeds
_, err = client.Organizations.Read(ctx, orgTest.Name)
assert.Error(t, err)
assert.ErrorContains(t, err, "unauthorized")
org, err = ownerClient.Organizations.Read(ctx, orgTest.Name)
assert.NoError(t, err)
assert.Equal(t, orgTest.Name, org.Name)
assert.False(t, *org.UserTokensEnabled, "user tokens disabled")
// re-enable user tokens
options = OrganizationUpdateOptions{
UserTokensEnabled: Bool(true),
}
org, err = ownerClient.Organizations.Update(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.True(t, *org.UserTokensEnabled, "user tokens re-enabled")
// try reading with the user token again and verify that it works
org, err = client.Organizations.Read(ctx, orgTest.Name)
assert.NoError(t, err)
assert.Equal(t, orgTest.Name, org.Name)
assert.True(t, *org.UserTokensEnabled, "user tokens re-enabled")
})
t.Run("with valid options", func(t *testing.T) {
orgTest, orgTestCleanup := createOrganization(t, client)
options := OrganizationUpdateOptions{
Name: String(randomString(t)),
Email: String(randomString(t) + "@tfe.local"),
SessionTimeout: Int(3600),
SessionRemember: Int(3600),
DefaultExecutionMode: String("local"),
}
org, err := client.Organizations.Update(ctx, orgTest.Name, options)
if err != nil {
orgTestCleanup()
}
require.NoError(t, err)
// Make sure we clean up the renamed org.
defer func() {
err := client.Organizations.Delete(ctx, org.Name)
if err != nil {
t.Logf("Error deleting organization (%s): %s", org.Name, err)
}
}()
// Also get a fresh result from the API to ensure we get the
// expected values back.
refreshed, err := client.Organizations.Read(ctx, *options.Name)
require.NoError(t, err)
for _, item := range []*Organization{
org,
refreshed,
} {
assert.Equal(t, *options.Name, item.Name)
assert.Equal(t, *options.Email, item.Email)
assert.Equal(t, *options.SessionTimeout, item.SessionTimeout)
assert.Equal(t, *options.SessionRemember, item.SessionRemember)
assert.Equal(t, *options.DefaultExecutionMode, item.DefaultExecutionMode)
}
})
t.Run("with invalid name", func(t *testing.T) {
org, err := client.Organizations.Update(ctx, badIdentifier, OrganizationUpdateOptions{})
assert.Nil(t, org)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("with agent pool provided, but remote execution mode", func(t *testing.T) {
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
pool, agentPoolCleanup := createAgentPool(t, client, orgTest)
t.Cleanup(agentPoolCleanup)
org, err := client.Organizations.Update(ctx, orgTest.Name, OrganizationUpdateOptions{
DefaultAgentPool: pool,
})
assert.Nil(t, org)
assert.ErrorContains(t, err, "must not be specified unless using 'agent' execution mode")
})
t.Run("when only updating a subset of fields", func(t *testing.T) {
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
org, err := client.Organizations.Update(ctx, orgTest.Name, OrganizationUpdateOptions{})
require.NoError(t, err)
assert.Equal(t, orgTest.Name, org.Name)
assert.Equal(t, orgTest.Email, org.Email)
})
t.Run("with different default execution modes", func(t *testing.T) {
// this helper creates an organization and then updates it to use a default agent pool, so it implicitly asserts
// that the organization's execution mode can be updated from 'remote' -> 'agent'
org, orgAgentTestCleanup := createOrganizationWithDefaultAgentPool(t, client)
assert.Equal(t, "agent", org.DefaultExecutionMode)
assert.NotNil(t, org.DefaultAgentPool)
// assert that organization's execution mode can be updated from 'agent' -> 'remote'
org, err := client.Organizations.Update(ctx, org.Name, OrganizationUpdateOptions{
DefaultExecutionMode: String("remote"),
})
require.NoError(t, err)
assert.Equal(t, "remote", org.DefaultExecutionMode)
assert.Nil(t, org.DefaultAgentPool)
// assert that organization's execution mode can be updated from 'remote' -> 'local'
org, err = client.Organizations.Update(ctx, org.Name, OrganizationUpdateOptions{
DefaultExecutionMode: String("local"),
})
require.NoError(t, err)
assert.Equal(t, "local", org.DefaultExecutionMode)
assert.Nil(t, org.DefaultAgentPool)
t.Cleanup(orgAgentTestCleanup)
})
t.Run("update enforce hyok of an organization to true", func(t *testing.T) {
skipHYOKIntegrationTests(t)
// replace the environment variable with a valid organization name with hyok permissions
hyokOrganizationName := os.Getenv("HYOK_ORGANIZATION_NAME")
if hyokOrganizationName == "" {
t.Fatal("Export a valid HYOK_ORGANIZATION_NAME before running this test!")
}
org, err := client.Organizations.Update(ctx, hyokOrganizationName, OrganizationUpdateOptions{
EnforceHYOK: Bool(true),
})
require.NoError(t, err)
assert.True(t, org.EnforceHYOK)
})
t.Run("update enforce hyok of an organization to false", func(t *testing.T) {
skipHYOKIntegrationTests(t)
// replace the environment variable with a valid organization name with hyok permissions
hyokOrganizationName := os.Getenv("HYOK_ORGANIZATION_NAME")
if hyokOrganizationName == "" {
t.Fatal("Export a valid HYOK_ORGANIZATION_NAME before running this test!")
}
org, err := client.Organizations.Update(ctx, hyokOrganizationName, OrganizationUpdateOptions{
EnforceHYOK: Bool(false),
})
require.NoError(t, err)
assert.False(t, org.EnforceHYOK)
})
}
func TestOrganizationsDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
t.Run("with valid options", func(t *testing.T) {
orgTest, _ := createOrganization(t, client)
err := client.Organizations.Delete(ctx, orgTest.Name)
require.NoError(t, err)
// Try fetching the org again - it should error.
_, err = client.Organizations.Read(ctx, orgTest.Name)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("with invalid name", func(t *testing.T) {
err := client.Organizations.Delete(ctx, badIdentifier)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
func TestOrganizationsReadCapacity_RunDependent(t *testing.T) {
t.Skip("Capacity queues are not available in the API")
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest1, wTestCleanup1 := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup1)
wTest2, wTestCleanup2 := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup2)
wTest3, wTestCleanup3 := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup3)
wTest4, wTestCleanup4 := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup4)
t.Run("without queued runs", func(t *testing.T) {
c, err := client.Organizations.ReadCapacity(ctx, orgTest.Name)
require.NoError(t, err)
assert.Equal(t, 0, c.Pending)
assert.Equal(t, 0, c.Running)
})
// For this test FRQ should be enabled and have a
// limit of 2 concurrent runs per organization.
t.Run("with queued runs", func(t *testing.T) {
_, runCleanup1 := createRun(t, client, wTest1)
t.Cleanup(runCleanup1)
_, runCleanup2 := createRun(t, client, wTest2)
t.Cleanup(runCleanup2)
_, runCleanup3 := createRun(t, client, wTest3)
t.Cleanup(runCleanup3)
_, runCleanup4 := createRun(t, client, wTest4)
t.Cleanup(runCleanup4)
c, err := client.Organizations.ReadCapacity(ctx, orgTest.Name)
require.NoError(t, err)
assert.Equal(t, 2, c.Pending)
assert.Equal(t, 2, c.Running)
})
t.Run("with invalid name", func(t *testing.T) {
org, err := client.Organizations.Read(ctx, badIdentifier)
assert.Nil(t, org)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("when the org does not exist", func(t *testing.T) {
_, err := client.Organizations.Read(ctx, randomString(t))
assert.Error(t, err)
})
}
func TestOrganizationsReadEntitlements(t *testing.T) {
t.Parallel()
skipIfEnterprise(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
newSubscriptionUpdater(orgTest).WithStandardEntitlementPlan().Update(t)
t.Run("when the org exists", func(t *testing.T) {
entitlements, err := client.Organizations.ReadEntitlements(ctx, orgTest.Name)
require.NoError(t, err)
assert.NotEmpty(t, entitlements.ID)
assert.True(t, entitlements.Agents)
assert.True(t, entitlements.AuditLogging)
assert.True(t, entitlements.CostEstimation)
assert.True(t, entitlements.Operations)
assert.True(t, entitlements.PrivateModuleRegistry)
assert.True(t, entitlements.SSO)
assert.True(t, entitlements.Sentinel)
assert.True(t, entitlements.StateStorage)
assert.True(t, entitlements.Teams)
assert.True(t, entitlements.VCSIntegrations)
assert.False(t, entitlements.WaypointActions)
assert.True(t, entitlements.WaypointTemplatesAndAddons)
})
t.Run("with invalid name", func(t *testing.T) {
entitlements, err := client.Organizations.ReadEntitlements(ctx, badIdentifier)
assert.Nil(t, entitlements)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("when the org does not exist", func(t *testing.T) {
_, err := client.Organizations.ReadEntitlements(ctx, randomString(t))
assert.Equal(t, ErrResourceNotFound, err)
})
}
func TestOrganizationsReadRunQueue_RunDependent(t *testing.T) {
t.Skip("Capacity queues are not available in the API")
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest1, wTestCleanup1 := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup1)
wTest2, wTestCleanup2 := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup2)
wTest3, wTestCleanup3 := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup3)
wTest4, wTestCleanup4 := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup4)
t.Run("without queued runs", func(t *testing.T) {
rq, err := client.Organizations.ReadRunQueue(ctx, orgTest.Name, ReadRunQueueOptions{})
require.NoError(t, err)
assert.Equal(t, 0, len(rq.Items))
})
// Create a couple or runs to fill the queue.
rTest1, rTestCleanup1 := createRun(t, client, wTest1)
t.Cleanup(rTestCleanup1)
rTest2, rTestCleanup2 := createRun(t, client, wTest2)
t.Cleanup(rTestCleanup2)
rTest3, rTestCleanup3 := createRun(t, client, wTest3)
t.Cleanup(rTestCleanup3)
rTest4, rTestCleanup4 := createRun(t, client, wTest4)
t.Cleanup(rTestCleanup4)
// For this test FRQ should be enabled and have a
// limit of 2 concurrent runs per organization.
t.Run("with queued runs", func(t *testing.T) {
rq, err := client.Organizations.ReadRunQueue(ctx, orgTest.Name, ReadRunQueueOptions{})
require.NoError(t, err)
found := []string{}
for _, r := range rq.Items {
found = append(found, r.ID)
}
assert.Contains(t, found, rTest1.ID)
assert.Contains(t, found, rTest2.ID)
assert.Contains(t, found, rTest3.ID)
assert.Contains(t, found, rTest4.ID)
})
t.Run("without queue options", func(t *testing.T) {
rq, err := client.Organizations.ReadRunQueue(ctx, orgTest.Name, ReadRunQueueOptions{})
require.NoError(t, err)
found := []string{}
for _, r := range rq.Items {
found = append(found, r.ID)
}
assert.Contains(t, found, rTest1.ID)
assert.Contains(t, found, rTest2.ID)
assert.Contains(t, found, rTest3.ID)
assert.Contains(t, found, rTest4.ID)
assert.Equal(t, 1, rq.CurrentPage)
assert.Equal(t, 4, rq.TotalCount)
})
t.Run("with queue options", func(t *testing.T) {
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
rq, err := client.Organizations.ReadRunQueue(ctx, orgTest.Name, ReadRunQueueOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, rq.Items)
assert.Equal(t, 999, rq.CurrentPage)
assert.Equal(t, 4, rq.TotalCount)
})
t.Run("with invalid name", func(t *testing.T) {
org, err := client.Organizations.Read(ctx, badIdentifier)
assert.Nil(t, org)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("when the org does not exist", func(t *testing.T) {
_, err := client.Organizations.Read(ctx, randomString(t))
assert.Error(t, err)
})
}
func TestOrganization_Unmarshal(t *testing.T) {
t.Parallel()
data := map[string]interface{}{
"data": map[string]interface{}{
"type": "organizations",
"id": "org-name",
"attributes": map[string]interface{}{
"assessments-enforced": true,
"collaborator-auth-policy": AuthPolicyPassword,
"cost-estimation-enabled": true,
"created-at": "2018-03-02T23:42:06.651Z",
"email": "test@hashicorp.com",
"permissions": map[string]interface{}{
"can-create-team": true,
},
},
},
}
byteData, err := json.Marshal(data)
require.NoError(t, err)
responseBody := bytes.NewReader(byteData)
org := &Organization{}
err = unmarshalResponse(responseBody, org)
require.NoError(t, err)
iso8601TimeFormat := "2006-01-02T15:04:05Z"
parsedTime, err := time.Parse(iso8601TimeFormat, "2018-03-02T23:42:06.651Z")
require.NoError(t, err)
assert.Equal(t, org.Name, "org-name")
assert.Equal(t, org.AssessmentsEnforced, true)
assert.Equal(t, org.CreatedAt, parsedTime)
assert.Equal(t, org.CollaboratorAuthPolicy, AuthPolicyPassword)
assert.Equal(t, org.CostEstimationEnabled, true)
assert.Equal(t, org.Email, "test@hashicorp.com")
assert.NotEmpty(t, org.Permissions)
assert.Equal(t, org.Permissions.CanCreateTeam, true)
}
func TestOrganizationsReadRunTasksPermission(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
t.Run("when the org exists", func(t *testing.T) {
org, err := client.Organizations.Read(ctx, orgTest.Name)
require.NoError(t, err)
assert.Equal(t, orgTest, org)
assert.NotEmpty(t, org.Permissions)
t.Run("permissions are properly decoded", func(t *testing.T) {
assert.True(t, org.Permissions.CanManageRunTasks)
})
})
}
func TestOrganizationsReadRunTasksEntitlement(t *testing.T) {
t.Parallel()
skipIfEnterprise(t)
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
t.Run("when the org exists", func(t *testing.T) {
entitlements, err := client.Organizations.ReadEntitlements(ctx, orgTest.Name)
require.NoError(t, err)
assert.NotEmpty(t, entitlements.ID)
assert.True(t, entitlements.RunTasks)
})
}
func TestOrganizationsAllowForceDeleteSetting(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
t.Run("creates and updates allow force delete", func(t *testing.T) {
options := OrganizationCreateOptions{
Name: String(randomString(t)),
Email: String(randomString(t) + "@tfe.local"),
AllowForceDeleteWorkspaces: Bool(true),
}
org, err := client.Organizations.Create(ctx, options)
require.NoError(t, err)
t.Cleanup(func() {
err := client.Organizations.Delete(ctx, org.Name)
if err != nil {
t.Errorf("error deleting organization (%s): %s", org.Name, err)
}
})
assert.Equal(t, *options.Name, org.Name)
assert.Equal(t, *options.Email, org.Email)
assert.True(t, org.AllowForceDeleteWorkspaces)
org, err = client.Organizations.Update(ctx, org.Name, OrganizationUpdateOptions{AllowForceDeleteWorkspaces: Bool(false)})
require.NoError(t, err)
assert.False(t, org.AllowForceDeleteWorkspaces)
org, err = client.Organizations.Read(ctx, org.Name)
require.NoError(t, err)
assert.False(t, org.AllowForceDeleteWorkspaces)
})
}
func TestOrganization_DataRetentionPolicy(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
organization, err := client.Organizations.Read(ctx, orgTest.Name)
require.NoError(t, err)
require.Nil(t, organization.DataRetentionPolicy)
require.Nil(t, organization.DataRetentionPolicyChoice)
dataRetentionPolicy, err := client.Organizations.ReadDataRetentionPolicyChoice(ctx, orgTest.Name)
require.NoError(t, err)
require.Nil(t, dataRetentionPolicy)
t.Run("set and update data retention policy to delete older", func(t *testing.T) {
createdDataRetentionPolicy, err := client.Organizations.SetDataRetentionPolicyDeleteOlder(ctx, orgTest.Name, DataRetentionPolicyDeleteOlderSetOptions{DeleteOlderThanNDays: 33})
require.NoError(t, err)
require.Equal(t, 33, createdDataRetentionPolicy.DeleteOlderThanNDays)
require.Contains(t, createdDataRetentionPolicy.ID, "drp-")
dataRetentionPolicy, err = client.Organizations.ReadDataRetentionPolicyChoice(ctx, orgTest.Name)
require.NoError(t, err)
require.NotNil(t, dataRetentionPolicy.DataRetentionPolicyDeleteOlder)
require.Equal(t, 33, dataRetentionPolicy.DataRetentionPolicyDeleteOlder.DeleteOlderThanNDays)
require.Equal(t, createdDataRetentionPolicy.ID, dataRetentionPolicy.DataRetentionPolicyDeleteOlder.ID)
require.Contains(t, dataRetentionPolicy.DataRetentionPolicyDeleteOlder.ID, "drp-")
organization, err := client.Organizations.Read(ctx, orgTest.Name)
require.NoError(t, err)
require.Equal(t, dataRetentionPolicy.DataRetentionPolicyDeleteOlder.ID, organization.DataRetentionPolicyChoice.DataRetentionPolicyDeleteOlder.ID)
// deprecated DataRetentionPolicy field should also have been populated
require.NotNil(t, organization.DataRetentionPolicy)
require.Equal(t, organization.DataRetentionPolicy.ID, dataRetentionPolicy.DataRetentionPolicyDeleteOlder.ID)
// try updating the number of days
createdDataRetentionPolicy, err = client.Organizations.SetDataRetentionPolicyDeleteOlder(ctx, orgTest.Name, DataRetentionPolicyDeleteOlderSetOptions{DeleteOlderThanNDays: 1})
require.NoError(t, err)
require.Equal(t, 1, createdDataRetentionPolicy.DeleteOlderThanNDays)
dataRetentionPolicy, err = client.Organizations.ReadDataRetentionPolicyChoice(ctx, orgTest.Name)
require.NoError(t, err)
require.NotNil(t, dataRetentionPolicy.DataRetentionPolicyDeleteOlder)
require.Equal(t, 1, dataRetentionPolicy.DataRetentionPolicyDeleteOlder.DeleteOlderThanNDays)
require.Equal(t, createdDataRetentionPolicy.ID, dataRetentionPolicy.DataRetentionPolicyDeleteOlder.ID)
})
t.Run("set data retention policy to not delete", func(t *testing.T) {
createdDataRetentionPolicy, err := client.Organizations.SetDataRetentionPolicyDontDelete(ctx, orgTest.Name, DataRetentionPolicyDontDeleteSetOptions{})
require.NoError(t, err)
require.Contains(t, createdDataRetentionPolicy.ID, "drp-")
dataRetentionPolicy, err = client.Organizations.ReadDataRetentionPolicyChoice(ctx, orgTest.Name)
require.NoError(t, err)
require.NotNil(t, dataRetentionPolicy.DataRetentionPolicyDontDelete)
require.Equal(t, createdDataRetentionPolicy.ID, dataRetentionPolicy.DataRetentionPolicyDontDelete.ID)
// dont delete policies should leave the legacy DataRetentionPolicy field on organizations empty
organization, err := client.Organizations.Read(ctx, orgTest.Name)
require.NoError(t, err)
require.Nil(t, organization.DataRetentionPolicy)
})
t.Run("change data retention policy type", func(t *testing.T) {
_, err = client.Organizations.SetDataRetentionPolicyDeleteOlder(ctx, orgTest.Name, DataRetentionPolicyDeleteOlderSetOptions{DeleteOlderThanNDays: 45})
require.NoError(t, err)
dataRetentionPolicy, err = client.Organizations.ReadDataRetentionPolicyChoice(ctx, orgTest.Name)
require.NoError(t, err)
require.NotNil(t, dataRetentionPolicy.DataRetentionPolicyDeleteOlder)
require.Equal(t, 45, dataRetentionPolicy.DataRetentionPolicyDeleteOlder.DeleteOlderThanNDays)
require.Nil(t, dataRetentionPolicy.DataRetentionPolicyDontDelete)
_, err = client.Organizations.SetDataRetentionPolicyDontDelete(ctx, orgTest.Name, DataRetentionPolicyDontDeleteSetOptions{})
require.NoError(t, err)
dataRetentionPolicy, err = client.Organizations.ReadDataRetentionPolicyChoice(ctx, orgTest.Name)
require.NoError(t, err)
require.Nil(t, dataRetentionPolicy.DataRetentionPolicyDeleteOlder)
require.NotNil(t, dataRetentionPolicy.DataRetentionPolicyDontDelete)
_, err = client.Organizations.SetDataRetentionPolicyDeleteOlder(ctx, orgTest.Name, DataRetentionPolicyDeleteOlderSetOptions{DeleteOlderThanNDays: 20})
require.NoError(t, err)
dataRetentionPolicy, err = client.Organizations.ReadDataRetentionPolicyChoice(ctx, orgTest.Name)
require.NoError(t, err)
require.NotNil(t, dataRetentionPolicy.DataRetentionPolicyDeleteOlder)
require.Equal(t, 20, dataRetentionPolicy.DataRetentionPolicyDeleteOlder.DeleteOlderThanNDays)
require.Nil(t, dataRetentionPolicy.DataRetentionPolicyDontDelete)
})
t.Run("delete data retention policy", func(t *testing.T) {
err = client.Organizations.DeleteDataRetentionPolicy(ctx, orgTest.Name)
require.NoError(t, err)
dataRetentionPolicy, err = client.Organizations.ReadDataRetentionPolicyChoice(ctx, orgTest.Name)
assert.Nil(t, err)
require.Nil(t, dataRetentionPolicy)
})
}
func orgItemsContainsName(items []*Organization, name string) bool {
hasName := false
for _, item := range items {
if item.Name == name {
hasName = true
break
}
}
return hasName
}
func orgItemsContainsEmail(items []*Organization, email string) bool {
hasEmail := false
for _, item := range items {
if item.Email == email {
hasEmail = true
break
}
}
return hasEmail
}
================================================
FILE: organization_membership.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ OrganizationMemberships = (*organizationMemberships)(nil)
// OrganizationMemberships describes all the organization membership related methods that
// the Terraform Enterprise API supports.
//
// TFE API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/organization-memberships
type OrganizationMemberships interface {
// List all the organization memberships of the given organization.
List(ctx context.Context, organization string, options *OrganizationMembershipListOptions) (*OrganizationMembershipList, error)
// Create a new organization membership with the given options.
Create(ctx context.Context, organization string, options OrganizationMembershipCreateOptions) (*OrganizationMembership, error)
// Read an organization membership by ID
Read(ctx context.Context, organizationMembershipID string) (*OrganizationMembership, error)
// Read an organization membership by ID with options
ReadWithOptions(ctx context.Context, organizationMembershipID string, options OrganizationMembershipReadOptions) (*OrganizationMembership, error)
// Delete an organization membership by its ID.
Delete(ctx context.Context, organizationMembershipID string) error
}
// organizationMemberships implements OrganizationMemberships.
type organizationMemberships struct {
client *Client
}
// OrganizationMembershipStatus represents an organization membership status.
type OrganizationMembershipStatus string
const (
OrganizationMembershipActive OrganizationMembershipStatus = "active"
OrganizationMembershipInvited OrganizationMembershipStatus = "invited"
)
// OrganizationMembershipList represents a list of organization memberships.
type OrganizationMembershipList struct {
*Pagination
Items []*OrganizationMembership
}
// OrganizationMembership represents a Terraform Enterprise organization membership.
type OrganizationMembership struct {
ID string `jsonapi:"primary,organization-memberships"`
Status OrganizationMembershipStatus `jsonapi:"attr,status"`
Email string `jsonapi:"attr,email"`
// Relations
Organization *Organization `jsonapi:"relation,organization"`
User *User `jsonapi:"relation,user"`
Teams []*Team `jsonapi:"relation,teams"`
}
// OrgMembershipIncludeOpt represents the available options for include query params.
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/organization-memberships#available-related-resources
type OrgMembershipIncludeOpt string
const (
OrgMembershipUser OrgMembershipIncludeOpt = "user"
OrgMembershipTeam OrgMembershipIncludeOpt = "teams"
)
// OrganizationMembershipListOptions represents the options for listing organization memberships.
type OrganizationMembershipListOptions struct {
ListOptions
// Optional: A list of relations to include. See available resources
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/organization-memberships#available-related-resources
Include []OrgMembershipIncludeOpt `url:"include,omitempty"`
// Optional: A list of organization member emails to filter by.
Emails []string `url:"filter[email],omitempty"`
// Optional: If specified, restricts results to those matching status value.
Status OrganizationMembershipStatus `url:"filter[status],omitempty"`
// Optional: A query string to search organization memberships by user name
// and email.
Query string `url:"q,omitempty"`
}
// OrganizationMembershipCreateOptions represents the options for creating an organization membership.
type OrganizationMembershipCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,organization-memberships"`
// Required: User's email address.
Email *string `jsonapi:"attr,email"`
// Optional: A list of teams in the organization to add the user to
Teams []*Team `jsonapi:"relation,teams,omitempty"`
}
// OrganizationMembershipReadOptions represents the options for reading organization memberships.
type OrganizationMembershipReadOptions struct {
// Optional: A list of relations to include. See available resources
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/organization-memberships#available-related-resources
Include []OrgMembershipIncludeOpt `url:"include,omitempty"`
}
// List all the organization memberships of the given organization.
func (s *organizationMemberships) List(ctx context.Context, organization string, options *OrganizationMembershipListOptions) (*OrganizationMembershipList, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("organizations/%s/organization-memberships", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
ml := &OrganizationMembershipList{}
err = req.Do(ctx, ml)
if err != nil {
return nil, err
}
return ml, nil
}
// Create an organization membership with the given options.
func (s *organizationMemberships) Create(ctx context.Context, organization string, options OrganizationMembershipCreateOptions) (*OrganizationMembership, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("organizations/%s/organization-memberships", url.PathEscape(organization))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
m := &OrganizationMembership{}
err = req.Do(ctx, m)
if err != nil {
return nil, err
}
return m, nil
}
// Read an organization membership by its ID.
func (s *organizationMemberships) Read(ctx context.Context, organizationMembershipID string) (*OrganizationMembership, error) {
return s.ReadWithOptions(ctx, organizationMembershipID, OrganizationMembershipReadOptions{})
}
// Read an organization membership by ID with options
func (s *organizationMemberships) ReadWithOptions(ctx context.Context, organizationMembershipID string, options OrganizationMembershipReadOptions) (*OrganizationMembership, error) {
if !validStringID(&organizationMembershipID) {
return nil, ErrInvalidMembership
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("organization-memberships/%s", url.PathEscape(organizationMembershipID))
req, err := s.client.NewRequest("GET", u, &options)
if err != nil {
return nil, err
}
mem := &OrganizationMembership{}
err = req.Do(ctx, mem)
if err != nil {
return nil, err
}
return mem, nil
}
// Delete an organization membership by its ID.
func (s *organizationMemberships) Delete(ctx context.Context, organizationMembershipID string) error {
if !validStringID(&organizationMembershipID) {
return ErrInvalidMembership
}
u := fmt.Sprintf("organization-memberships/%s", url.PathEscape(organizationMembershipID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o OrganizationMembershipCreateOptions) valid() error {
if o.Email == nil {
return ErrRequiredEmail
}
return nil
}
func (o *OrganizationMembershipListOptions) valid() error {
if o == nil {
return nil
}
if err := validateOrgMembershipEmailParams(o.Emails); err != nil {
return err
}
return nil
}
func (o OrganizationMembershipReadOptions) valid() error {
return nil
}
func validateOrgMembershipEmailParams(emails []string) error {
for _, email := range emails {
if !validEmail(email) {
return ErrInvalidEmail
}
}
return nil
}
================================================
FILE: organization_membership_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestOrganizationMembershipsList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
t.Run("without list options", func(t *testing.T) {
memTest1, memTest1Cleanup := createOrganizationMembership(t, client, orgTest)
defer memTest1Cleanup()
memTest2, memTest2Cleanup := createOrganizationMembership(t, client, orgTest)
defer memTest2Cleanup()
// The create helper includes the related user, so we should remove it for our equality test
memTest1.User = &User{ID: memTest1.User.ID}
memTest2.User = &User{ID: memTest2.User.ID}
ml, err := client.OrganizationMemberships.List(ctx, orgTest.Name, nil)
require.NoError(t, err)
assert.Contains(t, ml.Items, memTest1)
assert.Contains(t, ml.Items, memTest2)
})
t.Run("with pagination options", func(t *testing.T) {
_, memTest1Cleanup := createOrganizationMembership(t, client, orgTest)
defer memTest1Cleanup()
_, memTest2Cleanup := createOrganizationMembership(t, client, orgTest)
defer memTest2Cleanup()
ml, err := client.OrganizationMemberships.List(ctx, orgTest.Name, &OrganizationMembershipListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, ml.Items)
assert.Equal(t, 999, ml.CurrentPage)
// Three because the creator of the organization is a member, in addition to the two we added to setup the test.
assert.Equal(t, 3, ml.TotalCount)
})
t.Run("with include options", func(t *testing.T) {
memTest1, memTest1Cleanup := createOrganizationMembership(t, client, orgTest)
defer memTest1Cleanup()
memTest2, memTest2Cleanup := createOrganizationMembership(t, client, orgTest)
defer memTest2Cleanup()
ml, err := client.OrganizationMemberships.List(ctx, orgTest.Name, &OrganizationMembershipListOptions{
Include: []OrgMembershipIncludeOpt{OrgMembershipUser},
})
require.NoError(t, err)
assert.Contains(t, ml.Items, memTest1)
assert.Contains(t, ml.Items, memTest2)
})
t.Run("with email filter option", func(t *testing.T) {
_, memTest1Cleanup := createOrganizationMembership(t, client, orgTest)
defer memTest1Cleanup()
memTest2, memTest2Cleanup := createOrganizationMembership(t, client, orgTest)
defer memTest2Cleanup()
memTest3, memTest3Cleanup := createOrganizationMembership(t, client, orgTest)
defer memTest3Cleanup()
memTest2.User = &User{ID: memTest2.User.ID}
memTest3.User = &User{ID: memTest3.User.ID}
ml, err := client.OrganizationMemberships.List(ctx, orgTest.Name, &OrganizationMembershipListOptions{
Emails: []string{memTest2.Email, memTest3.Email},
})
require.NoError(t, err)
assert.Len(t, ml.Items, 2)
assert.Contains(t, ml.Items, memTest2)
assert.Contains(t, ml.Items, memTest3)
t.Run("with invalid email", func(t *testing.T) {
ml, err = client.OrganizationMemberships.List(ctx, orgTest.Name, &OrganizationMembershipListOptions{
Emails: []string{"foobar"},
})
assert.Equal(t, err, ErrInvalidEmail)
})
})
t.Run("with status filter option", func(t *testing.T) {
_, memTest1Cleanup := createOrganizationMembership(t, client, orgTest)
t.Cleanup(memTest1Cleanup)
_, memTest2Cleanup := createOrganizationMembership(t, client, orgTest)
t.Cleanup(memTest2Cleanup)
ml, err := client.OrganizationMemberships.List(ctx, orgTest.Name, &OrganizationMembershipListOptions{
Status: OrganizationMembershipInvited,
})
require.NoError(t, err)
require.Len(t, ml.Items, 2)
for _, member := range ml.Items {
assert.Equal(t, member.Status, OrganizationMembershipInvited)
}
})
t.Run("with search query string", func(t *testing.T) {
memTest1, memTest1Cleanup := createOrganizationMembership(t, client, orgTest)
t.Cleanup(memTest1Cleanup)
_, memTest2Cleanup := createOrganizationMembership(t, client, orgTest)
t.Cleanup(memTest2Cleanup)
_, memTest3Cleanup := createOrganizationMembership(t, client, orgTest)
t.Cleanup(memTest3Cleanup)
t.Run("using an email", func(t *testing.T) {
ml, err := client.OrganizationMemberships.List(ctx, orgTest.Name, &OrganizationMembershipListOptions{
Query: memTest1.Email,
})
require.NoError(t, err)
require.Len(t, ml.Items, 1)
assert.Equal(t, ml.Items[0].Email, memTest1.Email)
})
t.Run("using a user name", func(t *testing.T) {
t.Skip("Skipping, missing Account API support in order to set usernames")
})
})
t.Run("without a valid organization", func(t *testing.T) {
ml, err := client.OrganizationMemberships.List(ctx, badIdentifier, nil)
assert.Nil(t, ml)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
func TestOrganizationMembershipsCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
t.Run("with valid options", func(t *testing.T) {
options := OrganizationMembershipCreateOptions{
Email: String(fmt.Sprintf("%s@tfe.local", randomString(t))),
}
mem, err := client.OrganizationMemberships.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
// Get a refreshed view from the API.
refreshed, err := client.OrganizationMemberships.ReadWithOptions(ctx, mem.ID, OrganizationMembershipReadOptions{
Include: []OrgMembershipIncludeOpt{OrgMembershipUser},
})
require.NoError(t, err)
assert.Equal(t, refreshed, mem)
})
t.Run("when options is missing email", func(t *testing.T) {
mem, err := client.OrganizationMemberships.Create(ctx, orgTest.Name, OrganizationMembershipCreateOptions{})
assert.Nil(t, mem)
assert.Equal(t, err, ErrRequiredEmail)
})
t.Run("with an invalid organization", func(t *testing.T) {
mem, err := client.OrganizationMemberships.Create(ctx, badIdentifier, OrganizationMembershipCreateOptions{
Email: String(fmt.Sprintf("%s@tfe.local", randomString(t))),
})
assert.Nil(t, mem)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("when an error is returned from the api", func(t *testing.T) {
mem, err := client.OrganizationMemberships.Create(ctx, orgTest.Name, OrganizationMembershipCreateOptions{
Email: String("not-an-email-address"),
})
assert.Nil(t, mem)
assert.Error(t, err)
})
t.Run("with initial teams", func(t *testing.T) {
teamTest1, teamTestCleanup1 := createTeam(t, client, orgTest)
defer teamTestCleanup1()
teamTest2, teamTestCleanup2 := createTeam(t, client, orgTest)
defer teamTestCleanup2()
options := OrganizationMembershipCreateOptions{
Email: String(fmt.Sprintf("%s@tfe.local", randomString(t))),
Teams: []*Team{teamTest1, teamTest2},
}
mem, err := client.OrganizationMemberships.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
// Verify that the user is now in the org
refreshed, err := client.OrganizationMemberships.ReadWithOptions(ctx, mem.ID, OrganizationMembershipReadOptions{
Include: []OrgMembershipIncludeOpt{OrgMembershipUser},
})
require.NoError(t, err)
assert.Equal(t, refreshed, mem)
// Verify that the user is in the teams
// Cant read from the teams, b/c the user is invited to them not a full member yet
require.Equal(t, len(refreshed.Teams), 2)
refreshedTeamIds := make([]string, 2)
refreshedTeamIds[0] = refreshed.Teams[0].ID
refreshedTeamIds[1] = refreshed.Teams[1].ID
assert.Contains(t, refreshedTeamIds, teamTest1.ID)
assert.Contains(t, refreshedTeamIds, teamTest2.ID)
})
}
func TestOrganizationMembershipsRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
memTest, memTestCleanup := createOrganizationMembership(t, client, nil)
defer memTestCleanup()
// The create API endpoint automatically includes the related user, so we should drop
// the additional parts of the user which get deserialized.
memTest.User = &User{
ID: memTest.User.ID,
}
t.Run("when the membership exists", func(t *testing.T) {
mem, err := client.OrganizationMemberships.Read(ctx, memTest.ID)
require.NoError(t, err)
assert.Equal(t, memTest, mem)
})
t.Run("when the membership does not exist", func(t *testing.T) {
mem, err := client.OrganizationMemberships.Read(ctx, "nonexisting")
assert.Nil(t, mem)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("with invalid membership id", func(t *testing.T) {
mem, err := client.OrganizationMemberships.Read(ctx, badIdentifier)
assert.Nil(t, mem)
assert.Equal(t, err, ErrInvalidMembership)
})
}
func TestOrganizationMembershipsReadWithOptions(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
memTest, memTestCleanup := createOrganizationMembership(t, client, nil)
defer memTestCleanup()
options := OrganizationMembershipReadOptions{
Include: []OrgMembershipIncludeOpt{OrgMembershipUser},
}
t.Run("when the membership exists", func(t *testing.T) {
mem, err := client.OrganizationMemberships.ReadWithOptions(ctx, memTest.ID, options)
require.NoError(t, err)
assert.Equal(t, memTest, mem)
})
t.Run("without options", func(t *testing.T) {
_, err := client.OrganizationMemberships.ReadWithOptions(ctx, memTest.ID, OrganizationMembershipReadOptions{})
require.NoError(t, err)
})
t.Run("with invalid include option", func(t *testing.T) {
_, err := client.OrganizationMemberships.ReadWithOptions(ctx, memTest.ID, OrganizationMembershipReadOptions{
Include: []OrgMembershipIncludeOpt{"users"},
})
assert.Equal(t, err, ErrInvalidIncludeValue)
})
t.Run("when the membership does not exist", func(t *testing.T) {
mem, err := client.OrganizationMemberships.ReadWithOptions(ctx, "nonexisting", options)
assert.Nil(t, mem)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("with invalid membership id", func(t *testing.T) {
mem, err := client.OrganizationMemberships.ReadWithOptions(ctx, badIdentifier, options)
assert.Nil(t, mem)
assert.Equal(t, err, ErrInvalidMembership)
})
}
func TestOrganizationMembershipsDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
mem, _ := createOrganizationMembership(t, client, orgTest)
t.Run("with valid options", func(t *testing.T) {
err := client.OrganizationMemberships.Delete(ctx, mem.ID)
require.NoError(t, err)
// Get a refreshed view from the API.
refreshed, err := client.OrganizationMemberships.List(ctx, orgTest.Name, nil)
require.NoError(t, err)
assert.NotContains(t, refreshed.Items, mem)
})
t.Run("when membership is invalid", func(t *testing.T) {
err := client.OrganizationMemberships.Delete(ctx, badIdentifier)
assert.Equal(t, err, ErrInvalidMembership)
})
t.Run("when an error is returned from the api", func(t *testing.T) {
err := client.OrganizationMemberships.Delete(ctx, "not-an-identifier")
assert.Error(t, err)
})
}
================================================
FILE: organization_tags.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"errors"
"fmt"
"net/url"
)
var _ OrganizationTags = (*organizationTags)(nil)
// OrganizationMemberships describes all the list of tags used with all resources across the organization.
//
// TFE API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/organization-tags
type OrganizationTags interface {
// List all tags within an organization
List(ctx context.Context, organization string, options *OrganizationTagsListOptions) (*OrganizationTagsList, error)
// Delete tags from an organization
Delete(ctx context.Context, organization string, options OrganizationTagsDeleteOptions) error
// Associate an organization's workspace with a tag
AddWorkspaces(ctx context.Context, tag string, options AddWorkspacesToTagOptions) error
}
// organizationTags implements OrganizationTags.
type organizationTags struct {
client *Client
}
// OrganizationTagsList represents a list of organization tags
type OrganizationTagsList struct {
*Pagination
Items []*OrganizationTag
}
// OrganizationTag represents a Terraform Enterprise Organization tag
type OrganizationTag struct {
ID string `jsonapi:"primary,tags"`
// Optional:
Name string `jsonapi:"attr,name,omitempty"`
// Optional: Number of workspaces that have this tag
InstanceCount int `jsonapi:"attr,instance-count,omitempty"`
// The org this tag belongs to
Organization *Organization `jsonapi:"relation,organization"`
}
// OrganizationTagsListOptions represents the options for listing organization tags
type OrganizationTagsListOptions struct {
ListOptions
// Optional:
Filter string `url:"filter[exclude][taggable][id],omitempty"`
// Optional: A search query string. Organization tags are searchable by name likeness.
Query string `url:"q,omitempty"`
}
// OrganizationTagsDeleteOptions represents the request body for deleting a tag in an organization
type OrganizationTagsDeleteOptions struct {
IDs []string // Required
}
// AddWorkspacesToTagOptions represents the request body to add a workspace to a tag
type AddWorkspacesToTagOptions struct {
WorkspaceIDs []string // Required
}
// this represents a single tag ID
type tagID struct {
ID string `jsonapi:"primary,tags"`
}
// this represents a single workspace ID
type workspaceID struct {
ID string `jsonapi:"primary,workspaces"`
}
// List all the tags in an organization. You can provide query params through OrganizationTagsListOptions
func (s *organizationTags) List(ctx context.Context, organization string, options *OrganizationTagsListOptions) (*OrganizationTagsList, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf("organizations/%s/tags", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
tags := &OrganizationTagsList{}
err = req.Do(ctx, tags)
if err != nil {
return nil, err
}
return tags, nil
}
// Delete tags from a Terraform Enterprise organization
func (s *organizationTags) Delete(ctx context.Context, organization string, options OrganizationTagsDeleteOptions) error {
if !validStringID(&organization) {
return ErrInvalidOrg
}
if err := options.valid(); err != nil {
return err
}
u := fmt.Sprintf("organizations/%s/tags", url.PathEscape(organization))
var tagsToRemove []*tagID
for _, id := range options.IDs {
tagsToRemove = append(tagsToRemove, &tagID{ID: id})
}
req, err := s.client.NewRequest("DELETE", u, tagsToRemove)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// Add workspaces to a tag
func (s *organizationTags) AddWorkspaces(ctx context.Context, tag string, options AddWorkspacesToTagOptions) error {
if !validStringID(&tag) {
return ErrInvalidTag
}
if err := options.valid(); err != nil {
return err
}
var workspaces []*workspaceID
for _, id := range options.WorkspaceIDs {
workspaces = append(workspaces, &workspaceID{ID: id})
}
u := fmt.Sprintf("tags/%s/relationships/workspaces", url.PathEscape(tag))
req, err := s.client.NewRequest("POST", u, workspaces)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (opts *OrganizationTagsDeleteOptions) valid() error {
if len(opts.IDs) == 0 {
return ErrRequiredTagID
}
for _, id := range opts.IDs {
if !validStringID(&id) {
errorMsg := fmt.Sprintf("%s is not a valid id value", id)
return errors.New(errorMsg)
}
}
return nil
}
func (w *AddWorkspacesToTagOptions) valid() error {
if len(w.WorkspaceIDs) == 0 {
return ErrRequiredTagWorkspaceID
}
for _, id := range w.WorkspaceIDs {
if !validStringID(&id) {
errorMsg := fmt.Sprintf("%s is not a valid id value", id)
return errors.New(errorMsg)
}
}
return nil
}
================================================
FILE: organization_tags_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestOrganizationTagsList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
assert.NotNil(t, orgTest)
workspaceTest, workspaceTestCleanup := createWorkspace(t, client, orgTest)
defer workspaceTestCleanup()
assert.NotNil(t, workspaceTest)
var tags []*Tag
for i := 0; i < 10; i++ {
tags = append(tags, &Tag{
Name: fmt.Sprintf("tag%d", i),
})
}
err := client.Workspaces.AddTags(ctx, workspaceTest.ID, WorkspaceAddTagsOptions{
Tags: tags,
})
require.NoError(t, err)
// this is a tag id we'll use in the filter param of the second test
var testTagID string
// this is the tag Name we'll use with the query parameter in the third test
var testTagName string
t.Run("with no query params", func(t *testing.T) {
tags, err := client.OrganizationTags.List(ctx, orgTest.Name, nil)
require.NoError(t, err)
assert.Equal(t, 10, len(tags.Items))
testTagID = tags.Items[0].ID
testTagName = tags.Items[0].Name
for _, tag := range tags.Items {
assert.NotNil(t, tag.ID)
assert.NotNil(t, tag.Name)
assert.GreaterOrEqual(t, tag.InstanceCount, 1)
t.Run("ensure org relation is properly decoded", func(t *testing.T) {
assert.NotNil(t, tag.Organization)
})
}
})
t.Run("with query param Filter", func(t *testing.T) {
tags, err := client.OrganizationTags.List(ctx, orgTest.Name, &OrganizationTagsListOptions{
ListOptions: ListOptions{
PageNumber: 1,
PageSize: 5,
},
Filter: testTagID,
})
require.NoError(t, err)
assert.Equal(t, 5, len(tags.Items))
for _, tag := range tags.Items {
// ensure tag specified in filter param was omitted from results
assert.NotNil(t, tag.ID, testTagID)
t.Run("ensure org relation is properly decoded", func(t *testing.T) {
assert.NotNil(t, tag.Organization)
})
}
})
t.Run("with query param Query", func(t *testing.T) {
tags, err := client.OrganizationTags.List(ctx, orgTest.Name, &OrganizationTagsListOptions{
ListOptions: ListOptions{
PageNumber: 1,
PageSize: 5,
},
Query: testTagName,
})
require.NoError(t, err)
require.Len(t, tags.Items, 1)
assert.Equal(t, tags.Items[0].Name, testTagName)
assert.NotNil(t, tags.Items[0].Organization)
})
}
func TestOrganizationTagsDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
assert.NotNil(t, orgTest)
workspaceTest, workspaceTestCleanup := createWorkspace(t, client, orgTest)
defer workspaceTestCleanup()
assert.NotNil(t, workspaceTest)
var tags []*Tag
for i := 0; i < 10; i++ {
tags = append(tags, &Tag{
Name: fmt.Sprintf("tag%d", i),
})
}
err := client.Workspaces.AddTags(ctx, workspaceTest.ID, WorkspaceAddTagsOptions{
Tags: tags,
})
require.NoError(t, err)
t.Run("delete tags by id", func(t *testing.T) {
tags, err := client.OrganizationTags.List(ctx, orgTest.Name, nil)
require.NoError(t, err)
var tagIds []string
// since we added 10 tags to the org, grab a subset
for i := 0; i < 5; i++ {
assert.NotNil(t, tags.Items[i].ID)
tagIds = append(tagIds, tags.Items[i].ID)
}
err = client.OrganizationTags.Delete(ctx, orgTest.Name, OrganizationTagsDeleteOptions{
IDs: tagIds,
})
require.NoError(t, err)
// sanity check ensure tags were deleted from the organization
tags, err = client.OrganizationTags.List(ctx, orgTest.Name, nil)
require.NoError(t, err)
assert.Equal(t, 5, len(tags.Items))
})
}
func TestOrganizationTagsAddWorkspace(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
assert.NotNil(t, orgTest)
workspaceTest, workspaceTestCleanup := createWorkspace(t, client, orgTest)
defer workspaceTestCleanup()
assert.NotNil(t, workspaceTest)
var tags []*Tag
for i := 0; i < 2; i++ {
tags = append(tags, &Tag{
Name: fmt.Sprintf("tag%d", i),
})
}
err := client.Workspaces.AddTags(ctx, workspaceTest.ID, WorkspaceAddTagsOptions{
Tags: tags,
})
require.NoError(t, err)
t.Run("add tags to new workspaces", func(t *testing.T) {
// fetch tag ids to associate to workspace
tags, err := client.OrganizationTags.List(ctx, orgTest.Name, nil)
require.NoError(t, err)
tagID := tags.Items[0].ID
// create the workspaces we'll use to associate tags
workspaceToAdd1, workspaceToAdd1Cleanup := createWorkspace(t, client, orgTest)
defer workspaceToAdd1Cleanup()
workspaceToAdd2, workspaceToAdd2Cleanup := createWorkspace(t, client, orgTest)
defer workspaceToAdd2Cleanup()
err = client.OrganizationTags.AddWorkspaces(ctx, tagID, AddWorkspacesToTagOptions{
WorkspaceIDs: []string{workspaceToAdd1.ID, workspaceToAdd2.ID},
})
require.NoError(t, err)
// Ensure the tag was properly associated with the workspaces
fetched, err := client.Workspaces.ListTags(ctx, workspaceToAdd1.ID, nil)
require.NoError(t, err)
assert.Equal(t, fetched.Items[0].ID, tagID)
fetched, err = client.Workspaces.ListTags(ctx, workspaceToAdd2.ID, nil)
require.NoError(t, err)
assert.Equal(t, fetched.Items[0].ID, tagID)
})
}
================================================
FILE: organization_token.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ OrganizationTokens = (*organizationTokens)(nil)
type TokenType string
const (
// A token which can only access the Audit Trails of an HCP Terraform Organization.
// See https://developer.hashicorp.com/terraform/cloud-docs/api-docs/audit-trails-tokens
AuditTrailToken TokenType = "audit-trails"
)
// OrganizationTokens describes all the organization token related methods
// that the Terraform Enterprise API supports.
//
// TFE API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/organization-tokens
type OrganizationTokens interface {
// Create a new organization token, replacing any existing token.
Create(ctx context.Context, organization string) (*OrganizationToken, error)
// CreateWithOptions a new organization token with options, replacing any existing token.
CreateWithOptions(ctx context.Context, organization string, options OrganizationTokenCreateOptions) (*OrganizationToken, error)
// Read an organization token.
Read(ctx context.Context, organization string) (*OrganizationToken, error)
// Read an organization token with options.
ReadWithOptions(ctx context.Context, organization string, options OrganizationTokenReadOptions) (*OrganizationToken, error)
// Delete an organization token.
Delete(ctx context.Context, organization string) error
// Delete an organization token with options.
DeleteWithOptions(ctx context.Context, organization string, options OrganizationTokenDeleteOptions) error
}
// organizationTokens implements OrganizationTokens.
type organizationTokens struct {
client *Client
}
// OrganizationToken represents a Terraform Enterprise organization token.
type OrganizationToken struct {
ID string `jsonapi:"primary,authentication-tokens"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Description string `jsonapi:"attr,description"`
LastUsedAt time.Time `jsonapi:"attr,last-used-at,iso8601"`
Token string `jsonapi:"attr,token"`
ExpiredAt time.Time `jsonapi:"attr,expired-at,iso8601"`
CreatedBy *CreatedByChoice `jsonapi:"polyrelation,created-by"`
}
// OrganizationTokenCreateOptions contains the options for creating an organization token.
type OrganizationTokenCreateOptions struct {
// Optional: The token's expiration date.
// This feature is available in TFE release v202305-1 and later
ExpiredAt *time.Time `jsonapi:"attr,expired-at,iso8601,omitempty" url:"-"`
// Optional: What type of token to create
// This option is only applicable to HCP Terraform and is ignored by TFE.
TokenType *TokenType `url:"token,omitempty"`
}
// OrganizationTokenReadOptions contains the options for reading an organization token.
type OrganizationTokenReadOptions struct {
// Optional: What type of token to read
// This option is only applicable to HCP Terraform and is ignored by TFE.
TokenType *TokenType `url:"token,omitempty"`
}
// OrganizationTokenDeleteOptions contains the options for deleting an organization token.
type OrganizationTokenDeleteOptions struct {
// Optional: What type of token to delete
// This option is only applicable to HCP Terraform and is ignored by TFE.
TokenType *TokenType `url:"token,omitempty"`
}
// Create a new organization token, replacing any existing token.
func (s *organizationTokens) Create(ctx context.Context, organization string) (*OrganizationToken, error) {
return s.CreateWithOptions(ctx, organization, OrganizationTokenCreateOptions{})
}
// CreateWithOptions a new organization token with options, replacing any existing token.
func (s *organizationTokens) CreateWithOptions(ctx context.Context, organization string, options OrganizationTokenCreateOptions) (*OrganizationToken, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf("organizations/%s/authentication-token", url.PathEscape(organization))
qp, err := decodeQueryParams(options)
if err != nil {
return nil, err
}
req, err := s.client.NewRequestWithAdditionalQueryParams("POST", u, &options, qp)
if err != nil {
return nil, err
}
ot := &OrganizationToken{}
err = req.Do(ctx, ot)
if err != nil {
return nil, err
}
return ot, err
}
// Read an organization token.
func (s *organizationTokens) Read(ctx context.Context, organization string) (*OrganizationToken, error) {
return s.ReadWithOptions(ctx, organization, OrganizationTokenReadOptions{})
}
// Read an organization token with options.
func (s *organizationTokens) ReadWithOptions(ctx context.Context, organization string, options OrganizationTokenReadOptions) (*OrganizationToken, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf("organizations/%s/authentication-token", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
ot := &OrganizationToken{}
err = req.Do(ctx, ot)
if err != nil {
return nil, err
}
return ot, err
}
// Delete an organization token.
func (s *organizationTokens) Delete(ctx context.Context, organization string) error {
return s.DeleteWithOptions(ctx, organization, OrganizationTokenDeleteOptions{})
}
// Delete an organization token with options
func (s *organizationTokens) DeleteWithOptions(ctx context.Context, organization string, options OrganizationTokenDeleteOptions) error {
if !validStringID(&organization) {
return ErrInvalidOrg
}
u := fmt.Sprintf("organizations/%s/authentication-token", url.PathEscape(organization))
qp, err := decodeQueryParams(options)
if err != nil {
return err
}
req, err := s.client.NewRequestWithAdditionalQueryParams("DELETE", u, nil, qp)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
================================================
FILE: organization_token_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestOrganizationTokensCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
var tkToken string
t.Run("with valid options", func(t *testing.T) {
ot, err := client.OrganizationTokens.Create(ctx, orgTest.Name)
require.NoError(t, err)
require.NotEmpty(t, ot.Token)
requireExactlyOneNotEmpty(t, ot.CreatedBy.Organization, ot.CreatedBy.Team, ot.CreatedBy.User)
tkToken = ot.Token
})
t.Run("when a token already exists", func(t *testing.T) {
ot, err := client.OrganizationTokens.Create(ctx, orgTest.Name)
require.NoError(t, err)
require.NotEmpty(t, ot.Token)
assert.NotEqual(t, tkToken, ot.Token)
})
t.Run("without valid organization", func(t *testing.T) {
ot, err := client.OrganizationTokens.Create(ctx, badIdentifier)
assert.Nil(t, ot)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
func TestOrganizationTokens_CreateWithOptions(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
// We need to update the organization to business so we can create an audit trails token later.
newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t)
var tkToken string
t.Run("with valid options", func(t *testing.T) {
ot, err := client.OrganizationTokens.CreateWithOptions(ctx, orgTest.Name, OrganizationTokenCreateOptions{})
require.NoError(t, err)
require.NotEmpty(t, ot.Token)
tkToken = ot.Token
})
t.Run("when a token already exists", func(t *testing.T) {
ot, err := client.OrganizationTokens.CreateWithOptions(ctx, orgTest.Name, OrganizationTokenCreateOptions{})
require.NoError(t, err)
require.NotEmpty(t, ot.Token)
assert.NotEqual(t, tkToken, ot.Token)
})
t.Run("without valid organization", func(t *testing.T) {
ot, err := client.OrganizationTokens.CreateWithOptions(ctx, badIdentifier, OrganizationTokenCreateOptions{})
assert.Nil(t, ot)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("without an expiration date", func(t *testing.T) {
ot, err := client.OrganizationTokens.CreateWithOptions(ctx, orgTest.Name, OrganizationTokenCreateOptions{})
require.NoError(t, err)
require.NotEmpty(t, ot.Token)
assert.NotZero(t, ot.ExpiredAt)
expectedExpiry := ot.CreatedAt.AddDate(defaultTokenExpirationYears, 0, 0)
// Allow a small buffer (1 minute) for timestamp precision differences.
assert.WithinDuration(t, expectedExpiry, ot.ExpiredAt, time.Minute)
tkToken = ot.Token
})
t.Run("with an expiration date", func(t *testing.T) {
currentTime := time.Now().UTC().Truncate(time.Second)
oneDayLater := currentTime.Add(24 * time.Hour)
ot, err := client.OrganizationTokens.CreateWithOptions(ctx, orgTest.Name, OrganizationTokenCreateOptions{
ExpiredAt: &oneDayLater,
})
require.NoError(t, err)
require.NotEmpty(t, ot.Token)
assert.Equal(t, ot.ExpiredAt, oneDayLater)
tkToken = ot.Token
})
t.Run("with a token type", func(t *testing.T) {
tt := AuditTrailToken
ot, err := client.OrganizationTokens.CreateWithOptions(ctx, orgTest.Name, OrganizationTokenCreateOptions{
TokenType: &tt,
})
require.NoError(t, err)
require.NotEmpty(t, ot.Token)
})
}
func TestOrganizationTokensRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
t.Run("with valid options", func(t *testing.T) {
_, otTestCleanup := createOrganizationToken(t, client, orgTest)
ot, err := client.OrganizationTokens.Read(ctx, orgTest.Name)
require.NoError(t, err)
assert.NotEmpty(t, ot)
otTestCleanup()
})
t.Run("with an expiration date passed as a valid option", func(t *testing.T) {
currentTime := time.Now().UTC().Truncate(time.Second)
oneDayLater := currentTime.Add(24 * time.Hour)
_, otTestCleanup := createOrganizationTokenWithOptions(t, client, orgTest, OrganizationTokenCreateOptions{ExpiredAt: &oneDayLater})
ot, err := client.OrganizationTokens.Read(ctx, orgTest.Name)
require.NoError(t, err)
assert.NotEmpty(t, ot)
assert.Equal(t, ot.ExpiredAt, oneDayLater)
otTestCleanup()
})
t.Run("when a token doesn't exists", func(t *testing.T) {
ot, err := client.OrganizationTokens.Read(ctx, orgTest.Name)
assert.Equal(t, ErrResourceNotFound, err)
assert.Nil(t, ot)
})
t.Run("without valid organization", func(t *testing.T) {
ot, err := client.OrganizationTokens.Read(ctx, badIdentifier)
assert.Nil(t, ot)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
func TestOrganizationTokensReadWithOptions(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
// We need to update the organization to business so we can create an audit trails token later.
newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t)
tt := AuditTrailToken
noTypeToken, _ := createOrganizationToken(t, client, orgTest)
auditTypeToken, _ := createOrganizationTokenWithOptions(t, client, orgTest, OrganizationTokenCreateOptions{TokenType: &tt})
t.Run("with empty options", func(t *testing.T) {
ot, err := client.OrganizationTokens.ReadWithOptions(ctx, orgTest.Name, OrganizationTokenReadOptions{})
require.NoError(t, err)
assert.NotEmpty(t, ot)
assert.Equal(t, ot.ID, noTypeToken.ID)
})
t.Run("with a specific token type", func(t *testing.T) {
ot, err := client.OrganizationTokens.ReadWithOptions(ctx, orgTest.Name, OrganizationTokenReadOptions{TokenType: &tt})
require.NoError(t, err)
assert.NotEmpty(t, ot)
assert.Equal(t, ot.ID, auditTypeToken.ID)
})
t.Run("without valid organization", func(t *testing.T) {
ot, err := client.OrganizationTokens.Read(ctx, badIdentifier)
assert.Nil(t, ot)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
func TestOrganizationTokensDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
createOrganizationToken(t, client, orgTest)
t.Run("with valid options", func(t *testing.T) {
err := client.OrganizationTokens.Delete(ctx, orgTest.Name)
require.NoError(t, err)
})
t.Run("when a token does not exist", func(t *testing.T) {
err := client.OrganizationTokens.Delete(ctx, orgTest.Name)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("without valid organization", func(t *testing.T) {
err := client.OrganizationTokens.Delete(ctx, badIdentifier)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
func TestOrganizationTokensDeleteWithOptions(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
// We need to update the organization to business so we can create an audit trails token later.
newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t)
t.Run("with a token type", func(t *testing.T) {
// Create the token
tt := AuditTrailToken
_, err := client.OrganizationTokens.CreateWithOptions(ctx, orgTest.Name, OrganizationTokenCreateOptions{
TokenType: &tt,
})
require.NoError(t, err)
// Delete it
deleteOptions := OrganizationTokenDeleteOptions{
TokenType: &tt,
}
err = client.OrganizationTokens.DeleteWithOptions(ctx, orgTest.Name, deleteOptions)
require.NoError(t, err)
// Reload the token
ot, err := client.OrganizationTokens.ReadWithOptions(ctx, orgTest.Name, OrganizationTokenReadOptions{
TokenType: &tt,
})
// ... it should fail
assert.Nil(t, ot)
assert.Equal(t, err, ErrResourceNotFound)
// Delete it again
err = client.OrganizationTokens.DeleteWithOptions(ctx, orgTest.Name, deleteOptions)
// ... it should fail
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("without valid organization", func(t *testing.T) {
deleteOptions := OrganizationTokenDeleteOptions{}
err := client.OrganizationTokens.DeleteWithOptions(ctx, badIdentifier, deleteOptions)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
================================================
FILE: plan.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"fmt"
"io"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ Plans = (*plans)(nil)
// Plans describes all the plan related methods that the Terraform Enterprise
// API supports.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/plans
type Plans interface {
// Read a plan by its ID.
Read(ctx context.Context, planID string) (*Plan, error)
// Logs retrieves the logs of a plan.
Logs(ctx context.Context, planID string) (io.Reader, error)
// Retrieve the JSON execution plan
ReadJSONOutput(ctx context.Context, planID string) ([]byte, error)
}
// plans implements Plans.
type plans struct {
client *Client
}
// PlanStatus represents a plan state.
type PlanStatus string
// List all available plan statuses.
const (
PlanCanceled PlanStatus = "canceled"
PlanCreated PlanStatus = "created"
PlanErrored PlanStatus = "errored"
PlanFinished PlanStatus = "finished"
PlanMFAWaiting PlanStatus = "mfa_waiting"
PlanPending PlanStatus = "pending"
PlanQueued PlanStatus = "queued"
PlanRunning PlanStatus = "running"
PlanUnreachable PlanStatus = "unreachable"
)
// Plan represents a Terraform Enterprise plan.
type Plan struct {
ID string `jsonapi:"primary,plans"`
HasChanges bool `jsonapi:"attr,has-changes"`
GeneratedConfiguration bool `jsonapi:"attr,generated-configuration"`
LogReadURL string `jsonapi:"attr,log-read-url"`
ResourceAdditions int `jsonapi:"attr,resource-additions"`
ResourceChanges int `jsonapi:"attr,resource-changes"`
ResourceDestructions int `jsonapi:"attr,resource-destructions"`
ResourceImports int `jsonapi:"attr,resource-imports"`
Status PlanStatus `jsonapi:"attr,status"`
StatusTimestamps *PlanStatusTimestamps `jsonapi:"attr,status-timestamps"`
// Relations
Exports []*PlanExport `jsonapi:"relation,exports"`
HYOKEncryptedDataKey *HYOKEncryptedDataKey `jsonapi:"relation,hyok-encrypted-data-key"`
// Links
Links map[string]interface{} `jsonapi:"links,omitempty"`
}
// PlanStatusTimestamps holds the timestamps for individual plan statuses.
type PlanStatusTimestamps struct {
CanceledAt time.Time `jsonapi:"attr,canceled-at,rfc3339"`
ErroredAt time.Time `jsonapi:"attr,errored-at,rfc3339"`
FinishedAt time.Time `jsonapi:"attr,finished-at,rfc3339"`
ForceCanceledAt time.Time `jsonapi:"attr,force-canceled-at,rfc3339"`
QueuedAt time.Time `jsonapi:"attr,queued-at,rfc3339"`
StartedAt time.Time `jsonapi:"attr,started-at,rfc3339"`
}
// Read a plan by its ID.
func (s *plans) Read(ctx context.Context, planID string) (*Plan, error) {
if !validStringID(&planID) {
return nil, ErrInvalidPlanID
}
u := fmt.Sprintf("plans/%s", url.PathEscape(planID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
p := &Plan{}
err = req.Do(ctx, p)
if err != nil {
return nil, err
}
return p, nil
}
// Logs retrieves the logs of a plan.
func (s *plans) Logs(ctx context.Context, planID string) (io.Reader, error) {
if !validStringID(&planID) {
return nil, ErrInvalidPlanID
}
// Get the plan to make sure it exists.
p, err := s.Read(ctx, planID)
if err != nil {
return nil, err
}
// Return an error if the log URL is empty.
if p.LogReadURL == "" {
return nil, fmt.Errorf("plan %s does not have a log URL", planID)
}
u, err := url.Parse(p.LogReadURL)
if err != nil {
return nil, fmt.Errorf("invalid log URL: %w", err)
}
done := func() (bool, error) {
p, err := s.Read(ctx, p.ID)
if err != nil {
return false, err
}
switch p.Status {
case PlanCanceled, PlanErrored, PlanFinished, PlanUnreachable:
return true, nil
default:
return false, nil
}
}
return &LogReader{
client: s.client,
ctx: ctx,
done: done,
logURL: u,
}, nil
}
// Retrieve the JSON execution plan
func (s *plans) ReadJSONOutput(ctx context.Context, planID string) ([]byte, error) {
if !validStringID(&planID) {
return nil, ErrInvalidPlanID
}
u := fmt.Sprintf("plans/%s/json-output", url.PathEscape(planID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
var buf bytes.Buffer
err = req.Do(ctx, &buf)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
================================================
FILE: plan_export.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ PlanExports = (*planExports)(nil)
// PlanExports describes all the plan export related methods that the Terraform
// Enterprise API supports.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/plan-exports
type PlanExports interface {
// Export a plan by its ID with the given options.
Create(ctx context.Context, options PlanExportCreateOptions) (*PlanExport, error)
// Read a plan export by its ID.
Read(ctx context.Context, planExportID string) (*PlanExport, error)
// Delete a plan export by its ID.
Delete(ctx context.Context, planExportID string) error
// Download the data of an plan export.
Download(ctx context.Context, planExportID string) ([]byte, error)
}
// planExports implements PlanExports.
type planExports struct {
client *Client
}
// PlanExportDataType represents the type of data exported from a plan.
type PlanExportDataType string
// List all available plan export data types.
const (
PlanExportSentinelMockBundleV0 PlanExportDataType = "sentinel-mock-bundle-v0"
)
// PlanExportStatus represents a plan export state.
type PlanExportStatus string
// List all available plan export statuses.
const (
PlanExportCanceled PlanExportStatus = "canceled"
PlanExportErrored PlanExportStatus = "errored"
PlanExportExpired PlanExportStatus = "expired"
PlanExportFinished PlanExportStatus = "finished"
PlanExportPending PlanExportStatus = "pending"
PlanExportQueued PlanExportStatus = "queued"
)
// PlanExportStatusTimestamps holds the timestamps for plan export statuses.
type PlanExportStatusTimestamps struct {
CanceledAt time.Time `jsonapi:"attr,canceled-at,rfc3339"`
ErroredAt time.Time `jsonapi:"attr,errored-at,rfc3339"`
ExpiredAt time.Time `jsonapi:"attr,expired-at,rfc3339"`
FinishedAt time.Time `jsonapi:"attr,finished-at,rfc3339"`
QueuedAt time.Time `jsonapi:"attr,queued-at,rfc3339"`
}
// PlanExport represents an export of Terraform Enterprise plan data.
type PlanExport struct {
ID string `jsonapi:"primary,plan-exports"`
DataType PlanExportDataType `jsonapi:"attr,data-type"`
Status PlanExportStatus `jsonapi:"attr,status"`
StatusTimestamps *PlanExportStatusTimestamps `jsonapi:"attr,status-timestamps"`
}
// PlanExportCreateOptions represents the options for exporting data from a plan.
type PlanExportCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,plan-exports"`
// Required: The plan to export.
Plan *Plan `jsonapi:"relation,plan"`
// Required: The name of the policy set.
DataType *PlanExportDataType `jsonapi:"attr,data-type"`
}
// Create a plan export
func (s *planExports) Create(ctx context.Context, options PlanExportCreateOptions) (*PlanExport, error) {
if err := options.valid(); err != nil {
return nil, err
}
req, err := s.client.NewRequest("POST", "plan-exports", &options)
if err != nil {
return nil, err
}
pe := &PlanExport{}
err = req.Do(ctx, pe)
if err != nil {
return nil, err
}
return pe, err
}
// Read a plan export by its ID.
func (s *planExports) Read(ctx context.Context, planExportID string) (*PlanExport, error) {
if !validStringID(&planExportID) {
return nil, ErrInvalidPlanExportID
}
u := fmt.Sprintf("plan-exports/%s", url.PathEscape(planExportID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
pe := &PlanExport{}
err = req.Do(ctx, pe)
if err != nil {
return nil, err
}
return pe, nil
}
// Delete a plan export by ID.
func (s *planExports) Delete(ctx context.Context, planExportID string) error {
if !validStringID(&planExportID) {
return ErrInvalidPlanExportID
}
u := fmt.Sprintf("plan-exports/%s", url.PathEscape(planExportID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// Download a plan export's data. Data is exported in a .tar.gz format.
func (s *planExports) Download(ctx context.Context, planExportID string) ([]byte, error) {
if !validStringID(&planExportID) {
return nil, ErrInvalidPlanExportID
}
u := fmt.Sprintf("plan-exports/%s/download", url.PathEscape(planExportID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
var buf bytes.Buffer
err = req.Do(ctx, &buf)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (o PlanExportCreateOptions) valid() error {
if o.Plan == nil {
return ErrRequiredPlan
}
if o.DataType == nil {
return ErrRequiredDataType
}
return nil
}
================================================
FILE: plan_export_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPlanExportsCreate_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
rTest, rTestCleanup := createPlannedRun(t, client, nil)
defer rTestCleanup()
pTest, err := client.Plans.Read(ctx, rTest.Plan.ID)
require.NoError(t, err)
t.Run("with valid options", func(t *testing.T) {
options := PlanExportCreateOptions{
Plan: pTest,
DataType: PlanExportType(PlanExportSentinelMockBundleV0),
}
pe, err := client.PlanExports.Create(ctx, options)
require.NoError(t, err)
assert.NotEmpty(t, pe.ID)
assert.Equal(t, PlanExportSentinelMockBundleV0, pe.DataType)
})
t.Run("without a plan", func(t *testing.T) {
options := PlanExportCreateOptions{
Plan: nil,
DataType: PlanExportType(PlanExportSentinelMockBundleV0),
}
pe, err := client.PlanExports.Create(ctx, options)
assert.Nil(t, pe)
assert.Equal(t, err, ErrRequiredPlan)
})
t.Run("without a data type", func(t *testing.T) {
options := PlanExportCreateOptions{
Plan: pTest,
DataType: nil,
}
pe, err := client.PlanExports.Create(ctx, options)
assert.Nil(t, pe)
assert.Equal(t, err, ErrRequiredDataType)
})
}
func TestPlanExportsRead_RunDependent(t *testing.T) {
// TODO: Investigate why this test keeps tripping the test suite timeout
t.Skip()
client := testClient(t)
ctx := context.Background()
peTest, peTestCleanup := createPlanExport(t, client, nil)
defer peTestCleanup()
t.Run("with a valid ID", func(t *testing.T) {
pe, err := client.PlanExports.Read(ctx, peTest.ID)
require.NoError(t, err)
assert.Equal(t, peTest.ID, pe.ID)
assert.Equal(t, peTest.DataType, pe.DataType)
assert.NotEmpty(t, pe.StatusTimestamps)
assert.NotNil(t, pe.StatusTimestamps.QueuedAt)
})
t.Run("without a valid ID", func(t *testing.T) {
pe, err := client.PlanExports.Read(ctx, badIdentifier)
assert.Nil(t, pe)
assert.Equal(t, err, ErrInvalidPlanExportID)
})
}
func TestPlanExportsDelete_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
peTest, peTestCleanup := createPlanExport(t, client, nil)
defer peTestCleanup()
t.Run("with a valid ID", func(t *testing.T) {
err := client.PlanExports.Delete(ctx, peTest.ID)
require.NoError(t, err)
})
t.Run("when the export does not exist", func(t *testing.T) {
err := client.Policies.Delete(ctx, "pe-doesntexist")
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("without a valid ID", func(t *testing.T) {
err := client.PlanExports.Delete(ctx, badIdentifier)
assert.Equal(t, err, ErrInvalidPlanExportID)
})
}
func TestPlanExportsDownload_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
peTest, peCleanup := createPlanExport(t, client, nil)
defer peCleanup()
t.Run("with a valid ID", func(t *testing.T) {
pe, err := client.PlanExports.Download(ctx, peTest.ID)
assert.NotNil(t, pe)
require.NoError(t, err)
})
t.Run("without a valid ID", func(t *testing.T) {
pe, err := client.PlanExports.Download(ctx, badIdentifier)
assert.Nil(t, pe)
assert.Equal(t, err, ErrInvalidPlanExportID)
})
}
func TestPlanExport_Unmarshal(t *testing.T) {
t.Parallel()
data := map[string]interface{}{
"data": map[string]interface{}{
"type": "plan-exports",
"id": "1",
"attributes": map[string]interface{}{
"data-type": PlanExportSentinelMockBundleV0,
"status": PlanExportCanceled,
"status-timestamps": map[string]string{
"queued-at": "2020-03-16T23:15:59+00:00",
"errored-at": "2019-03-16T23:23:59+00:00",
},
},
},
}
byteData, err := json.Marshal(data)
require.NoError(t, err)
responseBody := bytes.NewReader(byteData)
pe := &PlanExport{}
err = unmarshalResponse(responseBody, pe)
require.NoError(t, err)
queuedParsedTime, err := time.Parse(time.RFC3339, "2020-03-16T23:15:59+00:00")
require.NoError(t, err)
erroredParsedTime, err := time.Parse(time.RFC3339, "2019-03-16T23:23:59+00:00")
require.NoError(t, err)
assert.Equal(t, pe.DataType, PlanExportSentinelMockBundleV0)
assert.Equal(t, pe.Status, PlanExportCanceled)
assert.NotEmpty(t, pe.StatusTimestamps)
assert.Equal(t, pe.StatusTimestamps.QueuedAt, queuedParsedTime)
assert.Equal(t, pe.StatusTimestamps.ErroredAt, erroredParsedTime)
}
================================================
FILE: plan_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"encoding/json"
"io"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPlansRead_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
rTest, rTestCleanup := createPlannedRun(t, client, nil)
defer rTestCleanup()
t.Run("when the plan exists", func(t *testing.T) {
p, err := client.Plans.Read(ctx, rTest.Plan.ID)
require.NoError(t, err)
assert.True(t, p.HasChanges)
assert.NotEmpty(t, p.LogReadURL)
assert.Equal(t, p.Status, PlanFinished)
assert.NotEmpty(t, p.StatusTimestamps)
assert.NotNil(t, p.StatusTimestamps.StartedAt)
})
t.Run("when the plan does not exist", func(t *testing.T) {
p, err := client.Plans.Read(ctx, "nonexisting")
assert.Nil(t, p)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("with invalid plan ID", func(t *testing.T) {
p, err := client.Plans.Read(ctx, badIdentifier)
assert.Nil(t, p)
assert.Equal(t, err, ErrInvalidPlanID)
})
t.Run("read hyok encrypted data key of a plan", func(t *testing.T) {
skipHYOKIntegrationTests(t)
// replace the environment variable with a valid plan ID that has a hyok encrypted data key
hyokPlanID := os.Getenv("HYOK_PLAN_ID")
if hyokPlanID == "" {
t.Fatal("Export a valid HYOK_PLAN_ID before running this test!")
}
p, err := client.Plans.Read(ctx, hyokPlanID)
require.NoError(t, err)
assert.NotNil(t, p.HYOKEncryptedDataKey)
})
t.Run("read sanitized plan of a plan", func(t *testing.T) {
skipHYOKIntegrationTests(t)
// replace the environment variable with a valid plan ID that has a sanitized plan link
hyokPlanID := os.Getenv("HYOK_PLAN_ID")
if hyokPlanID == "" {
t.Fatal("Export a valid HYOK_PLAN_ID before running this test!")
}
p, err := client.Plans.Read(ctx, hyokPlanID)
require.NoError(t, err)
assert.NotEmpty(t, p.Links["sanitized-plan"])
})
}
func TestPlansLogs_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
rTest, rTestCleanup := createPlannedRun(t, client, nil)
defer rTestCleanup()
t.Run("when the log exists", func(t *testing.T) {
p, err := client.Plans.Read(ctx, rTest.Plan.ID)
require.NoError(t, err)
logReader, err := client.Plans.Logs(ctx, p.ID)
require.NoError(t, err)
logs, err := io.ReadAll(logReader)
require.NoError(t, err)
assert.Contains(t, string(logs), "1 to add, 0 to change, 0 to destroy")
})
t.Run("when the log does not exist", func(t *testing.T) {
logs, err := client.Plans.Logs(ctx, "nonexisting")
assert.Nil(t, logs)
assert.Error(t, err)
})
}
func TestPlan_Unmarshal(t *testing.T) {
t.Parallel()
data := map[string]interface{}{
"data": map[string]interface{}{
"type": "plans",
"id": "1",
"attributes": map[string]interface{}{
"has-changes": true,
"log-read-url": "hashicorp.com",
"resource-additions": 1,
"resource-changes": 1,
"resource-destructions": 1,
"status": PlanCanceled,
"status-timestamps": map[string]string{
"queued-at": "2020-03-16T23:15:59+00:00",
"errored-at": "2019-03-16T23:23:59+00:00",
},
},
},
}
byteData, err := json.Marshal(data)
require.NoError(t, err)
responseBody := bytes.NewReader(byteData)
plan := &Plan{}
err = unmarshalResponse(responseBody, plan)
require.NoError(t, err)
queuedParsedTime, err := time.Parse(time.RFC3339, "2020-03-16T23:15:59+00:00")
require.NoError(t, err)
erroredParsedTime, err := time.Parse(time.RFC3339, "2019-03-16T23:23:59+00:00")
require.NoError(t, err)
assert.Equal(t, plan.HasChanges, true)
assert.Equal(t, plan.LogReadURL, "hashicorp.com")
assert.Equal(t, plan.ResourceAdditions, 1)
assert.Equal(t, plan.ResourceChanges, 1)
assert.Equal(t, plan.ResourceDestructions, 1)
assert.Equal(t, plan.Status, PlanCanceled)
assert.NotEmpty(t, plan.StatusTimestamps)
assert.Equal(t, plan.StatusTimestamps.QueuedAt, queuedParsedTime)
assert.Equal(t, plan.StatusTimestamps.ErroredAt, erroredParsedTime)
}
func TestPlansJSONOutput_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
rTest, rTestCleanup := createPlannedRun(t, client, nil)
defer rTestCleanup()
t.Run("when the JSON output exists", func(t *testing.T) {
d, err := client.Plans.ReadJSONOutput(ctx, rTest.Plan.ID)
require.NoError(t, err)
var m map[string]interface{}
err = json.Unmarshal(d, &m)
require.NoError(t, err)
assert.Contains(t, m, "planned_values")
assert.Contains(t, m, "terraform_version")
})
t.Run("when the JSON output does not exist", func(t *testing.T) {
d, err := client.Plans.ReadJSONOutput(ctx, "nonexisting")
assert.Nil(t, d)
assert.Error(t, err)
})
}
================================================
FILE: policy.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ Policies = (*policies)(nil)
// Policies describes all the policy related methods that the Terraform
// Enterprise API supports.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/policies
type Policies interface {
// List all the policies for a given organization
List(ctx context.Context, organization string, options *PolicyListOptions) (*PolicyList, error)
// Create a policy and associate it with an organization.
Create(ctx context.Context, organization string, options PolicyCreateOptions) (*Policy, error)
// Read a policy by its ID.
Read(ctx context.Context, policyID string) (*Policy, error)
// Update an existing policy.
Update(ctx context.Context, policyID string, options PolicyUpdateOptions) (*Policy, error)
// Delete a policy by its ID.
Delete(ctx context.Context, policyID string) error
// Upload the policy content of the policy.
Upload(ctx context.Context, policyID string, content []byte) error
// Download the policy content of the policy.
Download(ctx context.Context, policyID string) ([]byte, error)
}
// policies implements Policies.
type policies struct {
client *Client
}
// EnforcementLevel represents an enforcement level.
type EnforcementLevel string
// List the available enforcement types.
const (
EnforcementAdvisory EnforcementLevel = "advisory"
EnforcementHard EnforcementLevel = "hard-mandatory"
EnforcementSoft EnforcementLevel = "soft-mandatory"
EnforcementMandatory EnforcementLevel = "mandatory"
)
// PolicyList represents a list of policies..
type PolicyList struct {
*Pagination
Items []*Policy
}
// Policy represents a Terraform Enterprise policy.
type Policy struct {
ID string `jsonapi:"primary,policies"`
Name string `jsonapi:"attr,name"`
Kind PolicyKind `jsonapi:"attr,kind"`
Query *string `jsonapi:"attr,query"`
Description string `jsonapi:"attr,description"`
// Deprecated: Use EnforcementLevel instead.
Enforce []*Enforcement `jsonapi:"attr,enforce"`
EnforcementLevel EnforcementLevel `jsonapi:"attr,enforcement-level"`
PolicySetCount int `jsonapi:"attr,policy-set-count"`
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
// Relations
Organization *Organization `jsonapi:"relation,organization"`
}
// Enforcement describes a enforcement.
type Enforcement struct {
Path string `jsonapi:"attr,path"`
Mode EnforcementLevel `jsonapi:"attr,mode"`
}
// EnforcementOptions represents the enforcement options of a policy.
type EnforcementOptions struct {
Path *string `json:"path"`
Mode *EnforcementLevel `json:"mode"`
}
// PolicyListOptions represents the options for listing policies.
type PolicyListOptions struct {
ListOptions
// Optional: A search string (partial policy name) used to filter the results.
Search string `url:"search[name],omitempty"`
// Optional: A kind string used to filter the results by the policy kind.
Kind PolicyKind `url:"filter[kind],omitempty"`
}
// PolicyCreateOptions represents the options for creating a new policy.
type PolicyCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,policies"`
// Required: The name of the policy.
Name *string `jsonapi:"attr,name"`
// Optional: The underlying technology that the policy supports. Defaults to Sentinel if not specified for PolicyCreate.
Kind PolicyKind `jsonapi:"attr,kind,omitempty"`
// Optional: The query passed to policy evaluation to determine the result of the policy. Only valid for OPA.
Query *string `jsonapi:"attr,query,omitempty"`
// Optional: A description of the policy's purpose.
Description *string `jsonapi:"attr,description,omitempty"`
// The enforcements of the policy.
//
// Deprecated: Use EnforcementLevel instead.
Enforce []*EnforcementOptions `jsonapi:"attr,enforce,omitempty"`
// Required: The enforcement level of the policy.
// Either EnforcementLevel or Enforce must be set.
EnforcementLevel *EnforcementLevel `jsonapi:"attr,enforcement-level,omitempty"`
}
// PolicyUpdateOptions represents the options for updating a policy.
type PolicyUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,policies"`
// Optional: A description of the policy's purpose.
Description *string `jsonapi:"attr,description,omitempty"`
// Optional: The query passed to policy evaluation to determine the result of the policy. Only valid for OPA.
Query *string `jsonapi:"attr,query,omitempty"`
// Optional: The enforcements of the policy.
//
// Deprecated: Use EnforcementLevel instead.
Enforce []*EnforcementOptions `jsonapi:"attr,enforce,omitempty"`
// Optional: The enforcement level of the policy.
EnforcementLevel *EnforcementLevel `jsonapi:"attr,enforcement-level,omitempty"`
}
// List all the policies for a given organization
func (s *policies) List(ctx context.Context, organization string, options *PolicyListOptions) (*PolicyList, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf("organizations/%s/policies", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
pl := &PolicyList{}
err = req.Do(ctx, pl)
if err != nil {
return nil, err
}
return pl, nil
}
// Create a policy and associate it with an organization.
func (s *policies) Create(ctx context.Context, organization string, options PolicyCreateOptions) (*Policy, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("organizations/%s/policies", url.PathEscape(organization))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
p := &Policy{}
err = req.Do(ctx, p)
if err != nil {
return nil, err
}
return p, err
}
// Read a policy by its ID.
func (s *policies) Read(ctx context.Context, policyID string) (*Policy, error) {
if !validStringID(&policyID) {
return nil, ErrInvalidPolicyID
}
u := fmt.Sprintf("policies/%s", url.PathEscape(policyID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
p := &Policy{}
err = req.Do(ctx, p)
if err != nil {
return nil, err
}
return p, err
}
// Update an existing policy.
func (s *policies) Update(ctx context.Context, policyID string, options PolicyUpdateOptions) (*Policy, error) {
if !validStringID(&policyID) {
return nil, ErrInvalidPolicyID
}
u := fmt.Sprintf("policies/%s", url.PathEscape(policyID))
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
p := &Policy{}
err = req.Do(ctx, p)
if err != nil {
return nil, err
}
return p, err
}
// Delete a policy by its ID.
func (s *policies) Delete(ctx context.Context, policyID string) error {
if !validStringID(&policyID) {
return ErrInvalidPolicyID
}
u := fmt.Sprintf("policies/%s", url.PathEscape(policyID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// Upload the policy content of the policy.
func (s *policies) Upload(ctx context.Context, policyID string, content []byte) error {
if !validStringID(&policyID) {
return ErrInvalidPolicyID
}
u := fmt.Sprintf("policies/%s/upload", url.PathEscape(policyID))
req, err := s.client.NewRequest("PUT", u, content)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// Download the policy content of the policy.
func (s *policies) Download(ctx context.Context, policyID string) ([]byte, error) {
if !validStringID(&policyID) {
return nil, ErrInvalidPolicyID
}
u := fmt.Sprintf("policies/%s/download", url.PathEscape(policyID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
var buf bytes.Buffer
err = req.Do(ctx, &buf)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (o PolicyCreateOptions) valid() error {
if !validString(o.Name) {
return ErrRequiredName
}
if !validStringID(o.Name) {
return ErrInvalidName
}
if o.Kind == OPA && !validString(o.Query) {
return ErrRequiredQuery
}
if o.Enforce == nil && o.EnforcementLevel == nil {
return ErrRequiredEnforce
}
if o.Enforce != nil && o.EnforcementLevel != nil {
return ErrConflictingEnforceEnforcementLevel
}
if o.Enforce != nil {
for _, e := range o.Enforce {
if !validString(e.Path) {
return ErrRequiredEnforcementPath
}
if e.Mode == nil {
return ErrRequiredEnforcementMode
}
}
}
return nil
}
================================================
FILE: policy_check.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"fmt"
"io"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ PolicyChecks = (*policyChecks)(nil)
// PolicyChecks describes all the policy check related methods that the
// Terraform Enterprise API supports.
//
// TFE API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/policy-checks
type PolicyChecks interface {
// List all policy checks of the given run.
List(ctx context.Context, runID string, options *PolicyCheckListOptions) (*PolicyCheckList, error)
// Read a policy check by its ID.
Read(ctx context.Context, policyCheckID string) (*PolicyCheck, error)
// Override a soft-mandatory or warning policy.
Override(ctx context.Context, policyCheckID string) (*PolicyCheck, error)
// Logs retrieves the logs of a policy check.
Logs(ctx context.Context, policyCheckID string) (io.Reader, error)
}
// policyChecks implements PolicyChecks.
type policyChecks struct {
client *Client
}
// PolicyScope represents a policy scope.
type PolicyScope string
// List all available policy scopes.
const (
PolicyScopeOrganization PolicyScope = "organization"
PolicyScopeWorkspace PolicyScope = "workspace"
)
// PolicyStatus represents a policy check state.
type PolicyStatus string
// List all available policy check statuses.
const (
PolicyCanceled PolicyStatus = "canceled"
PolicyErrored PolicyStatus = "errored"
PolicyHardFailed PolicyStatus = "hard_failed"
PolicyOverridden PolicyStatus = "overridden"
PolicyPasses PolicyStatus = "passed"
PolicyPending PolicyStatus = "pending"
PolicyQueued PolicyStatus = "queued"
PolicySoftFailed PolicyStatus = "soft_failed"
PolicyUnreachable PolicyStatus = "unreachable"
)
// PolicyCheckList represents a list of policy checks.
type PolicyCheckList struct {
*Pagination
Items []*PolicyCheck
}
// PolicyCheck represents a Terraform Enterprise policy check..
type PolicyCheck struct {
ID string `jsonapi:"primary,policy-checks"`
Actions *PolicyActions `jsonapi:"attr,actions"`
Permissions *PolicyPermissions `jsonapi:"attr,permissions"`
Result *PolicyResult `jsonapi:"attr,result"`
Scope PolicyScope `jsonapi:"attr,scope"`
Status PolicyStatus `jsonapi:"attr,status"`
StatusTimestamps *PolicyStatusTimestamps `jsonapi:"attr,status-timestamps"`
Run *Run `jsonapi:"relation,run"`
}
// PolicyActions represents the policy check actions.
type PolicyActions struct {
IsOverridable bool `jsonapi:"attr,is-overridable"`
}
// PolicyPermissions represents the policy check permissions.
type PolicyPermissions struct {
CanOverride bool `jsonapi:"attr,can-override"`
}
// PolicyResult represents the complete policy check result,
type PolicyResult struct {
AdvisoryFailed int `jsonapi:"attr,advisory-failed"`
Duration int `jsonapi:"attr,duration"`
HardFailed int `jsonapi:"attr,hard-failed"`
Passed int `jsonapi:"attr,passed"`
Result bool `jsonapi:"attr,result"`
SoftFailed int `jsonapi:"attr,soft-failed"`
TotalFailed int `jsonapi:"attr,total-failed"`
Sentinel any `jsonapi:"attr,sentinel"`
}
// PolicyStatusTimestamps holds the timestamps for individual policy check
// statuses.
type PolicyStatusTimestamps struct {
ErroredAt time.Time `jsonapi:"attr,errored-at,rfc3339"`
HardFailedAt time.Time `jsonapi:"attr,hard-failed-at,rfc3339"`
PassedAt time.Time `jsonapi:"attr,passed-at,rfc3339"`
QueuedAt time.Time `jsonapi:"attr,queued-at,rfc3339"`
SoftFailedAt time.Time `jsonapi:"attr,soft-failed-at,rfc3339"`
}
// A list of relations to include
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/policy-checks#available-related-resources
type PolicyCheckIncludeOpt string
const (
PolicyCheckRunWorkspace PolicyCheckIncludeOpt = "run.workspace"
PolicyCheckRun PolicyCheckIncludeOpt = "run"
)
// PolicyCheckListOptions represents the options for listing policy checks.
type PolicyCheckListOptions struct {
ListOptions
// Optional: A list of relations to include. See available resources
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/policy-checks#available-related-resources
Include []PolicyCheckIncludeOpt `url:"include,omitempty"`
}
// List all policy checks of the given run.
func (s *policyChecks) List(ctx context.Context, runID string, options *PolicyCheckListOptions) (*PolicyCheckList, error) {
if !validStringID(&runID) {
return nil, ErrInvalidRunID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("runs/%s/policy-checks", url.PathEscape(runID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
pcl := &PolicyCheckList{}
err = req.Do(ctx, pcl)
if err != nil {
return nil, err
}
return pcl, nil
}
// Read a policy check by its ID.
func (s *policyChecks) Read(ctx context.Context, policyCheckID string) (*PolicyCheck, error) {
if !validStringID(&policyCheckID) {
return nil, ErrInvalidPolicyCheckID
}
u := fmt.Sprintf("policy-checks/%s", url.PathEscape(policyCheckID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
pc := &PolicyCheck{}
err = req.Do(ctx, pc)
if err != nil {
return nil, err
}
return pc, nil
}
// Override a soft-mandatory or warning policy.
func (s *policyChecks) Override(ctx context.Context, policyCheckID string) (*PolicyCheck, error) {
if !validStringID(&policyCheckID) {
return nil, ErrInvalidPolicyCheckID
}
u := fmt.Sprintf("policy-checks/%s/actions/override", url.PathEscape(policyCheckID))
req, err := s.client.NewRequest("POST", u, nil)
if err != nil {
return nil, err
}
pc := &PolicyCheck{}
err = req.Do(ctx, pc)
if err != nil {
return nil, err
}
return pc, nil
}
// Logs retrieves the logs of a policy check.
func (s *policyChecks) Logs(ctx context.Context, policyCheckID string) (io.Reader, error) {
if !validStringID(&policyCheckID) {
return nil, ErrInvalidPolicyCheckID
}
// Loop until the context is canceled or the policy check is finished
// running. The policy check logs are not streamed and so only available
// once the check is finished.
for {
pc, err := s.Read(ctx, policyCheckID)
if err != nil {
return nil, err
}
switch pc.Status {
case PolicyPending, PolicyQueued:
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(500 * time.Millisecond):
continue
}
}
u := fmt.Sprintf("policy-checks/%s/output", url.PathEscape(policyCheckID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
logs := bytes.NewBuffer(nil)
err = req.Do(ctx, logs)
if err != nil {
return nil, err
}
return logs, nil
}
}
func (o *PolicyCheckListOptions) valid() error {
return nil
}
================================================
FILE: policy_check_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"encoding/json"
"io"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPolicyChecksList_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
pTest1, policyCleanup1 := createUploadedPolicy(t, client, true, orgTest)
defer policyCleanup1()
pTest2, policyCleanup2 := createUploadedPolicy(t, client, true, orgTest)
defer policyCleanup2()
wTest, wsCleanup := createWorkspace(t, client, orgTest)
defer wsCleanup()
createPolicySet(t, client, orgTest, []*Policy{pTest1, pTest2}, []*Workspace{wTest}, nil, nil, "")
rTest, runCleanup := createPolicyCheckedRun(t, client, wTest)
defer runCleanup()
t.Run("without list options", func(t *testing.T) {
pcl, err := client.PolicyChecks.List(ctx, rTest.ID, nil)
require.NoError(t, err)
require.Equal(t, 1, len(pcl.Items))
assert.NotEmpty(t, pcl.Items[0].Permissions)
require.NotEmpty(t, pcl.Items[0].Result)
assert.Equal(t, 2, pcl.Items[0].Result.Passed)
assert.NotEmpty(t, pcl.Items[0].StatusTimestamps)
assert.NotNil(t, pcl.Items[0].StatusTimestamps.QueuedAt)
})
t.Run("with list options", func(t *testing.T) {
t.Skip("paging not supported yet in API")
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
pcl, err := client.PolicyChecks.List(ctx, rTest.ID, &PolicyCheckListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, pcl.Items)
assert.Equal(t, 999, pcl.CurrentPage)
assert.Equal(t, 1, pcl.TotalCount)
})
t.Run("with Include option", func(t *testing.T) {
pcl, err := client.PolicyChecks.List(ctx, rTest.ID, &PolicyCheckListOptions{
Include: []PolicyCheckIncludeOpt{PolicyCheckRun},
})
require.NoError(t, err)
require.NotEmpty(t, pcl.Items)
require.NotNil(t, pcl.Items[0])
require.NotNil(t, pcl.Items[0].Run)
assert.NotEmpty(t, pcl.Items[0].Run.Status)
})
t.Run("without a valid run ID", func(t *testing.T) {
pcl, err := client.PolicyChecks.List(ctx, badIdentifier, nil)
assert.Nil(t, pcl)
assert.EqualError(t, err, ErrInvalidRunID.Error())
})
}
func TestPolicyChecksRead_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
pTest, _ := createUploadedPolicy(t, client, true, orgTest)
wTest, _ := createWorkspace(t, client, orgTest)
createPolicySet(t, client, orgTest, []*Policy{pTest}, []*Workspace{wTest}, nil, nil, "")
rTest, _ := createPolicyCheckedRun(t, client, wTest)
require.Equal(t, 1, len(rTest.PolicyChecks))
t.Run("when the policy check exists", func(t *testing.T) {
pc, err := client.PolicyChecks.Read(ctx, rTest.PolicyChecks[0].ID)
require.NoError(t, err)
require.NotEmpty(t, pc.Result)
assert.NotEmpty(t, pc.Permissions)
assert.Equal(t, PolicyScopeOrganization, pc.Scope)
assert.Equal(t, PolicyPasses, pc.Status)
assert.NotEmpty(t, pc.StatusTimestamps)
assert.Equal(t, 1, pc.Result.Passed)
assert.NotEmpty(t, pc.Run)
assert.NotEmpty(t, pc.Result.Sentinel)
if reflect.TypeOf(pc.Result.Sentinel) != reflect.TypeOf(map[string]interface{}{}) {
assert.Fail(t, "Sentinel is not a map[string]interface{}")
}
})
t.Run("when the policy check does not exist", func(t *testing.T) {
pc, err := client.PolicyChecks.Read(ctx, "nonexisting")
assert.Nil(t, pc)
assert.Equal(t, ErrResourceNotFound, err)
})
t.Run("without a valid policy check ID", func(t *testing.T) {
pc, err := client.PolicyChecks.Read(ctx, badIdentifier)
assert.Nil(t, pc)
assert.Equal(t, err, ErrInvalidPolicyCheckID)
})
}
func TestPolicyChecksOverride_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
t.Run("when the policy failed", func(t *testing.T) {
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
pTest, pTestCleanup := createUploadedPolicy(t, client, false, orgTest)
defer pTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
createPolicySet(t, client, orgTest, []*Policy{pTest}, []*Workspace{wTest}, nil, nil, "")
rTest, tTestCleanup := createPolicyCheckedRun(t, client, wTest)
defer tTestCleanup()
pcl, err := client.PolicyChecks.List(ctx, rTest.ID, nil)
require.NoError(t, err)
require.Equal(t, 1, len(pcl.Items))
require.Equal(t, PolicySoftFailed, pcl.Items[0].Status)
pc, err := client.PolicyChecks.Override(ctx, pcl.Items[0].ID)
require.NoError(t, err)
assert.NotEmpty(t, pc.Result)
assert.Equal(t, PolicyOverridden, pc.Status)
})
t.Run("when the policy passed", func(t *testing.T) {
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
pTest, pTestCleanup := createUploadedPolicy(t, client, true, orgTest)
defer pTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
createPolicySet(t, client, orgTest, []*Policy{pTest}, []*Workspace{wTest}, nil, nil, "")
rTest, rTestCleanup := createPolicyCheckedRun(t, client, wTest)
defer rTestCleanup()
pcl, err := client.PolicyChecks.List(ctx, rTest.ID, nil)
require.NoError(t, err)
require.Equal(t, 1, len(pcl.Items))
require.Equal(t, PolicyPasses, pcl.Items[0].Status)
_, err = client.PolicyChecks.Override(ctx, pcl.Items[0].ID)
assert.Error(t, err)
})
t.Run("without a valid policy check ID", func(t *testing.T) {
p, err := client.PolicyChecks.Override(ctx, badIdentifier)
assert.Nil(t, p)
assert.Equal(t, err, ErrInvalidPolicyCheckID)
})
}
func TestPolicyChecksLogs_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
pTest, pTestCleanup := createUploadedPolicy(t, client, true, orgTest)
defer pTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
createPolicySet(t, client, orgTest, []*Policy{pTest}, []*Workspace{wTest}, nil, nil, "")
rTest, rTestCleanup := createPolicyCheckedRun(t, client, wTest)
defer rTestCleanup()
require.Equal(t, 1, len(rTest.PolicyChecks))
t.Run("when the log exists", func(t *testing.T) {
pc, err := client.PolicyChecks.Read(ctx, rTest.PolicyChecks[0].ID)
require.NoError(t, err)
logReader, err := client.PolicyChecks.Logs(ctx, pc.ID)
require.NoError(t, err)
logs, err := io.ReadAll(logReader)
require.NoError(t, err)
assert.Contains(t, string(logs), "1 policies evaluated")
})
t.Run("when the log does not exist", func(t *testing.T) {
logs, err := client.PolicyChecks.Logs(ctx, "nonexisting")
assert.Nil(t, logs)
assert.Error(t, err)
})
}
func TestPolicyCheck_Unmarshal(t *testing.T) {
t.Parallel()
data := map[string]interface{}{
"data": map[string]interface{}{
"type": "policy-checks",
"id": "1",
"attributes": map[string]interface{}{
"actions": map[string]interface{}{
"is-overridable": true,
},
"permissions": map[string]interface{}{
"can-override": true,
},
"result": map[string]interface{}{
"advisory-failed": 1,
"duration": 1,
"hard-failed": 1,
"passed": 1,
"result": true,
"soft-failed": 1,
"total-failed": 1,
},
"scope": PolicyScopeOrganization,
"status": PolicyOverridden,
"status-timestamps": map[string]string{
"queued-at": "2020-03-16T23:15:59+00:00",
"errored-at": "2019-03-16T23:23:59+00:00",
},
},
},
}
byteData, err := json.Marshal(data)
require.NoError(t, err)
responseBody := bytes.NewReader(byteData)
pc := &PolicyCheck{}
err = unmarshalResponse(responseBody, pc)
require.NoError(t, err)
queuedParsedTime, err := time.Parse(time.RFC3339, "2020-03-16T23:15:59+00:00")
require.NoError(t, err)
erroredParsedTime, err := time.Parse(time.RFC3339, "2019-03-16T23:23:59+00:00")
require.NoError(t, err)
assert.Equal(t, pc.ID, "1")
assert.Equal(t, pc.Actions.IsOverridable, true)
assert.Equal(t, pc.Permissions.CanOverride, true)
assert.Equal(t, pc.Result.AdvisoryFailed, 1)
assert.Equal(t, pc.Result.Duration, 1)
assert.Equal(t, pc.Result.HardFailed, 1)
assert.Equal(t, pc.Result.Passed, 1)
assert.Equal(t, pc.Result.Result, true)
assert.Equal(t, pc.Result.SoftFailed, 1)
assert.Equal(t, pc.Result.TotalFailed, 1)
assert.Equal(t, pc.Scope, PolicyScopeOrganization)
assert.Equal(t, pc.Status, PolicyOverridden)
assert.Equal(t, pc.StatusTimestamps.QueuedAt, queuedParsedTime)
assert.Equal(t, pc.StatusTimestamps.ErroredAt, erroredParsedTime)
}
================================================
FILE: policy_evaluation.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ PolicyEvaluations = (*policyEvaluation)(nil)
// PolicyEvaluationStatus is an enum that represents all possible statuses for a policy evaluation
type PolicyEvaluationStatus string
const (
PolicyEvaluationPassed PolicyEvaluationStatus = "passed"
PolicyEvaluationFailed PolicyEvaluationStatus = "failed"
PolicyEvaluationPending PolicyEvaluationStatus = "pending"
PolicyEvaluationRunning PolicyEvaluationStatus = "running"
PolicyEvaluationUnreachable PolicyEvaluationStatus = "unreachable"
PolicyEvaluationOverridden PolicyEvaluationStatus = "overridden"
PolicyEvaluationCanceled PolicyEvaluationStatus = "canceled"
PolicyEvaluationErrored PolicyEvaluationStatus = "errored"
)
// PolicyResultCount represents the count of the policy results
type PolicyResultCount struct {
AdvisoryFailed int `jsonapi:"attr,advisory-failed"`
MandatoryFailed int `jsonapi:"attr,mandatory-failed"`
Passed int `jsonapi:"attr,passed"`
Errored int `jsonapi:"attr,errored"`
}
// The task stage the policy evaluation belongs to
type PolicyAttachable struct {
ID string `jsonapi:"attr,id"`
Type string `jsonapi:"attr,type"`
}
// PolicyEvaluationStatusTimestamps represents the set of timestamps recorded for a policy evaluation
type PolicyEvaluationStatusTimestamps struct {
ErroredAt time.Time `jsonapi:"attr,errored-at,rfc3339"`
RunningAt time.Time `jsonapi:"attr,running-at,rfc3339"`
CanceledAt time.Time `jsonapi:"attr,canceled-at,rfc3339"`
FailedAt time.Time `jsonapi:"attr,failed-at,rfc3339"`
PassedAt time.Time `jsonapi:"attr,passed-at,rfc3339"`
}
// PolicyEvaluation represents the policy evaluations that are part of the task stage.
type PolicyEvaluation struct {
ID string `jsonapi:"primary,policy-evaluations"`
Status PolicyEvaluationStatus `jsonapi:"attr,status"`
PolicyKind PolicyKind `jsonapi:"attr,policy-kind"`
StatusTimestamps PolicyEvaluationStatusTimestamps `jsonapi:"attr,status-timestamps"`
ResultCount *PolicyResultCount `jsonapi:"attr,result-count"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
// The task stage this evaluation belongs to
TaskStage *PolicyAttachable `jsonapi:"relation,policy-attachable"`
}
// PolicyEvalutations describes all the policy evaluation related methods that the
// Terraform Enterprise API supports.
//
// TFE API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/policy-checks
type PolicyEvaluations interface {
// **Note: This method is still in BETA and subject to change.**
// List all policy evaluations in the task stage. Only available for OPA policies.
List(ctx context.Context, taskStageID string, options *PolicyEvaluationListOptions) (*PolicyEvaluationList, error)
}
// policyEvaluation implements PolicyEvaluations.
type policyEvaluation struct {
client *Client
}
// PolicyEvaluationListOptions represents the options for listing policy evaluations.
type PolicyEvaluationListOptions struct {
ListOptions
}
// PolicyEvaluationList represents a list of policy evaluation.
type PolicyEvaluationList struct {
*Pagination
Items []*PolicyEvaluation
}
// List all policy evaluations in a task stage.
func (s *policyEvaluation) List(ctx context.Context, taskStageID string, options *PolicyEvaluationListOptions) (*PolicyEvaluationList, error) {
if !validStringID(&taskStageID) {
return nil, ErrInvalidTaskStageID
}
u := fmt.Sprintf("task-stages/%s/policy-evaluations", url.PathEscape(taskStageID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
pcl := &PolicyEvaluationList{}
err = req.Do(ctx, pcl)
if err != nil {
return nil, err
}
return pcl, nil
}
// Compile-time proof of interface implementation.
var _ PolicySetOutcomes = (*policySetOutcome)(nil)
// PolicySetOutcomes describes all the policy set outcome related methods that the
// Terraform Enterprise API supports.
//
// TFE API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/policy-checks
type PolicySetOutcomes interface {
// **Note: This method is still in BETA and subject to change.**
// List all policy set outcomes in the policy evaluation. Only available for OPA policies.
List(ctx context.Context, policyEvaluationID string, options *PolicySetOutcomeListOptions) (*PolicySetOutcomeList, error)
// **Note: This method is still in BETA and subject to change.**
// Read a policy set outcome by its ID. Only available for OPA policies.
Read(ctx context.Context, policySetOutcomeID string) (*PolicySetOutcome, error)
}
// policySetOutcome implements PolicySetOutcomes.
type policySetOutcome struct {
client *Client
}
// PolicySetOutcomeListFilter represents the filters that are supported while listing a policy set outcome
type PolicySetOutcomeListFilter struct {
// Optional: A status string used to filter the results.
// Must be either "passed", "failed", or "errored".
Status string
// Optional: The enforcement level used to filter the results.
// Must be either "advisory" or "mandatory".
EnforcementLevel string
}
// PolicySetOutcomeListOptions represents the options for listing policy set outcomes.
type PolicySetOutcomeListOptions struct {
*ListOptions
// Optional: A filter map used to filter the results of the policy outcome.
// You can use filter[n] to combine combinations of statuses and enforcement levels filters
Filter map[string]PolicySetOutcomeListFilter
}
// PolicySetOutcomeList represents a list of policy set outcomes.
type PolicySetOutcomeList struct {
*Pagination
Items []*PolicySetOutcome
}
// Outcome represents the outcome of the individual policy
type Outcome struct {
EnforcementLevel EnforcementLevel `jsonapi:"attr,enforcement_level"`
Query string `jsonapi:"attr,query"`
Status string `jsonapi:"attr,status"`
PolicyName string `jsonapi:"attr,policy_name"`
Description string `jsonapi:"attr,description"`
}
// PolicySetOutcome represents outcome of the policy set that are part of the policy evaluation
type PolicySetOutcome struct {
ID string `jsonapi:"primary,policy-set-outcomes"`
Outcomes []Outcome `jsonapi:"attr,outcomes"`
Error string `jsonapi:"attr,error"`
Overridable *bool `jsonapi:"attr,overridable"`
PolicySetName string `jsonapi:"attr,policy-set-name"`
PolicySetDescription string `jsonapi:"attr,policy-set-description"`
ResultCount PolicyResultCount `jsonapi:"attr,result_count"`
// The policy evaluation that this outcome belongs to
PolicyEvaluation *PolicyEvaluation `jsonapi:"relation,policy-evaluation"`
}
// List all policy set outcomes in a policy evaluation.
func (s *policySetOutcome) List(ctx context.Context, policyEvaluationID string, options *PolicySetOutcomeListOptions) (*PolicySetOutcomeList, error) {
if !validStringID(&policyEvaluationID) {
return nil, ErrInvalidPolicyEvaluationID
}
additionalQueryParams := options.buildQueryString()
u := fmt.Sprintf("policy-evaluations/%s/policy-set-outcomes", url.QueryEscape(policyEvaluationID))
var opts *ListOptions
if options != nil && options.ListOptions != nil {
opts = options.ListOptions
}
req, err := s.client.NewRequestWithAdditionalQueryParams("GET", u, opts, additionalQueryParams)
if err != nil {
return nil, err
}
psol := &PolicySetOutcomeList{}
err = req.Do(ctx, psol)
if err != nil {
return nil, err
}
return psol, nil
}
// buildQueryString takes the PolicySetOutcomeListOptions and returns a filters map.
// This function is required due to the limitations of the current library,
// we cannot encode map of objects using the current library that is used by go-tfe: https://github.com/google/go-querystring/issues/7
func (opts *PolicySetOutcomeListOptions) buildQueryString() map[string][]string {
result := make(map[string][]string)
if opts == nil || opts.Filter == nil {
return nil
}
for k, v := range opts.Filter {
if v.Status != "" {
newKey := fmt.Sprintf("filter[%s][status]", k)
result[newKey] = append(result[newKey], v.Status)
}
if v.EnforcementLevel != "" {
newKey := fmt.Sprintf("filter[%s][enforcement_level]", k)
result[newKey] = append(result[newKey], v.EnforcementLevel)
}
}
return result
}
// Read reads a policy set outcome by its ID
func (s *policySetOutcome) Read(ctx context.Context, policySetOutcomeID string) (*PolicySetOutcome, error) {
if !validStringID(&policySetOutcomeID) {
return nil, ErrInvalidPolicySetOutcomeID
}
u := fmt.Sprintf("policy-set-outcomes/%s", url.PathEscape(policySetOutcomeID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
pso := &PolicySetOutcome{}
err = req.Do(ctx, pso)
if err != nil {
return nil, err
}
return pso, err
}
================================================
FILE: policy_evaluation_beta_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPolicyEvaluationList_Beta_RunDependent(t *testing.T) {
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest)
defer wkspaceTestCleanup()
options := PolicyCreateOptions{
Description: String("A sample policy"),
Kind: OPA,
Query: String("data.example.rule"),
Enforce: []*EnforcementOptions{
{
Mode: EnforcementMode(EnforcementAdvisory),
},
},
}
policyTest, policyTestCleanup := createUploadedPolicyWithOptions(t, client, true, orgTest, options)
defer policyTestCleanup()
policySet := []*Policy{policyTest}
_, psTestCleanup1 := createPolicySet(t, client, orgTest, policySet, []*Workspace{wkspaceTest}, nil, nil, OPA)
defer psTestCleanup1()
rTest, rTestCleanup := createRun(t, client, wkspaceTest)
defer rTestCleanup()
t.Run("with no params", func(t *testing.T) {
taskStageList, err := client.TaskStages.List(ctx, rTest.ID, nil)
require.NoError(t, err)
require.NotEmpty(t, taskStageList.Items)
assert.NotEmpty(t, taskStageList.Items[0].ID)
assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluations))
polEvaluation, err := client.PolicyEvaluations.List(ctx, taskStageList.Items[0].ID, nil)
require.NoError(t, err)
require.NotEmpty(t, polEvaluation.Items)
assert.NotEmpty(t, polEvaluation.Items[0].ID)
})
t.Run("with a invalid policy evaluation ID", func(t *testing.T) {
policyEvaluationeID := "invalid ID"
_, err := client.PolicyEvaluations.List(ctx, policyEvaluationeID, nil)
require.Errorf(t, err, "invalid value for policy evaluation ID")
})
}
func TestPolicySetOutcomeList_Beta_RunDependent(t *testing.T) {
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest)
defer wkspaceTestCleanup()
options := PolicyCreateOptions{
Description: String("A sample policy"),
Kind: OPA,
Query: String("data.example.rule"),
Enforce: []*EnforcementOptions{
{
Mode: EnforcementMode(EnforcementAdvisory),
},
},
}
policyTest, policyTestCleanup := createUploadedPolicyWithOptions(t, client, true, orgTest, options)
defer policyTestCleanup()
policySet := []*Policy{policyTest}
_, psTestCleanup1 := createPolicySet(t, client, orgTest, policySet, []*Workspace{wkspaceTest}, nil, nil, OPA)
defer psTestCleanup1()
rTest, rTestCleanup := createPlannedRun(t, client, wkspaceTest)
defer rTestCleanup()
t.Run("with no params", func(t *testing.T) {
taskStageList, err := client.TaskStages.List(ctx, rTest.ID, nil)
require.NoError(t, err)
require.NotEmpty(t, taskStageList.Items)
assert.NotEmpty(t, taskStageList.Items[0].ID)
assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluations))
assert.NotEmpty(t, 1, len(taskStageList.Items[0].PolicyEvaluations[0].ID))
polEvaluationID := taskStageList.Items[0].PolicyEvaluations[0].ID
polSetOutcomesList, err := client.PolicySetOutcomes.List(ctx, polEvaluationID, nil)
require.NoError(t, err)
require.NotEmpty(t, polSetOutcomesList.Items)
assert.NotEmpty(t, polSetOutcomesList.Items[0].ID)
assert.NotEmpty(t, polSetOutcomesList.Items[0].Outcomes)
assert.NotEmpty(t, polSetOutcomesList.Items[0].PolicySetName)
})
t.Run("with non-matching filters", func(t *testing.T) {
taskStageList, err := client.TaskStages.List(ctx, rTest.ID, nil)
require.NoError(t, err)
require.NotEmpty(t, taskStageList.Items)
assert.NotEmpty(t, taskStageList.Items[0].ID)
assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluations))
assert.NotEmpty(t, 1, len(taskStageList.Items[0].PolicyEvaluations[0].ID))
polEvaluationID := taskStageList.Items[0].PolicyEvaluations[0].ID
opts := &PolicySetOutcomeListOptions{
Filter: map[string]PolicySetOutcomeListFilter{
"0": {
Status: "errored",
},
"1": {
EnforcementLevel: "mandatory",
Status: "failed",
},
},
}
polSetOutcomesList, err := client.PolicySetOutcomes.List(ctx, polEvaluationID, opts)
require.NoError(t, err)
require.Empty(t, polSetOutcomesList.Items)
})
t.Run("with matching filters", func(t *testing.T) {
taskStageList, err := client.TaskStages.List(ctx, rTest.ID, nil)
require.NoError(t, err)
require.NotEmpty(t, taskStageList.Items)
assert.NotEmpty(t, taskStageList.Items[0].ID)
assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluations))
assert.NotEmpty(t, 1, len(taskStageList.Items[0].PolicyEvaluations[0].ID))
polEvaluationID := taskStageList.Items[0].PolicyEvaluations[0].ID
opts := &PolicySetOutcomeListOptions{
Filter: map[string]PolicySetOutcomeListFilter{
"0": {
Status: "passed",
EnforcementLevel: "advisory",
},
},
}
polSetOutcomesList, err := client.PolicySetOutcomes.List(ctx, polEvaluationID, opts)
require.NoError(t, err)
require.NotEmpty(t, polSetOutcomesList.Items)
assert.NotEmpty(t, polSetOutcomesList.Items[0].ID)
assert.Equal(t, 1, len(polSetOutcomesList.Items[0].Outcomes))
assert.NotEmpty(t, polSetOutcomesList.Items[0].PolicySetName)
})
}
func TestPolicySetOutcomeRead_Beta_RunDependent(t *testing.T) {
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest)
defer wkspaceTestCleanup()
options := PolicyCreateOptions{
Description: String("A sample policy"),
Kind: OPA,
Query: String("data.example.rule"),
Enforce: []*EnforcementOptions{
{
Mode: EnforcementMode(EnforcementAdvisory),
},
},
}
policyTest, policyTestCleanup := createUploadedPolicyWithOptions(t, client, true, orgTest, options)
defer policyTestCleanup()
policySet := []*Policy{policyTest}
_, psTestCleanup1 := createPolicySet(t, client, orgTest, policySet, []*Workspace{wkspaceTest}, nil, nil, OPA)
defer psTestCleanup1()
rTest, rTestCleanup := createPlannedRun(t, client, wkspaceTest)
defer rTestCleanup()
t.Run("with a valid policy set outcome ID", func(t *testing.T) {
taskStageList, err := client.TaskStages.List(ctx, rTest.ID, nil)
require.NoError(t, err)
require.NotEmpty(t, taskStageList.Items)
assert.NotEmpty(t, taskStageList.Items[0].ID)
assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluations))
assert.NotEmpty(t, 1, len(taskStageList.Items[0].PolicyEvaluations[0].ID))
polEvaluationID := taskStageList.Items[0].PolicyEvaluations[0].ID
polSetOutcomesList, err := client.PolicySetOutcomes.List(ctx, polEvaluationID, nil)
require.NoError(t, err)
require.NotEmpty(t, polSetOutcomesList.Items)
assert.NotEmpty(t, polSetOutcomesList.Items[0].ID)
assert.NotEmpty(t, polSetOutcomesList.Items[0].Outcomes)
assert.NotEmpty(t, polSetOutcomesList.Items[0].PolicySetName)
policySetOutcomeID := polSetOutcomesList.Items[0].ID
policyOutcome, err := client.PolicySetOutcomes.Read(ctx, policySetOutcomeID)
require.NoError(t, err)
assert.NotEmpty(t, policyOutcome.ID)
assert.NotEmpty(t, policyOutcome.Outcomes)
})
t.Run("with a invalid policy set outcome ID", func(t *testing.T) {
policySetOutcomeID := "invalid ID"
_, err := client.PolicySetOutcomes.Read(ctx, policySetOutcomeID)
require.Errorf(t, err, "invalid value for policy set outcome ID")
})
}
================================================
FILE: policy_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"encoding/json"
"testing"
"time"
retryablehttp "github.com/hashicorp/go-retryablehttp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPoliciesList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
pTest1, pTestCleanup1 := createPolicy(t, client, orgTest)
defer pTestCleanup1()
pTest2, pTestCleanup2 := createPolicy(t, client, orgTest)
defer pTestCleanup2()
opaOptions := PolicyCreateOptions{
Kind: OPA,
Query: String("data.example.rule"),
Enforce: []*EnforcementOptions{
{
Mode: EnforcementMode(EnforcementMandatory),
},
},
}
pTest3, pTestCleanup3 := createPolicyWithOptions(t, client, orgTest, opaOptions)
defer pTestCleanup3()
t.Run("without list options", func(t *testing.T) {
pl, err := client.Policies.List(ctx, orgTest.Name, nil)
require.NoError(t, err)
assert.Contains(t, pl.Items, pTest1)
assert.Contains(t, pl.Items, pTest2)
assert.Contains(t, pl.Items, pTest3)
assert.Equal(t, 1, pl.CurrentPage)
assert.Equal(t, 3, pl.TotalCount)
})
t.Run("with pagination", func(t *testing.T) {
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
pl, err := client.Policies.List(ctx, orgTest.Name, &PolicyListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, pl.Items)
assert.Equal(t, 999, pl.CurrentPage)
assert.Equal(t, 3, pl.TotalCount)
})
t.Run("with search", func(t *testing.T) {
// Search by one of the policy's names; we should get only that policy
// and pagination data should reflect the search as well
pl, err := client.Policies.List(ctx, orgTest.Name, &PolicyListOptions{
Search: pTest1.Name,
})
require.NoError(t, err)
assert.Contains(t, pl.Items, pTest1)
assert.NotContains(t, pl.Items, pTest2)
assert.NotContains(t, pl.Items, pTest3)
assert.Equal(t, 1, pl.CurrentPage)
assert.Equal(t, 1, pl.TotalCount)
})
t.Run("with filter by kind", func(t *testing.T) {
pl, err := client.Policies.List(ctx, orgTest.Name, &PolicyListOptions{
Kind: OPA,
})
require.NoError(t, err)
assert.Contains(t, pl.Items, pTest3)
assert.NotContains(t, pl.Items, pTest1)
assert.NotContains(t, pl.Items, pTest2)
assert.Equal(t, 1, pl.CurrentPage)
assert.Equal(t, 1, pl.TotalCount)
})
t.Run("without a valid organization", func(t *testing.T) {
ps, err := client.Policies.List(ctx, badIdentifier, nil)
assert.Nil(t, ps)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
func TestPoliciesCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
t.Run("with no kind", func(t *testing.T) {
name := randomString(t)
options := PolicyCreateOptions{
Name: String(name),
Description: String("A sample policy"),
Enforce: []*EnforcementOptions{
{
Path: String(name + ".sentinel"),
Mode: EnforcementMode(EnforcementSoft),
},
},
}
p, err := client.Policies.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
// Get a refreshed view from the API.
refreshed, err := client.Policies.Read(ctx, p.ID)
require.NoError(t, err)
for _, item := range []*Policy{
p,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, *options.Name, item.Name)
assert.Equal(t, Sentinel, item.Kind)
assert.Equal(t, *options.Description, item.Description)
}
})
t.Run("with valid options - Sentinel", func(t *testing.T) {
name := randomString(t)
options := PolicyCreateOptions{
Name: String(name),
Description: String("A sample policy"),
Kind: Sentinel,
Enforce: []*EnforcementOptions{
{
Path: String(name + ".sentinel"),
Mode: EnforcementMode(EnforcementSoft),
},
},
}
p, err := client.Policies.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
// Get a refreshed view from the API.
refreshed, err := client.Policies.Read(ctx, p.ID)
require.NoError(t, err)
for _, item := range []*Policy{
p,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, *options.Name, item.Name)
assert.Equal(t, options.Kind, item.Kind)
assert.Nil(t, options.Query)
assert.Equal(t, *options.Description, item.Description)
}
})
t.Run("with valid options - OPA", func(t *testing.T) {
name := randomString(t)
options := PolicyCreateOptions{
Name: String(name),
Description: String("A sample policy"),
Kind: OPA,
Query: String("terraform.main"),
Enforce: []*EnforcementOptions{
{
Path: String(name + ".rego"),
Mode: EnforcementMode(EnforcementMandatory),
},
},
}
p, err := client.Policies.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
// Get a refreshed view from the API.
refreshed, err := client.Policies.Read(ctx, p.ID)
require.NoError(t, err)
for _, item := range []*Policy{
p,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, *options.Name, item.Name)
assert.Equal(t, options.Kind, item.Kind)
assert.Equal(t, *options.Query, *item.Query)
assert.Equal(t, *options.Description, item.Description)
}
})
t.Run("with valid options - Enforcement Level", func(t *testing.T) {
name := randomString(t)
options := PolicyCreateOptions{
Name: String(name),
Description: String("A sample policy"),
Kind: Sentinel,
EnforcementLevel: EnforcementMode(EnforcementHard),
}
p, err := client.Policies.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
// Get a refreshed view from the API.
refreshed, err := client.Policies.Read(ctx, p.ID)
require.NoError(t, err)
for _, item := range []*Policy{
p,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, *options.Name, item.Name)
assert.Equal(t, options.Kind, item.Kind)
assert.Nil(t, options.Query)
assert.Equal(t, *options.Description, item.Description)
assert.Equal(t, *options.EnforcementLevel, item.EnforcementLevel)
}
})
t.Run("when options has an invalid name", func(t *testing.T) {
p, err := client.Policies.Create(ctx, orgTest.Name, PolicyCreateOptions{
Name: String(badIdentifier),
Enforce: []*EnforcementOptions{
{
Path: String(badIdentifier + ".sentinel"),
Mode: EnforcementMode(EnforcementSoft),
},
},
})
assert.Nil(t, p)
assert.EqualError(t, err, ErrInvalidName.Error())
})
t.Run("when options has an invalid name - OPA", func(t *testing.T) {
p, err := client.Policies.Create(ctx, orgTest.Name, PolicyCreateOptions{
Name: String(badIdentifier),
Kind: OPA,
Query: String("terraform.main"),
Enforce: []*EnforcementOptions{
{
Path: String(badIdentifier + ".rego"),
Mode: EnforcementMode(EnforcementAdvisory),
},
},
})
assert.Nil(t, p)
assert.EqualError(t, err, ErrInvalidName.Error())
})
t.Run("when options is missing name", func(t *testing.T) {
p, err := client.Policies.Create(ctx, orgTest.Name, PolicyCreateOptions{
Enforce: []*EnforcementOptions{
{
Path: String(randomString(t) + ".sentinel"),
Mode: EnforcementMode(EnforcementSoft),
},
},
})
assert.Nil(t, p)
assert.EqualError(t, err, ErrRequiredName.Error())
})
t.Run("when options is missing name - OPA", func(t *testing.T) {
p, err := client.Policies.Create(ctx, orgTest.Name, PolicyCreateOptions{
Kind: OPA,
Query: String("terraform.main"),
Enforce: []*EnforcementOptions{
{
Path: String(randomString(t) + ".rego"),
Mode: EnforcementMode(EnforcementSoft),
},
},
})
assert.Nil(t, p)
assert.EqualError(t, err, ErrRequiredName.Error())
})
t.Run("when options is missing query - OPA", func(t *testing.T) {
name := randomString(t)
p, err := client.Policies.Create(ctx, orgTest.Name, PolicyCreateOptions{
Name: String(name),
Kind: OPA,
Enforce: []*EnforcementOptions{
{
Path: String(randomString(t) + ".rego"),
Mode: EnforcementMode(EnforcementSoft),
},
},
})
assert.Nil(t, p)
assert.Equal(t, err, ErrRequiredQuery)
})
t.Run("when options is missing an enforcement-OPA", func(t *testing.T) {
options := PolicyCreateOptions{
Name: String(randomString(t)),
Kind: OPA,
Query: String("terraform.main"),
}
p, err := client.Policies.Create(ctx, orgTest.Name, options)
assert.Nil(t, p)
assert.Equal(t, err, ErrRequiredEnforce)
})
t.Run("when options is missing an enforcement-Sentinel", func(t *testing.T) {
options := PolicyCreateOptions{
Name: String(randomString(t)),
}
p, err := client.Policies.Create(ctx, orgTest.Name, options)
assert.Nil(t, p)
assert.Equal(t, err, ErrRequiredEnforce)
})
t.Run("when options is missing enforcement path-Sentinel", func(t *testing.T) {
options := PolicyCreateOptions{
Name: String(randomString(t)),
Enforce: []*EnforcementOptions{
{
Mode: EnforcementMode(EnforcementSoft),
},
},
}
p, err := client.Policies.Create(ctx, orgTest.Name, options)
assert.Nil(t, p)
assert.Equal(t, err, ErrRequiredEnforcementPath)
})
t.Run("when options is missing enforcement path-OPA", func(t *testing.T) {
options := PolicyCreateOptions{
Name: String(randomString(t)),
Kind: OPA,
Query: String("terraform.main"),
Enforce: []*EnforcementOptions{
{
Mode: EnforcementMode(EnforcementSoft),
},
},
}
p, err := client.Policies.Create(ctx, orgTest.Name, options)
assert.Nil(t, p)
assert.Equal(t, err, ErrRequiredEnforcementPath)
})
t.Run("when options is missing enforcement path", func(t *testing.T) {
name := randomString(t)
options := PolicyCreateOptions{
Name: String(name),
Enforce: []*EnforcementOptions{
{
Path: String(name + ".sentinel"),
},
},
}
p, err := client.Policies.Create(ctx, orgTest.Name, options)
assert.Nil(t, p)
assert.Equal(t, err, ErrRequiredEnforcementMode)
})
t.Run("when options is missing enforcement mode-OPA", func(t *testing.T) {
name := randomString(t)
options := PolicyCreateOptions{
Name: String(name),
Kind: OPA,
Query: String("terraform.main"),
Enforce: []*EnforcementOptions{
{
Path: String(name + ".sentinel"),
},
},
}
p, err := client.Policies.Create(ctx, orgTest.Name, options)
assert.Nil(t, p)
assert.Equal(t, err, ErrRequiredEnforcementMode)
})
t.Run("when options is have both enforcement level and enforcement path", func(t *testing.T) {
name := randomString(t)
options := PolicyCreateOptions{
Name: String(name),
Enforce: []*EnforcementOptions{
{
Path: String(randomString(t) + ".sentinel"),
Mode: EnforcementMode(EnforcementSoft),
},
},
EnforcementLevel: EnforcementMode(EnforcementMandatory),
}
p, err := client.Policies.Create(ctx, orgTest.Name, options)
assert.Nil(t, p)
assert.Equal(t, err, ErrConflictingEnforceEnforcementLevel)
})
t.Run("when options has an invalid organization", func(t *testing.T) {
p, err := client.Policies.Create(ctx, badIdentifier, PolicyCreateOptions{
Name: String("foo"),
})
assert.Nil(t, p)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
func TestPoliciesRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
pTest, pTestCleanup := createPolicy(t, client, orgTest)
defer pTestCleanup()
t.Run("when the policy exists without content", func(t *testing.T) {
p, err := client.Policies.Read(ctx, pTest.ID)
require.NoError(t, err)
assert.Equal(t, pTest.ID, p.ID)
assert.Equal(t, pTest.Name, p.Name)
assert.Equal(t, pTest.PolicySetCount, p.PolicySetCount)
assert.Empty(t, p.Enforce)
assert.Equal(t, p.EnforcementLevel, pTest.EnforcementLevel)
assert.Equal(t, pTest.Organization.Name, p.Organization.Name)
})
err := client.Policies.Upload(ctx, pTest.ID, []byte(`main = rule { true }`))
require.NoError(t, err)
t.Run("when the policy exists with content", func(t *testing.T) {
p, err := client.Policies.Read(ctx, pTest.ID)
require.NoError(t, err)
assert.Equal(t, pTest.ID, p.ID)
assert.Equal(t, pTest.Name, p.Name)
assert.Equal(t, pTest.Description, p.Description)
assert.Equal(t, pTest.PolicySetCount, p.PolicySetCount)
assert.NotEmpty(t, p.Enforce)
assert.NotEmpty(t, p.Enforce[0].Path)
assert.NotEmpty(t, p.Enforce[0].Mode)
assert.Equal(t, p.EnforcementLevel, pTest.EnforcementLevel)
assert.Equal(t, pTest.Organization.Name, p.Organization.Name)
})
t.Run("when the policy does not exist", func(t *testing.T) {
p, err := client.Policies.Read(ctx, "nonexisting")
assert.Nil(t, p)
assert.Equal(t, ErrResourceNotFound, err)
})
t.Run("without a valid policy ID", func(t *testing.T) {
p, err := client.Policies.Read(ctx, badIdentifier)
assert.Nil(t, p)
assert.Equal(t, err, ErrInvalidPolicyID)
})
}
func TestPoliciesUpdate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
t.Run("when updating with an existing path", func(t *testing.T) {
pBefore, pBeforeCleanup := createUploadedPolicy(t, client, true, orgTest)
defer pBeforeCleanup()
require.Equal(t, 1, len(pBefore.Enforce))
pAfter, err := client.Policies.Update(ctx, pBefore.ID, PolicyUpdateOptions{
Enforce: []*EnforcementOptions{
{
Path: String(pBefore.Enforce[0].Path),
Mode: EnforcementMode(EnforcementAdvisory),
},
},
})
require.NoError(t, err)
require.Equal(t, 1, len(pAfter.Enforce))
assert.Equal(t, pBefore.ID, pAfter.ID)
assert.Equal(t, pBefore.Name, pAfter.Name)
assert.Equal(t, pBefore.Description, pAfter.Description)
assert.Equal(t, pBefore.Enforce[0].Path, pAfter.Enforce[0].Path)
assert.Equal(t, EnforcementAdvisory, pAfter.Enforce[0].Mode)
})
t.Run("when updating with a nonexisting path", func(t *testing.T) {
// Weirdly enough pAfter is not equal to pBefore as updating
// a nonexisting path causes the enforce mode to reset to the default
// hard-mandatory
t.Skip("see comment...")
pBefore, pBeforeCleanup := createUploadedPolicy(t, client, true, orgTest)
defer pBeforeCleanup()
require.Equal(t, 1, len(pBefore.Enforce))
pathBefore := pBefore.Enforce[0].Path
modeBefore := pBefore.Enforce[0].Mode
pAfter, err := client.Policies.Update(ctx, pBefore.ID, PolicyUpdateOptions{
Enforce: []*EnforcementOptions{
{
Path: String("nonexisting"),
Mode: EnforcementMode(EnforcementAdvisory),
},
},
})
require.NoError(t, err)
require.Equal(t, 1, len(pAfter.Enforce))
assert.Equal(t, pBefore, pAfter)
assert.Equal(t, pathBefore, pAfter.Enforce[0].Path)
assert.Equal(t, modeBefore, pAfter.Enforce[0].Mode)
})
t.Run("with a new description", func(t *testing.T) {
pBefore, pBeforeCleanup := createUploadedPolicy(t, client, true, orgTest)
defer pBeforeCleanup()
pAfter, err := client.Policies.Update(ctx, pBefore.ID, PolicyUpdateOptions{
Description: String("A brand new description"),
})
require.NoError(t, err)
assert.Equal(t, pBefore.Name, pAfter.Name)
assert.Equal(t, pBefore.Enforce, pAfter.Enforce)
assert.NotEqual(t, pBefore.Description, pAfter.Description)
assert.Equal(t, "A brand new description", pAfter.Description)
})
t.Run("with a new query", func(t *testing.T) {
options := PolicyCreateOptions{
Description: String("A sample OPA policy"),
Kind: OPA,
Query: String("data.example.rule"),
Enforce: []*EnforcementOptions{
{
Mode: EnforcementMode(EnforcementMandatory),
},
},
}
pBefore, pBeforeCleanup := createUploadedPolicyWithOptions(t, client, true, orgTest, options)
defer pBeforeCleanup()
pAfter, err := client.Policies.Update(ctx, pBefore.ID, PolicyUpdateOptions{
Query: String("terraform.policy1.deny"),
})
require.NoError(t, err)
assert.Equal(t, pBefore.Name, pAfter.Name)
assert.Equal(t, pBefore.Enforce, pAfter.Enforce)
assert.NotEqual(t, *pBefore.Query, *pAfter.Query)
assert.Equal(t, "terraform.policy1.deny", *pAfter.Query)
})
t.Run("with a new enforcement level", func(t *testing.T) {
options := PolicyCreateOptions{
Description: String("A sample OPA policy"),
Kind: OPA,
Query: String("data.example.rule"),
EnforcementLevel: EnforcementMode(EnforcementMandatory),
}
pBefore, pBeforeCleanup := createUploadedPolicyWithOptions(t, client, true, orgTest, options)
defer pBeforeCleanup()
pAfter, err := client.Policies.Update(ctx, pBefore.ID, PolicyUpdateOptions{
EnforcementLevel: EnforcementMode(EnforcementAdvisory),
})
require.NoError(t, err)
assert.Equal(t, pBefore.Name, pAfter.Name)
assert.Equal(t, pBefore.EnforcementLevel, EnforcementMandatory)
assert.Equal(t, pAfter.EnforcementLevel, EnforcementAdvisory)
})
t.Run("update query when kind is not OPA", func(t *testing.T) {
pBefore, pBeforeCleanup := createUploadedPolicy(t, client, true, orgTest)
defer pBeforeCleanup()
pAfter, err := client.Policies.Update(ctx, pBefore.ID, PolicyUpdateOptions{
Query: String("terraform.policy1.deny"),
})
require.NoError(t, err)
assert.Equal(t, pBefore.Name, pAfter.Name)
assert.Equal(t, pBefore.Enforce, pAfter.Enforce)
assert.Equal(t, Sentinel, pAfter.Kind)
assert.Nil(t, pAfter.Query)
})
t.Run("without a valid policy ID", func(t *testing.T) {
p, err := client.Policies.Update(ctx, badIdentifier, PolicyUpdateOptions{})
assert.Nil(t, p)
assert.Equal(t, err, ErrInvalidPolicyID)
})
}
func TestPoliciesDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
pTest, _ := createPolicy(t, client, orgTest)
t.Run("with valid options", func(t *testing.T) {
err := client.Policies.Delete(ctx, pTest.ID)
require.NoError(t, err)
// Try loading the policy - it should fail.
_, err = client.Policies.Read(ctx, pTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the policy does not exist", func(t *testing.T) {
err := client.Policies.Delete(ctx, pTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the policy ID is invalid", func(t *testing.T) {
err := client.Policies.Delete(ctx, badIdentifier)
assert.Equal(t, err, ErrInvalidPolicyID)
})
}
func TestPoliciesUpload(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
pTest, pTestCleanup := createPolicy(t, client, nil)
defer pTestCleanup()
t.Run("with valid options", func(t *testing.T) {
err := client.Policies.Upload(ctx, pTest.ID, []byte(`main = rule { true }`))
require.NoError(t, err)
})
t.Run("with empty content", func(t *testing.T) {
err := client.Policies.Upload(ctx, pTest.ID, []byte{})
require.NoError(t, err)
})
t.Run("without any content", func(t *testing.T) {
err := client.Policies.Upload(ctx, pTest.ID, nil)
require.NoError(t, err)
})
t.Run("without a valid policy ID", func(t *testing.T) {
err := client.Policies.Upload(ctx, badIdentifier, []byte(`main = rule { true }`))
assert.Equal(t, err, ErrInvalidPolicyID)
})
}
func TestPoliciesDownload(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
pTest, pTestCleanup := createPolicy(t, client, nil)
defer pTestCleanup()
testContent := []byte(`main = rule { true }`)
t.Run("without existing content", func(t *testing.T) {
content, err := client.Policies.Download(ctx, pTest.ID)
assert.Equal(t, ErrResourceNotFound, err)
assert.Nil(t, content)
})
t.Run("with valid options", func(t *testing.T) {
err := client.Policies.Upload(ctx, pTest.ID, testContent)
require.NoError(t, err)
content, err := client.Policies.Download(ctx, pTest.ID)
require.NoError(t, err)
assert.Equal(t, testContent, content)
})
t.Run("without a valid policy ID", func(t *testing.T) {
content, err := client.Policies.Download(ctx, badIdentifier)
assert.Equal(t, err, ErrInvalidPolicyID)
assert.Nil(t, content)
})
}
func TestPolicy_Unmarshal(t *testing.T) {
t.Parallel()
data := map[string]interface{}{
"data": map[string]interface{}{
"type": "policies",
"id": "policy-ntv3HbhJqvFzamy7",
"attributes": map[string]interface{}{
"name": "general",
"description": "general policy",
"enforce": []interface{}{
map[string]interface{}{
"path": "some/path",
"mode": string(EnforcementAdvisory),
},
},
"updated-at": "2018-03-02T23:42:06.651Z",
"policy-set-count": 1,
},
},
}
byteData, err := json.Marshal(data)
require.NoError(t, err)
responseBody := bytes.NewReader(byteData)
policy := &Policy{}
err = unmarshalResponse(responseBody, policy)
require.NoError(t, err)
iso8601TimeFormat := "2006-01-02T15:04:05Z"
parsedTime, err := time.Parse(iso8601TimeFormat, "2018-03-02T23:42:06.651Z")
require.NoError(t, err)
assert.Equal(t, policy.ID, "policy-ntv3HbhJqvFzamy7")
assert.Equal(t, policy.Name, "general")
assert.Equal(t, policy.Description, "general policy")
assert.Equal(t, policy.PolicySetCount, 1)
assert.Equal(t, policy.Enforce[0].Path, "some/path")
assert.Equal(t, policy.Enforce[0].Mode, EnforcementAdvisory)
assert.Equal(t, policy.UpdatedAt, parsedTime)
}
func TestPolicyCreateOptions_Marshal(t *testing.T) {
t.Parallel()
opts := PolicyCreateOptions{
Name: String("my-policy"),
Description: String("details"),
Enforce: []*EnforcementOptions{
{
Path: String("/foo"),
Mode: EnforcementMode(EnforcementSoft),
},
{
Path: String("/bar"),
Mode: EnforcementMode(EnforcementSoft),
},
},
}
reqBody, err := serializeRequestBody(&opts)
require.NoError(t, err)
req, err := retryablehttp.NewRequest("POST", "url", reqBody)
require.NoError(t, err)
bodyBytes, err := req.BodyBytes()
require.NoError(t, err)
expectedBody := `{"data":{"type":"policies","attributes":{"description":"details","enforce":[{"path":"/foo","mode":"soft-mandatory"},{"path":"/bar","mode":"soft-mandatory"}],"name":"my-policy"}}}
`
assert.Equal(t, expectedBody, string(bodyBytes))
}
func TestPolicyUpdateOptions_Marshal(t *testing.T) {
t.Parallel()
opts := PolicyUpdateOptions{
Description: String("details"),
Enforce: []*EnforcementOptions{
{
Path: String("/foo"),
Mode: EnforcementMode(EnforcementSoft),
},
{
Path: String("/bar"),
Mode: EnforcementMode(EnforcementSoft),
},
},
}
reqBody, err := serializeRequestBody(&opts)
require.NoError(t, err)
req, err := retryablehttp.NewRequest("POST", "url", reqBody)
require.NoError(t, err)
bodyBytes, err := req.BodyBytes()
require.NoError(t, err)
expectedBody := `{"data":{"type":"policies","attributes":{"description":"details","enforce":[{"path":"/foo","mode":"soft-mandatory"},{"path":"/bar","mode":"soft-mandatory"}]}}}
`
assert.Equal(t, expectedBody, string(bodyBytes))
}
================================================
FILE: policy_set.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ PolicySets = (*policySets)(nil)
// PolicyKind is an indicator of the underlying technology that the policy or policy set supports.
// There are two kinds documented in the enum.
type PolicyKind string
const (
OPA PolicyKind = "opa"
Sentinel PolicyKind = "sentinel"
)
// PolicySets describes all the policy set related methods that the Terraform
// Enterprise API supports.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/policy-sets
type PolicySets interface {
// List all the policy sets for a given organization.
List(ctx context.Context, organization string, options *PolicySetListOptions) (*PolicySetList, error)
// Create a policy set and associate it with an organization.
Create(ctx context.Context, organization string, options PolicySetCreateOptions) (*PolicySet, error)
// Read a policy set by its ID.
Read(ctx context.Context, policySetID string) (*PolicySet, error)
// ReadWithOptions reads a policy set by its ID using the options supplied.
ReadWithOptions(ctx context.Context, policySetID string, options *PolicySetReadOptions) (*PolicySet, error)
// Update an existing policy set.
Update(ctx context.Context, policySetID string, options PolicySetUpdateOptions) (*PolicySet, error)
// Add policies to a policy set. This function can only be used when
// there is no VCS repository associated with the policy set.
AddPolicies(ctx context.Context, policySetID string, options PolicySetAddPoliciesOptions) error
// Remove policies from a policy set. This function can only be used
// when there is no VCS repository associated with the policy set.
RemovePolicies(ctx context.Context, policySetID string, options PolicySetRemovePoliciesOptions) error
// Add workspaces to a policy set.
AddWorkspaces(ctx context.Context, policySetID string, options PolicySetAddWorkspacesOptions) error
// Remove workspaces from a policy set.
RemoveWorkspaces(ctx context.Context, policySetID string, options PolicySetRemoveWorkspacesOptions) error
// Add workspace exclusions to a policy set.
AddWorkspaceExclusions(ctx context.Context, policySetID string, options PolicySetAddWorkspaceExclusionsOptions) error
// Remove workspace exclusions from a policy set.
RemoveWorkspaceExclusions(ctx context.Context, policySetID string, options PolicySetRemoveWorkspaceExclusionsOptions) error
// Add projects to a policy set.
AddProjects(ctx context.Context, policySetID string, options PolicySetAddProjectsOptions) error
// Remove projects from a policy set.
RemoveProjects(ctx context.Context, policySetID string, options PolicySetRemoveProjectsOptions) error
// Add Project exclusions to a policy set.
AddProjectExclusions(ctx context.Context, policySetID string, options PolicySetAddProjectExclusionsOptions) error
// Remove project exclusions from a policy set.
RemoveProjectExclusions(ctx context.Context, policySetID string, options PolicySetRemoveProjectExclusionsOptions) error
// Delete a policy set by its ID.
Delete(ctx context.Context, policyID string) error
}
// policySets implements PolicySets.
type policySets struct {
client *Client
}
// PolicySetList represents a list of policy sets.
type PolicySetList struct {
*Pagination
Items []*PolicySet
}
// PolicySet represents a Terraform Enterprise policy set.
type PolicySet struct {
ID string `jsonapi:"primary,policy-sets"`
Name string `jsonapi:"attr,name"`
Description string `jsonapi:"attr,description"`
Kind PolicyKind `jsonapi:"attr,kind"`
Overridable *bool `jsonapi:"attr,overridable"`
Global bool `jsonapi:"attr,global"`
PoliciesPath string `jsonapi:"attr,policies-path"`
// **Note: This field is still in BETA and subject to change.**
PolicyCount int `jsonapi:"attr,policy-count"`
VCSRepo *VCSRepo `jsonapi:"attr,vcs-repo"`
WorkspaceCount int `jsonapi:"attr,workspace-count"`
ProjectCount int `jsonapi:"attr,project-count"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
AgentEnabled bool `jsonapi:"attr,agent-enabled"`
PolicyToolVersion string `jsonapi:"attr,policy-tool-version"`
PolicyUpdatePatterns []string `jsonapi:"attr,policy-update-patterns"`
// Relations
// The organization to which the policy set belongs to.
Organization *Organization `jsonapi:"relation,organization"`
// The workspaces to which the policy set applies.
Workspaces []*Workspace `jsonapi:"relation,workspaces"`
// Individually managed policies which are associated with the policy set.
Policies []*Policy `jsonapi:"relation,policies"`
// The most recently created policy set version, regardless of status.
// Note that this relationship may include an errored and unusable version,
// and is intended to allow checking for errors.
NewestVersion *PolicySetVersion `jsonapi:"relation,newest-version"`
// The most recent successful policy set version.
CurrentVersion *PolicySetVersion `jsonapi:"relation,current-version"`
// The workspace exclusions to which the policy set applies.
WorkspaceExclusions []*Workspace `jsonapi:"relation,workspace-exclusions"`
// The projects to which the policy set applies.
Projects []*Project `jsonapi:"relation,projects"`
// The project exclusions to which the policy set applies.
ProjectExclusions []*Project `jsonapi:"relation,project-exclusions"`
}
// PolicySetIncludeOpt represents the available options for include query params.
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/policy-sets#available-related-resources
type PolicySetIncludeOpt string
const (
PolicySetPolicies PolicySetIncludeOpt = "policies"
PolicySetWorkspaces PolicySetIncludeOpt = "workspaces"
PolicySetCurrentVersion PolicySetIncludeOpt = "current_version"
PolicySetNewestVersion PolicySetIncludeOpt = "newest_version"
PolicySetProjects PolicySetIncludeOpt = "projects"
PolicySetWorkspaceExclusions PolicySetIncludeOpt = "workspace_exclusions"
PolicySetProjectExclusions PolicySetIncludeOpt = "project_exclusions"
)
// PolicySetListOptions represents the options for listing policy sets.
type PolicySetListOptions struct {
ListOptions
// Optional: A search string (partial policy set name) used to filter the results.
Search string `url:"search[name],omitempty"`
// Optional: A kind string used to filter the results by the policy set kind.
Kind PolicyKind `url:"filter[kind],omitempty"`
// Optional: A list of relations to include. See available resources
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/policy-sets#available-related-resources
Include []PolicySetIncludeOpt `url:"include,omitempty"`
}
// PolicySetReadOptions are read options.
// For a full list of relations, please see:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/policy-sets#relationships
type PolicySetReadOptions struct {
// Optional: A list of relations to include. See available resources
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/policy-sets#available-related-resources
Include []PolicySetIncludeOpt `url:"include,omitempty"`
}
// PolicySetCreateOptions represents the options for creating a new policy set.
type PolicySetCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,policy-sets"`
// Required: The name of the policy set.
Name *string `jsonapi:"attr,name"`
// Optional: The description of the policy set.
Description *string `jsonapi:"attr,description,omitempty"`
// Optional: Whether or not the policy set is global.
Global *bool `jsonapi:"attr,global,omitempty"`
// Optional: The underlying technology that the policy set supports
Kind PolicyKind `jsonapi:"attr,kind,omitempty"`
// Optional: Whether or not users can override this policy when it fails during a run. Only valid for policy evaluations.
// https://developer.hashicorp.com/terraform/cloud-docs/policy-enforcement/manage-policy-sets#policy-checks-versus-policy-evaluations
Overridable *bool `jsonapi:"attr,overridable,omitempty"`
// Optional: Whether or not the policy is run as an evaluation inside the agent.
AgentEnabled *bool `jsonapi:"attr,agent-enabled,omitempty"`
// Optional: The policy tool version to run the evaluation against.
PolicyToolVersion *string `jsonapi:"attr,policy-tool-version,omitempty"`
// Optional: A list of glob patterns that trigger policy set updates.
PolicyUpdatePatterns []string `jsonapi:"attr,policy-update-patterns,omitempty"`
// Optional: The sub-path within the attached VCS repository to ingress. All
// files and directories outside of this sub-path will be ignored.
// This option may only be specified when a VCS repo is present.
PoliciesPath *string `jsonapi:"attr,policies-path,omitempty"`
// Optional: The initial members of the policy set.
Policies []*Policy `jsonapi:"relation,policies,omitempty"`
// Optional: VCS repository information. When present, the policies and
// configuration will be sourced from the specified VCS repository
// instead of being defined within the policy set itself. Note that
// this option is mutually exclusive with the Policies option and
// both cannot be used at the same time.
VCSRepo *VCSRepoOptions `jsonapi:"attr,vcs-repo,omitempty"`
// Optional: The initial list of workspaces for which the policy set should be enforced.
Workspaces []*Workspace `jsonapi:"relation,workspaces,omitempty"`
// Optional: The initial list of workspace exclusions for which the policy set should be enforced.
WorkspaceExclusions []*Workspace `jsonapi:"relation,workspace-exclusions,omitempty"`
// Optional: The initial list of projects for which the policy set should be enforced.
Projects []*Project `jsonapi:"relation,projects,omitempty"`
// Optional: The initial list of project exclusions for which the policy set should be enforced.
ProjectExclusions []*Project `jsonapi:"relation,project-exclusions,omitempty"`
}
// PolicySetUpdateOptions represents the options for updating a policy set.
type PolicySetUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,policy-sets"`
// Optional: The name of the policy set.
Name *string `jsonapi:"attr,name,omitempty"`
// Optional: The description of the policy set.
Description *string `jsonapi:"attr,description,omitempty"`
// Optional: Whether or not the policy set is global.
Global *bool `jsonapi:"attr,global,omitempty"`
// Optional: Whether or not users can override this policy when it fails during a run. Only valid for policy evaluations.
// https://developer.hashicorp.com/terraform/cloud-docs/policy-enforcement/manage-policy-sets#policy-checks-versus-policy-evaluations
Overridable *bool `jsonapi:"attr,overridable,omitempty"`
// Optional: Whether or not the policy is run as an evaluation inside the agent.
AgentEnabled *bool `jsonapi:"attr,agent-enabled,omitempty"`
// Optional: The policy tool version to run the evaluation against.
PolicyToolVersion *string `jsonapi:"attr,policy-tool-version,omitempty"`
// Optional: A list of glob patterns that trigger policy set updates.
PolicyUpdatePatterns []string `jsonapi:"attr,policy-update-patterns,omitempty"`
// Optional: The sub-path within the attached VCS repository to ingress. All
// files and directories outside of this sub-path will be ignored.
// This option may only be specified when a VCS repo is present.
PoliciesPath *string `jsonapi:"attr,policies-path,omitempty"`
// Optional: VCS repository information. When present, the policies and
// configuration will be sourced from the specified VCS repository
// instead of being defined within the policy set itself. Note that
// specifying this option may only be used on policy sets with no
// directly-attached policies (*PolicySet.Policies). Specifying this
// option when policies are already present will result in an error.
VCSRepo *VCSRepoOptions `jsonapi:"attr,vcs-repo,omitempty"`
}
// PolicySetAddPoliciesOptions represents the options for adding policies
// to a policy set.
type PolicySetAddPoliciesOptions struct {
// The policies to add to the policy set.
Policies []*Policy
}
// PolicySetRemovePoliciesOptions represents the options for removing
// policies from a policy set.
type PolicySetRemovePoliciesOptions struct {
// The policies to remove from the policy set.
Policies []*Policy
}
// PolicySetAddWorkspacesOptions represents the options for adding workspaces
// to a policy set.
type PolicySetAddWorkspacesOptions struct {
// The workspaces to add to the policy set.
Workspaces []*Workspace
}
// PolicySetRemoveWorkspacesOptions represents the options for removing
// workspaces from a policy set.
type PolicySetRemoveWorkspacesOptions struct {
// The workspaces to remove from the policy set.
Workspaces []*Workspace
}
// PolicySetAddWorkspaceExclusionsOptions represents the options for adding workspace exclusions to a policy set.
type PolicySetAddWorkspaceExclusionsOptions struct {
// The workspaces to add to the policy set exclusion list.
WorkspaceExclusions []*Workspace
}
// PolicySetRemoveWorkspaceExclusionsOptions represents the options for removing workspace exclusions from a policy set.
type PolicySetRemoveWorkspaceExclusionsOptions struct {
// The workspaces to remove from the policy set exclusion list.
WorkspaceExclusions []*Workspace
}
// PolicySetAddProjectExclusionsOptions represents the options for adding project exclusions to a policy set.
type PolicySetAddProjectExclusionsOptions struct {
// The projects to add to the policy set exclusion list.
ProjectExclusions []*Project
}
// PolicySetRemoveProjectExclusionsOptions represents the options for removing project exclusions from a policy set.
type PolicySetRemoveProjectExclusionsOptions struct {
// The projects to remove from the policy set exclusion list.
ProjectExclusions []*Project
}
// PolicySetAddProjectsOptions represents the options for adding projects
// to a policy set.
type PolicySetAddProjectsOptions struct {
// The projects to add to the policy set.
Projects []*Project
}
// PolicySetRemoveProjectsOptions represents the options for removing
// projects from a policy set.
type PolicySetRemoveProjectsOptions struct {
// The projects to remove from the policy set.
Projects []*Project
}
// List all the policies for a given organization.
func (s *policySets) List(ctx context.Context, organization string, options *PolicySetListOptions) (*PolicySetList, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf("organizations/%s/policy-sets", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
psl := &PolicySetList{}
err = req.Do(ctx, psl)
if err != nil {
return nil, err
}
return psl, nil
}
// Create a policy set and associate it with an organization.
func (s *policySets) Create(ctx context.Context, organization string, options PolicySetCreateOptions) (*PolicySet, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("organizations/%s/policy-sets", url.PathEscape(organization))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
ps := &PolicySet{}
err = req.Do(ctx, ps)
if err != nil {
return nil, err
}
return ps, err
}
// Read a policy set by its ID.
func (s *policySets) Read(ctx context.Context, policySetID string) (*PolicySet, error) {
return s.ReadWithOptions(ctx, policySetID, nil)
}
// ReadWithOptions reads a policy by its ID using the options supplied.
func (s *policySets) ReadWithOptions(ctx context.Context, policySetID string, options *PolicySetReadOptions) (*PolicySet, error) {
if !validStringID(&policySetID) {
return nil, ErrInvalidPolicySetID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("policy-sets/%s", url.PathEscape(policySetID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
ps := &PolicySet{}
err = req.Do(ctx, ps)
if err != nil {
return nil, err
}
return ps, err
}
// Update an existing policy set.
func (s *policySets) Update(ctx context.Context, policySetID string, options PolicySetUpdateOptions) (*PolicySet, error) {
if !validStringID(&policySetID) {
return nil, ErrInvalidPolicySetID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("policy-sets/%s", url.PathEscape(policySetID))
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
ps := &PolicySet{}
err = req.Do(ctx, ps)
if err != nil {
return nil, err
}
return ps, err
}
// AddPolicies adds policies to a policy set
func (s *policySets) AddPolicies(ctx context.Context, policySetID string, options PolicySetAddPoliciesOptions) error {
if !validStringID(&policySetID) {
return ErrInvalidPolicySetID
}
if err := options.valid(); err != nil {
return err
}
u := fmt.Sprintf("policy-sets/%s/relationships/policies", url.PathEscape(policySetID))
req, err := s.client.NewRequest("POST", u, options.Policies)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// RemovePolicies remove policies from a policy set
func (s *policySets) RemovePolicies(ctx context.Context, policySetID string, options PolicySetRemovePoliciesOptions) error {
if !validStringID(&policySetID) {
return ErrInvalidPolicySetID
}
if err := options.valid(); err != nil {
return err
}
u := fmt.Sprintf("policy-sets/%s/relationships/policies", url.PathEscape(policySetID))
req, err := s.client.NewRequest("DELETE", u, options.Policies)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// Addworkspaces adds workspaces to a policy set.
func (s *policySets) AddWorkspaces(ctx context.Context, policySetID string, options PolicySetAddWorkspacesOptions) error {
if !validStringID(&policySetID) {
return ErrInvalidPolicySetID
}
if err := options.valid(); err != nil {
return err
}
u := fmt.Sprintf("policy-sets/%s/relationships/workspaces", url.PathEscape(policySetID))
req, err := s.client.NewRequest("POST", u, options.Workspaces)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// RemoveWorkspaces removes workspaces from a policy set.
func (s *policySets) RemoveWorkspaces(ctx context.Context, policySetID string, options PolicySetRemoveWorkspacesOptions) error {
if !validStringID(&policySetID) {
return ErrInvalidPolicySetID
}
if err := options.valid(); err != nil {
return err
}
u := fmt.Sprintf("policy-sets/%s/relationships/workspaces", url.PathEscape(policySetID))
req, err := s.client.NewRequest("DELETE", u, options.Workspaces)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// AddWorkspaceExclusions adds workspace exclusions to a policy set.
func (s *policySets) AddWorkspaceExclusions(ctx context.Context, policySetID string, options PolicySetAddWorkspaceExclusionsOptions) error {
if !validStringID(&policySetID) {
return ErrInvalidPolicySetID
}
if err := options.valid(); err != nil {
return err
}
u := fmt.Sprintf("policy-sets/%s/relationships/workspace-exclusions", url.PathEscape(policySetID))
req, err := s.client.NewRequest("POST", u, options.WorkspaceExclusions)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// RemoveWorkspaceExclusions removes workspace exclusions from a policy set.
func (s *policySets) RemoveWorkspaceExclusions(ctx context.Context, policySetID string, options PolicySetRemoveWorkspaceExclusionsOptions) error {
if !validStringID(&policySetID) {
return ErrInvalidPolicySetID
}
if err := options.valid(); err != nil {
return err
}
u := fmt.Sprintf("policy-sets/%s/relationships/workspace-exclusions", url.PathEscape(policySetID))
req, err := s.client.NewRequest("DELETE", u, options.WorkspaceExclusions)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// AddProjects adds projects to a given policy set.
func (s *policySets) AddProjects(ctx context.Context, policySetID string, options PolicySetAddProjectsOptions) error {
if !validStringID(&policySetID) {
return ErrInvalidPolicySetID
}
if err := options.valid(); err != nil {
return err
}
u := fmt.Sprintf("policy-sets/%s/relationships/projects", url.PathEscape(policySetID))
req, err := s.client.NewRequest("POST", u, options.Projects)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// RemoveProjects removes projects from a policy set.
func (s *policySets) RemoveProjects(ctx context.Context, policySetID string, options PolicySetRemoveProjectsOptions) error {
if !validStringID(&policySetID) {
return ErrInvalidPolicySetID
}
if err := options.valid(); err != nil {
return err
}
u := fmt.Sprintf("policy-sets/%s/relationships/projects", url.PathEscape(policySetID))
req, err := s.client.NewRequest("DELETE", u, options.Projects)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// AddProjectExclusions adds project exclusions to a given policy set.
func (s *policySets) AddProjectExclusions(ctx context.Context, policySetID string, options PolicySetAddProjectExclusionsOptions) error {
if !validStringID(&policySetID) {
return ErrInvalidPolicySetID
}
if err := options.valid(); err != nil {
return err
}
u := fmt.Sprintf("policy-sets/%s/relationships/project-exclusions", url.PathEscape(policySetID))
req, err := s.client.NewRequest("POST", u, options.ProjectExclusions)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// RemoveProjectExclusions removes project exclusions to a given policy set.
func (s *policySets) RemoveProjectExclusions(ctx context.Context, policySetID string, options PolicySetRemoveProjectExclusionsOptions) error {
if !validStringID(&policySetID) {
return ErrInvalidPolicySetID
}
if err := options.valid(); err != nil {
return err
}
u := fmt.Sprintf("policy-sets/%s/relationships/project-exclusions", url.PathEscape(policySetID))
req, err := s.client.NewRequest("DELETE", u, options.ProjectExclusions)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// Delete a policy set by its ID.
func (s *policySets) Delete(ctx context.Context, policySetID string) error {
if !validStringID(&policySetID) {
return ErrInvalidPolicySetID
}
u := fmt.Sprintf("policy-sets/%s", url.PathEscape(policySetID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o PolicySetCreateOptions) valid() error {
if !validString(o.Name) {
return ErrRequiredName
}
if !validStringID(o.Name) {
return ErrInvalidName
}
return nil
}
func (o PolicySetRemoveWorkspacesOptions) valid() error {
if o.Workspaces == nil {
return ErrWorkspacesRequired
}
if len(o.Workspaces) == 0 {
return ErrWorkspaceMinLimit
}
return nil
}
func (o PolicySetRemoveWorkspaceExclusionsOptions) valid() error {
if o.WorkspaceExclusions == nil {
return ErrWorkspacesRequired
}
if len(o.WorkspaceExclusions) == 0 {
return ErrWorkspaceMinLimit
}
return nil
}
func (o PolicySetRemoveProjectsOptions) valid() error {
if o.Projects == nil {
return ErrRequiredProject
}
if len(o.Projects) == 0 {
return ErrProjectMinLimit
}
return nil
}
func (o PolicySetUpdateOptions) valid() error {
if o.Name != nil && !validStringID(o.Name) {
return ErrInvalidName
}
return nil
}
func (o PolicySetAddPoliciesOptions) valid() error {
if o.Policies == nil {
return ErrRequiredPolicies
}
if len(o.Policies) == 0 {
return ErrInvalidPolicies
}
return nil
}
func (o PolicySetRemovePoliciesOptions) valid() error {
if o.Policies == nil {
return ErrRequiredPolicies
}
if len(o.Policies) == 0 {
return ErrInvalidPolicies
}
return nil
}
func (o PolicySetAddWorkspacesOptions) valid() error {
if o.Workspaces == nil {
return ErrWorkspacesRequired
}
if len(o.Workspaces) == 0 {
return ErrWorkspaceMinLimit
}
return nil
}
func (o PolicySetAddWorkspaceExclusionsOptions) valid() error {
if o.WorkspaceExclusions == nil {
return ErrWorkspacesRequired
}
if len(o.WorkspaceExclusions) == 0 {
return ErrWorkspaceMinLimit
}
return nil
}
func (o PolicySetAddProjectsOptions) valid() error {
if o.Projects == nil {
return ErrRequiredProject
}
if len(o.Projects) == 0 {
return ErrProjectMinLimit
}
return nil
}
func (o PolicySetAddProjectExclusionsOptions) valid() error {
if o.ProjectExclusions == nil {
return ErrRequiredProject
}
if len(o.ProjectExclusions) == 0 {
return ErrProjectMinLimit
}
return nil
}
func (o PolicySetRemoveProjectExclusionsOptions) valid() error {
if o.ProjectExclusions == nil {
return ErrRequiredProject
}
if len(o.ProjectExclusions) == 0 {
return ErrProjectMinLimit
}
return nil
}
func (o *PolicySetReadOptions) valid() error {
return nil
}
================================================
FILE: policy_set_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"os"
"regexp"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPolicySetsList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
version := createAdminSentinelVersion()
opts := AdminSentinelVersionCreateOptions{
Version: version,
URL: "https://www.hashicorp.com",
SHA: genSha(t),
Official: Bool(false),
Deprecated: Bool(false),
Enabled: Bool(true),
Beta: Bool(false),
}
sv, err := client.Admin.SentinelVersions.Create(ctx, opts)
require.NoError(t, err)
workspace, workspaceCleanup := createWorkspace(t, client, orgTest)
defer workspaceCleanup()
excludedWorkspace, excludedWorkspaceCleanup := createWorkspace(t, client, orgTest)
defer excludedWorkspaceCleanup()
options := PolicySetCreateOptions{
Kind: Sentinel,
AgentEnabled: Bool(true),
PolicyToolVersion: String(sv.Version),
Overridable: Bool(true),
}
psTest1, psTestCleanup1 := createPolicySetWithOptions(t, client, orgTest, nil, []*Workspace{workspace}, []*Workspace{excludedWorkspace}, nil, options)
defer psTestCleanup1()
psTest2, psTestCleanup2 := createPolicySetWithOptions(t, client, orgTest, nil, []*Workspace{workspace}, []*Workspace{excludedWorkspace}, nil, options)
defer psTestCleanup2()
psTest3, psTestCleanup3 := createPolicySet(t, client, orgTest, nil, []*Workspace{workspace}, []*Workspace{excludedWorkspace}, nil, OPA)
defer psTestCleanup3()
defer func() {
err := client.Admin.SentinelVersions.Delete(ctx, sv.ID)
require.NoError(t, err)
}()
t.Run("without list options", func(t *testing.T) {
psl, err := client.PolicySets.List(ctx, orgTest.Name, nil)
require.NoError(t, err)
assert.Contains(t, psl.Items, psTest1)
assert.Contains(t, psl.Items, psTest2)
assert.Contains(t, psl.Items, psTest3)
assert.Equal(t, true, psl.Items[0].AgentEnabled)
assert.Equal(t, 1, psl.CurrentPage)
assert.Equal(t, 3, psl.TotalCount)
})
t.Run("with pagination", func(t *testing.T) {
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
psl, err := client.PolicySets.List(ctx, orgTest.Name, &PolicySetListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, psl.Items)
assert.Equal(t, 999, psl.CurrentPage)
assert.Equal(t, 3, psl.TotalCount)
})
t.Run("with search", func(t *testing.T) {
// Search by one of the policy set's names; we should get only that policy
// set and pagination data should reflect the search as well
psl, err := client.PolicySets.List(ctx, orgTest.Name, &PolicySetListOptions{
Search: psTest1.Name,
})
require.NoError(t, err)
assert.Contains(t, psl.Items, psTest1)
assert.NotContains(t, psl.Items, psTest2)
assert.Equal(t, 1, psl.CurrentPage)
assert.Equal(t, 1, psl.TotalCount)
})
t.Run("with include param", func(t *testing.T) {
psl, err := client.PolicySets.List(ctx, orgTest.Name, &PolicySetListOptions{
Include: []PolicySetIncludeOpt{PolicySetWorkspaces},
})
require.NoError(t, err)
assert.Equal(t, 3, len(psl.Items))
assert.NotNil(t, psl.Items[0].Workspaces)
assert.Equal(t, 1, len(psl.Items[0].Workspaces))
assert.Equal(t, workspace.ID, psl.Items[0].Workspaces[0].ID)
})
t.Run("with workspace exclusion include param", func(t *testing.T) {
psl, err := client.PolicySets.List(ctx, orgTest.Name, &PolicySetListOptions{
Include: []PolicySetIncludeOpt{PolicySetWorkspaceExclusions},
})
require.NoError(t, err)
assert.Equal(t, 3, len(psl.Items))
assert.NotNil(t, psl.Items[0].WorkspaceExclusions)
assert.Equal(t, 1, len(psl.Items[0].WorkspaceExclusions))
assert.Equal(t, excludedWorkspace.ID, psl.Items[0].WorkspaceExclusions[0].ID)
assert.Equal(t, excludedWorkspace.Name, psl.Items[0].WorkspaceExclusions[0].Name)
assert.Equal(t, excludedWorkspace.CreatedAt, psl.Items[0].WorkspaceExclusions[0].CreatedAt)
})
t.Run("without a valid organization", func(t *testing.T) {
ps, err := client.PolicySets.List(ctx, badIdentifier, nil)
assert.Nil(t, ps)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
func TestPolicySetsCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
version := createAdminSentinelVersion()
opts := AdminSentinelVersionCreateOptions{
Version: version,
URL: "https://www.hashicorp.com",
SHA: genSha(t),
Official: Bool(false),
Deprecated: Bool(false),
Enabled: Bool(true),
Beta: Bool(false),
}
sv, err := client.Admin.SentinelVersions.Create(ctx, opts)
require.NoError(t, err)
defer func() {
err := client.Admin.SentinelVersions.Delete(ctx, sv.ID)
require.NoError(t, err)
}()
var vcsPolicyID string
t.Run("with valid attributes", func(t *testing.T) {
options := PolicySetCreateOptions{
Name: String(randomString(t)),
PolicyToolVersion: String(sv.Version),
}
ps, err := client.PolicySets.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, ps.Name, *options.Name)
assert.Equal(t, ps.Description, "")
assert.False(t, ps.Global)
})
t.Run("OPA policy set with valid attributes", func(t *testing.T) {
options := PolicySetCreateOptions{
Name: String("opa-policy-set"),
Kind: OPA,
}
ps, err := client.PolicySets.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, ps.Name, *options.Name)
assert.Equal(t, ps.Description, "")
assert.Equal(t, ps.Kind, OPA)
assert.False(t, ps.Global)
})
t.Run("OPA policy set with policy update patterns", func(t *testing.T) {
options := PolicySetCreateOptions{
Name: String(randomString(t)),
Kind: OPA,
PolicyUpdatePatterns: []string{"*.rego", "policies/**"},
}
ps, err := client.PolicySets.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, ps.Name, *options.Name)
assert.Equal(t, ps.Kind, OPA)
assert.Equal(t, options.PolicyUpdatePatterns, ps.PolicyUpdatePatterns)
})
t.Run("with pinned policy runtime version valid attributes", func(t *testing.T) {
options := PolicySetCreateOptions{
Name: String(randomString(t)),
Kind: Sentinel,
AgentEnabled: Bool(true),
PolicyToolVersion: String(sv.Version),
}
ps, err := client.PolicySets.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, ps.Name, *options.Name)
assert.Equal(t, ps.Description, "")
assert.Equal(t, ps.Kind, Sentinel)
assert.Equal(t, ps.AgentEnabled, true)
assert.Equal(t, ps.PolicyToolVersion, sv.Version)
assert.False(t, ps.Global)
})
t.Run("with pinned policy runtime version and missing kind", func(t *testing.T) {
options := PolicySetCreateOptions{
Name: String(randomString(t)),
AgentEnabled: Bool(true),
PolicyToolVersion: String(sv.Version),
Overridable: Bool(true),
}
ps, err := client.PolicySets.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, ps.Name, *options.Name)
assert.Equal(t, ps.Description, "")
assert.Equal(t, ps.Kind, Sentinel)
assert.Equal(t, ps.AgentEnabled, true)
assert.Equal(t, ps.PolicyToolVersion, sv.Version)
assert.False(t, ps.Global)
})
t.Run("with kind missing", func(t *testing.T) {
options := PolicySetCreateOptions{
Name: String("policy-set1"),
}
ps, err := client.PolicySets.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, ps.Name, *options.Name)
assert.Equal(t, ps.Description, "")
assert.Equal(t, ps.Kind, Sentinel)
assert.False(t, ps.Global)
})
t.Run("with agent enabled missing", func(t *testing.T) {
options := PolicySetCreateOptions{
Name: String(randomString(t)),
Kind: Sentinel,
}
ps, err := client.PolicySets.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, ps.Name, *options.Name)
assert.Equal(t, ps.Description, "")
assert.Equal(t, ps.Kind, Sentinel)
assert.Equal(t, ps.AgentEnabled, false)
assert.False(t, ps.Global)
})
t.Run("with all attributes provided - sentinel", func(t *testing.T) {
options := PolicySetCreateOptions{
Name: String("global"),
Description: String("Policies in this set will be checked in ALL workspaces!"),
Kind: Sentinel,
Global: Bool(true),
}
ps, err := client.PolicySets.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, ps.Name, *options.Name)
assert.Equal(t, ps.Description, *options.Description)
assert.Equal(t, ps.Kind, Sentinel)
assert.True(t, ps.Global)
})
t.Run("with all attributes provided - OPA", func(t *testing.T) {
options := PolicySetCreateOptions{
Name: String("global1"),
Description: String("Policies in this set will be checked in ALL workspaces!"),
Kind: OPA,
Overridable: Bool(true),
Global: Bool(true),
}
ps, err := client.PolicySets.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, ps.Name, *options.Name)
assert.Equal(t, ps.Description, *options.Description)
assert.Equal(t, ps.Overridable, options.Overridable)
assert.Equal(t, ps.Kind, OPA)
assert.True(t, ps.Global)
})
t.Run("with missing overridable attribute", func(t *testing.T) {
options := PolicySetCreateOptions{
Name: String("global2"),
Description: String("Policies in this set will be checked in ALL workspaces!"),
Kind: OPA,
Global: Bool(true),
}
ps, err := client.PolicySets.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, ps.Name, *options.Name)
assert.Equal(t, ps.Description, *options.Description)
assert.Equal(t, ps.Overridable, Bool(false))
assert.Equal(t, ps.Kind, OPA)
assert.True(t, ps.Global)
})
t.Run("with policies and workspaces provided", func(t *testing.T) {
pTest, pTestCleanup := createPolicy(t, client, orgTest)
defer pTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
options := PolicySetCreateOptions{
Name: String("populated-policy-set"),
Policies: []*Policy{pTest},
Kind: Sentinel,
Workspaces: []*Workspace{wTest},
}
ps, err := client.PolicySets.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, ps.Name, *options.Name)
assert.Equal(t, ps.PolicyCount, 1)
assert.Equal(t, ps.Policies[0].ID, pTest.ID)
assert.Equal(t, ps.WorkspaceCount, 1)
assert.Equal(t, ps.Kind, Sentinel)
assert.Equal(t, ps.Workspaces[0].ID, wTest.ID)
})
t.Run("with policies, workspaces and projects provided", func(t *testing.T) {
pTest, pTestCleanup := createPolicy(t, client, orgTest)
defer pTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
prjTest, prjTestCleanup := createProject(t, client, orgTest)
defer prjTestCleanup()
options := PolicySetCreateOptions{
Name: String("project-policy-set"),
Policies: []*Policy{pTest},
Workspaces: []*Workspace{wTest},
Projects: []*Project{prjTest},
}
ps, err := client.PolicySets.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, ps.Name, *options.Name)
assert.Equal(t, ps.PolicyCount, 1)
assert.Equal(t, ps.Policies[0].ID, pTest.ID)
assert.Equal(t, ps.WorkspaceCount, 1)
assert.Equal(t, ps.Workspaces[0].ID, wTest.ID)
assert.Equal(t, ps.ProjectCount, 1)
assert.Equal(t, ps.Projects[0].ID, prjTest.ID)
})
t.Run("with policies and excluded workspaces provided", func(t *testing.T) {
pTest, pTestCleanup := createPolicy(t, client, orgTest)
defer pTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
options := PolicySetCreateOptions{
Name: String("exclusion-policy-set"),
Policies: []*Policy{pTest},
WorkspaceExclusions: []*Workspace{wTest},
}
ps, err := client.PolicySets.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, ps.Name, *options.Name)
assert.Equal(t, ps.PolicyCount, 1)
assert.Equal(t, ps.Policies[0].ID, pTest.ID)
assert.Equal(t, ps.WorkspaceExclusions[0].ID, wTest.ID)
assert.Equal(t, len(ps.WorkspaceExclusions), 1)
})
t.Run("with vcs policy set", func(t *testing.T) {
githubIdentifier := os.Getenv("GITHUB_POLICY_SET_IDENTIFIER")
if githubIdentifier == "" {
t.Skip("Export a valid GITHUB_POLICY_SET_IDENTIFIER before running this test")
}
// We are deliberately ignoring the cleanup func here, because there's a potential race condition
// against the subsequent subtest -- HCP Terraform performs some async cleanup on VCS repos when deleting an
// OAuthClient, and we've seen evidence that it will zero out the next test's NEW VCSRepo values if
// they manage to slip in before the async stuff completes, even though the new values link it to a
// new OAuthToken. Anyway, there's a deferred cleanup for orgTest in the outer scope, so the org's
// dependent: destroy clause on OAuthClients will clean this up when the test as a whole ends.
oc, _ := createOAuthToken(t, client, orgTest)
options := PolicySetCreateOptions{
Name: String("vcs-policy-set"),
Kind: Sentinel,
PoliciesPath: String("/policy-sets/foo"),
VCSRepo: &VCSRepoOptions{
Branch: String("policies"),
Identifier: String(githubIdentifier),
OAuthTokenID: String(oc.ID),
IngressSubmodules: Bool(true),
},
}
ps, err := client.PolicySets.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
// Save policy ID to be used by update func
vcsPolicyID = ps.ID
assert.Equal(t, ps.Name, *options.Name)
assert.Equal(t, ps.Description, "")
assert.False(t, ps.Global)
assert.Equal(t, ps.Kind, Sentinel)
assert.Equal(t, ps.PoliciesPath, "/policy-sets/foo")
assert.Equal(t, ps.VCSRepo.Branch, "policies")
assert.Equal(t, ps.VCSRepo.DisplayIdentifier, githubIdentifier)
assert.Equal(t, ps.VCSRepo.Identifier, githubIdentifier)
assert.Equal(t, ps.VCSRepo.IngressSubmodules, true)
assert.Equal(t, ps.VCSRepo.OAuthTokenID, oc.ID)
assert.Equal(t, ps.VCSRepo.RepositoryHTTPURL, fmt.Sprintf("https://github.com/%s", githubIdentifier))
assert.Equal(t, ps.VCSRepo.ServiceProvider, string(ServiceProviderGithub))
assert.Regexp(t, fmt.Sprintf("^%s/webhooks/vcs/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$", regexp.QuoteMeta(DefaultConfig().Address)), ps.VCSRepo.WebhookURL)
})
t.Run("with vcs policy updated", func(t *testing.T) {
githubIdentifier := os.Getenv("GITHUB_POLICY_SET_IDENTIFIER")
if githubIdentifier == "" {
t.Skip("Export a valid GITHUB_POLICY_SET_IDENTIFIER before running this test")
}
// We are deliberately ignoring the cleanup func here, because it's not really necessary: there's a
// deferred cleanup for orgTest in the outer scope, so the org's dependent: destroy clause on
// OAuthClients will clean this up when the test as a whole ends. Unlike the one in the previous
// subtest, there's no known race condition here because there aren't any later subtests that modify
// this same policy set. But I'm being consistent with the prior case just to reduce risks from future
// copypasta code.
oc, _ := createOAuthToken(t, client, orgTest)
options := PolicySetUpdateOptions{
Name: String("vcs-policy-set"),
PoliciesPath: String("/policy-sets/bar"),
VCSRepo: &VCSRepoOptions{
Branch: String("policies"),
Identifier: String(githubIdentifier),
OAuthTokenID: String(oc.ID),
IngressSubmodules: Bool(false),
},
}
ps, err := client.PolicySets.Update(ctx, vcsPolicyID, options)
require.NoError(t, err)
assert.Equal(t, ps.Name, *options.Name)
assert.Equal(t, ps.Description, "")
assert.False(t, ps.Global)
assert.Equal(t, ps.PoliciesPath, "/policy-sets/bar")
assert.Equal(t, ps.VCSRepo.Branch, "policies")
assert.Equal(t, ps.VCSRepo.DisplayIdentifier, githubIdentifier)
assert.Equal(t, ps.VCSRepo.Identifier, githubIdentifier)
assert.Equal(t, ps.VCSRepo.IngressSubmodules, false)
assert.Equal(t, ps.VCSRepo.OAuthTokenID, oc.ID)
assert.Equal(t, ps.VCSRepo.RepositoryHTTPURL, fmt.Sprintf("https://github.com/%s", githubIdentifier))
assert.Equal(t, ps.VCSRepo.ServiceProvider, string(ServiceProviderGithub))
assert.Regexp(t, fmt.Sprintf("^%s/webhooks/vcs/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$", regexp.QuoteMeta(DefaultConfig().Address)), ps.VCSRepo.WebhookURL)
})
t.Run("without a name provided", func(t *testing.T) {
ps, err := client.PolicySets.Create(ctx, orgTest.Name, PolicySetCreateOptions{})
assert.Nil(t, ps)
assert.EqualError(t, err, ErrRequiredName.Error())
})
t.Run("with an invalid name provided", func(t *testing.T) {
ps, err := client.PolicySets.Create(ctx, orgTest.Name, PolicySetCreateOptions{
Name: String("nope/nope!"),
})
assert.Nil(t, ps)
assert.EqualError(t, err, ErrInvalidName.Error())
})
t.Run("without a valid organization", func(t *testing.T) {
ps, err := client.PolicySets.Create(ctx, badIdentifier, PolicySetCreateOptions{
Name: String("policy-set"),
})
assert.Nil(t, ps)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
func TestPolicySetsCreateWithGithubApp(t *testing.T) {
t.Parallel()
gHAInstallationID := os.Getenv("GITHUB_APP_INSTALLATION_ID")
if gHAInstallationID == "" {
t.Skip("Export a valid GITHUB_APP_INSTALLATION_ID before running this test!")
}
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
var vcsPolicyID string
t.Run("with vcs policy set", func(t *testing.T) {
githubIdentifier := os.Getenv("GITHUB_POLICY_SET_IDENTIFIER")
if githubIdentifier == "" {
t.Skip("Export a valid GITHUB_POLICY_SET_IDENTIFIER before running this test")
}
options := PolicySetCreateOptions{
Name: String("vcs-policy-set"),
PoliciesPath: String("/policy-sets/foo"),
VCSRepo: &VCSRepoOptions{
Branch: String("policies"),
Identifier: String(githubIdentifier),
GHAInstallationID: String(gHAInstallationID),
IngressSubmodules: Bool(true),
},
}
ps, err := client.PolicySets.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
// Save policy ID to be used by update func
vcsPolicyID = ps.ID
assert.Equal(t, ps.Name, *options.Name)
assert.Equal(t, ps.Description, "")
assert.False(t, ps.Global)
assert.Equal(t, ps.PoliciesPath, "/policy-sets/foo")
assert.Equal(t, ps.VCSRepo.Branch, "policies")
assert.Equal(t, ps.VCSRepo.DisplayIdentifier, githubIdentifier)
assert.Equal(t, ps.VCSRepo.Identifier, githubIdentifier)
assert.Equal(t, ps.VCSRepo.IngressSubmodules, true)
assert.Equal(t, ps.VCSRepo.GHAInstallationID, gHAInstallationID)
assert.Equal(t, ps.VCSRepo.RepositoryHTTPURL, fmt.Sprintf("https://github.com/%s", githubIdentifier))
})
t.Run("with vcs policy updated", func(t *testing.T) {
githubIdentifier := os.Getenv("GITHUB_POLICY_SET_IDENTIFIER")
if githubIdentifier == "" {
t.Skip("Export a valid GITHUB_POLICY_SET_IDENTIFIER before running this test")
}
options := PolicySetUpdateOptions{
Name: String("vcs-policy-set"),
PoliciesPath: String("/policy-sets/bar"),
VCSRepo: &VCSRepoOptions{
Branch: String("policies"),
Identifier: String(githubIdentifier),
GHAInstallationID: String(gHAInstallationID),
IngressSubmodules: Bool(false),
},
}
ps, err := client.PolicySets.Update(ctx, vcsPolicyID, options)
require.NoError(t, err)
assert.Equal(t, ps.Name, *options.Name)
assert.Equal(t, ps.Description, "")
assert.False(t, ps.Global)
assert.Equal(t, ps.PoliciesPath, "/policy-sets/bar")
assert.Equal(t, ps.VCSRepo.Branch, "policies")
assert.Equal(t, ps.VCSRepo.DisplayIdentifier, githubIdentifier)
assert.Equal(t, ps.VCSRepo.Identifier, githubIdentifier)
assert.Equal(t, ps.VCSRepo.IngressSubmodules, false)
assert.Equal(t, ps.VCSRepo.GHAInstallationID, gHAInstallationID)
assert.Equal(t, ps.VCSRepo.RepositoryHTTPURL, fmt.Sprintf("https://github.com/%s", githubIdentifier))
assert.Equal(t, ps.VCSRepo.ServiceProvider, string("github_app"))
})
}
func TestPolicySetsRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
psTest, psTestCleanup := createPolicySet(t, client, orgTest, nil, nil, nil, nil, "")
defer psTestCleanup()
t.Run("with a valid ID", func(t *testing.T) {
ps, err := client.PolicySets.Read(ctx, psTest.ID)
require.NoError(t, err)
assert.Equal(t, ps.ID, psTest.ID)
})
t.Run("without a valid ID", func(t *testing.T) {
ps, err := client.PolicySets.Read(ctx, badIdentifier)
assert.Nil(t, ps)
assert.Equal(t, err, ErrInvalidPolicySetID)
})
t.Run("with policy set version", func(t *testing.T) {
psv, psvCleanup := createPolicySetVersion(t, client, psTest)
defer psvCleanup()
ps, err := client.PolicySets.Read(ctx, psTest.ID)
require.NoError(t, err)
// The newest one is the policy set version created in this test.
assert.Equal(t, ps.NewestVersion.ID, psv.ID)
// The current policy set version is nil because nothing has been uploaded
assert.Nil(t, ps.CurrentVersion)
psvNew, psvCleanupNew := createPolicySetVersion(t, client, psTest)
defer psvCleanupNew()
err = client.PolicySetVersions.Upload(
ctx,
*psv,
"test-fixtures/policy-set-version",
)
require.NoError(t, err)
// give HCP Terraform some time to process uploading the
// policy set version before reading.
time.Sleep(waitForPolicySetVersionUpload)
opts := &PolicySetReadOptions{
Include: []PolicySetIncludeOpt{PolicySetCurrentVersion, PolicySetNewestVersion},
}
psWithOptions, err := client.PolicySets.ReadWithOptions(ctx, psTest.ID, opts)
require.NoError(t, err)
// The newest policy set version is changed to the most recent one
// that was created.
require.NotNil(t, psWithOptions.NewestVersion)
assert.Equal(t, psWithOptions.NewestVersion.ID, psvNew.ID)
assert.Equal(t, psWithOptions.NewestVersion.Status, PolicySetVersionPending)
// The current one is now set because policies were uploaded to the
// policy set version. Notice how it is set to the one that was uplaoded,
// not the newest policy set version.
require.NotNil(t, psWithOptions.CurrentVersion)
assert.Equal(t, psWithOptions.CurrentVersion.ID, psv.ID)
assert.Equal(t, psWithOptions.CurrentVersion.Status, PolicySetVersionReady)
})
}
func TestPolicySetsUpdate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
version := createAdminSentinelVersion()
opts := AdminSentinelVersionCreateOptions{
Version: version,
URL: "https://www.hashicorp.com",
SHA: genSha(t),
Official: Bool(false),
Deprecated: Bool(false),
Enabled: Bool(true),
Beta: Bool(false),
}
sv, err := client.Admin.SentinelVersions.Create(ctx, opts)
require.NoError(t, err)
defer func() {
err := client.Admin.SentinelVersions.Delete(ctx, sv.ID)
require.NoError(t, err)
}()
options := PolicySetCreateOptions{
Kind: Sentinel,
AgentEnabled: Bool(true),
PolicyToolVersion: String(sv.Version),
Overridable: Bool(true),
}
psTest, psTestCleanup := createPolicySetWithOptions(t, client, orgTest, nil, nil, nil, nil, options)
defer psTestCleanup()
psTest2, psTestCleanup2 := createPolicySet(t, client, orgTest, nil, nil, nil, nil, "opa")
defer psTestCleanup2()
t.Run("with valid attributes", func(t *testing.T) {
options := PolicySetUpdateOptions{
AgentEnabled: Bool(false),
Name: String("global"),
Description: String("Policies in this set will be checked in ALL workspaces!"),
Global: Bool(true),
}
ps, err := client.PolicySets.Update(ctx, psTest.ID, options)
require.NoError(t, err)
assert.Equal(t, ps.AgentEnabled, false)
assert.Equal(t, ps.PolicyToolVersion, "")
assert.Nil(t, ps.Overridable)
assert.Equal(t, ps.Name, *options.Name)
assert.Equal(t, ps.Description, *options.Description)
assert.True(t, ps.Global)
})
t.Run("with valid attributes-OPA", func(t *testing.T) {
options := PolicySetUpdateOptions{
Name: String("global2"),
Description: String("Policies in this set will be checked in ALL workspaces!"),
Global: Bool(true),
Overridable: Bool(true),
}
ps, err := client.PolicySets.Update(ctx, psTest2.ID, options)
require.NoError(t, err)
assert.Equal(t, ps.Name, *options.Name)
assert.Equal(t, ps.Description, *options.Description)
assert.True(t, ps.Global)
assert.True(t, *ps.Overridable)
})
t.Run("with policy update patterns", func(t *testing.T) {
options := PolicySetUpdateOptions{
PolicyUpdatePatterns: []string{"*.rego", "policies/**"},
}
ps, err := client.PolicySets.Update(ctx, psTest2.ID, options)
require.NoError(t, err)
assert.Equal(t, options.PolicyUpdatePatterns, ps.PolicyUpdatePatterns)
})
t.Run("with invalid attributes", func(t *testing.T) {
ps, err := client.PolicySets.Update(ctx, psTest.ID, PolicySetUpdateOptions{
Name: String("nope/nope!"),
})
assert.Nil(t, ps)
assert.EqualError(t, err, ErrInvalidName.Error())
})
t.Run("without a valid ID", func(t *testing.T) {
ps, err := client.PolicySets.Update(ctx, badIdentifier, PolicySetUpdateOptions{
Name: String("policy-set"),
})
assert.Nil(t, ps)
assert.Equal(t, err, ErrInvalidPolicySetID)
})
}
func TestPolicySetsAddPolicies(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
pTest1, pTestCleanup1 := createPolicy(t, client, orgTest)
defer pTestCleanup1()
pTest2, pTestCleanup2 := createPolicy(t, client, orgTest)
defer pTestCleanup2()
psTest, psTestCleanup := createPolicySet(t, client, orgTest, nil, nil, nil, nil, "")
defer psTestCleanup()
t.Run("with policies provided", func(t *testing.T) {
err := client.PolicySets.AddPolicies(ctx, psTest.ID, PolicySetAddPoliciesOptions{
Policies: []*Policy{pTest1, pTest2},
})
require.NoError(t, err)
ps, err := client.PolicySets.Read(ctx, psTest.ID)
require.NoError(t, err)
assert.Equal(t, ps.PolicyCount, 2)
ids := []string{}
for _, policy := range ps.Policies {
ids = append(ids, policy.ID)
}
assert.Contains(t, ids, pTest1.ID)
assert.Contains(t, ids, pTest2.ID)
})
t.Run("without policies provided", func(t *testing.T) {
err := client.PolicySets.AddPolicies(ctx, psTest.ID, PolicySetAddPoliciesOptions{})
assert.Equal(t, err, ErrRequiredPolicies)
})
t.Run("with empty policies slice", func(t *testing.T) {
err := client.PolicySets.AddPolicies(ctx, psTest.ID, PolicySetAddPoliciesOptions{
Policies: []*Policy{},
})
assert.Equal(t, err, ErrInvalidPolicies)
})
t.Run("without a valid ID", func(t *testing.T) {
err := client.PolicySets.AddPolicies(ctx, badIdentifier, PolicySetAddPoliciesOptions{
Policies: []*Policy{pTest1, pTest2},
})
assert.Equal(t, err, ErrInvalidPolicySetID)
})
}
func TestPolicySetsRemovePolicies(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
pTest1, pTestCleanup1 := createPolicy(t, client, orgTest)
defer pTestCleanup1()
pTest2, pTestCleanup2 := createPolicy(t, client, orgTest)
defer pTestCleanup2()
psTest, psTestCleanup := createPolicySet(t, client, orgTest, nil, nil, nil, nil, "")
defer psTestCleanup()
t.Run("with policies provided", func(t *testing.T) {
err := client.PolicySets.RemovePolicies(ctx, psTest.ID, PolicySetRemovePoliciesOptions{
Policies: []*Policy{pTest1, pTest2},
})
require.NoError(t, err)
ps, err := client.PolicySets.Read(ctx, psTest.ID)
require.NoError(t, err)
assert.Equal(t, 0, ps.PolicyCount)
assert.Empty(t, ps.Policies)
})
t.Run("without policies provided", func(t *testing.T) {
err := client.PolicySets.RemovePolicies(ctx, psTest.ID, PolicySetRemovePoliciesOptions{})
assert.Equal(t, err, ErrRequiredPolicies)
})
t.Run("with empty policies slice", func(t *testing.T) {
err := client.PolicySets.RemovePolicies(ctx, psTest.ID, PolicySetRemovePoliciesOptions{
Policies: []*Policy{},
})
assert.Equal(t, err, ErrInvalidPolicies)
})
t.Run("without a valid ID", func(t *testing.T) {
err := client.PolicySets.RemovePolicies(ctx, badIdentifier, PolicySetRemovePoliciesOptions{
Policies: []*Policy{pTest1, pTest2},
})
assert.Equal(t, err, ErrInvalidPolicySetID)
})
}
func TestPolicySetsAddWorkspaces(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
wTest1, wTestCleanup1 := createWorkspace(t, client, orgTest)
defer wTestCleanup1()
wTest2, wTestCleanup2 := createWorkspace(t, client, orgTest)
defer wTestCleanup2()
psTest, psTestCleanup := createPolicySet(t, client, orgTest, nil, nil, nil, nil, "")
defer psTestCleanup()
t.Run("with workspaces provided", func(t *testing.T) {
err := client.PolicySets.AddWorkspaces(
ctx,
psTest.ID,
PolicySetAddWorkspacesOptions{
Workspaces: []*Workspace{wTest1, wTest2},
},
)
require.NoError(t, err)
ps, err := client.PolicySets.Read(ctx, psTest.ID)
require.NoError(t, err)
assert.Equal(t, 2, ps.WorkspaceCount)
ids := []string{}
for _, ws := range ps.Workspaces {
ids = append(ids, ws.ID)
}
assert.Contains(t, ids, wTest1.ID)
assert.Contains(t, ids, wTest2.ID)
})
t.Run("without workspaces provided", func(t *testing.T) {
err := client.PolicySets.AddWorkspaces(
ctx,
psTest.ID,
PolicySetAddWorkspacesOptions{},
)
assert.Equal(t, err, ErrWorkspacesRequired)
})
t.Run("with empty workspaces slice", func(t *testing.T) {
err := client.PolicySets.AddWorkspaces(
ctx,
psTest.ID,
PolicySetAddWorkspacesOptions{Workspaces: []*Workspace{}},
)
assert.Equal(t, err, ErrWorkspaceMinLimit)
})
t.Run("without a valid ID", func(t *testing.T) {
err := client.PolicySets.AddWorkspaces(
ctx,
badIdentifier,
PolicySetAddWorkspacesOptions{
Workspaces: []*Workspace{wTest1, wTest2},
},
)
assert.Equal(t, err, ErrInvalidPolicySetID)
})
}
func TestPolicySetsRemoveWorkspaces(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
wTest1, wTestCleanup1 := createWorkspace(t, client, orgTest)
defer wTestCleanup1()
wTest2, wTestCleanup2 := createWorkspace(t, client, orgTest)
defer wTestCleanup2()
psTest, psTestCleanup := createPolicySet(t, client, orgTest, nil, []*Workspace{wTest1, wTest2}, nil, nil, "")
defer psTestCleanup()
t.Run("with workspaces provided", func(t *testing.T) {
err := client.PolicySets.RemoveWorkspaces(
ctx,
psTest.ID,
PolicySetRemoveWorkspacesOptions{
Workspaces: []*Workspace{wTest1, wTest2},
},
)
require.NoError(t, err)
ps, err := client.PolicySets.Read(ctx, psTest.ID)
require.NoError(t, err)
assert.Equal(t, 0, ps.WorkspaceCount)
assert.Empty(t, ps.Workspaces)
})
t.Run("without workspaces provided", func(t *testing.T) {
err := client.PolicySets.RemoveWorkspaces(
ctx,
psTest.ID,
PolicySetRemoveWorkspacesOptions{},
)
assert.Equal(t, err, ErrWorkspacesRequired)
})
t.Run("with empty workspaces slice", func(t *testing.T) {
err := client.PolicySets.RemoveWorkspaces(
ctx,
psTest.ID,
PolicySetRemoveWorkspacesOptions{Workspaces: []*Workspace{}},
)
assert.Equal(t, err, ErrWorkspaceMinLimit)
})
t.Run("without a valid ID", func(t *testing.T) {
err := client.PolicySets.RemoveWorkspaces(
ctx,
badIdentifier,
PolicySetRemoveWorkspacesOptions{
Workspaces: []*Workspace{wTest1, wTest2},
},
)
assert.Equal(t, err, ErrInvalidPolicySetID)
})
}
func TestPolicySetsAddWorkspaceExclusions(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
wTest1, wTestCleanup1 := createWorkspace(t, client, orgTest)
defer wTestCleanup1()
wTest2, wTestCleanup2 := createWorkspace(t, client, orgTest)
defer wTestCleanup2()
psTest, psTestCleanup := createPolicySet(t, client, orgTest, nil, nil, nil, nil, "")
defer psTestCleanup()
t.Run("with workspace exclusions provided", func(t *testing.T) {
err := client.PolicySets.AddWorkspaceExclusions(
ctx,
psTest.ID,
PolicySetAddWorkspaceExclusionsOptions{
WorkspaceExclusions: []*Workspace{wTest1, wTest2},
},
)
require.NoError(t, err)
ps, err := client.PolicySets.Read(ctx, psTest.ID)
require.NoError(t, err)
assert.Equal(t, 2, len(ps.WorkspaceExclusions))
ids := []string{}
for _, ws := range ps.WorkspaceExclusions {
ids = append(ids, ws.ID)
}
assert.Contains(t, ids, wTest1.ID)
assert.Contains(t, ids, wTest2.ID)
})
t.Run("without workspace exclusions provided", func(t *testing.T) {
err := client.PolicySets.AddWorkspaceExclusions(
ctx,
psTest.ID,
PolicySetAddWorkspaceExclusionsOptions{},
)
assert.Equal(t, err, ErrWorkspacesRequired)
})
t.Run("with empty workspace exclusions slice", func(t *testing.T) {
err := client.PolicySets.AddWorkspaceExclusions(
ctx,
psTest.ID,
PolicySetAddWorkspaceExclusionsOptions{WorkspaceExclusions: []*Workspace{}},
)
assert.Equal(t, err, ErrWorkspaceMinLimit)
})
t.Run("without a valid ID", func(t *testing.T) {
err := client.PolicySets.AddWorkspaceExclusions(
ctx,
badIdentifier,
PolicySetAddWorkspaceExclusionsOptions{
WorkspaceExclusions: []*Workspace{wTest1, wTest2},
},
)
assert.Equal(t, err, ErrInvalidPolicySetID)
})
}
func TestPolicySetsRemoveWorkspaceExclusions(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
wTest1, wTestCleanup1 := createWorkspace(t, client, orgTest)
defer wTestCleanup1()
wTest2, wTestCleanup2 := createWorkspace(t, client, orgTest)
defer wTestCleanup2()
psTest, psTestCleanup := createPolicySet(t, client, orgTest, nil, nil, []*Workspace{wTest1, wTest2}, nil, "")
defer psTestCleanup()
t.Run("with workspace exclusions provided", func(t *testing.T) {
err := client.PolicySets.RemoveWorkspaceExclusions(
ctx,
psTest.ID,
PolicySetRemoveWorkspaceExclusionsOptions{
WorkspaceExclusions: []*Workspace{wTest1, wTest2},
},
)
require.NoError(t, err)
ps, err := client.PolicySets.Read(ctx, psTest.ID)
require.NoError(t, err)
assert.Equal(t, 0, len(ps.WorkspaceExclusions))
assert.Empty(t, ps.WorkspaceExclusions)
})
t.Run("without workspaces provided", func(t *testing.T) {
err := client.PolicySets.RemoveWorkspaceExclusions(
ctx,
psTest.ID,
PolicySetRemoveWorkspaceExclusionsOptions{},
)
assert.Equal(t, err, ErrWorkspacesRequired)
})
t.Run("with empty workspaces slice", func(t *testing.T) {
err := client.PolicySets.RemoveWorkspaceExclusions(
ctx,
psTest.ID,
PolicySetRemoveWorkspaceExclusionsOptions{WorkspaceExclusions: []*Workspace{}},
)
assert.Equal(t, err, ErrWorkspaceMinLimit)
})
t.Run("without a valid ID", func(t *testing.T) {
err := client.PolicySets.RemoveWorkspaceExclusions(
ctx,
badIdentifier,
PolicySetRemoveWorkspaceExclusionsOptions{
WorkspaceExclusions: []*Workspace{wTest1, wTest2},
},
)
assert.Equal(t, err, ErrInvalidPolicySetID)
})
}
func TestPolicySetsAddProjects(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
pTest1, pTestCleanup1 := createProject(t, client, orgTest)
defer pTestCleanup1()
pTest2, pTestCleanup2 := createProject(t, client, orgTest)
defer pTestCleanup2()
psTest, psTestCleanup := createPolicySet(t, client, orgTest, nil, nil, nil, nil, "")
defer psTestCleanup()
t.Run("with projects provided", func(t *testing.T) {
err := client.PolicySets.AddProjects(
ctx,
psTest.ID,
PolicySetAddProjectsOptions{
Projects: []*Project{pTest1, pTest2},
},
)
require.NoError(t, err)
ps, err := client.PolicySets.Read(ctx, psTest.ID)
require.NoError(t, err)
assert.Equal(t, 2, ps.ProjectCount)
ids := []string{}
for _, ws := range ps.Projects {
ids = append(ids, ws.ID)
}
assert.Contains(t, ids, pTest1.ID)
assert.Contains(t, ids, pTest2.ID)
})
t.Run("without projects provided", func(t *testing.T) {
err := client.PolicySets.AddProjects(
ctx,
psTest.ID,
PolicySetAddProjectsOptions{},
)
assert.Equal(t, err, ErrRequiredProject)
})
t.Run("with empty projects slice", func(t *testing.T) {
err := client.PolicySets.AddProjects(
ctx,
psTest.ID,
PolicySetAddProjectsOptions{Projects: []*Project{}},
)
assert.Equal(t, err, ErrProjectMinLimit)
})
t.Run("without a valid ID", func(t *testing.T) {
err := client.PolicySets.AddProjects(
ctx,
badIdentifier,
PolicySetAddProjectsOptions{
Projects: []*Project{pTest1, pTest2},
},
)
assert.Equal(t, err, ErrInvalidPolicySetID)
})
}
func TestPolicySetsRemoveProjects(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
pTest1, pTestCleanup1 := createProject(t, client, orgTest)
defer pTestCleanup1()
pTest2, pTestCleanup2 := createProject(t, client, orgTest)
defer pTestCleanup2()
psTest, psTestCleanup := createPolicySet(t, client, orgTest, nil, nil, nil, []*Project{pTest1, pTest2}, "")
defer psTestCleanup()
t.Run("with projects provided", func(t *testing.T) {
err := client.PolicySets.RemoveProjects(
ctx,
psTest.ID,
PolicySetRemoveProjectsOptions{
Projects: []*Project{pTest1, pTest2},
},
)
require.NoError(t, err)
ps, err := client.PolicySets.Read(ctx, psTest.ID)
require.NoError(t, err)
assert.Equal(t, 0, ps.ProjectCount)
assert.Empty(t, ps.Projects)
})
t.Run("without projects provided", func(t *testing.T) {
err := client.PolicySets.RemoveProjects(
ctx,
psTest.ID,
PolicySetRemoveProjectsOptions{},
)
assert.Equal(t, err, ErrRequiredProject)
})
t.Run("with empty projects slice", func(t *testing.T) {
err := client.PolicySets.RemoveProjects(
ctx,
psTest.ID,
PolicySetRemoveProjectsOptions{Projects: []*Project{}},
)
assert.Equal(t, err, ErrProjectMinLimit)
})
t.Run("without a valid ID", func(t *testing.T) {
err := client.PolicySets.RemoveProjects(
ctx,
badIdentifier,
PolicySetRemoveProjectsOptions{
Projects: []*Project{pTest1, pTest2},
},
)
assert.Equal(t, err, ErrInvalidPolicySetID)
})
}
func TestPolicySetAddProjectExclusions(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
pTest1, pTestCleanup1 := createProject(t, client, orgTest)
defer pTestCleanup1()
pTest2, pTestCleanup2 := createProject(t, client, orgTest)
defer pTestCleanup2()
pTest3, pTestCleanup3 := createProject(t, client, orgTest)
defer pTestCleanup3()
psTest, psTestCleanup := createPolicySetWithOptions(t, client, orgTest, nil, nil, nil, nil, PolicySetCreateOptions{
Global: Bool(true),
})
defer psTestCleanup()
t.Run("with project exclusions provided", func(t *testing.T) {
err := client.PolicySets.AddProjectExclusions(
ctx,
psTest.ID,
PolicySetAddProjectExclusionsOptions{
ProjectExclusions: []*Project{pTest1, pTest2, pTest3},
},
)
require.NoError(t, err)
ps, err := client.PolicySets.ReadWithOptions(ctx, psTest.ID, &PolicySetReadOptions{
Include: []PolicySetIncludeOpt{
PolicySetProjectExclusions,
},
})
require.NoError(t, err)
assert.Equal(t, 3, len(ps.ProjectExclusions))
ids := []string{}
for _, p := range ps.ProjectExclusions {
ids = append(ids, p.ID)
}
assert.Contains(t, ids, pTest1.ID)
assert.Contains(t, ids, pTest2.ID)
assert.Contains(t, ids, pTest3.ID)
})
t.Run("without project exclusions provided", func(t *testing.T) {
err := client.PolicySets.AddProjectExclusions(
ctx,
psTest.ID,
PolicySetAddProjectExclusionsOptions{},
)
assert.Equal(t, err, ErrRequiredProject)
})
t.Run("with empty project exclusions slice", func(t *testing.T) {
err := client.PolicySets.AddProjectExclusions(
ctx,
psTest.ID,
PolicySetAddProjectExclusionsOptions{ProjectExclusions: []*Project{}},
)
assert.Equal(t, err, ErrProjectMinLimit)
})
t.Run("without a valid ID", func(t *testing.T) {
err := client.PolicySets.AddProjectExclusions(
ctx,
badIdentifier,
PolicySetAddProjectExclusionsOptions{
ProjectExclusions: []*Project{pTest1, pTest2, pTest3},
},
)
assert.Equal(t, err, ErrInvalidPolicySetID)
})
}
func TestPolicySetRemoveProjectExclusions(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
pTest1, pTestCleanup1 := createProject(t, client, orgTest)
defer pTestCleanup1()
pTest2, pTestCleanup2 := createProject(t, client, orgTest)
defer pTestCleanup2()
pTest3, pTestCleanup3 := createProject(t, client, orgTest)
defer pTestCleanup3()
psTest, psTestCleanup := createPolicySetWithOptions(t, client, orgTest, nil, nil, nil, nil, PolicySetCreateOptions{
Global: Bool(true),
ProjectExclusions: []*Project{
pTest1,
pTest2,
pTest3,
},
})
defer psTestCleanup()
t.Run("with project exclusions provided", func(t *testing.T) {
err := client.PolicySets.RemoveProjectExclusions(
ctx,
psTest.ID,
PolicySetRemoveProjectExclusionsOptions{
ProjectExclusions: []*Project{pTest1, pTest2, pTest3},
},
)
require.NoError(t, err)
ps, err := client.PolicySets.ReadWithOptions(ctx, psTest.ID, &PolicySetReadOptions{
Include: []PolicySetIncludeOpt{
"project_exclusions",
},
})
require.NoError(t, err)
assert.Equal(t, 0, len(ps.ProjectExclusions))
assert.Empty(t, ps.ProjectExclusions)
})
t.Run("without project exclusions provided", func(t *testing.T) {
err := client.PolicySets.RemoveProjectExclusions(
ctx,
psTest.ID,
PolicySetRemoveProjectExclusionsOptions{},
)
assert.Equal(t, err, ErrRequiredProject)
})
t.Run("with empty project exclusions slice", func(t *testing.T) {
err := client.PolicySets.RemoveProjectExclusions(
ctx,
psTest.ID,
PolicySetRemoveProjectExclusionsOptions{ProjectExclusions: []*Project{}},
)
assert.Equal(t, err, ErrProjectMinLimit)
})
t.Run("without a valid ID", func(t *testing.T) {
err := client.PolicySets.RemoveProjectExclusions(
ctx,
badIdentifier,
PolicySetRemoveProjectExclusionsOptions{
ProjectExclusions: []*Project{pTest1, pTest2, pTest3},
},
)
assert.Equal(t, err, ErrInvalidPolicySetID)
})
}
func TestPolicySetsDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
psTest, _ := createPolicySet(t, client, orgTest, nil, nil, nil, nil, "")
t.Run("with valid options", func(t *testing.T) {
err := client.PolicySets.Delete(ctx, psTest.ID)
require.NoError(t, err)
// Try loading the policy - it should fail.
_, err = client.PolicySets.Read(ctx, psTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the policy does not exist", func(t *testing.T) {
err := client.PolicySets.Delete(ctx, psTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the policy ID is invalid", func(t *testing.T) {
err := client.PolicySets.Delete(ctx, badIdentifier)
assert.Equal(t, err, ErrInvalidPolicySetID)
})
}
================================================
FILE: policy_set_parameter.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ PolicySetParameters = (*policySetParameters)(nil)
// PolicySetParameters describes all the parameter related methods that the Terraform
// Enterprise API supports.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/policy-set-params
type PolicySetParameters interface {
// List all the parameters associated with the given policy-set.
List(ctx context.Context, policySetID string, options *PolicySetParameterListOptions) (*PolicySetParameterList, error)
// Create is used to create a new parameter.
Create(ctx context.Context, policySetID string, options PolicySetParameterCreateOptions) (*PolicySetParameter, error)
// Read a parameter by its ID.
Read(ctx context.Context, policySetID string, parameterID string) (*PolicySetParameter, error)
// Update values of an existing parameter.
Update(ctx context.Context, policySetID string, parameterID string, options PolicySetParameterUpdateOptions) (*PolicySetParameter, error)
// Delete a parameter by its ID.
Delete(ctx context.Context, policySetID string, parameterID string) error
}
// policySetParameters implements Parameters.
type policySetParameters struct {
client *Client
}
// PolicySetParameterList represents a list of parameters.
type PolicySetParameterList struct {
*Pagination
Items []*PolicySetParameter
}
// PolicySetParameter represents a Policy Set parameter
type PolicySetParameter struct {
ID string `jsonapi:"primary,vars"`
Key string `jsonapi:"attr,key"`
Value string `jsonapi:"attr,value"`
Category CategoryType `jsonapi:"attr,category"`
Sensitive bool `jsonapi:"attr,sensitive"`
// Relations
PolicySet *PolicySet `jsonapi:"relation,configurable"`
}
// PolicySetParameterListOptions represents the options for listing parameters.
type PolicySetParameterListOptions struct {
ListOptions
}
// PolicySetParameterCreateOptions represents the options for creating a new parameter.
type PolicySetParameterCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,vars"`
// Required: The name of the parameter.
Key *string `jsonapi:"attr,key"`
// Optional: The value of the parameter.
Value *string `jsonapi:"attr,value,omitempty"`
// Required: The Category of the parameter, should always be "policy-set"
Category *CategoryType `jsonapi:"attr,category"`
// Optional: Whether the value is sensitive.
Sensitive *bool `jsonapi:"attr,sensitive,omitempty"`
}
// PolicySetParameterUpdateOptions represents the options for updating a parameter.
type PolicySetParameterUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,vars"`
// Optional: The name of the parameter.
Key *string `jsonapi:"attr,key,omitempty"`
// Optional: The value of the parameter.
Value *string `jsonapi:"attr,value,omitempty"`
// Optional: Whether the value is sensitive.
Sensitive *bool `jsonapi:"attr,sensitive,omitempty"`
}
// List all the parameters associated with the given policy-set.
func (s *policySetParameters) List(ctx context.Context, policySetID string, options *PolicySetParameterListOptions) (*PolicySetParameterList, error) {
if !validStringID(&policySetID) {
return nil, ErrInvalidPolicySetID
}
u := fmt.Sprintf("policy-sets/%s/parameters", policySetID)
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
vl := &PolicySetParameterList{}
err = req.Do(ctx, vl)
if err != nil {
return nil, err
}
return vl, nil
}
// Create is used to create a new parameter.
func (s *policySetParameters) Create(ctx context.Context, policySetID string, options PolicySetParameterCreateOptions) (*PolicySetParameter, error) {
if !validStringID(&policySetID) {
return nil, ErrInvalidPolicySetID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("policy-sets/%s/parameters", url.PathEscape(policySetID))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
p := &PolicySetParameter{}
err = req.Do(ctx, p)
if err != nil {
return nil, err
}
return p, nil
}
// Read a parameter by its ID.
func (s *policySetParameters) Read(ctx context.Context, policySetID, parameterID string) (*PolicySetParameter, error) {
if !validStringID(&policySetID) {
return nil, ErrInvalidPolicySetID
}
if !validStringID(¶meterID) {
return nil, ErrInvalidParamID
}
u := fmt.Sprintf("policy-sets/%s/parameters/%s", url.PathEscape(policySetID), url.PathEscape(parameterID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
p := &PolicySetParameter{}
err = req.Do(ctx, p)
if err != nil {
return nil, err
}
return p, err
}
// Update values of an existing parameter.
func (s *policySetParameters) Update(ctx context.Context, policySetID, parameterID string, options PolicySetParameterUpdateOptions) (*PolicySetParameter, error) {
if !validStringID(&policySetID) {
return nil, ErrInvalidPolicySetID
}
if !validStringID(¶meterID) {
return nil, ErrInvalidParamID
}
u := fmt.Sprintf("policy-sets/%s/parameters/%s", url.PathEscape(policySetID), url.PathEscape(parameterID))
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
p := &PolicySetParameter{}
err = req.Do(ctx, p)
if err != nil {
return nil, err
}
return p, nil
}
// Delete a parameter by its ID.
func (s *policySetParameters) Delete(ctx context.Context, policySetID, parameterID string) error {
if !validStringID(&policySetID) {
return ErrInvalidPolicySetID
}
if !validStringID(¶meterID) {
return ErrInvalidParamID
}
u := fmt.Sprintf("policy-sets/%s/parameters/%s", url.PathEscape(policySetID), url.PathEscape(parameterID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o PolicySetParameterCreateOptions) valid() error {
if !validString(o.Key) {
return ErrRequiredKey
}
if o.Category == nil {
return ErrRequiredCategory
}
if *o.Category != CategoryPolicySet {
return ErrInvalidCategory
}
return nil
}
================================================
FILE: policy_set_parameter_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPolicySetParametersList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
psTest, pTestCleanup := createPolicySet(t, client, orgTest, nil, nil, nil, nil, "")
defer pTestCleanup()
pTest1, pTestCleanup1 := createPolicySetParameter(t, client, psTest)
defer pTestCleanup1()
pTest2, pTestCleanup2 := createPolicySetParameter(t, client, psTest)
defer pTestCleanup2()
t.Run("without list options", func(t *testing.T) {
pl, err := client.PolicySetParameters.List(ctx, psTest.ID, nil)
require.NoError(t, err)
assert.Contains(t, pl.Items, pTest1)
assert.Contains(t, pl.Items, pTest2)
t.Skip("paging not supported yet in API")
assert.Equal(t, 1, pl.CurrentPage)
assert.Equal(t, 2, pl.TotalCount)
})
t.Run("with list options", func(t *testing.T) {
t.Skip("paging not supported yet in API")
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
pl, err := client.PolicySetParameters.List(ctx, psTest.ID, &PolicySetParameterListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, pl.Items)
assert.Equal(t, 999, pl.CurrentPage)
assert.Equal(t, 2, pl.TotalCount)
})
t.Run("when policy set ID is invalid ID", func(t *testing.T) {
pl, err := client.PolicySetParameters.List(ctx, badIdentifier, nil)
assert.Nil(t, pl)
assert.Equal(t, err, ErrInvalidPolicySetID)
})
}
func TestPolicySetParametersCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
psTest, psTestCleanup := createPolicySet(t, client, nil, nil, nil, nil, nil, "")
defer psTestCleanup()
t.Run("with valid options", func(t *testing.T) {
options := PolicySetParameterCreateOptions{
Key: String(randomKeyValue(t)),
Value: String(randomKeyValue(t)),
Category: Category(CategoryPolicySet),
}
p, err := client.PolicySetParameters.Create(ctx, psTest.ID, options)
require.NoError(t, err)
assert.NotEmpty(t, p.ID)
assert.Equal(t, *options.Key, p.Key)
assert.Equal(t, *options.Value, p.Value)
assert.Equal(t, *options.Category, p.Category)
assert.Equal(t, psTest.ID, p.PolicySet.ID)
// The policy set isn't returned correcly by the API.
// assert.Equal(t, *options.PolicySet, v.PolicySet)
})
t.Run("when options has an empty string value", func(t *testing.T) {
options := PolicySetParameterCreateOptions{
Key: String(randomKeyValue(t)),
Value: String(""),
Category: Category(CategoryPolicySet),
}
p, err := client.PolicySetParameters.Create(ctx, psTest.ID, options)
require.NoError(t, err)
assert.NotEmpty(t, p.ID)
assert.Equal(t, *options.Key, p.Key)
assert.Equal(t, *options.Value, p.Value)
assert.Equal(t, *options.Category, p.Category)
})
t.Run("when options is missing value", func(t *testing.T) {
options := PolicySetParameterCreateOptions{
Key: String(randomKeyValue(t)),
Category: Category(CategoryPolicySet),
}
p, err := client.PolicySetParameters.Create(ctx, psTest.ID, options)
require.NoError(t, err)
assert.NotEmpty(t, p.ID)
assert.Equal(t, *options.Key, p.Key)
assert.Equal(t, "", p.Value)
assert.Equal(t, *options.Category, p.Category)
})
t.Run("when options is missing key", func(t *testing.T) {
options := PolicySetParameterCreateOptions{
Value: String(randomKeyValue(t)),
Category: Category(CategoryPolicySet),
}
_, err := client.PolicySetParameters.Create(ctx, psTest.ID, options)
assert.Equal(t, err, ErrRequiredKey)
})
t.Run("when options has an empty key", func(t *testing.T) {
options := PolicySetParameterCreateOptions{
Key: String(""),
Value: String(randomKeyValue(t)),
Category: Category(CategoryPolicySet),
}
_, err := client.PolicySetParameters.Create(ctx, psTest.ID, options)
assert.Equal(t, err, ErrRequiredKey)
})
t.Run("when options is missing category", func(t *testing.T) {
options := PolicySetParameterCreateOptions{
Key: String(randomKeyValue(t)),
Value: String(randomKeyValue(t)),
}
_, err := client.PolicySetParameters.Create(ctx, psTest.ID, options)
assert.Equal(t, err, ErrRequiredCategory)
})
t.Run("when policy set ID is invalid", func(t *testing.T) {
options := PolicySetParameterCreateOptions{
Key: String(randomKeyValue(t)),
Value: String(randomKeyValue(t)),
Category: Category(CategoryPolicySet),
}
_, err := client.PolicySetParameters.Create(ctx, badIdentifier, options)
assert.Equal(t, err, ErrInvalidPolicySetID)
})
}
func TestPolicySetParametersRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
pTest, pTestCleanup := createPolicySetParameter(t, client, nil)
defer pTestCleanup()
t.Run("when the parameter exists", func(t *testing.T) {
p, err := client.PolicySetParameters.Read(ctx, pTest.PolicySet.ID, pTest.ID)
require.NoError(t, err)
assert.Equal(t, pTest.ID, p.ID)
assert.Equal(t, pTest.Category, p.Category)
assert.Equal(t, pTest.Key, p.Key)
assert.Equal(t, pTest.Sensitive, p.Sensitive)
assert.Equal(t, pTest.Value, p.Value)
assert.Equal(t, pTest.PolicySet.ID, p.PolicySet.ID)
})
t.Run("when the parameter does not exist", func(t *testing.T) {
p, err := client.PolicySetParameters.Read(ctx, pTest.PolicySet.ID, "nonexisting")
assert.Nil(t, p)
assert.Equal(t, ErrResourceNotFound, err)
})
t.Run("without a valid policy set ID", func(t *testing.T) {
p, err := client.PolicySetParameters.Read(ctx, badIdentifier, pTest.ID)
assert.Nil(t, p)
assert.Equal(t, err, ErrInvalidPolicySetID)
})
t.Run("without a valid parameter ID", func(t *testing.T) {
p, err := client.PolicySetParameters.Read(ctx, pTest.PolicySet.ID, badIdentifier)
assert.Nil(t, p)
assert.Equal(t, err, ErrInvalidParamID)
})
}
func TestPolicySetParametersUpdate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
pTest, pTestCleanup := createPolicySetParameter(t, client, nil)
defer pTestCleanup()
t.Run("with valid options", func(t *testing.T) {
options := PolicySetParameterUpdateOptions{
Key: String("newname"),
Value: String("newvalue"),
}
p, err := client.PolicySetParameters.Update(ctx, pTest.PolicySet.ID, pTest.ID, options)
require.NoError(t, err)
assert.Equal(t, *options.Key, p.Key)
assert.Equal(t, *options.Value, p.Value)
})
t.Run("when updating a subset of values", func(t *testing.T) {
options := PolicySetParameterUpdateOptions{
Key: String("someothername"),
}
p, err := client.PolicySetParameters.Update(ctx, pTest.PolicySet.ID, pTest.ID, options)
require.NoError(t, err)
assert.Equal(t, *options.Key, p.Key)
})
t.Run("with sensitive set", func(t *testing.T) {
options := PolicySetParameterUpdateOptions{
Sensitive: Bool(true),
}
p, err := client.PolicySetParameters.Update(ctx, pTest.PolicySet.ID, pTest.ID, options)
require.NoError(t, err)
assert.Equal(t, *options.Sensitive, p.Sensitive)
assert.Empty(t, p.Value) // Because its now sensitive
})
t.Run("without any changes", func(t *testing.T) {
pTest, pTestCleanup := createPolicySetParameter(t, client, nil)
defer pTestCleanup()
p, err := client.PolicySetParameters.Update(ctx, pTest.PolicySet.ID, pTest.ID, PolicySetParameterUpdateOptions{})
require.NoError(t, err)
assert.Equal(t, pTest, p)
})
t.Run("with invalid parameter ID", func(t *testing.T) {
_, err := client.PolicySetParameters.Update(ctx, badIdentifier, pTest.ID, PolicySetParameterUpdateOptions{})
assert.Equal(t, err, ErrInvalidPolicySetID)
})
t.Run("with invalid parameter ID", func(t *testing.T) {
_, err := client.PolicySetParameters.Update(ctx, pTest.PolicySet.ID, badIdentifier, PolicySetParameterUpdateOptions{})
assert.Equal(t, err, ErrInvalidParamID)
})
}
func TestPolicySetParametersDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
psTest, psTestCleanup := createPolicySet(t, client, nil, nil, nil, nil, nil, "")
defer psTestCleanup()
pTest, _ := createPolicySetParameter(t, client, psTest)
t.Run("with valid options", func(t *testing.T) {
err := client.PolicySetParameters.Delete(ctx, psTest.ID, pTest.ID)
require.NoError(t, err)
})
t.Run("with non existing parameter ID", func(t *testing.T) {
err := client.PolicySetParameters.Delete(ctx, psTest.ID, "nonexisting")
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("with invalid policy set ID", func(t *testing.T) {
err := client.PolicySetParameters.Delete(ctx, badIdentifier, pTest.ID)
assert.Equal(t, err, ErrInvalidPolicySetID)
})
t.Run("with invalid parameter ID", func(t *testing.T) {
err := client.PolicySetParameters.Delete(ctx, psTest.ID, badIdentifier)
assert.Equal(t, err, ErrInvalidParamID)
})
}
================================================
FILE: policy_set_version.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ PolicySetVersions = (*policySetVersions)(nil)
// PolicySetVersions describes all the Policy Set Version related methods that the Terraform
// Enterprise API supports.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/policy-sets#create-a-policy-set-version
type PolicySetVersions interface {
// Create is used to create a new Policy Set Version.
Create(ctx context.Context, policySetID string) (*PolicySetVersion, error)
// Read is used to read a Policy Set Version by its ID.
Read(ctx context.Context, policySetVersionID string) (*PolicySetVersion, error)
// Upload uploads policy files. It takes a Policy Set Version and a path
// to the set of sentinel files, which will be packaged by hashicorp/go-slug
// before being uploaded.
Upload(ctx context.Context, psv PolicySetVersion, path string) error
}
// policySetVersions implements PolicySetVersions.
type policySetVersions struct {
client *Client
}
// PolicySetVersionSource represents a source type of a policy set version.
type PolicySetVersionSource string
// List all available sources for a Policy Set Version.
const (
PolicySetVersionSourceAPI PolicySetVersionSource = "tfe-api"
PolicySetVersionSourceADO PolicySetVersionSource = "ado"
PolicySetVersionSourceBitBucket PolicySetVersionSource = "bitbucket"
PolicySetVersionSourceGitHub PolicySetVersionSource = "github"
PolicySetVersionSourceGitLab PolicySetVersionSource = "gitlab"
)
// PolicySetVersionStatus represents a policy set version status.
type PolicySetVersionStatus string
// List all available policy set version statuses.
const (
PolicySetVersionErrored PolicySetVersionStatus = "errored"
PolicySetVersionIngressing PolicySetVersionStatus = "ingressing"
PolicySetVersionPending PolicySetVersionStatus = "pending"
PolicySetVersionReady PolicySetVersionStatus = "ready"
)
// PolicySetVersionStatusTimestamps holds the timestamps for individual policy
// set version statuses.
type PolicySetVersionStatusTimestamps struct {
PendingAt time.Time `jsonapi:"attr,pending-at,rfc3339"`
IngressingAt time.Time `jsonapi:"attr,ingressing-at,rfc3339"`
ReadyAt time.Time `jsonapi:"attr,ready-at,rfc3339"`
ErroredAt time.Time `jsonapi:"attr,errored-at,rfc3339"`
}
type PolicySetIngressAttributes struct {
CommitSHA string `jsonapi:"attr,commit-sha"`
CommitURL string `jsonapi:"attr,commit-url"`
Identifier string `jsonapi:"attr,identifier"`
}
// PolicySetVersion represents a Terraform Enterprise Policy Set Version
type PolicySetVersion struct {
ID string `jsonapi:"primary,policy-set-versions"`
Source PolicySetVersionSource `jsonapi:"attr,source"`
Status PolicySetVersionStatus `jsonapi:"attr,status"`
StatusTimestamps PolicySetVersionStatusTimestamps `jsonapi:"attr,status-timestamps"`
Error string `jsonapi:"attr,error"`
ErrorMessage string `jsonapi:"attr,error-message"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
IngressAttributes *PolicySetIngressAttributes `jsonapi:"attr,ingress-attributes"`
// Relations
PolicySet *PolicySet `jsonapi:"relation,policy-set"`
// Links
Links map[string]interface{} `jsonapi:"links,omitempty"`
}
func (p PolicySetVersion) uploadURL() (string, error) {
uploadURL, ok := p.Links["upload"].(string)
if !ok {
return uploadURL, fmt.Errorf("the Policy Set Version does not contain an upload link")
}
if uploadURL == "" {
return uploadURL, fmt.Errorf("the Policy Set Version upload URL is empty")
}
return uploadURL, nil
}
// Create is used to create a new Policy Set Version.
func (p *policySetVersions) Create(ctx context.Context, policySetID string) (*PolicySetVersion, error) {
if !validStringID(&policySetID) {
return nil, ErrInvalidPolicySetID
}
u := fmt.Sprintf("policy-sets/%s/versions", url.PathEscape(policySetID))
req, err := p.client.NewRequest("POST", u, nil)
if err != nil {
return nil, err
}
psv := &PolicySetVersion{}
err = req.Do(ctx, psv)
if err != nil {
return nil, err
}
return psv, nil
}
// Read is used to read a Policy Set Version by its ID.
func (p *policySetVersions) Read(ctx context.Context, policySetVersionID string) (*PolicySetVersion, error) {
if !validStringID(&policySetVersionID) {
return nil, ErrInvalidPolicySetID
}
u := fmt.Sprintf("policy-set-versions/%s", url.PathEscape(policySetVersionID))
req, err := p.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
psv := &PolicySetVersion{}
err = req.Do(ctx, psv)
if err != nil {
return nil, err
}
return psv, nil
}
// Upload uploads policy files. It takes a Policy Set Version and a path
// to the set of sentinel files, which will be packaged by hashicorp/go-slug
// before being uploaded.
func (p *policySetVersions) Upload(ctx context.Context, psv PolicySetVersion, path string) error {
uploadURL, err := psv.uploadURL()
if err != nil {
return err
}
body, err := packContents(path)
if err != nil {
return err
}
return p.client.doForeignPUTRequest(ctx, uploadURL, body)
}
================================================
FILE: policy_set_version_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const waitForPolicySetVersionUpload = 500 * time.Millisecond
func TestPolicySetVersionsCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
psTest, psTestCleanup := createPolicySet(t, client, nil, nil, nil, nil, nil, "")
defer psTestCleanup()
t.Run("with valid identifier", func(t *testing.T) {
psv, err := client.PolicySetVersions.Create(ctx, psTest.ID)
require.NoError(t, err)
assert.NotEmpty(t, psv.ID)
assert.Equal(t, psv.Source, PolicySetVersionSourceAPI)
assert.Equal(t, psv.PolicySet.ID, psTest.ID)
})
t.Run("with invalid identifier", func(t *testing.T) {
_, err := client.PolicySetVersions.Create(ctx, badIdentifier)
assert.Equal(t, err, ErrInvalidPolicySetID)
})
}
func TestPolicySetVersionsRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
psTest, psTestCleanup := createPolicySet(t, client, nil, nil, nil, nil, nil, "")
defer psTestCleanup()
origPSV, err := client.PolicySetVersions.Create(ctx, psTest.ID)
require.NoError(t, err)
t.Run("with valid id", func(t *testing.T) {
psv, err := client.PolicySetVersions.Read(ctx, origPSV.ID)
require.NoError(t, err)
assert.Equal(t, psv.Source, origPSV.Source)
assert.Equal(t, psv.Status, origPSV.Status)
})
t.Run("with invalid id", func(t *testing.T) {
_, err := client.PolicySetVersions.Read(ctx, randomString(t))
require.Error(t, err)
})
}
func TestPolicySetVersionsUpload(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
psv, psvCleanup := createPolicySetVersion(t, client, nil)
defer psvCleanup()
t.Run("with valid upload URL", func(t *testing.T) {
psv, err := client.PolicySetVersions.Read(ctx, psv.ID)
require.NoError(t, err)
assert.Equal(t, psv.Status, PolicySetVersionPending)
err = client.PolicySetVersions.Upload(
ctx,
*psv,
"test-fixtures/policy-set-version",
)
require.NoError(t, err)
// give HCP Terraform some time to process uploading the
// policy set version before reading.
time.Sleep(waitForPolicySetVersionUpload)
psv, err = client.PolicySetVersions.Read(ctx, psv.ID)
require.NoError(t, err)
assert.Equal(t, PolicySetVersionReady, psv.Status)
})
t.Run("with missing upload URL", func(t *testing.T) {
delete(psv.Links, "upload")
err := client.PolicySetVersions.Upload(
ctx,
*psv,
"test-fixtures/policy-set-version",
)
assert.EqualError(t, err, "the Policy Set Version does not contain an upload link")
})
}
func TestPolicySetVersionsUploadURL(t *testing.T) {
t.Parallel()
t.Run("successfully returns upload link", func(t *testing.T) {
links := map[string]interface{}{
"upload": "example.com",
}
psv := PolicySetVersion{
Links: links,
}
uploadURL, err := psv.uploadURL()
require.NoError(t, err)
assert.Equal(t, uploadURL, "example.com")
})
t.Run("errors when there is no upload key in the Links", func(t *testing.T) {
links := map[string]interface{}{
"bad-field": "example.com",
}
psv := PolicySetVersion{
Links: links,
}
_, err := psv.uploadURL()
assert.EqualError(t, err, "the Policy Set Version does not contain an upload link")
})
t.Run("errors when the upload link is empty", func(t *testing.T) {
links := map[string]interface{}{
"upload": "",
}
psv := PolicySetVersion{
Links: links,
}
_, err := psv.uploadURL()
assert.EqualError(t, err, "the Policy Set Version upload URL is empty")
})
}
func TestPolicySetVersionsIngressAttributes(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
upgradeOrganizationSubscription(t, client, orgTest)
t.Run("with vcs", func(t *testing.T) {
githubIdentifier := os.Getenv("GITHUB_POLICY_SET_IDENTIFIER")
if githubIdentifier == "" {
t.Skip("Export a valid GITHUB_POLICY_SET_IDENTIFIER before running this test")
}
otTest, otTestCleanup := createOAuthToken(t, client, orgTest)
t.Cleanup(otTestCleanup)
options := PolicySetCreateOptions{
Name: String("vcs-policy-set"),
Kind: Sentinel,
PoliciesPath: String("policy-sets/foo"),
VCSRepo: &VCSRepoOptions{
Branch: String("policies"),
Identifier: String(githubIdentifier),
OAuthTokenID: String(otTest.ID),
IngressSubmodules: Bool(true),
},
}
ps, err := client.PolicySets.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
ps, err = client.PolicySets.ReadWithOptions(ctx, ps.ID, &PolicySetReadOptions{
Include: []PolicySetIncludeOpt{
PolicySetNewestVersion,
},
})
require.NoError(t, err)
psv, err := client.PolicySetVersions.Read(ctx, ps.NewestVersion.ID)
require.NoError(t, err)
require.NotNil(t, psv.IngressAttributes)
assert.NotZero(t, psv.IngressAttributes.CommitSHA)
assert.NotZero(t, psv.IngressAttributes.CommitURL)
assert.NotZero(t, psv.IngressAttributes.Identifier)
})
t.Run("without vcs", func(t *testing.T) {
psTest, psTestCleanup := createPolicySet(t, client, nil, nil, nil, nil, nil, "")
t.Cleanup(psTestCleanup)
psv, err := client.PolicySetVersions.Create(ctx, psTest.ID)
require.NoError(t, err)
assert.NotEmpty(t, psv.ID)
assert.Equal(t, psv.Source, PolicySetVersionSourceAPI)
assert.Equal(t, psv.PolicySet.ID, psTest.ID)
assert.Nil(t, psv.IngressAttributes)
})
}
================================================
FILE: project.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"github.com/hashicorp/jsonapi"
)
// Compile-time proof of interface implementation.
var _ Projects = (*projects)(nil)
// Projects describes all the project related methods that the Terraform
// Enterprise API supports
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/projects
type Projects interface {
// List all projects in the given organization
List(ctx context.Context, organization string, options *ProjectListOptions) (*ProjectList, error)
// Create a new project.
Create(ctx context.Context, organization string, options ProjectCreateOptions) (*Project, error)
// Read a project by its ID.
Read(ctx context.Context, projectID string) (*Project, error)
// ReadWithOptions a project by its ID.
ReadWithOptions(ctx context.Context, projectID string, options ProjectReadOptions) (*Project, error)
// Update a project.
Update(ctx context.Context, projectID string, options ProjectUpdateOptions) (*Project, error)
// Delete a project.
Delete(ctx context.Context, projectID string) error
// ListTagBindings lists all tag bindings associated with the project.
ListTagBindings(ctx context.Context, projectID string) ([]*TagBinding, error)
// ListEffectiveTagBindings lists all tag bindings associated with the project. In practice,
// this should be the same as ListTagBindings since projects do not currently inherit
// tag bindings.
ListEffectiveTagBindings(ctx context.Context, workspaceID string) ([]*EffectiveTagBinding, error)
// AddTagBindings adds or modifies the value of existing tag binding keys for a project.
AddTagBindings(ctx context.Context, projectID string, options ProjectAddTagBindingsOptions) ([]*TagBinding, error)
// DeleteAllTagBindings removes all existing tag bindings for a project.
DeleteAllTagBindings(ctx context.Context, projectID string) error
}
// projects implements Projects
type projects struct {
client *Client
}
// ProjectList represents a list of projects
type ProjectList struct {
*Pagination
Items []*Project
}
// Project represents a Terraform Enterprise project
type Project struct {
ID string `jsonapi:"primary,projects"`
AutoDestroyActivityDuration jsonapi.NullableAttr[string] `jsonapi:"attr,auto-destroy-activity-duration,omitempty"`
DefaultExecutionMode string `jsonapi:"attr,default-execution-mode"`
Description string `jsonapi:"attr,description"`
IsUnified bool `jsonapi:"attr,is-unified"`
Name string `jsonapi:"attr,name"`
SettingOverwrites *ProjectSettingOverwrites `jsonapi:"attr,setting-overwrites"`
// Relations
DefaultAgentPool *AgentPool `jsonapi:"relation,default-agent-pool"`
EffectiveTagBindings []*EffectiveTagBinding `jsonapi:"relation,effective-tag-bindings"`
Organization *Organization `jsonapi:"relation,organization"`
}
// Note: the fields of this struct are bool pointers instead of bool values, in order to simplify support for
// future TFE versions that support *some but not all* of the inherited defaults that go-tfe knows about.
type ProjectSettingOverwrites struct {
ExecutionMode *bool `jsonapi:"attr,default-execution-mode"`
AgentPool *bool `jsonapi:"attr,default-agent-pool"`
}
type ProjectIncludeOpt string
const (
ProjectEffectiveTagBindings ProjectIncludeOpt = "effective_tag_bindings"
)
// ProjectListOptions represents the options for listing projects
type ProjectListOptions struct {
ListOptions
// Optional: String (complete project name) used to filter the results.
// If multiple, comma separated values are specified, projects matching
// any of the names are returned.
Name string `url:"filter[names],omitempty"`
// Optional: A query string to search projects by names.
Query string `url:"q,omitempty"`
// Optional: A filter string to list projects filtered by key/value tags.
// These are not annotated and therefore not encoded by go-querystring
TagBindings []*TagBinding
// Optional: A list of relations to include
Include []ProjectIncludeOpt `url:"include,omitempty"`
}
type ProjectReadOptions struct {
// Optional: A list of relations to include
Include []ProjectIncludeOpt `url:"include,omitempty"`
}
// ProjectCreateOptions represents the options for creating a project
type ProjectCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,projects"`
// Required: A name to identify the project.
Name string `jsonapi:"attr,name"`
// Optional: A description for the project.
Description *string `jsonapi:"attr,description,omitempty"`
// Associated TagBindings of the project.
TagBindings []*TagBinding `jsonapi:"relation,tag-bindings,omitempty"`
// Optional: For all workspaces in the project, the period of time to wait
// after workspace activity to trigger a destroy run. The format should roughly
// match a Go duration string limited to days and hours, e.g. "24h" or "1d".
AutoDestroyActivityDuration jsonapi.NullableAttr[string] `jsonapi:"attr,auto-destroy-activity-duration,omitempty"`
// Optional: DefaultExecutionMode the default execution mode for workspaces in the project
DefaultExecutionMode *string `jsonapi:"attr,default-execution-mode,omitempty"`
// Optional: DefaultAgentPoolID default agent pool for workspaces in the project,
// required when DefaultExecutionMode is set to `agent`
DefaultAgentPoolID *string `jsonapi:"attr,default-agent-pool-id,omitempty"`
// Optional: Struct of booleans, which indicate whether the project
// specifies its own values for various settings. If you mark a setting as
// `false` in this struct, it will clear the project's existing value for
// that setting and defer to the default value that its organization provides.
//
// In general, it's not necessary to mark a setting as `true` in this
// struct; if you provide a literal value for a setting, HCP Terraform will
// automatically update its overwrites field to `true`. If you do choose to
// manually mark a setting as overwritten, you must provide a value for that
// setting at the same time.
SettingOverwrites *ProjectSettingOverwrites `jsonapi:"attr,setting-overwrites,omitempty"`
}
// ProjectUpdateOptions represents the options for updating a project
type ProjectUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,projects"`
// Optional: A name to identify the project
Name *string `jsonapi:"attr,name,omitempty"`
// Optional: A description for the project.
Description *string `jsonapi:"attr,description,omitempty"`
// Associated TagBindings of the project. Note that this will replace
// all existing tag bindings.
TagBindings []*TagBinding `jsonapi:"relation,tag-bindings,omitempty"`
// Optional: For all workspaces in the project, the period of time to wait
// after workspace activity to trigger a destroy run. The format should roughly
// match a Go duration string limited to days and hours, e.g. "24h" or "1d".
AutoDestroyActivityDuration jsonapi.NullableAttr[string] `jsonapi:"attr,auto-destroy-activity-duration,omitempty"`
// Optional: DefaultExecutionMode the default execution mode for workspaces
DefaultExecutionMode *string `jsonapi:"attr,default-execution-mode,omitempty"`
// Optional: DefaultAgentPoolID default agent pool for workspaces in the project,
// required when DefaultExecutionMode is set to `agent`
DefaultAgentPoolID *string `jsonapi:"attr,default-agent-pool-id,omitempty"`
// Optional: Struct of booleans, which indicate whether the project
// specifies its own values for various settings. If you mark a setting as
// `false` in this struct, it will clear the project's existing value for
// that setting and defer to the default value that its organization provides.
//
// In general, it's not necessary to mark a setting as `true` in this
// struct; if you provide a literal value for a setting, HCP Terraform will
// automatically update its overwrites field to `true`. If you do choose to
// manually mark a setting as overwritten, you must provide a value for that
// setting at the same time.
SettingOverwrites *ProjectSettingOverwrites `jsonapi:"attr,setting-overwrites,omitempty"`
}
// ProjectAddTagBindingsOptions represents the options for adding tag bindings
// to a project.
type ProjectAddTagBindingsOptions struct {
TagBindings []*TagBinding
}
// List all projects.
func (s *projects) List(ctx context.Context, organization string, options *ProjectListOptions) (*ProjectList, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
var tagFilters map[string][]string
if options != nil {
tagFilters = encodeTagFiltersAsParams(options.TagBindings)
}
u := fmt.Sprintf("organizations/%s/projects", url.PathEscape(organization))
req, err := s.client.NewRequestWithAdditionalQueryParams("GET", u, options, tagFilters)
if err != nil {
return nil, err
}
p := &ProjectList{}
err = req.Do(ctx, p)
if err != nil {
return nil, err
}
return p, nil
}
// Create a project with the given options
func (s *projects) Create(ctx context.Context, organization string, options ProjectCreateOptions) (*Project, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("organizations/%s/projects", url.PathEscape(organization))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
p := &Project{}
err = req.Do(ctx, p)
if err != nil {
return nil, err
}
return p, nil
}
// ReadWithOptions a project by its ID.
func (s *projects) ReadWithOptions(ctx context.Context, projectID string, options ProjectReadOptions) (*Project, error) {
if !validStringID(&projectID) {
return nil, ErrInvalidProjectID
}
u := fmt.Sprintf("projects/%s", url.PathEscape(projectID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
p := &Project{}
err = req.Do(ctx, p)
if err != nil {
return nil, err
}
return p, nil
}
// Read a single project by its ID.
func (s *projects) Read(ctx context.Context, projectID string) (*Project, error) {
if !validStringID(&projectID) {
return nil, ErrInvalidProjectID
}
u := fmt.Sprintf("projects/%s", url.PathEscape(projectID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
p := &Project{}
err = req.Do(ctx, p)
if err != nil {
return nil, err
}
return p, nil
}
func (s *projects) ListTagBindings(ctx context.Context, projectID string) ([]*TagBinding, error) {
if !validStringID(&projectID) {
return nil, ErrInvalidProjectID
}
u := fmt.Sprintf("projects/%s/tag-bindings", url.PathEscape(projectID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
var list struct {
*Pagination
Items []*TagBinding
}
err = req.Do(ctx, &list)
if err != nil {
return nil, err
}
return list.Items, nil
}
func (s *projects) ListEffectiveTagBindings(ctx context.Context, projectID string) ([]*EffectiveTagBinding, error) {
if !validStringID(&projectID) {
return nil, ErrInvalidProjectID
}
u := fmt.Sprintf("projects/%s/effective-tag-bindings", url.PathEscape(projectID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
var list struct {
*Pagination
Items []*EffectiveTagBinding
}
err = req.Do(ctx, &list)
if err != nil {
return nil, err
}
return list.Items, nil
}
// AddTagBindings adds or modifies the value of existing tag binding keys for a project
func (s *projects) AddTagBindings(ctx context.Context, projectID string, options ProjectAddTagBindingsOptions) ([]*TagBinding, error) {
if !validStringID(&projectID) {
return nil, ErrInvalidProjectID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("projects/%s/tag-bindings", url.PathEscape(projectID))
req, err := s.client.NewRequest("PATCH", u, options.TagBindings)
if err != nil {
return nil, err
}
var response = struct {
*Pagination
Items []*TagBinding
}{}
err = req.Do(ctx, &response)
return response.Items, err
}
// Update a project by its ID
func (s *projects) Update(ctx context.Context, projectID string, options ProjectUpdateOptions) (*Project, error) {
if !validStringID(&projectID) {
return nil, ErrInvalidProjectID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("projects/%s", url.PathEscape(projectID))
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
p := &Project{}
err = req.Do(ctx, p)
if err != nil {
return nil, err
}
return p, nil
}
// Delete a project by its ID
func (s *projects) Delete(ctx context.Context, projectID string) error {
if !validStringID(&projectID) {
return ErrInvalidProjectID
}
u := fmt.Sprintf("projects/%s", url.PathEscape(projectID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// Delete all tag bindings associated with a project.
func (s *projects) DeleteAllTagBindings(ctx context.Context, projectID string) error {
if !validStringID(&projectID) {
return ErrInvalidProjectID
}
type aliasOpts struct {
Type string `jsonapi:"primary,projects"`
TagBindings []*TagBinding `jsonapi:"relation,tag-bindings"`
}
opts := &aliasOpts{
TagBindings: []*TagBinding{},
}
u := fmt.Sprintf("projects/%s", url.PathEscape(projectID))
req, err := s.client.NewRequest("PATCH", u, opts)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o ProjectCreateOptions) valid() error {
if !validString(&o.Name) {
return ErrRequiredName
}
return nil
}
func (o ProjectUpdateOptions) valid() error {
return nil
}
func (o ProjectAddTagBindingsOptions) valid() error {
if len(o.TagBindings) == 0 {
return ErrRequiredTagBindings
}
return nil
}
================================================
FILE: projects_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/hashicorp/jsonapi"
)
func TestProjectsList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
pTest1, pTestCleanup := createProject(t, client, orgTest)
t.Cleanup(pTestCleanup)
pTest2, pTestCleanup := createProject(t, client, orgTest)
t.Cleanup(pTestCleanup)
t.Run("without list options", func(t *testing.T) {
pl, err := client.Projects.List(ctx, orgTest.Name, nil)
require.NoError(t, err)
assert.Contains(t, pl.Items, pTest1)
assert.Equal(t, 1, pl.CurrentPage)
assert.Equal(t, 3, pl.TotalCount)
})
t.Run("with pagination list options", func(t *testing.T) {
pl, err := client.Projects.List(ctx, orgTest.Name, &ProjectListOptions{
ListOptions: ListOptions{
PageNumber: 1,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Contains(t, pl.Items, pTest1)
assert.Contains(t, pl.Items, pTest2)
assert.Equal(t, true, containsProject(pl.Items, "Default Project"))
assert.Equal(t, 3, len(pl.Items))
})
t.Run("with query list option", func(t *testing.T) {
pl, err := client.Projects.List(ctx, orgTest.Name, &ProjectListOptions{
Query: "Default",
})
require.NoError(t, err)
assert.Equal(t, true, containsProject(pl.Items, "Default Project"))
assert.Equal(t, 1, len(pl.Items))
})
t.Run("without a valid organization", func(t *testing.T) {
pl, err := client.Projects.List(ctx, badIdentifier, nil)
assert.Nil(t, pl)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("when using a tags filter", func(t *testing.T) {
skipUnlessBeta(t)
p1, pTestCleanup1 := createProjectWithOptions(t, client, orgTest, ProjectCreateOptions{
Name: randomStringWithoutSpecialChar(t),
TagBindings: []*TagBinding{
{Key: "key1", Value: "value1"},
{Key: "key2", Value: "value2a"},
},
})
p2, pTestCleanup2 := createProjectWithOptions(t, client, orgTest, ProjectCreateOptions{
Name: randomStringWithoutSpecialChar(t),
TagBindings: []*TagBinding{
{Key: "key2", Value: "value2b"},
{Key: "key3", Value: "value3"},
},
})
t.Cleanup(pTestCleanup1)
t.Cleanup(pTestCleanup2)
// List all the workspaces under the given tag
pl, err := client.Projects.List(ctx, orgTest.Name, &ProjectListOptions{
TagBindings: []*TagBinding{
{Key: "key1"},
},
})
assert.NoError(t, err)
assert.Len(t, pl.Items, 1)
assert.Contains(t, pl.Items, p1)
pl2, err := client.Projects.List(ctx, orgTest.Name, &ProjectListOptions{
TagBindings: []*TagBinding{
{Key: "key2"},
},
})
assert.NoError(t, err)
assert.Len(t, pl2.Items, 2)
assert.Contains(t, pl2.Items, p1, p2)
pl3, err := client.Projects.List(ctx, orgTest.Name, &ProjectListOptions{
TagBindings: []*TagBinding{
{Key: "key2", Value: "value2b"},
},
})
assert.NoError(t, err)
assert.Len(t, pl3.Items, 1)
assert.Contains(t, pl3.Items, p2)
})
t.Run("when including effective tags relationship", func(t *testing.T) {
skipUnlessBeta(t)
orgTest2, orgTest2Cleanup := createOrganization(t, client)
t.Cleanup(orgTest2Cleanup)
_, pTestCleanup1 := createProjectWithOptions(t, client, orgTest2, ProjectCreateOptions{
Name: randomStringWithoutSpecialChar(t),
TagBindings: []*TagBinding{
{Key: "key1", Value: "value1"},
{Key: "key2", Value: "value2a"},
},
})
t.Cleanup(pTestCleanup1)
pl, err := client.Projects.List(ctx, orgTest2.Name, &ProjectListOptions{
Include: []ProjectIncludeOpt{ProjectEffectiveTagBindings},
})
require.NoError(t, err)
require.Len(t, pl.Items, 2)
require.Len(t, pl.Items[0].EffectiveTagBindings, 2)
assert.NotEmpty(t, pl.Items[0].EffectiveTagBindings[0].Key)
assert.NotEmpty(t, pl.Items[0].EffectiveTagBindings[0].Value)
assert.NotEmpty(t, pl.Items[0].EffectiveTagBindings[1].Key)
assert.NotEmpty(t, pl.Items[0].EffectiveTagBindings[1].Value)
})
}
func TestProjectsReadWithOptions(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
pTest, pTestCleanup := createProjectWithOptions(t, client, orgTest, ProjectCreateOptions{
Name: "project-with-tags",
TagBindings: []*TagBinding{
{Key: "foo", Value: "bar"},
},
})
t.Cleanup(pTestCleanup)
t.Run("when the project exists", func(t *testing.T) {
p, err := client.Projects.ReadWithOptions(ctx, pTest.ID, ProjectReadOptions{
Include: []ProjectIncludeOpt{ProjectEffectiveTagBindings},
})
require.NoError(t, err)
assert.Equal(t, orgTest.Name, p.Organization.Name)
// Tag data is included
require.Len(t, p.EffectiveTagBindings, 1)
assert.Equal(t, "foo", p.EffectiveTagBindings[0].Key)
assert.Equal(t, "bar", p.EffectiveTagBindings[0].Value)
})
}
func TestProjectsRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
pTest, pTestCleanup := createProject(t, client, orgTest)
t.Cleanup(pTestCleanup)
t.Run("when the project exists", func(t *testing.T) {
w, err := client.Projects.Read(ctx, pTest.ID)
require.NoError(t, err)
assert.Equal(t, pTest, w)
assert.Equal(t, orgTest.Name, w.Organization.Name)
})
t.Run("when the project does not exist", func(t *testing.T) {
w, err := client.Projects.Read(ctx, "nonexisting")
assert.Nil(t, w)
assert.Error(t, err)
})
t.Run("without a valid project ID", func(t *testing.T) {
w, err := client.Projects.Read(ctx, badIdentifier)
assert.Nil(t, w)
assert.EqualError(t, err, ErrInvalidProjectID.Error())
})
t.Run("with default execution mode of 'agent'", func(t *testing.T) {
agentPoolTest, agentPoolTestCleanup := createAgentPool(t, client, orgTest)
t.Cleanup(agentPoolTestCleanup)
proj, projCleanup := createProjectWithOptions(t, client, orgTest, ProjectCreateOptions{
Name: "project-with-agent-pool",
DefaultExecutionMode: String("agent"),
DefaultAgentPoolID: String(agentPoolTest.ID),
})
t.Cleanup(projCleanup)
t.Run("execution mode and agent pool are properly decoded", func(t *testing.T) {
assert.Equal(t, "agent", proj.DefaultExecutionMode)
assert.NotNil(t, proj.DefaultAgentPool)
assert.Equal(t, proj.DefaultAgentPool.ID, agentPoolTest.ID)
})
})
t.Run("when project is inheriting the default execution mode", func(t *testing.T) {
defaultExecutionOrgTest, defaultExecutionOrgTestCleanup := createOrganizationWithDefaultAgentPool(t, client)
t.Cleanup(defaultExecutionOrgTestCleanup)
options := ProjectCreateOptions{
Name: fmt.Sprintf("tst-%s", randomString(t)[0:20]),
SettingOverwrites: &ProjectSettingOverwrites{
ExecutionMode: Bool(false),
AgentPool: Bool(false),
},
}
pDefaultTest, pDefaultTestCleanup := createProjectWithOptions(t, client, defaultExecutionOrgTest, options)
t.Cleanup(pDefaultTestCleanup)
t.Run("and project execution mode is default", func(t *testing.T) {
p, err := client.Projects.Read(ctx, pDefaultTest.ID)
assert.NoError(t, err)
assert.NotEmpty(t, p)
assert.Equal(t, defaultExecutionOrgTest.DefaultExecutionMode, p.DefaultExecutionMode)
require.NotNil(t, p.SettingOverwrites)
assert.Equal(t, false, *p.SettingOverwrites.ExecutionMode)
assert.Equal(t, false, *p.SettingOverwrites.ExecutionMode)
})
})
}
func TestProjectsCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t)
t.Run("with valid options", func(t *testing.T) {
options := ProjectCreateOptions{
Name: "foo",
Description: String("qux"),
}
w, err := client.Projects.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
refreshed, err := client.Projects.Read(ctx, w.ID)
require.NoError(t, err)
for _, item := range []*Project{
w,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, options.Name, item.Name)
assert.Equal(t, *options.Description, item.Description)
}
})
t.Run("when options is missing name", func(t *testing.T) {
w, err := client.Projects.Create(ctx, orgTest.Name, ProjectCreateOptions{})
assert.Nil(t, w)
assert.EqualError(t, err, ErrRequiredName.Error())
})
t.Run("when options has an invalid name", func(t *testing.T) {
w, err := client.Projects.Create(ctx, orgTest.Name, ProjectCreateOptions{
Name: badIdentifier,
})
assert.Nil(t, w)
assert.Contains(t, err.Error(), "invalid attribute\n\nName may only contain")
})
t.Run("when options has an invalid organization", func(t *testing.T) {
w, err := client.Projects.Create(ctx, badIdentifier, ProjectCreateOptions{
Name: "foo",
})
assert.Nil(t, w)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("when options has an invalid auto destroy activity duration", func(t *testing.T) {
skipUnlessBeta(t)
w, err := client.Projects.Create(ctx, orgTest.Name, ProjectCreateOptions{
Name: "foo",
AutoDestroyActivityDuration: jsonapi.NewNullableAttrWithValue("20m"),
})
assert.Nil(t, w)
assert.Contains(t, err.Error(), "invalid attribute\n\nAuto destroy activity duration has an incorrect format, we expect up to 4 numeric digits and 1 unit ('d' or 'h')")
})
t.Run("when a default agent pool ID is specified without 'agent' execution mode", func(t *testing.T) {
agentPoolTest, agentPoolTestCleanup := createAgentPool(t, client, orgTest)
t.Cleanup(agentPoolTestCleanup)
p, err := client.Projects.Create(ctx, orgTest.Name, ProjectCreateOptions{
Name: fmt.Sprintf("foo-%s", randomString(t)),
DefaultExecutionMode: String("remote"),
DefaultAgentPoolID: String(agentPoolTest.ID),
})
assert.Nil(t, p)
assert.ErrorContains(t, err, "must not be specified unless using 'agent' execution mode")
})
t.Run("when 'agent' execution mode is specified without an a default agent pool ID", func(t *testing.T) {
p, err := client.Projects.Create(ctx, orgTest.Name, ProjectCreateOptions{
Name: fmt.Sprintf("foo-%s", randomString(t)),
DefaultExecutionMode: String("agent"),
})
assert.Nil(t, p)
assert.ErrorContains(t, err, "must be specified when using 'agent' execution mode")
})
t.Run("when no execution mode is specified, in an organization with local as default execution mode", func(t *testing.T) {
orgTest, orgTestCleanup := createOrganizationWithOptions(t, client, OrganizationCreateOptions{
Name: String("tst-" + randomString(t)[0:20]),
Email: String(fmt.Sprintf("%s@tfe.local", randomString(t))),
DefaultExecutionMode: String("local"),
})
t.Cleanup(orgTestCleanup)
options := ProjectCreateOptions{
Name: fmt.Sprintf("foo-%s", randomString(t)),
SettingOverwrites: &ProjectSettingOverwrites{
ExecutionMode: Bool(false),
AgentPool: Bool(false),
},
}
p, err := client.Projects.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
// Get a refreshed view from the API.
refreshed, err := client.Projects.Read(ctx, p.ID)
require.NoError(t, err)
assert.Equal(t, "local", refreshed.DefaultExecutionMode)
})
t.Run("when agent pool and execution mode setting overwrites do not match", func(t *testing.T) {
agentPoolTest, agentPoolTestCleanup := createAgentPool(t, client, orgTest)
t.Cleanup(agentPoolTestCleanup)
p, err := client.Projects.Create(ctx, orgTest.Name, ProjectCreateOptions{
Name: fmt.Sprintf("foo-%s", randomString(t)),
DefaultExecutionMode: String("agent"),
DefaultAgentPoolID: String(agentPoolTest.ID),
SettingOverwrites: &ProjectSettingOverwrites{
AgentPool: Bool(false),
ExecutionMode: Bool(true),
},
})
assert.Nil(t, p)
assert.Contains(t, err.Error(), "If agent-pool and execution-mode are both included in setting-overwrites, their values must be the same.")
})
t.Run("when organization has a default execution mode", func(t *testing.T) {
defaultExecutionOrgTest, defaultExecutionOrgTestCleanup := createOrganizationWithDefaultAgentPool(t, client)
t.Cleanup(defaultExecutionOrgTestCleanup)
t.Run("with setting overwrites set to false, project inherits the default execution mode", func(t *testing.T) {
options := ProjectCreateOptions{
Name: fmt.Sprintf("tst-proj-%s", randomString(t)[0:20]),
SettingOverwrites: &ProjectSettingOverwrites{
ExecutionMode: Bool(false),
AgentPool: Bool(false),
},
}
p, err := client.Projects.Create(ctx, defaultExecutionOrgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, "agent", p.DefaultExecutionMode)
})
t.Run("with setting overwrites set to true, project ignores the default execution mode", func(t *testing.T) {
options := ProjectCreateOptions{
Name: fmt.Sprintf("tst-proj-%s", randomString(t)[0:20]),
DefaultExecutionMode: String("local"),
SettingOverwrites: &ProjectSettingOverwrites{
ExecutionMode: Bool(true),
AgentPool: Bool(true),
},
}
p, err := client.Projects.Create(ctx, defaultExecutionOrgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, "local", p.DefaultExecutionMode)
})
t.Run("when explicitly setting default execution mode, project ignores the org default execution mode", func(t *testing.T) {
options := ProjectCreateOptions{
Name: fmt.Sprintf("tst-proj-%s", randomString(t)[0:20]),
DefaultExecutionMode: String("remote"),
}
p, err := client.Projects.Create(ctx, defaultExecutionOrgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, "remote", p.DefaultExecutionMode)
})
})
}
func TestProjectsUpdate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
agentPoolTest, agentPoolTestCleanup := createAgentPool(t, client, orgTest)
t.Cleanup(agentPoolTestCleanup)
t.Run("with valid options", func(t *testing.T) {
kBefore, kTestCleanup := createProject(t, client, orgTest)
t.Cleanup(kTestCleanup)
kAfter, err := client.Projects.Update(ctx, kBefore.ID, ProjectUpdateOptions{
Name: String("new project name"),
Description: String("updated description"),
TagBindings: []*TagBinding{
{Key: "foo", Value: "bar"},
},
DefaultExecutionMode: String("agent"),
DefaultAgentPoolID: String(agentPoolTest.ID),
})
require.NoError(t, err)
assert.Equal(t, kBefore.ID, kAfter.ID)
assert.NotEqual(t, kBefore.Name, kAfter.Name)
assert.NotEqual(t, kBefore.Description, kAfter.Description)
assert.NotEqual(t, kBefore.DefaultExecutionMode, kAfter.DefaultExecutionMode)
assert.NotEqual(t, kBefore.DefaultAgentPool, kAfter.DefaultAgentPool)
if betaFeaturesEnabled() {
bindings, err := client.Projects.ListTagBindings(ctx, kAfter.ID)
require.NoError(t, err)
require.Len(t, bindings, 1)
assert.Equal(t, "foo", bindings[0].Key)
assert.Equal(t, "bar", bindings[0].Value)
effectiveBindings, err := client.Projects.ListEffectiveTagBindings(ctx, kAfter.ID)
require.NoError(t, err)
require.Len(t, effectiveBindings, 1)
assert.Equal(t, "foo", effectiveBindings[0].Key)
assert.Equal(t, "bar", effectiveBindings[0].Value)
ws, err := client.Workspaces.Create(ctx, orgTest.Name, WorkspaceCreateOptions{
Name: String("new-workspace-inherits-tags"),
Project: kAfter,
TagBindings: []*TagBinding{
{Key: "baz", Value: "qux"},
},
})
require.NoError(t, err)
t.Cleanup(func() {
err := client.Workspaces.DeleteByID(ctx, ws.ID)
if err != nil {
t.Errorf("Error destroying workspace! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"Error: %s", err)
}
})
wsEffectiveBindings, err := client.Workspaces.ListEffectiveTagBindings(ctx, ws.ID)
require.NoError(t, err)
assert.Len(t, wsEffectiveBindings, 2)
for _, b := range wsEffectiveBindings {
switch b.Key {
case "foo":
assert.Equal(t, "bar", b.Value)
case "baz":
assert.Equal(t, "qux", b.Value)
default:
assert.Fail(t, "unexpected tag binding %q", b.Key)
}
}
}
})
t.Run("when updating with invalid name", func(t *testing.T) {
kBefore, kTestCleanup := createProject(t, client, orgTest)
t.Cleanup(kTestCleanup)
kAfter, err := client.Projects.Update(ctx, kBefore.ID, ProjectUpdateOptions{
Name: String(badIdentifier),
})
assert.Nil(t, kAfter)
assert.Contains(t, err.Error(), "invalid attribute\n\nName may only contain")
})
t.Run("without a valid projects ID", func(t *testing.T) {
w, err := client.Projects.Update(ctx, badIdentifier, ProjectUpdateOptions{})
assert.Nil(t, w)
assert.EqualError(t, err, ErrInvalidProjectID.Error())
})
t.Run("without a valid projects auto destroy activity duration", func(t *testing.T) {
skipUnlessBeta(t)
newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t)
kBefore, kTestCleanup := createProject(t, client, orgTest)
t.Cleanup(kTestCleanup)
w, err := client.Projects.Update(ctx, kBefore.ID, ProjectUpdateOptions{
AutoDestroyActivityDuration: jsonapi.NewNullableAttrWithValue("bar"),
})
assert.Nil(t, w)
assert.Contains(t, err.Error(), "invalid attribute\n\nAuto destroy activity duration has an incorrect format, we expect up to 4 numeric digits and 1 unit ('d' or 'h')")
})
t.Run("with agent pool provided, but remote execution mode", func(t *testing.T) {
kBefore, kTestCleanup := createProject(t, client, orgTest)
t.Cleanup(kTestCleanup)
pool, agentPoolCleanup := createAgentPool(t, client, orgTest)
t.Cleanup(agentPoolCleanup)
proj, err := client.Projects.Update(ctx, kBefore.ID, ProjectUpdateOptions{
DefaultExecutionMode: String("local"),
DefaultAgentPoolID: String(pool.ID),
})
assert.Nil(t, proj)
assert.ErrorContains(t, err, "must not be specified unless using 'agent' execution mode")
})
t.Run("with different default execution modes", func(t *testing.T) {
proj, projCleanup := createProject(t, client, orgTest)
t.Cleanup(projCleanup)
agentPool, agenPoolCleanup := createAgentPool(t, client, orgTest)
t.Cleanup(agenPoolCleanup)
assert.Equal(t, "remote", proj.DefaultExecutionMode)
assert.Nil(t, proj.DefaultAgentPool)
// assert that project's execution mode can be updated from 'remote' -> 'agent'
proj, err := client.Projects.Update(ctx, proj.ID, ProjectUpdateOptions{
DefaultExecutionMode: String("agent"),
DefaultAgentPoolID: String(agentPool.ID),
})
require.NoError(t, err)
assert.Equal(t, "agent", proj.DefaultExecutionMode)
assert.Equal(t, agentPool.ID, proj.DefaultAgentPool.ID)
// assert that project's execution mode can be updated from 'agent' -> 'remote'
proj, err = client.Projects.Update(ctx, proj.ID, ProjectUpdateOptions{
DefaultExecutionMode: String("remote"),
})
require.NoError(t, err)
assert.Equal(t, "remote", proj.DefaultExecutionMode)
assert.Nil(t, proj.DefaultAgentPool)
// assert that project's execution mode can be updated from 'remote' -> 'local'
proj, err = client.Projects.Update(ctx, proj.ID, ProjectUpdateOptions{
DefaultExecutionMode: String("local"),
})
require.NoError(t, err)
assert.Equal(t, "local", proj.DefaultExecutionMode)
assert.Nil(t, proj.DefaultAgentPool)
})
t.Run("with setting overwrites set to true, project ignores the default execution mode", func(t *testing.T) {
defaultExecutionOrgTest, defaultExecutionOrgTestCleanup := createOrganizationWithDefaultAgentPool(t, client)
t.Cleanup(defaultExecutionOrgTestCleanup)
kBefore, kTestCleanup := createProject(t, client, defaultExecutionOrgTest)
t.Cleanup(kTestCleanup)
options := ProjectUpdateOptions{
DefaultExecutionMode: String("local"),
SettingOverwrites: &ProjectSettingOverwrites{
ExecutionMode: Bool(true),
AgentPool: Bool(true),
},
}
p, err := client.Projects.Update(ctx, kBefore.ID, options)
require.NoError(t, err)
assert.Equal(t, "local", p.DefaultExecutionMode)
})
t.Run("with setting overwrites set to false, project inherits the default execution mode", func(t *testing.T) {
defaultExecutionOrgTest, defaultExecutionOrgTestCleanup := createOrganizationWithDefaultAgentPool(t, client)
t.Cleanup(defaultExecutionOrgTestCleanup)
kBefore, kTestCleanup := createProject(t, client, defaultExecutionOrgTest)
t.Cleanup(kTestCleanup)
options := ProjectUpdateOptions{
SettingOverwrites: &ProjectSettingOverwrites{
ExecutionMode: Bool(false),
AgentPool: Bool(false),
},
}
p, err := client.Projects.Update(ctx, kBefore.ID, options)
require.NoError(t, err)
assert.Equal(t, "agent", p.DefaultExecutionMode)
})
}
func TestProjectsAddTagBindings(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
pTest, wCleanup := createProject(t, client, nil)
t.Cleanup(wCleanup)
t.Run("when adding tag bindings to a project", func(t *testing.T) {
tagBindings := []*TagBinding{
{Key: "foo", Value: "bar"},
{Key: "baz", Value: "qux"},
}
bindings, err := client.Projects.AddTagBindings(ctx, pTest.ID, ProjectAddTagBindingsOptions{
TagBindings: tagBindings,
})
require.NoError(t, err)
require.Len(t, bindings, 2)
assert.Equal(t, tagBindings[0].Key, bindings[0].Key)
assert.Equal(t, tagBindings[0].Value, bindings[0].Value)
assert.Equal(t, tagBindings[1].Key, bindings[1].Key)
assert.Equal(t, tagBindings[1].Value, bindings[1].Value)
})
t.Run("when adding 26 tags", func(t *testing.T) {
tagBindings := []*TagBinding{
{Key: "alpha"},
{Key: "bravo"},
{Key: "charlie"},
{Key: "delta"},
{Key: "echo"},
{Key: "foxtrot"},
{Key: "golf"},
{Key: "hotel"},
{Key: "india"},
{Key: "juliet"},
{Key: "kilo"},
{Key: "lima"},
{Key: "mike"},
{Key: "november"},
{Key: "oscar"},
{Key: "papa"},
{Key: "quebec"},
{Key: "romeo"},
{Key: "sierra"},
{Key: "tango"},
{Key: "uniform"},
{Key: "victor"},
{Key: "whiskey"},
{Key: "xray"},
{Key: "yankee"},
{Key: "zulu"},
}
_, err := client.Workspaces.AddTagBindings(ctx, pTest.ID, WorkspaceAddTagBindingsOptions{
TagBindings: tagBindings,
})
require.Error(t, err, "cannot exceed 10 bindings per resource")
})
}
func TestProjects_DeleteAllTagBindings(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
pTest, wCleanup := createProject(t, client, nil)
t.Cleanup(wCleanup)
tagBindings := []*TagBinding{
{Key: "foo", Value: "bar"},
{Key: "baz", Value: "qux"},
}
_, err := client.Projects.AddTagBindings(ctx, pTest.ID, ProjectAddTagBindingsOptions{
TagBindings: tagBindings,
})
require.NoError(t, err)
err = client.Projects.DeleteAllTagBindings(ctx, pTest.ID)
require.NoError(t, err)
bindings, err := client.Projects.ListTagBindings(ctx, pTest.ID)
require.NoError(t, err)
require.Empty(t, bindings)
}
func TestProjectsDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
pTest, _ := createProject(t, client, orgTest)
t.Run("with valid options", func(t *testing.T) {
err := client.Projects.Delete(ctx, pTest.ID)
require.NoError(t, err)
// Try loading the project - it should fail.
_, err = client.Projects.Read(ctx, pTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the project does not exist", func(t *testing.T) {
err := client.Projects.Delete(ctx, pTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the project ID is invalid", func(t *testing.T) {
err := client.Projects.Delete(ctx, badIdentifier)
assert.EqualError(t, err, ErrInvalidProjectID.Error())
})
}
func TestProjectsAutoDestroy(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t)
t.Run("when creating workspace in project with autodestroy", func(t *testing.T) {
options := ProjectCreateOptions{
Name: "foo",
Description: String("qux"),
AutoDestroyActivityDuration: jsonapi.NewNullableAttrWithValue("3d"),
}
p, err := client.Projects.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
w, _ := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{
Name: String(randomString(t)),
Project: p,
})
assert.Equal(t, p.AutoDestroyActivityDuration, w.AutoDestroyActivityDuration)
})
}
================================================
FILE: query_runs.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"io"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ QueryRuns = (*queryRuns)(nil)
// QueryRuns describes all the run related methods that the Terraform Enterprise
// API supports.
//
// TFE API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/query-runs
type QueryRuns interface {
// List all the query runs of the given workspace.
List(ctx context.Context, workspaceID string, options *QueryRunListOptions) (*QueryRunList, error)
// Create a new query run with the given options.
Create(ctx context.Context, options QueryRunCreateOptions) (*QueryRun, error)
// Read a query run by its ID.
Read(ctx context.Context, queryRunID string) (*QueryRun, error)
// ReadWithOptions reads a query run by its ID using the options supplied
ReadWithOptions(ctx context.Context, queryRunID string, options *QueryRunReadOptions) (*QueryRun, error)
// Logs retrieves the logs of a query run.
Logs(ctx context.Context, queryRunID string) (io.Reader, error)
// Cancel a query run by its ID.
Cancel(ctx context.Context, runID string) error
// Force-cancel a query run by its ID.
ForceCancel(ctx context.Context, runID string) error
}
// QueryRunCreateOptions represents the options for creating a new run.
type QueryRunCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,queries"`
// TerraformVersion specifies the Terraform version to use in this query run.
TerraformVersion *string `jsonapi:"attr,terraform-version,omitempty"`
Source QueryRunSource `jsonapi:"attr,source"`
// Specifies the configuration version to use for this query run. If the
// configuration version object is omitted, the run will be created using the
// workspace's latest configuration version.
ConfigurationVersion *ConfigurationVersion `jsonapi:"relation,configuration-version"`
// Specifies the workspace where the query run will be executed.
Workspace *Workspace `jsonapi:"relation,workspace"`
// Variables allows you to specify terraform input variables for
// a particular run, prioritized over variables defined on the workspace.
Variables []*RunVariable `jsonapi:"attr,variables,omitempty"`
}
// QueryRunStatusTimestamps holds the timestamps for individual run statuses.
type QueryRunStatusTimestamps struct {
CanceledAt time.Time `jsonapi:"attr,canceled-at,rfc3339"`
ErroredAt time.Time `jsonapi:"attr,errored-at,rfc3339"`
ForceCanceledAt time.Time `jsonapi:"attr,force-canceled-at,rfc3339"`
QueuingAt time.Time `jsonapi:"attr,queuing-at,rfc3339"`
FinishedAt time.Time `jsonapi:"attr,finished-at,rfc3339"`
RunningAt time.Time `jsonapi:"attr,running-at,rfc3339"`
}
// QueryRunIncludeOpt represents the available options for include query params.
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#available-related-resources
type QueryRunIncludeOpt string
// QueryRunSource represents the available sources for query runs.
type QueryRunSource string
// QueryRunStatus is the query run state
type QueryRunStatus string
// List all available run statuses.
const (
QueryRunCanceled QueryRunStatus = "canceled"
QueryRunErrored QueryRunStatus = "errored"
QueryRunPending QueryRunStatus = "pending"
QueryRunQueued QueryRunStatus = "queued"
QueryRunRunning QueryRunStatus = "running"
QueryRunFinished QueryRunStatus = "finished"
)
// List all available run sources.
const (
QueryRunSourceAPI QueryRunSource = "tfe-api"
)
const (
QueryRunCreatedBy QueryRunIncludeOpt = "created_by"
QueryRunConfigVer QueryRunIncludeOpt = "configuration_version"
)
// queryRuns implements QueryRuns.
type queryRuns struct {
client *Client
}
// QueryRunList represents a list of query runs.
type QueryRunList struct {
*Pagination
Items []*QueryRun
}
// QueryRunListOptions represents the options for listing query runs.
type QueryRunListOptions struct {
ListOptions
Include []QueryRunIncludeOpt `url:"include,omitempty"`
}
// QueryRunReadOptions represents the options for reading a query run.
type QueryRunReadOptions struct {
Include []QueryRunIncludeOpt `url:"include,omitempty"`
}
// QueryRun represents a Terraform Enterprise query run.
type QueryRun struct {
ID string `jsonapi:"primary,queries"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Source QueryRunSource `jsonapi:"attr,source"`
Status QueryRunStatus `jsonapi:"attr,status"`
StatusTimestamps *QueryRunStatusTimestamps `jsonapi:"attr,status-timestamps"`
TerraformVersion string `jsonapi:"attr,terraform-version"`
Variables []*RunVariableAttr `jsonapi:"attr,variables"`
LogReadURL string `jsonapi:"attr,log-read-url"`
// Relations
ConfigurationVersion *ConfigurationVersion `jsonapi:"relation,configuration-version"`
CreatedBy *User `jsonapi:"relation,created-by"`
CanceledBy *User `jsonapi:"relation,canceled-by"`
Workspace *Workspace `jsonapi:"relation,workspace"`
}
func (o *QueryRunListOptions) valid() error {
return nil
}
func (o QueryRunCreateOptions) valid() error {
if o.Workspace == nil {
return ErrRequiredWorkspace
}
return nil
}
func (r *queryRuns) List(ctx context.Context, workspaceID string, options *QueryRunListOptions) (*QueryRunList, error) {
if workspaceID == "" {
return nil, ErrInvalidWorkspaceID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("workspaces/%s/queries", url.PathEscape(workspaceID))
req, err := r.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
var runs QueryRunList
if err := req.Do(ctx, &runs); err != nil {
return nil, err
}
return &runs, nil
}
func (r *queryRuns) Create(ctx context.Context, options QueryRunCreateOptions) (*QueryRun, error) {
if err := options.valid(); err != nil {
return nil, err
}
req, err := r.client.NewRequest("POST", "queries", &options)
if err != nil {
return nil, err
}
var run QueryRun
if err := req.Do(ctx, &run); err != nil {
return nil, err
}
return &run, nil
}
func (r *queryRuns) Read(ctx context.Context, queryRunID string) (*QueryRun, error) {
return r.ReadWithOptions(ctx, queryRunID, &QueryRunReadOptions{})
}
func (r *queryRuns) ReadWithOptions(ctx context.Context, queryRunID string, options *QueryRunReadOptions) (*QueryRun, error) {
if queryRunID == "" {
return nil, ErrInvalidQueryRunID
}
u := fmt.Sprintf("queries/%s", url.PathEscape(queryRunID))
req, err := r.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
var run QueryRun
if err := req.Do(ctx, &run); err != nil {
return nil, err
}
return &run, nil
}
func (r *queryRuns) Logs(ctx context.Context, queryRunID string) (io.Reader, error) {
if !validStringID(&queryRunID) {
return nil, ErrInvalidQueryRunID
}
// Get the query to make sure it exists.
q, err := r.Read(ctx, queryRunID)
if err != nil {
return nil, err
}
// Return an error if the log URL is empty.
if q.LogReadURL == "" {
return nil, fmt.Errorf("query %s does not have a log URL", queryRunID)
}
u, err := url.Parse(q.LogReadURL)
if err != nil {
return nil, fmt.Errorf("invalid log URL: %w", err)
}
done := func() (bool, error) {
p, err := r.Read(ctx, q.ID)
if err != nil {
return false, err
}
switch p.Status {
case QueryRunCanceled, QueryRunErrored, QueryRunFinished:
return true, nil
default:
return false, nil
}
}
return &LogReader{
client: r.client,
ctx: ctx,
done: done,
logURL: u,
}, nil
}
func (r *queryRuns) Cancel(ctx context.Context, queryRunID string) error {
if queryRunID == "" {
return ErrInvalidQueryRunID
}
u := fmt.Sprintf("queries/%s/actions/cancel", url.PathEscape(queryRunID))
req, err := r.client.NewRequest("POST", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (r *queryRuns) ForceCancel(ctx context.Context, queryRunID string) error {
if queryRunID == "" {
return ErrInvalidQueryRunID
}
u := fmt.Sprintf("queries/%s/actions/force-cancel", url.PathEscape(queryRunID))
req, err := r.client.NewRequest("POST", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
================================================
FILE: query_runs_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// createQueryRun creates a query run in the given workspace.
// A configuration version is created and uploaded to ensure the run can be processed.
func createQueryRun(t *testing.T, client *Client, workspace *Workspace) *QueryRun {
t.Helper()
createUploadedConfigurationVersion(t, client, workspace)
options := QueryRunCreateOptions{
Workspace: workspace,
Source: QueryRunSourceAPI,
}
queryRun, err := client.QueryRuns.Create(context.Background(), options)
require.NoError(t, err)
return queryRun
}
func TestQueryRunsList_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wTest, _ := createWorkspace(t, client, orgTest)
_ = createQueryRun(t, client, wTest)
_ = createQueryRun(t, client, wTest)
t.Run("without list options", func(t *testing.T) {
qrl, err := client.QueryRuns.List(ctx, wTest.ID, nil)
require.NoError(t, err)
// The API returns Run objects, not QueryRun objects.
// We can't easily correlate the created QueryRun with the returned Run.
// So we just check the count.
assert.Len(t, qrl.Items, 2)
assert.Equal(t, 1, qrl.CurrentPage)
assert.Equal(t, 2, qrl.TotalCount)
})
t.Run("without list options and include as nil", func(t *testing.T) {
qrl, err := client.QueryRuns.List(ctx, wTest.ID, &QueryRunListOptions{
Include: []QueryRunIncludeOpt{},
})
require.NoError(t, err)
require.NotEmpty(t, qrl.Items)
assert.Len(t, qrl.Items, 2)
assert.Equal(t, 1, qrl.CurrentPage)
assert.Equal(t, 2, qrl.TotalCount)
})
t.Run("with list options", func(t *testing.T) {
qrl, err := client.QueryRuns.List(ctx, wTest.ID, &QueryRunListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, qrl.Items)
assert.Equal(t, 999, qrl.CurrentPage)
assert.Equal(t, 2, qrl.TotalCount)
})
t.Run("with created_by included", func(t *testing.T) {
qrl, err := client.QueryRuns.List(ctx, wTest.ID, &QueryRunListOptions{
// The QueryRunIncludeOpt constants in query_runs.go have the wrong type.
// We use a string literal here as a workaround.
Include: []QueryRunIncludeOpt{"created-by"},
})
require.NoError(t, err)
require.NotEmpty(t, qrl.Items)
// The items are of type *Run, which has a CreatedBy field.
require.NotNil(t, qrl.Items[0].CreatedBy)
assert.NotEmpty(t, qrl.Items[0].CreatedBy.Username)
})
t.Run("without a valid workspace ID", func(t *testing.T) {
qrl, err := client.QueryRuns.List(ctx, badIdentifier, nil)
assert.Nil(t, qrl)
assert.EqualError(t, err, ErrResourceNotFound.Error())
})
}
func TestQueryRunsCreate_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
wTest, wTestCleanup := createWorkspace(t, client, nil)
defer wTestCleanup()
cvTest, _ := createUploadedConfigurationVersion(t, client, wTest)
t.Run("without a configuration version", func(t *testing.T) {
options := QueryRunCreateOptions{
Workspace: wTest,
Source: QueryRunSourceAPI,
}
qr, err := client.QueryRuns.Create(ctx, options)
require.NoError(t, err)
assert.NotNil(t, qr.ID)
assert.NotNil(t, qr.CreatedAt)
assert.NotNil(t, qr.Source)
require.NotNil(t, qr.StatusTimestamps)
})
t.Run("with a configuration version", func(t *testing.T) {
options := QueryRunCreateOptions{
ConfigurationVersion: cvTest,
Workspace: wTest,
Source: QueryRunSourceAPI,
}
qr, err := client.QueryRuns.Create(ctx, options)
require.NoError(t, err)
require.NotNil(t, qr.ConfigurationVersion)
assert.Equal(t, cvTest.ID, qr.ConfigurationVersion.ID)
})
t.Run("without a workspace", func(t *testing.T) {
qr, err := client.QueryRuns.Create(ctx, QueryRunCreateOptions{
Source: QueryRunSourceAPI,
})
assert.Nil(t, qr)
assert.Equal(t, err, ErrRequiredWorkspace)
})
t.Run("with variables", func(t *testing.T) {
t.Skip("Variables not yet implemented")
vars := []*RunVariable{
{
Key: "test_variable",
Value: "Hello, World!",
},
{
Key: "test_foo",
Value: "Hello, Foo!",
},
}
options := QueryRunCreateOptions{
Workspace: wTest,
Variables: vars,
Source: QueryRunSourceAPI,
}
qr, err := client.QueryRuns.Create(ctx, options)
require.NoError(t, err)
assert.NotNil(t, qr.Variables)
assert.Equal(t, len(vars), len(qr.Variables))
for _, v := range qr.Variables {
switch v.Key {
case "test_foo":
assert.Equal(t, v.Value, "Hello, Foo!")
case "test_variable":
assert.Equal(t, v.Value, "Hello, World!")
default:
t.Fatalf("Unexpected variable key: %s", v.Key)
}
}
})
}
func TestQueryRunsRead_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
wTest, wTestCleanup := createWorkspace(t, client, nil)
defer wTestCleanup()
qrTest := createQueryRun(t, client, wTest)
t.Run("when the query run exists", func(t *testing.T) {
qr, err := client.QueryRuns.Read(ctx, qrTest.ID)
require.NoError(t, err)
assert.Equal(t, qrTest.ID, qr.ID)
})
t.Run("when the query run does not exist", func(t *testing.T) {
qr, err := client.QueryRuns.Read(ctx, "nonexisting")
assert.Nil(t, qr)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("with invalid query run ID", func(t *testing.T) {
qr, err := client.QueryRuns.Read(ctx, badIdentifier)
assert.Nil(t, qr)
assert.EqualError(t, err, ErrResourceNotFound.Error())
})
}
func TestQueryRunsReadWithOptions_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
wTest, wTestCleanup := createWorkspace(t, client, nil)
defer wTestCleanup()
qrTest := createQueryRun(t, client, wTest)
t.Run("when the query run exists", func(t *testing.T) {
curOpts := &QueryRunReadOptions{
// The QueryRunIncludeOpt constants in query_runs.go have the wrong type.
// We use a string literal here as a workaround.
Include: []QueryRunIncludeOpt{"created-by"},
}
qr, err := client.QueryRuns.ReadWithOptions(ctx, qrTest.ID, curOpts)
require.NoError(t, err)
require.NotEmpty(t, qr.CreatedBy)
assert.NotEmpty(t, qr.CreatedBy.Username)
})
}
func TestQueryRunsCancel_RunDependent(t *testing.T) {
t.Skip("Cancel not yet implemented")
client := testClient(t)
ctx := context.Background()
wTest, wTestCleanup := createWorkspace(t, client, nil)
defer wTestCleanup()
// We need to create 2 query runs here. The first run associated with the query
// will automatically be planned so that one cannot be cancelled. The second one will
// be pending until the first one is confirmed or discarded, so we
// can cancel that one.
_ = createQueryRun(t, client, wTest)
qrTest2 := createQueryRun(t, client, wTest)
t.Run("when the query run exists and is cancelable", func(t *testing.T) {
// We assume the second query run is in a state that can be canceled.
err := client.QueryRuns.Cancel(ctx, qrTest2.ID)
require.NoError(t, err)
})
t.Run("when the query run does not exist", func(t *testing.T) {
err := client.QueryRuns.Cancel(ctx, "nonexisting")
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("with invalid query run ID", func(t *testing.T) {
err := client.QueryRuns.Cancel(ctx, badIdentifier)
assert.EqualError(t, err, ErrResourceNotFound.Error())
})
}
func TestQueryRunsLogs_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
wTest, wTestCleanup := createWorkspace(t, client, nil)
defer wTestCleanup()
qr, cleanup := createQueryRunWaitForAnyStatuses(t, client, wTest, []QueryRunStatus{QueryRunErrored, QueryRunFinished})
t.Cleanup(cleanup)
t.Run("when the query run exists", func(t *testing.T) {
// We assume the second query run is in a state that can be canceled.
reader, err := client.QueryRuns.Logs(ctx, qr.ID)
require.NoError(t, err)
logs, err := io.ReadAll(reader)
require.NoError(t, err)
assert.NotEmpty(t, logs, "some logs should be returned")
})
}
func TestQueryRunsForceCancel_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
wTest, wTestCleanup := createWorkspace(t, client, nil)
defer wTestCleanup()
_ = createQueryRun(t, client, wTest)
qrTest2 := createQueryRun(t, client, wTest)
// A force-cancel is not needed in any normal circumstance.
// We can't easily get a query run into a state where it can be force-canceled.
// So we'll just test the negative paths.
t.Run("when the query run is not in a force-cancelable state", func(t *testing.T) {
// This will likely return an error, but we are testing that the call can be made.
// The API should return a 409 Conflict in this case.
err := client.QueryRuns.ForceCancel(ctx, qrTest2.ID)
assert.Error(t, err)
})
t.Run("when the query run does not exist", func(t *testing.T) {
err := client.QueryRuns.ForceCancel(ctx, "nonexisting")
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("with invalid query run ID", func(t *testing.T) {
err := client.QueryRuns.ForceCancel(ctx, badIdentifier)
assert.EqualError(t, err, ErrResourceNotFound.Error())
})
}
================================================
FILE: registry_module.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strings"
)
type AgentExecutionMode string
const (
AgentExecutionModeAgent AgentExecutionMode = "agent"
AgentExecutionModeRemote AgentExecutionMode = "remote"
)
func (a *AgentExecutionMode) UnmarshalText(text []byte) error {
*a = AgentExecutionMode(string(text))
return nil
}
func (a AgentExecutionMode) MarshalText() ([]byte, error) {
return []byte(string(a)), nil
}
// Compile-time proof of interface implementation.
var _ RegistryModules = (*registryModules)(nil)
// RegistryModules describes all the registry module related methods that the Terraform
// Enterprise API supports.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/private-registry/modules
type RegistryModules interface {
// List all the registry modules within an organization
List(ctx context.Context, organization string, options *RegistryModuleListOptions) (*RegistryModuleList, error)
// ListCommits List the commits for the registry module
// This returns the latest 20 commits for the connected VCS repo.
// Pagination is not applicable due to inconsistent support from the VCS providers.
ListCommits(ctx context.Context, moduleID RegistryModuleID) (*CommitList, error)
// Create a registry module without a VCS repo
Create(ctx context.Context, organization string, options RegistryModuleCreateOptions) (*RegistryModule, error)
// Create a registry module version
CreateVersion(ctx context.Context, moduleID RegistryModuleID, options RegistryModuleCreateVersionOptions) (*RegistryModuleVersion, error)
// Create and publish a registry module with a VCS repo
CreateWithVCSConnection(ctx context.Context, options RegistryModuleCreateWithVCSConnectionOptions) (*RegistryModule, error)
// Read a registry module
Read(ctx context.Context, moduleID RegistryModuleID) (*RegistryModule, error)
// ReadVersion Read a registry module version
ReadVersion(ctx context.Context, moduleID RegistryModuleID, version string) (*RegistryModuleVersion, error)
// ReadTerraformRegistryModule Reads a registry module from the Terraform
// Registry, as opposed to Read or ReadVersion which read from the private
// registry of a Terraform organization.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/private-registry/modules#hcp-terraform-registry-implementation
ReadTerraformRegistryModule(ctx context.Context, moduleID RegistryModuleID, version string) (*TerraformRegistryModule, error)
// Delete a registry module
// Warning: This method is deprecated and will be removed from a future version of go-tfe. Use DeleteByName instead.
Delete(ctx context.Context, organization string, name string) error
// Delete a registry module by name
DeleteByName(ctx context.Context, module RegistryModuleID) error
// Delete a specified provider for the given module along with all its versions
DeleteProvider(ctx context.Context, moduleID RegistryModuleID) error
// Delete a specified version for the given provider of the module
DeleteVersion(ctx context.Context, moduleID RegistryModuleID, version string) error
// Update properties of a registry module
Update(ctx context.Context, moduleID RegistryModuleID, options RegistryModuleUpdateOptions) (*RegistryModule, error)
// Upload Terraform configuration files for the provided registry module version. It
// requires a path to the configuration files on disk, which will be packaged by
// hashicorp/go-slug before being uploaded.
Upload(ctx context.Context, rmv RegistryModuleVersion, path string) error
// Upload a tar gzip archive to the specified configuration version upload URL.
UploadTarGzip(ctx context.Context, url string, r io.Reader) error
}
// TerraformRegistryModule contains data about a module from the Terraform Registry.
type TerraformRegistryModule struct {
ID string `json:"id"`
Owner string `json:"owner"`
Namespace string `json:"namespace"`
Name string `json:"name"`
Version string `json:"version"`
Provider string `json:"provider"`
ProviderLogoURL string `json:"provider_logo_url"`
Description string `json:"description"`
Source string `json:"source"`
Tag string `json:"tag"`
PublishedAt string `json:"published_at"`
Downloads int `json:"downloads"`
Verified bool `json:"verified"`
Root Root `json:"root"`
Providers []string `json:"providers"`
Versions []string `json:"versions"`
}
type Root struct {
Path string `json:"path"`
Name string `json:"name"`
Readme string `json:"readme"`
Empty bool `json:"empty"`
Inputs []Input `json:"inputs"`
Outputs []Output `json:"outputs"`
ProviderDependencies []ProviderDependency `json:"provider_dependencies"`
Resources []Resource `json:"resources"`
}
type Input struct {
Name string `json:"name"`
Type string `json:"type"`
Description string `json:"description"`
Default string `json:"default"`
Required bool `json:"required"`
}
type Output struct {
Name string `json:"name"`
Description string `json:"description"`
}
type ProviderDependency struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Source string `json:"source"`
Version string `json:"version"`
}
type Resource struct {
Name string `json:"name"`
Type string `json:"type"`
}
// registryModules implements RegistryModules.
type registryModules struct {
client *Client
}
// RegistryModuleStatus represents the status of the registry module
type RegistryModuleStatus string
// List of available registry module statuses
const (
RegistryModuleStatusPending RegistryModuleStatus = "pending"
RegistryModuleStatusNoVersionTags RegistryModuleStatus = "no_version_tags"
RegistryModuleStatusSetupFailed RegistryModuleStatus = "setup_failed"
RegistryModuleStatusSetupComplete RegistryModuleStatus = "setup_complete"
)
// RegistryModuleVersionStatus represents the status of a specific version of a registry module
type RegistryModuleVersionStatus string
// List of available registry module version statuses
const (
RegistryModuleVersionStatusPending RegistryModuleVersionStatus = "pending"
RegistryModuleVersionStatusCloning RegistryModuleVersionStatus = "cloning"
RegistryModuleVersionStatusCloneFailed RegistryModuleVersionStatus = "clone_failed"
RegistryModuleVersionStatusRegIngressReqFailed RegistryModuleVersionStatus = "reg_ingress_req_failed"
RegistryModuleVersionStatusRegIngressing RegistryModuleVersionStatus = "reg_ingressing"
RegistryModuleVersionStatusRegIngressFailed RegistryModuleVersionStatus = "reg_ingress_failed"
RegistryModuleVersionStatusOk RegistryModuleVersionStatus = "ok"
)
type PublishingMechanism string
const (
PublishingMechanismBranch PublishingMechanism = "branch"
PublishingMechanismTag PublishingMechanism = "git_tag"
)
// RegistryModuleID represents the set of IDs that identify a RegistryModule
// Use NewPublicRegistryModuleID or NewPrivateRegistryModuleID to build one
type RegistryModuleID struct {
// The unique ID of the module. If given, the other fields are ignored.
ID string
// The organization the module belongs to, see RegistryModule.Organization.Name
Organization string
// The name of the module, see RegistryModule.Name
Name string
// The module's provider, see RegistryModule.Provider
Provider string
// The namespace of the module. For private modules this is the name of the organization that owns the module
// Required for public modules
Namespace string
// Either public or private. If not provided, defaults to private
RegistryName RegistryName
}
// RegistryModuleList represents a list of registry modules.
type RegistryModuleList struct {
*Pagination
Items []*RegistryModule
}
// CommitList represents a list of the latest commits from the registry module
type CommitList struct {
*Pagination
Items []*Commit
}
// RegistryModule represents a registry module
type RegistryModule struct {
ID string `jsonapi:"primary,registry-modules"`
Name string `jsonapi:"attr,name"`
Provider string `jsonapi:"attr,provider"`
RegistryName RegistryName `jsonapi:"attr,registry-name"`
Namespace string `jsonapi:"attr,namespace"`
NoCode bool `jsonapi:"attr,no-code"`
Permissions *RegistryModulePermissions `jsonapi:"attr,permissions"`
PublishingMechanism PublishingMechanism `jsonapi:"attr,publishing-mechanism"`
Status RegistryModuleStatus `jsonapi:"attr,status"`
TestConfig *TestConfig `jsonapi:"attr,test-config"`
VCSRepo *VCSRepo `jsonapi:"attr,vcs-repo"`
VersionStatuses []RegistryModuleVersionStatuses `jsonapi:"attr,version-statuses"`
CreatedAt string `jsonapi:"attr,created-at"`
UpdatedAt string `jsonapi:"attr,updated-at"`
// Relations
Organization *Organization `jsonapi:"relation,organization"`
RegistryNoCodeModule []*RegistryNoCodeModule `jsonapi:"relation,no-code-modules"`
}
// Commit represents a commit
type Commit struct {
ID string `jsonapi:"primary,commit"`
Sha string `jsonapi:"attr,sha"`
Date string `jsonapi:"attr,date"`
URL string `jsonapi:"attr,url"`
Author string `jsonapi:"attr,author"`
AuthorAvatarURL string `jsonapi:"attr,author-avatar-url"`
AuthorHTMLURL string `jsonapi:"attr,author-html-url"`
Message string `jsonapi:"attr,message"`
}
// RegistryModuleVersion represents a registry module version
type RegistryModuleVersion struct {
ID string `jsonapi:"primary,registry-module-versions"`
Source string `jsonapi:"attr,source"`
Status RegistryModuleVersionStatus `jsonapi:"attr,status"`
Version string `jsonapi:"attr,version"`
CreatedAt string `jsonapi:"attr,created-at"`
UpdatedAt string `jsonapi:"attr,updated-at"`
// Relations
RegistryModule *RegistryModule `jsonapi:"relation,registry-module"`
// Links
Links map[string]interface{} `jsonapi:"links,omitempty"`
}
type RegistryModulePermissions struct {
CanDelete bool `jsonapi:"attr,can-delete"`
CanResync bool `jsonapi:"attr,can-resync"`
CanRetry bool `jsonapi:"attr,can-retry"`
}
type RegistryModuleVersionStatuses struct {
Version string `jsonapi:"attr,version"`
Status RegistryModuleVersionStatus `jsonapi:"attr,status"`
Error string `jsonapi:"attr,error"`
}
// RegistryModuleListOptions represents the options for listing registry modules.
type RegistryModuleListOptions struct {
ListOptions
// Include is a list of relations to include.
Include []RegistryModuleListIncludeOpt `url:"include,omitempty"`
// Search is a search query string. Modules are searchable by name, namespace, provider fields.
Search string `url:"q,omitempty"`
// Provider filters results by provider name
Provider string `url:"filter[provider],omitempty"`
// RegistryName filters results by registry name (public or private)
RegistryName RegistryName `url:"filter[registry_name],omitempty"`
// OrganizationName filters results by organization name
OrganizationName string `url:"filter[organization_name],omitempty"`
}
type RegistryModuleListIncludeOpt string
const IncludeNoCodeModules RegistryModuleListIncludeOpt = "no-code-modules"
// RegistryModuleCreateOptions is used when creating a registry module without a VCS repo
type RegistryModuleCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,registry-modules"`
// Required:
Name *string `jsonapi:"attr,name"`
// Required:
Provider *string `jsonapi:"attr,provider"`
// Optional: Whether this is a publicly maintained module or private. Must be either public or private.
// Defaults to private if not specified
RegistryName RegistryName `jsonapi:"attr,registry-name,omitempty"`
// Optional: The namespace of this module. Required for public modules only.
Namespace string `jsonapi:"attr,namespace,omitempty"`
// Optional: If set to true the module is enabled for no-code provisioning.
// **Note: This field is still in BETA and subject to change.**
NoCode *bool `jsonapi:"attr,no-code,omitempty"`
}
// RegistryModuleCreateVersionOptions is used when creating a registry module version
type RegistryModuleCreateVersionOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,registry-module-versions"`
Version *string `jsonapi:"attr,version"`
CommitSHA *string `jsonapi:"attr,commit-sha"`
}
// RegistryModuleCreateWithVCSConnectionOptions is used when creating a registry module with a VCS repo
type RegistryModuleCreateWithVCSConnectionOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,registry-modules"`
// Optional: The Name of the Module. If not provided, will be inferred from the VCS repository identifier.
// Required for monorepos with source_directory where the repository name doesn't follow the terraform-- convention.
Name *string `jsonapi:"attr,name,omitempty"`
// Optional: The Name of the Provider. If not provided, will be inferred from the VCS repository identifier.
// Required for monorepos with source_directory where the repository name doesn't follow the terraform-- convention.
Provider *string `jsonapi:"attr,provider,omitempty"`
// Required: VCS repository information
VCSRepo *RegistryModuleVCSRepoOptions `jsonapi:"attr,vcs-repo"`
// Optional: If Branch is set within VCSRepo then InitialVersion sets the
// initial version of the newly created branch-based registry module. If
// Branch is not set within VCSRepo then InitialVersion is ignored.
//
// Defaults to "0.0.0".
//
// **Note: This field is still in BETA and subject to change.**
InitialVersion *string `jsonapi:"attr,initial-version,omitempty"`
// Optional: Flag to enable tests for the module
// **Note: This field is still in BETA and subject to change.**
TestConfig *RegistryModuleTestConfigOptions `jsonapi:"attr,test-config,omitempty"`
}
// RegistryModuleCreateVersionOptions is used when updating a registry module
type RegistryModuleUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-updating
Type string `jsonapi:"primary,registry-modules"`
// Optional: Flag to enable no-code provisioning for the whole module.
// **Note: This field is still in BETA and subject to change.**
NoCode *bool `jsonapi:"attr,no-code,omitempty"`
// Optional: Flag to enable tests for the module
// **Note: This field is still in BETA and subject to change.**
TestConfig *RegistryModuleTestConfigOptions `jsonapi:"attr,test-config,omitempty"`
VCSRepo *RegistryModuleVCSRepoUpdateOptions `jsonapi:"attr,vcs-repo,omitempty"`
}
type RegistryModuleTestConfigOptions struct {
TestsEnabled *bool `jsonapi:"attr,tests-enabled,omitempty"`
AgentExecutionMode *AgentExecutionMode `jsonapi:"attr,agent-execution-mode,omitempty"`
AgentPoolID *string `jsonapi:"attr,agent-pool-id,omitempty"`
}
type RegistryModuleVCSRepoOptions struct {
Identifier *string `json:"identifier"` // Required
OAuthTokenID *string `json:"oauth-token-id,omitempty"`
DisplayIdentifier *string `json:"display-identifier,omitempty"` // Required
GHAInstallationID *string `json:"github-app-installation-id,omitempty"`
OrganizationName *string `json:"organization-name,omitempty"`
// Optional: If set, the newly created registry module will be branch-based
// with the starting branch set to Branch.
//
// **Note: This field is still in BETA and subject to change.**
Branch *string `json:"branch,omitempty"`
Tags *bool `json:"tags,omitempty"`
// Optional: If set, the registry module will be branch-based or tag-based
SourceDirectory *string `json:"source-directory,omitempty"`
TagPrefix *string `json:"tag-prefix,omitempty"`
}
type RegistryModuleVCSRepoUpdateOptions struct {
// The Branch and Tag fields are used to determine
// the PublishingMechanism for a RegistryModule that has a VCS a connection.
// When a value for Branch is provided, the Tags field is removed on the server
// When a value for Tags is provided, the Branch field is removed on the server
// **Note: This field is still in BETA and subject to change.**
Branch *string `json:"branch,omitempty"`
Tags *bool `json:"tags,omitempty"`
// Optional: If set, the registry module will be branch-based or tag-based
SourceDirectory *string `json:"source-directory,omitempty"`
TagPrefix *string `json:"tag-prefix,omitempty"`
}
// List all the registry modules within an organization.
func (r *registryModules) List(ctx context.Context, organization string, options *RegistryModuleListOptions) (*RegistryModuleList, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf("organizations/%s/registry-modules", url.PathEscape(organization))
req, err := r.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
ml := &RegistryModuleList{}
err = req.Do(ctx, ml)
if err != nil {
return nil, err
}
return ml, nil
}
// List the last 20 commits for the registry modules within an organization.
func (r *registryModules) ListCommits(ctx context.Context, moduleID RegistryModuleID) (*CommitList, error) {
if !validStringID(&moduleID.Organization) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf(
"organizations/%s/registry-modules/private/%s/%s/%s/commits",
url.PathEscape(moduleID.Organization),
url.PathEscape(moduleID.Organization),
url.PathEscape(moduleID.Name),
url.PathEscape(moduleID.Provider),
)
req, err := r.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
cl := &CommitList{}
err = req.Do(ctx, cl)
if err != nil {
return nil, err
}
return cl, nil
}
// Upload uploads Terraform configuration files for the provided registry module version. It
// requires a path to the configuration files on disk, which will be packaged by
// hashicorp/go-slug before being uploaded.
func (r *registryModules) Upload(ctx context.Context, rmv RegistryModuleVersion, path string) error {
uploadURL, ok := rmv.Links["upload"].(string)
if !ok {
return fmt.Errorf("provided RegistryModuleVersion does not contain an upload link")
}
body, err := packContents(path)
if err != nil {
return err
}
return r.UploadTarGzip(ctx, uploadURL, body)
}
// UploadTarGzip is used to upload Terraform configuration files contained a tar gzip archive.
// Any stream implementing io.Reader can be passed into this method. This method is also
// particularly useful for tar streams created by non-default go-slug configurations.
//
// **Note**: This method does not validate the content being uploaded and is therefore the caller's
// responsibility to ensure the raw content is a valid Terraform configuration.
func (r *registryModules) UploadTarGzip(ctx context.Context, uploadURL string, archive io.Reader) error {
return r.client.doForeignPUTRequest(ctx, uploadURL, archive)
}
// Create a new registry module without a VCS repo
func (r *registryModules) Create(ctx context.Context, organization string, options RegistryModuleCreateOptions) (*RegistryModule, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
if options.NoCode != nil {
log.Println("[WARN] Support for using the NoCode field is deprecated as of release 1.22.0 and may be removed in a future version. The preferred way to create a no-code module is with the registryNoCodeModules.Create method.")
}
u := fmt.Sprintf(
"organizations/%s/registry-modules",
url.PathEscape(organization),
)
req, err := r.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
rm := &RegistryModule{}
err = req.Do(ctx, rm)
if err != nil {
return nil, err
}
return rm, nil
}
func (r *registryModules) Update(ctx context.Context, moduleID RegistryModuleID, options RegistryModuleUpdateOptions) (*RegistryModule, error) {
if err := moduleID.valid(); err != nil {
return nil, err
}
if moduleID.RegistryName == "" {
log.Println("[WARN] Support for using the RegistryModuleID without RegistryName is deprecated as of release 1.5.0 and may be removed in a future version. The preferred method is to include the RegistryName in RegistryModuleID.")
moduleID.RegistryName = PrivateRegistry
}
if moduleID.RegistryName == PrivateRegistry && strings.TrimSpace(moduleID.Namespace) == "" {
log.Println("[WARN] Support for using the RegistryModuleID without Namespace is deprecated as of release 1.5.0 and may be removed in a future version. The preferred method is to include the Namespace in RegistryModuleID.")
moduleID.Namespace = moduleID.Organization
}
if options.NoCode != nil {
log.Println("[WARN] Support for using the NoCode field is deprecated as of release 1.22.0 and may be removed in a future version. The preferred way to update a no-code module is with the registryNoCodeModules.Update method.")
}
if options.VCSRepo != nil {
if options.VCSRepo.Tags != nil && *options.VCSRepo.Tags && validString(options.VCSRepo.Branch) {
return nil, ErrBranchMustBeEmptyWhenTagsEnabled
}
}
if options.TestConfig != nil && options.TestConfig.AgentExecutionMode != nil {
if *options.TestConfig.AgentExecutionMode == AgentExecutionModeRemote && options.TestConfig.AgentPoolID != nil {
return nil, ErrAgentPoolNotRequiredForRemoteExecution
}
}
org := url.PathEscape(moduleID.Organization)
registryName := url.PathEscape(string(moduleID.RegistryName))
namespace := url.PathEscape(moduleID.Namespace)
name := url.PathEscape(moduleID.Name)
provider := url.PathEscape(moduleID.Provider)
registryModuleURL := fmt.Sprintf("organizations/%s/registry-modules/%s/%s/%s/%s", org, registryName, namespace, name, provider)
req, err := r.client.NewRequest(http.MethodPatch, registryModuleURL, &options)
if err != nil {
return nil, err
}
rm := &RegistryModule{}
if err := req.Do(ctx, rm); err != nil {
return nil, err
}
return rm, nil
}
// CreateVersion creates a new registry module version
func (r *registryModules) CreateVersion(ctx context.Context, moduleID RegistryModuleID, options RegistryModuleCreateVersionOptions) (*RegistryModuleVersion, error) {
if err := moduleID.valid(); err != nil {
return nil, err
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf(
"registry-modules/%s/%s/%s/versions",
url.PathEscape(moduleID.Organization),
url.PathEscape(moduleID.Name),
url.PathEscape(moduleID.Provider),
)
req, err := r.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
rmv := &RegistryModuleVersion{}
err = req.Do(ctx, rmv)
if err != nil {
return nil, err
}
return rmv, nil
}
// CreateWithVCSConnection is used to create and publish a new registry module with a VCS repo
func (r *registryModules) CreateWithVCSConnection(ctx context.Context, options RegistryModuleCreateWithVCSConnectionOptions) (*RegistryModule, error) {
if err := options.valid(); err != nil {
return nil, err
}
var u string
if options.VCSRepo.OAuthTokenID != nil && options.VCSRepo.Branch == nil {
u = "registry-modules"
} else {
u = fmt.Sprintf(
"organizations/%s/registry-modules/vcs",
url.PathEscape(*options.VCSRepo.OrganizationName),
)
}
if options.TestConfig != nil && options.TestConfig.AgentExecutionMode != nil {
if *options.TestConfig.AgentExecutionMode == AgentExecutionModeRemote && options.TestConfig.AgentPoolID != nil {
return nil, ErrAgentPoolNotRequiredForRemoteExecution
}
}
req, err := r.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
rm := &RegistryModule{}
err = req.Do(ctx, rm)
if err != nil {
return nil, err
}
return rm, nil
}
// Read a specific registry module
func (r *registryModules) Read(ctx context.Context, moduleID RegistryModuleID) (*RegistryModule, error) {
if err := moduleID.valid(); err != nil {
return nil, err
}
var u string
if moduleID.ID == "" {
if moduleID.RegistryName == "" {
log.Println("[WARN] Support for using the RegistryModuleID without RegistryName is deprecated as of release 1.5.0 and may be removed in a future version. The preferred method is to include the RegistryName in RegistryModuleID.")
moduleID.RegistryName = PrivateRegistry
}
if moduleID.RegistryName == PrivateRegistry && strings.TrimSpace(moduleID.Namespace) == "" {
log.Println("[WARN] Support for using the RegistryModuleID without Namespace is deprecated as of release 1.5.0 and may be removed in a future version. The preferred method is to include the Namespace in RegistryModuleID.")
moduleID.Namespace = moduleID.Organization
}
u = fmt.Sprintf(
"organizations/%s/registry-modules/%s/%s/%s/%s",
url.PathEscape(moduleID.Organization),
url.PathEscape(string(moduleID.RegistryName)),
url.PathEscape(moduleID.Namespace),
url.PathEscape(moduleID.Name),
url.PathEscape(moduleID.Provider),
)
} else {
u = fmt.Sprintf("registry-modules/%s", url.PathEscape(moduleID.ID))
}
req, err := r.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
rm := &RegistryModule{}
err = req.Do(ctx, rm)
if err != nil {
return nil, err
}
return rm, nil
}
// ReadTerraformRegistryModule fetches a registry module from the Terraform Registry.
func (r *registryModules) ReadTerraformRegistryModule(ctx context.Context, moduleID RegistryModuleID, version string) (*TerraformRegistryModule, error) {
u := fmt.Sprintf("/api/registry/v1/modules/%s/%s/%s/%s",
moduleID.Namespace,
moduleID.Name,
moduleID.Provider,
version,
)
if moduleID.RegistryName == PublicRegistry {
u = fmt.Sprintf("/api/registry/public/v1/modules/%s/%s/%s/%s",
moduleID.Namespace,
moduleID.Name,
moduleID.Provider,
version,
)
}
req, err := r.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
trm := &TerraformRegistryModule{}
err = req.DoJSON(ctx, trm)
if err != nil {
return nil, err
}
return trm, nil
}
func (r *registryModules) ReadVersion(ctx context.Context, moduleID RegistryModuleID, version string) (*RegistryModuleVersion, error) {
if err := moduleID.valid(); err != nil {
return nil, err
}
if !validString(&version) {
return nil, ErrRequiredVersion
}
if !validStringID(&version) {
return nil, ErrInvalidVersion
}
u := fmt.Sprintf(
"organizations/%s/registry-modules/private/%s/%s/%s/version?module_version=%s",
url.PathEscape(moduleID.Organization),
url.PathEscape(moduleID.Organization),
url.PathEscape(moduleID.Name),
url.PathEscape(moduleID.Provider),
url.PathEscape(version),
)
req, err := r.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
rmv := &RegistryModuleVersion{}
err = req.Do(ctx, rmv)
if err != nil {
return nil, err
}
return rmv, nil
}
// Delete is used to delete the entire registry module
// Warning: This method is deprecated and will be removed from a future version of go-tfe. Use DeleteByName instead.
// See API Docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/private-registry/modules#delete-a-module
func (r *registryModules) Delete(ctx context.Context, organization, name string) error {
if !validStringID(&organization) {
return ErrInvalidOrg
}
if !validString(&name) {
return ErrRequiredName
}
if !validStringID(&name) {
return ErrInvalidName
}
u := fmt.Sprintf(
"registry-modules/actions/delete/%s/%s",
url.PathEscape(organization),
url.PathEscape(name),
)
req, err := r.client.NewRequest("POST", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// DeleteByName is used to delete the entire registry module
func (r *registryModules) DeleteByName(ctx context.Context, module RegistryModuleID) error {
if err := module.validWhenDeleteByName(); err != nil {
return err
}
u := fmt.Sprintf(
"organizations/%s/registry-modules/%s/%s/%s",
url.PathEscape(module.Organization),
url.PathEscape(string(module.RegistryName)),
url.PathEscape(module.Namespace),
url.PathEscape(module.Name),
)
req, err := r.client.NewRequest("DELETE", u, nil)
if err != nil && errors.Is(err, ErrResourceNotFound) {
return r.Delete(ctx, module.Organization, module.Name)
}
return req.Do(ctx, nil)
}
// Delete a specified provider for the given module along with all its versions
func (r *registryModules) DeleteProvider(ctx context.Context, moduleID RegistryModuleID) error {
if err := moduleID.validWhenDeleteByProvider(); err != nil {
return err
}
u := fmt.Sprintf(
"organizations/%s/registry-modules/%s/%s/%s/%s",
url.PathEscape(moduleID.Organization),
url.PathEscape(string(moduleID.RegistryName)),
url.PathEscape(moduleID.Namespace),
url.PathEscape(moduleID.Name),
url.PathEscape(moduleID.Provider),
)
req, err := r.client.NewRequest("DELETE", u, nil)
if err != nil && errors.Is(err, ErrResourceNotFound) {
return r.deprecatedDeleteProvider(ctx, moduleID)
}
return req.Do(ctx, nil)
}
// Delete a specified version for the given provider of the module
func (r *registryModules) DeleteVersion(ctx context.Context, moduleID RegistryModuleID, version string) error {
if err := moduleID.valid(); err != nil {
return err
}
if !validString(&version) {
return ErrRequiredVersion
}
if !validVersion(version) {
return ErrInvalidVersion
}
u := fmt.Sprintf(
"organizations/%s/registry-modules/%s/%s/%s/%s/%s",
url.PathEscape(moduleID.Organization),
url.PathEscape(string(moduleID.RegistryName)),
url.PathEscape(moduleID.Namespace),
url.PathEscape(moduleID.Name),
url.PathEscape(moduleID.Provider),
url.PathEscape(version),
)
req, err := r.client.NewRequest("DELETE", u, nil)
if err != nil && errors.Is(err, ErrResourceNotFound) {
return r.deprecatedDeleteVersion(ctx, moduleID, version)
}
return req.Do(ctx, nil)
}
func (o RegistryModuleID) valid() error {
if validString(&o.ID) && validStringID(&o.ID) {
return nil
}
if !validStringID(&o.Organization) {
return ErrInvalidOrg
}
if !validString(&o.Name) {
return ErrRequiredName
}
if !validStringID(&o.Name) {
return ErrInvalidName
}
if !validString(&o.Provider) {
return ErrRequiredProvider
}
if !validStringID(&o.Provider) {
return ErrInvalidProvider
}
switch o.RegistryName {
case PublicRegistry:
if !validString(&o.Namespace) {
return ErrRequiredNamespace
}
case PrivateRegistry:
case "":
// no-op: RegistryName is optional
// for all other string
default:
return ErrInvalidRegistryName
}
return nil
}
func (o RegistryModuleID) validWhenDeleteByProvider() error {
if !validStringID(&o.Organization) {
return ErrInvalidOrg
}
if !validString(&o.Name) {
return ErrRequiredName
}
if !validStringID(&o.Name) {
return ErrInvalidName
}
if !validString(&o.Provider) {
return ErrRequiredProvider
}
if !validStringID(&o.Provider) {
return ErrInvalidProvider
}
// RegistryName is required in this DELETE call
switch o.RegistryName {
case PublicRegistry:
if !validString(&o.Namespace) {
return ErrRequiredNamespace
}
case PrivateRegistry:
case "":
return ErrInvalidRegistryName
default:
return ErrInvalidRegistryName
}
return nil
}
func (o RegistryModuleID) validWhenDeleteByName() error {
if !validStringID(&o.Organization) {
return ErrInvalidOrg
}
if !validString(&o.Name) {
return ErrRequiredName
}
if !validStringID(&o.Name) {
return ErrInvalidName
}
// RegistryName is required in this DELETE call
switch o.RegistryName {
case PublicRegistry:
if !validString(&o.Namespace) {
return ErrRequiredNamespace
}
case PrivateRegistry:
case "":
return ErrInvalidRegistryName
default:
return ErrInvalidRegistryName
}
return nil
}
func (o RegistryModuleCreateOptions) valid() error {
if !validString(o.Name) {
return ErrRequiredName
}
if !validStringID(o.Name) {
return ErrInvalidName
}
if !validString(o.Provider) {
return ErrRequiredProvider
}
if !validStringID(o.Provider) {
return ErrInvalidProvider
}
switch o.RegistryName {
case PublicRegistry:
if !validString(&o.Namespace) {
return ErrRequiredNamespace
}
case PrivateRegistry:
if validString(&o.Namespace) {
return ErrUnsupportedBothNamespaceAndPrivateRegistryName
}
case "":
// no-op: RegistryName is optional
// for all other string
default:
return ErrInvalidRegistryName
}
return nil
}
func (o RegistryModuleCreateVersionOptions) valid() error {
if !validString(o.Version) {
return ErrRequiredVersion
}
if !validVersion(*o.Version) {
return ErrInvalidVersion
}
return nil
}
func (o RegistryModuleCreateWithVCSConnectionOptions) valid() error {
if o.VCSRepo == nil {
return ErrRequiredVCSRepo
}
if o.TestConfig != nil && o.TestConfig.TestsEnabled != nil {
if *o.TestConfig.TestsEnabled {
if !validString(o.VCSRepo.Branch) {
return ErrRequiredBranchWhenTestsEnabled
}
}
}
if o.VCSRepo.Tags != nil && *o.VCSRepo.Tags {
if validString(o.VCSRepo.Branch) {
return ErrBranchMustBeEmptyWhenTagsEnabled
}
}
return o.VCSRepo.valid()
}
func (o RegistryModuleVCSRepoOptions) valid() error {
if !validString(o.Identifier) {
return ErrRequiredIdentifier
}
if !validString(o.OAuthTokenID) && !validString(o.GHAInstallationID) {
return ErrRequiredOauthTokenOrGithubAppInstallationID
}
if (!validString(o.OAuthTokenID) && validString(o.GHAInstallationID)) || validString(o.Branch) {
if !validString(o.OrganizationName) {
return ErrInvalidOrg
}
}
if !validString(o.DisplayIdentifier) {
return ErrRequiredDisplayIdentifier
}
return nil
}
func (r *registryModules) deprecatedDeleteProvider(ctx context.Context, moduleID RegistryModuleID) error {
if err := moduleID.valid(); err != nil {
return err
}
u := fmt.Sprintf(
"registry-modules/actions/delete/%s/%s/%s",
url.PathEscape(moduleID.Organization),
url.PathEscape(moduleID.Name),
url.PathEscape(moduleID.Provider),
)
req, err := r.client.NewRequest("POST", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (r *registryModules) deprecatedDeleteVersion(ctx context.Context, moduleID RegistryModuleID, version string) error {
if err := moduleID.valid(); err != nil {
return err
}
if !validString(&version) {
return ErrRequiredVersion
}
if !validVersion(version) {
return ErrInvalidVersion
}
u := fmt.Sprintf(
"registry-modules/actions/delete/%s/%s/%s/%s",
url.PathEscape(moduleID.Organization),
url.PathEscape(moduleID.Name),
url.PathEscape(moduleID.Provider),
url.PathEscape(version),
)
req, err := r.client.NewRequest("POST", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func NewPublicRegistryModuleID(organization, namespace, name, provider string) RegistryModuleID {
return RegistryModuleID{
Organization: organization,
Namespace: namespace,
Name: name,
RegistryName: PublicRegistry,
Provider: provider,
}
}
func NewPrivateRegistryModuleID(organization, name, provider string) RegistryModuleID {
return RegistryModuleID{
Organization: organization,
Namespace: organization,
Name: name,
RegistryName: PrivateRegistry,
Provider: provider,
}
}
================================================
FILE: registry_module_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"regexp"
"strings"
"testing"
"time"
retryablehttp "github.com/hashicorp/go-retryablehttp"
slug "github.com/hashicorp/go-slug"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRegistryModulesList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
registryModuleTest1, registryModuleTest1Cleanup := createRegistryModule(t, client, orgTest, PrivateRegistry)
defer registryModuleTest1Cleanup()
registryModuleTest2, registryModuleTest2Cleanup := createRegistryModule(t, client, orgTest, PrivateRegistry)
defer registryModuleTest2Cleanup()
t.Run("with no list options", func(t *testing.T) {
modl, err := client.RegistryModules.List(ctx, orgTest.Name, &RegistryModuleListOptions{})
require.NoError(t, err)
assert.Contains(t, modl.Items, registryModuleTest1)
assert.Contains(t, modl.Items, registryModuleTest2)
assert.Equal(t, 1, modl.CurrentPage)
assert.Equal(t, 2, modl.TotalCount)
})
t.Run("with list options", func(t *testing.T) {
modl, err := client.RegistryModules.List(ctx, orgTest.Name, &RegistryModuleListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
// Out of range page number, so the items should be empty
assert.Empty(t, modl.Items)
assert.Equal(t, 999, modl.CurrentPage)
modl, err = client.RegistryModules.List(ctx, orgTest.Name, &RegistryModuleListOptions{
ListOptions: ListOptions{
PageNumber: 1,
PageSize: 100,
},
})
require.NoError(t, err)
assert.NotEmpty(t, modl.Items)
assert.Equal(t, 1, modl.CurrentPage)
})
t.Run("include no-code modules", func(t *testing.T) {
options := RegistryModuleCreateOptions{
Name: String("iam"),
Provider: String("aws"),
NoCode: Bool(true),
RegistryName: PrivateRegistry,
}
rm, err := client.RegistryModules.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
modl, err := client.RegistryModules.List(ctx, orgTest.Name, &RegistryModuleListOptions{
Include: []RegistryModuleListIncludeOpt{
IncludeNoCodeModules,
},
})
require.NoError(t, err)
assert.Len(t, modl.Items, 3)
for _, m := range modl.Items {
if m.ID == rm.ID {
assert.True(t, m.NoCode)
assert.Len(t, m.RegistryNoCodeModule, 1)
}
}
})
t.Run("with search query", func(t *testing.T) {
// Search for modules by name
modl, err := client.RegistryModules.List(ctx, orgTest.Name, &RegistryModuleListOptions{
Search: registryModuleTest1.Name,
})
require.NoError(t, err)
// Should find at least the first test module
found := false
for _, m := range modl.Items {
if m.ID == registryModuleTest1.ID {
found = true
break
}
}
assert.True(t, found, "Registry module should be found by name search")
})
t.Run("with provider filter", func(t *testing.T) {
// Filter by provider
modl, err := client.RegistryModules.List(ctx, orgTest.Name, &RegistryModuleListOptions{
Provider: registryModuleTest1.Provider,
})
require.NoError(t, err)
// All returned modules should have the specified provider
for _, m := range modl.Items {
assert.Equal(t, registryModuleTest1.Provider, m.Provider)
}
})
t.Run("with registry name filter", func(t *testing.T) {
// Filter by registry name
modl, err := client.RegistryModules.List(ctx, orgTest.Name, &RegistryModuleListOptions{
RegistryName: PrivateRegistry,
})
require.NoError(t, err)
// All returned modules should have the specified registry name
for _, m := range modl.Items {
assert.Equal(t, PrivateRegistry, m.RegistryName)
}
})
t.Run("with organization name filter", func(t *testing.T) {
// Filter by organization name
modl, err := client.RegistryModules.List(ctx, orgTest.Name, &RegistryModuleListOptions{
OrganizationName: orgTest.Name,
})
require.NoError(t, err)
// All returned modules should belong to the specified organization
for _, m := range modl.Items {
assert.Equal(t, orgTest.Name, m.Namespace)
}
})
t.Run("with combined search and filters", func(t *testing.T) {
// Combine search with filters
modl, err := client.RegistryModules.List(ctx, orgTest.Name, &RegistryModuleListOptions{
Search: registryModuleTest1.Name,
Provider: registryModuleTest1.Provider,
RegistryName: PrivateRegistry,
OrganizationName: orgTest.Name,
})
require.NoError(t, err)
// Should find the specific module when all criteria match
found := false
for _, m := range modl.Items {
if m.ID != registryModuleTest1.ID {
continue
}
found = true
assert.Equal(t, registryModuleTest1.Provider, m.Provider)
assert.Equal(t, PrivateRegistry, m.RegistryName)
assert.Equal(t, orgTest.Name, m.Namespace)
break
}
assert.True(t, found, "Registry module should be found with combined search and filters")
})
}
func TestRegistryModulesCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
t.Run("with valid options", func(t *testing.T) {
assertRegistryModuleAttributes := func(t *testing.T, registryModule *RegistryModule) {
t.Run("permissions are properly decoded", func(t *testing.T) {
require.NotEmpty(t, registryModule.Permissions)
assert.True(t, registryModule.Permissions.CanDelete)
assert.True(t, registryModule.Permissions.CanResync)
assert.True(t, registryModule.Permissions.CanRetry)
})
t.Run("relationships are properly decoded", func(t *testing.T) {
require.NotEmpty(t, registryModule.Organization)
assert.Equal(t, orgTest.Name, registryModule.Organization.Name)
})
t.Run("timestamps are properly decoded", func(t *testing.T) {
assert.NotEmpty(t, registryModule.CreatedAt)
assert.NotEmpty(t, registryModule.UpdatedAt)
})
}
t.Run("without RegistryName", func(t *testing.T) {
options := RegistryModuleCreateOptions{
Name: String("name"),
Provider: String("provider"),
}
rm, err := client.RegistryModules.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.NotEmpty(t, rm.ID)
assert.Equal(t, *options.Name, rm.Name)
assert.Equal(t, *options.Provider, rm.Provider)
assert.Equal(t, PrivateRegistry, rm.RegistryName)
assert.Equal(t, orgTest.Name, rm.Namespace)
assert.False(t, rm.NoCode, "no-code module attribute should be false by default")
assertRegistryModuleAttributes(t, rm)
})
t.Run("with private RegistryName", func(t *testing.T) {
options := RegistryModuleCreateOptions{
Name: String("another_name"),
Provider: String("provider"),
RegistryName: PrivateRegistry,
}
rm, err := client.RegistryModules.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.NotEmpty(t, rm.ID)
assert.Equal(t, *options.Name, rm.Name)
assert.Equal(t, *options.Provider, rm.Provider)
assert.Equal(t, options.RegistryName, rm.RegistryName)
assert.Equal(t, orgTest.Name, rm.Namespace)
assert.False(t, rm.NoCode, "no-code module attribute should be false by default")
assertRegistryModuleAttributes(t, rm)
})
t.Run("with public RegistryName", func(t *testing.T) {
options := RegistryModuleCreateOptions{
Name: String("vpc"),
Provider: String("aws"),
RegistryName: PublicRegistry,
Namespace: "terraform-aws-modules",
}
rm, err := client.RegistryModules.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.NotEmpty(t, rm.ID)
assert.Equal(t, *options.Name, rm.Name)
assert.Equal(t, *options.Provider, rm.Provider)
assert.Equal(t, options.RegistryName, rm.RegistryName)
assert.Equal(t, options.Namespace, rm.Namespace)
assert.False(t, rm.NoCode, "no-code module attribute should be false by default")
assertRegistryModuleAttributes(t, rm)
})
t.Run("with no-code attribute", func(t *testing.T) {
skipUnlessBeta(t)
options := RegistryModuleCreateOptions{
Name: String("iam"),
Provider: String("aws"),
NoCode: Bool(true),
RegistryName: PrivateRegistry,
}
rm, err := client.RegistryModules.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.NotEmpty(t, rm.ID)
assert.Equal(t, *options.Name, rm.Name)
assert.Equal(t, *options.Provider, rm.Provider)
assert.Equal(t, options.RegistryName, rm.RegistryName)
assert.Equal(t, orgTest.Name, rm.Namespace)
assert.Equal(t, options.NoCode, Bool(rm.NoCode))
assertRegistryModuleAttributes(t, rm)
})
})
t.Run("with invalid options", func(t *testing.T) {
t.Run("without a name", func(t *testing.T) {
options := RegistryModuleCreateOptions{
Provider: String("provider"),
}
rm, err := client.RegistryModules.Create(ctx, orgTest.Name, options)
assert.Nil(t, rm)
assert.EqualError(t, err, ErrRequiredName.Error())
})
t.Run("with an invalid name", func(t *testing.T) {
options := RegistryModuleCreateOptions{
Name: String("invalid name"),
Provider: String("provider"),
}
rm, err := client.RegistryModules.Create(ctx, orgTest.Name, options)
assert.Nil(t, rm)
assert.EqualError(t, err, ErrInvalidName.Error())
})
t.Run("without a provider", func(t *testing.T) {
options := RegistryModuleCreateOptions{
Name: String("name"),
}
rm, err := client.RegistryModules.Create(ctx, orgTest.Name, options)
assert.Nil(t, rm)
assert.Equal(t, err, ErrRequiredProvider)
})
t.Run("with an invalid provider", func(t *testing.T) {
options := RegistryModuleCreateOptions{
Name: String("name"),
Provider: String("invalid provider"),
}
rm, err := client.RegistryModules.Create(ctx, orgTest.Name, options)
assert.Nil(t, rm)
assert.Equal(t, err, ErrInvalidProvider)
})
t.Run("with an invalid registry name", func(t *testing.T) {
options := RegistryModuleCreateOptions{
Name: String("name"),
Provider: String("provider"),
RegistryName: "PRIVATE",
}
rm, err := client.RegistryModules.Create(ctx, orgTest.Name, options)
assert.Nil(t, rm)
assert.Equal(t, err, ErrInvalidRegistryName)
})
t.Run("without a namespace for public registry name", func(t *testing.T) {
options := RegistryModuleCreateOptions{
Name: String("name"),
Provider: String("provider"),
RegistryName: PublicRegistry,
}
rm, err := client.RegistryModules.Create(ctx, orgTest.Name, options)
assert.Nil(t, rm)
assert.Equal(t, err, ErrRequiredNamespace)
})
t.Run("with a namespace for private registry name", func(t *testing.T) {
options := RegistryModuleCreateOptions{
Name: String("name"),
Provider: String("provider"),
RegistryName: PrivateRegistry,
Namespace: "namespace",
}
rm, err := client.RegistryModules.Create(ctx, orgTest.Name, options)
assert.Nil(t, rm)
assert.Equal(t, err, ErrUnsupportedBothNamespaceAndPrivateRegistryName)
})
})
t.Run("without a valid organization", func(t *testing.T) {
options := RegistryModuleCreateOptions{
Name: String("name"),
Provider: String("provider"),
}
rm, err := client.RegistryModules.Create(ctx, badIdentifier, options)
assert.Nil(t, rm)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
func TestRegistryModuleUpdate(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
options := RegistryModuleCreateOptions{
Name: String("vault"),
Provider: String("aws"),
RegistryName: PublicRegistry,
Namespace: "hashicorp",
}
rm, err := client.RegistryModules.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.NotEmpty(t, rm.ID)
t.Run("enable no-code", func(t *testing.T) {
options := RegistryModuleUpdateOptions{
NoCode: Bool(true),
}
rm, err := client.RegistryModules.Update(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: "vault",
Provider: "aws",
Namespace: "hashicorp",
RegistryName: PublicRegistry,
}, options)
require.NoError(t, err)
assert.True(t, rm.NoCode)
})
t.Run("disable no-code", func(t *testing.T) {
options := RegistryModuleUpdateOptions{
NoCode: Bool(false),
}
rm, err := client.RegistryModules.Update(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: "vault",
Provider: "aws",
Namespace: "hashicorp",
RegistryName: PublicRegistry,
}, options)
require.NoError(t, err)
assert.False(t, rm.NoCode)
})
}
func TestRegistryModuleUpdateWithVCSConnection(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
githubBranch := os.Getenv("GITHUB_REGISTRY_MODULE_BRANCH")
if githubBranch == "" {
githubBranch = "main"
}
githubIdentifier := os.Getenv("GITHUB_REGISTRY_MODULE_IDENTIFIER")
if githubIdentifier == "" {
t.Skip("Export a valid GITHUB_REGISTRY_MODULE_IDENTIFIER before running this test")
}
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
oauthTokenTest, oauthTokenTestCleanup := createOAuthToken(t, client, orgTest)
defer oauthTokenTestCleanup()
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
OrganizationName: String(orgTest.Name),
Identifier: String(githubIdentifier),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(githubIdentifier),
},
}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
require.NoError(t, err)
assert.NotEmpty(t, rm.ID)
t.Run("enable no-code", func(t *testing.T) {
options := RegistryModuleUpdateOptions{
NoCode: Bool(true),
}
rm, err := client.RegistryModules.Update(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: rm.Name,
Provider: rm.Provider,
Namespace: rm.Namespace,
RegistryName: rm.RegistryName,
}, options)
require.NoError(t, err)
assert.True(t, rm.NoCode)
})
t.Run("disable no-code", func(t *testing.T) {
options := RegistryModuleUpdateOptions{
NoCode: Bool(false),
}
rm, err := client.RegistryModules.Update(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: rm.Name,
Provider: rm.Provider,
Namespace: rm.Namespace,
RegistryName: rm.RegistryName,
}, options)
require.NoError(t, err)
assert.False(t, rm.NoCode)
})
t.Run("prevents setting the branch when using tag based publishing", func(t *testing.T) {
options := RegistryModuleUpdateOptions{
VCSRepo: &RegistryModuleVCSRepoUpdateOptions{
Branch: String("main"),
Tags: Bool(true),
},
}
_, err = client.RegistryModules.Update(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: rm.Name,
Provider: rm.Provider,
Namespace: rm.Namespace,
RegistryName: rm.RegistryName,
}, options)
assert.Error(t, err)
assert.EqualError(t, err, ErrBranchMustBeEmptyWhenTagsEnabled.Error())
options = RegistryModuleUpdateOptions{
VCSRepo: &RegistryModuleVCSRepoUpdateOptions{
Branch: String(""),
Tags: Bool(true),
},
}
rm, err = client.RegistryModules.Update(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: rm.Name,
Provider: rm.Provider,
Namespace: rm.Namespace,
RegistryName: rm.RegistryName,
}, options)
assert.NoError(t, err)
})
t.Run("toggle between git tag-based and branch-based publishing", func(t *testing.T) {
assert.Equal(t, rm.PublishingMechanism, PublishingMechanismTag)
options := RegistryModuleUpdateOptions{
VCSRepo: &RegistryModuleVCSRepoUpdateOptions{
Branch: String(githubBranch),
},
}
rm, err := client.RegistryModules.Update(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: rm.Name,
Provider: rm.Provider,
Namespace: rm.Namespace,
RegistryName: rm.RegistryName,
}, options)
require.NoError(t, err)
assert.Equal(t, rm.PublishingMechanism, PublishingMechanismBranch)
assert.Equal(t, false, rm.VCSRepo.Tags)
assert.Equal(t, githubBranch, rm.VCSRepo.Branch)
options = RegistryModuleUpdateOptions{
VCSRepo: &RegistryModuleVCSRepoUpdateOptions{
Branch: String(""),
Tags: Bool(true),
},
}
rm, err = client.RegistryModules.Update(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: rm.Name,
Provider: rm.Provider,
Namespace: rm.Namespace,
RegistryName: rm.RegistryName,
}, options)
require.NoError(t, err)
assert.Equal(t, rm.PublishingMechanism, PublishingMechanismTag)
assert.Equal(t, true, rm.VCSRepo.Tags)
assert.Equal(t, "", rm.VCSRepo.Branch)
options = RegistryModuleUpdateOptions{
VCSRepo: &RegistryModuleVCSRepoUpdateOptions{
Branch: String(githubBranch),
},
}
rm, err = client.RegistryModules.Update(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: rm.Name,
Provider: rm.Provider,
Namespace: rm.Namespace,
RegistryName: rm.RegistryName,
}, options)
require.NoError(t, err)
assert.Equal(t, rm.PublishingMechanism, PublishingMechanismBranch)
assert.Equal(t, false, rm.VCSRepo.Tags)
assert.Equal(t, githubBranch, rm.VCSRepo.Branch)
})
}
func TestRegistryModulesCreateVersion(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
registryModuleTest, registryModuleTestCleanup := createRegistryModule(t, client, orgTest, PrivateRegistry)
defer registryModuleTestCleanup()
t.Run("with valid options", func(t *testing.T) {
options := RegistryModuleCreateVersionOptions{
Version: String("1.2.3"),
}
rmv, err := client.RegistryModules.CreateVersion(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: registryModuleTest.Name,
Provider: registryModuleTest.Provider,
}, options)
require.NoError(t, err)
assert.NotEmpty(t, rmv.ID)
assert.Equal(t, *options.Version, rmv.Version)
t.Run("relationships are properly decoded", func(t *testing.T) {
assert.Equal(t, registryModuleTest.ID, rmv.RegistryModule.ID)
})
t.Run("timestamps are properly decoded", func(t *testing.T) {
assert.NotEmpty(t, rmv.CreatedAt)
assert.NotEmpty(t, rmv.UpdatedAt)
})
t.Run("links are properly decoded", func(t *testing.T) {
assert.NotEmpty(t, rmv.Links["upload"])
assert.Contains(t, rmv.Links["upload"], "/object/")
})
})
t.Run("with prerelease and metadata version", func(t *testing.T) {
options := RegistryModuleCreateVersionOptions{
Version: String("1.2.3-alpha+feature"),
}
rmv, err := client.RegistryModules.CreateVersion(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: registryModuleTest.Name,
Provider: registryModuleTest.Provider,
}, options)
require.NoError(t, err)
assert.NotEmpty(t, rmv.ID)
assert.Equal(t, *options.Version, rmv.Version)
})
t.Run("with invalid options", func(t *testing.T) {
t.Run("without version", func(t *testing.T) {
options := RegistryModuleCreateVersionOptions{}
rmv, err := client.RegistryModules.CreateVersion(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: registryModuleTest.Name,
Provider: registryModuleTest.Provider,
}, options)
assert.Nil(t, rmv)
assert.Equal(t, err, ErrRequiredVersion)
})
t.Run("with invalid version", func(t *testing.T) {
options := RegistryModuleCreateVersionOptions{
Version: String("invalid version"),
}
rmv, err := client.RegistryModules.CreateVersion(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: registryModuleTest.Name,
Provider: registryModuleTest.Provider,
}, options)
assert.Nil(t, rmv)
assert.Equal(t, err, ErrInvalidVersion)
})
})
t.Run("without a name", func(t *testing.T) {
options := RegistryModuleCreateVersionOptions{
Version: String("1.2.3"),
}
rmv, err := client.RegistryModules.CreateVersion(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: "",
Provider: registryModuleTest.Provider,
}, options)
assert.Nil(t, rmv)
assert.EqualError(t, err, ErrRequiredName.Error())
})
t.Run("with an invalid name", func(t *testing.T) {
options := RegistryModuleCreateVersionOptions{
Version: String("1.2.3"),
}
rmv, err := client.RegistryModules.CreateVersion(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: badIdentifier,
Provider: registryModuleTest.Provider,
}, options)
assert.Nil(t, rmv)
assert.EqualError(t, err, ErrInvalidName.Error())
})
t.Run("without a provider", func(t *testing.T) {
options := RegistryModuleCreateVersionOptions{
Version: String("1.2.3"),
}
rmv, err := client.RegistryModules.CreateVersion(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: registryModuleTest.Name,
Provider: "",
}, options)
assert.Nil(t, rmv)
assert.Equal(t, err, ErrRequiredProvider)
})
t.Run("with an invalid provider", func(t *testing.T) {
options := RegistryModuleCreateVersionOptions{
Version: String("1.2.3"),
}
rmv, err := client.RegistryModules.CreateVersion(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: registryModuleTest.Name,
Provider: badIdentifier,
}, options)
assert.Nil(t, rmv)
assert.Equal(t, err, ErrInvalidProvider)
})
t.Run("without a valid organization", func(t *testing.T) {
options := RegistryModuleCreateVersionOptions{
Version: String("1.2.3"),
}
rmv, err := client.RegistryModules.CreateVersion(ctx, RegistryModuleID{
Organization: badIdentifier,
Name: registryModuleTest.Name,
Provider: registryModuleTest.Provider,
}, options)
assert.Nil(t, rmv)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
func TestRegistryModulesShowVersion(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
registryModuleTest, registryModuleTestCleanup := createRegistryModule(t, client, orgTest, PrivateRegistry)
defer registryModuleTestCleanup()
t.Run("when the version exists", func(t *testing.T) {
options := RegistryModuleCreateVersionOptions{
Version: String("1.2.7"),
}
registryModuleIDTest := RegistryModuleID{
Organization: orgTest.Name,
Name: registryModuleTest.Name,
Provider: registryModuleTest.Provider,
}
rmv, err := client.RegistryModules.CreateVersion(ctx, registryModuleIDTest, options)
require.NoError(t, err)
assert.NotEmpty(t, rmv.ID)
assert.Equal(t, *options.Version, rmv.Version)
rmvRead, errRead := client.RegistryModules.ReadVersion(ctx, registryModuleIDTest, *options.Version)
require.NoError(t, errRead)
assert.NotEmpty(t, rmvRead.ID)
t.Run("relationships are properly decoded", func(t *testing.T) {
assert.Equal(t, registryModuleTest.ID, rmvRead.RegistryModule.ID)
})
t.Run("timestamps are properly decoded", func(t *testing.T) {
assert.NotEmpty(t, rmvRead.CreatedAt)
assert.NotEmpty(t, rmvRead.UpdatedAt)
})
t.Run("links are properly decoded", func(t *testing.T) {
assert.NotEmpty(t, rmvRead.Links["upload"])
assert.Contains(t, rmvRead.Links["upload"], "/object/")
})
})
t.Run("when reading a version that does not exist", func(t *testing.T) {
options := RegistryModuleCreateVersionOptions{
Version: String("1.2.3"),
}
registryModuleIDTest := RegistryModuleID{
Organization: orgTest.Name,
Name: registryModuleTest.Name,
Provider: registryModuleTest.Provider,
}
rmv, err := client.RegistryModules.CreateVersion(ctx, registryModuleIDTest, options)
require.NoError(t, err)
assert.NotEmpty(t, rmv.ID)
assert.Equal(t, *options.Version, rmv.Version)
invalidVersion := String("1.5.5")
rmvRead, errRead := client.RegistryModules.ReadVersion(ctx, registryModuleIDTest, *invalidVersion)
require.Error(t, errRead)
assert.Equal(t, ErrResourceNotFound, errRead)
assert.Empty(t, rmvRead)
})
}
func TestRegistryModulesListCommit(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
githubIdentifier := os.Getenv("GITHUB_REGISTRY_MODULE_IDENTIFIER")
if githubIdentifier == "" {
t.Skip("Export a valid GITHUB_REGISTRY_MODULE_IDENTIFIER before running this test")
}
repositoryName := strings.Split(githubIdentifier, "/")[1]
registryModuleProvider := strings.SplitN(repositoryName, "-", 3)[1]
registryModuleName := strings.SplitN(repositoryName, "-", 3)[2]
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
oauthTokenTest, oauthTokenTestCleanup := createOAuthToken(t, client, orgTest)
defer oauthTokenTestCleanup()
t.Run("with valid options", func(t *testing.T) {
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
Identifier: String(githubIdentifier),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(githubIdentifier),
},
}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
require.NoError(t, err)
assert.NotEmpty(t, rm.ID)
assert.Equal(t, registryModuleName, rm.Name)
assert.Equal(t, registryModuleProvider, rm.Provider)
assert.Equal(t, rm.VCSRepo.Branch, "")
assert.Equal(t, rm.VCSRepo.DisplayIdentifier, githubIdentifier)
assert.Equal(t, rm.VCSRepo.Identifier, githubIdentifier)
assert.Equal(t, rm.VCSRepo.IngressSubmodules, true)
assert.Equal(t, rm.VCSRepo.OAuthTokenID, oauthTokenTest.ID)
assert.Equal(t, rm.VCSRepo.RepositoryHTTPURL, fmt.Sprintf("https://github.com/%s", githubIdentifier))
assert.Equal(t, rm.VCSRepo.ServiceProvider, string(ServiceProviderGithub))
assert.Regexp(t, fmt.Sprintf("^%s/webhooks/vcs/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$", regexp.QuoteMeta(DefaultConfig().Address)), rm.VCSRepo.WebhookURL)
t.Run("permissions are properly decoded", func(t *testing.T) {
assert.True(t, rm.Permissions.CanDelete)
assert.True(t, rm.Permissions.CanResync)
assert.True(t, rm.Permissions.CanRetry)
})
t.Run("listing commits", func(t *testing.T) {
cm, errCm := client.RegistryModules.ListCommits(ctx, RegistryModuleID{
Organization: orgTest.Name,
Provider: registryModuleProvider,
Name: registryModuleName,
})
assert.NotEmpty(t, cm)
assert.NotEmpty(t, cm.Items[0])
assert.NotEmpty(t, cm.Items[0].ID)
assert.NotEmpty(t, cm.Items[0].Sha)
assert.NotEmpty(t, cm.Items[0].Message)
assert.NotEmpty(t, cm.Items[0].Date)
require.NoError(t, errCm)
})
})
t.Run("when a VCS connection is not present", func(t *testing.T) {
registryModuleTest, registryModuleTestCleanup := createRegistryModule(t, client, orgTest, PrivateRegistry)
defer registryModuleTestCleanup()
t.Run("listing commits", func(t *testing.T) {
cm, errCm := client.RegistryModules.ListCommits(ctx, RegistryModuleID{
Organization: orgTest.Name,
Provider: registryModuleTest.Provider,
Name: registryModuleTest.Name,
})
assert.Empty(t, cm)
require.Error(t, errCm)
assert.Equal(t, ErrResourceNotFound, errCm)
})
})
}
func TestRegistryModulesCreateWithVCSConnection(t *testing.T) {
t.Parallel()
githubIdentifier := os.Getenv("GITHUB_REGISTRY_MODULE_IDENTIFIER")
if githubIdentifier == "" {
t.Skip("Export a valid GITHUB_REGISTRY_MODULE_IDENTIFIER before running this test")
}
repositoryName := strings.Split(githubIdentifier, "/")[1]
registryModuleProvider := strings.SplitN(repositoryName, "-", 3)[1]
registryModuleName := strings.SplitN(repositoryName, "-", 3)[2]
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
oauthTokenTest, oauthTokenTestCleanup := createOAuthToken(t, client, orgTest)
defer oauthTokenTestCleanup()
t.Run("with valid options", func(t *testing.T) {
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
Identifier: String(githubIdentifier),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(githubIdentifier),
},
}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
require.NoError(t, err)
assert.NotEmpty(t, rm.ID)
assert.Equal(t, registryModuleName, rm.Name)
assert.Equal(t, registryModuleProvider, rm.Provider)
assert.Equal(t, rm.VCSRepo.Branch, "")
assert.Equal(t, rm.VCSRepo.DisplayIdentifier, githubIdentifier)
assert.Equal(t, rm.VCSRepo.Identifier, githubIdentifier)
assert.Equal(t, rm.VCSRepo.IngressSubmodules, true)
assert.Equal(t, rm.VCSRepo.OAuthTokenID, oauthTokenTest.ID)
assert.Equal(t, rm.VCSRepo.RepositoryHTTPURL, fmt.Sprintf("https://github.com/%s", githubIdentifier))
assert.Equal(t, rm.VCSRepo.ServiceProvider, string(ServiceProviderGithub))
assert.Regexp(t, fmt.Sprintf("^%s/webhooks/vcs/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$", regexp.QuoteMeta(DefaultConfig().Address)), rm.VCSRepo.WebhookURL)
t.Run("permissions are properly decoded", func(t *testing.T) {
assert.True(t, rm.Permissions.CanDelete)
assert.True(t, rm.Permissions.CanResync)
assert.True(t, rm.Permissions.CanRetry)
})
t.Run("relationships are properly decoded", func(t *testing.T) {
assert.Equal(t, orgTest.Name, rm.Organization.Name)
})
t.Run("timestamps are properly decoded", func(t *testing.T) {
assert.NotEmpty(t, rm.CreatedAt)
assert.NotEmpty(t, rm.UpdatedAt)
})
})
t.Run("with invalid options", func(t *testing.T) {
t.Run("without an identifier", func(t *testing.T) {
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
Identifier: String(""),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(githubIdentifier),
},
}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
assert.Nil(t, rm)
assert.Equal(t, err, ErrRequiredIdentifier)
})
t.Run("without an oauth token ID", func(t *testing.T) {
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
Identifier: String(githubIdentifier),
OAuthTokenID: String(""),
DisplayIdentifier: String(githubIdentifier),
},
}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
assert.Nil(t, rm)
assert.Equal(t, err, ErrRequiredOauthTokenOrGithubAppInstallationID)
})
t.Run("without a display identifier", func(t *testing.T) {
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
Identifier: String(githubIdentifier),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(""),
},
}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
assert.Nil(t, rm)
assert.Equal(t, err, ErrRequiredDisplayIdentifier)
})
t.Run("when tags are enabled and a branch is provided", func(t *testing.T) {
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
Identifier: String(githubIdentifier),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(githubIdentifier),
Tags: Bool(true),
Branch: String("main"),
},
}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
assert.Nil(t, rm)
assert.Equal(t, err, ErrBranchMustBeEmptyWhenTagsEnabled)
})
})
t.Run("without options", func(t *testing.T) {
options := RegistryModuleCreateWithVCSConnectionOptions{}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
assert.Nil(t, rm)
assert.Equal(t, err, ErrRequiredVCSRepo)
})
}
func TestRegistryModulesCreateBranchBasedWithVCSConnection(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
githubIdentifier := os.Getenv("GITHUB_REGISTRY_MODULE_IDENTIFIER")
if githubIdentifier == "" {
t.Skip("Export a valid GITHUB_REGISTRY_MODULE_IDENTIFIER before running this test")
}
repositoryName := strings.Split(githubIdentifier, "/")[1]
registryModuleProvider := strings.SplitN(repositoryName, "-", 3)[1]
registryModuleName := strings.SplitN(repositoryName, "-", 3)[2]
githubBranch := os.Getenv("GITHUB_REGISTRY_MODULE_BRANCH")
if githubBranch == "" {
githubBranch = "main"
}
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
oauthTokenTest, oauthTokenTestCleanup := createOAuthToken(t, client, orgTest)
defer oauthTokenTestCleanup()
t.Run("with valid options", func(t *testing.T) {
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
OrganizationName: String(orgTest.Name),
Identifier: String(githubIdentifier),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(githubIdentifier),
Branch: String(githubBranch),
},
}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
require.NoError(t, err)
assert.NotEmpty(t, rm.ID)
assert.Equal(t, registryModuleName, rm.Name)
assert.Equal(t, registryModuleProvider, rm.Provider)
assert.Equal(t, githubBranch, rm.VCSRepo.Branch)
assert.Equal(t, false, rm.VCSRepo.Tags)
})
t.Run("with invalid options", func(t *testing.T) {
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
Identifier: String(githubIdentifier),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(githubIdentifier),
Branch: String(githubBranch),
},
}
_, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
require.Equal(t, err, ErrInvalidOrg)
})
}
func TestRegistryModulesCreateMonorepoBranchBasedWithVCSConnection(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
githubIdentifier := os.Getenv("GITHUB_REGISTRY_MODULE_IDENTIFIER")
if githubIdentifier == "" {
t.Skip("Export a valid GITHUB_REGISTRY_MODULE_IDENTIFIER before running this test")
}
repositoryName := strings.Split(githubIdentifier, "/")[1]
registryModuleProvider := strings.SplitN(repositoryName, "-", 3)[1]
registryModuleName := strings.SplitN(repositoryName, "-", 3)[2]
githubBranch := os.Getenv("GITHUB_REGISTRY_MODULE_BRANCH")
if githubBranch == "" {
githubBranch = "main"
}
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthTokenTest, oauthTokenTestCleanup := createOAuthToken(t, client, orgTest)
t.Cleanup(oauthTokenTestCleanup)
t.Run("with valid options including source directory", func(t *testing.T) {
sourceDirectory := "src"
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
OrganizationName: String(orgTest.Name),
Identifier: String(githubIdentifier),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(githubIdentifier),
Branch: String(githubBranch),
SourceDirectory: String(sourceDirectory),
},
}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
require.NoError(t, err)
assert.NotEmpty(t, rm.ID)
assert.Equal(t, registryModuleName, rm.Name)
assert.Equal(t, registryModuleProvider, rm.Provider)
assert.Equal(t, githubBranch, rm.VCSRepo.Branch)
assert.Equal(t, false, rm.VCSRepo.Tags)
assert.Equal(t, sourceDirectory, rm.VCSRepo.SourceDirectory)
})
}
func TestRegistryModulesCreateMonorepoTagBasedWithVCSConnection(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
githubIdentifier := os.Getenv("GITHUB_REGISTRY_MODULE_IDENTIFIER")
if githubIdentifier == "" {
t.Skip("Export a valid GITHUB_REGISTRY_MODULE_IDENTIFIER before running this test")
}
repositoryName := strings.Split(githubIdentifier, "/")[1]
registryModuleProvider := strings.SplitN(repositoryName, "-", 3)[1]
registryModuleName := strings.SplitN(repositoryName, "-", 3)[2]
githubBranch := os.Getenv("GITHUB_REGISTRY_MODULE_BRANCH")
if githubBranch == "" {
githubBranch = "main"
}
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthTokenTest, oauthTokenTestCleanup := createOAuthToken(t, client, orgTest)
t.Cleanup(oauthTokenTestCleanup)
t.Run("with monorepo publishing", func(t *testing.T) {
sourceDirectory := "src"
tagPrefix := "v"
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
OrganizationName: String(orgTest.Name),
Identifier: String(githubIdentifier),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(githubIdentifier),
Branch: String(githubBranch),
SourceDirectory: String(sourceDirectory),
TagPrefix: String(tagPrefix),
},
}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
require.NoError(t, err)
assert.NotEmpty(t, rm.ID)
assert.Equal(t, registryModuleName, rm.Name)
assert.Equal(t, registryModuleProvider, rm.Provider)
assert.Equal(t, rm.VCSRepo.Branch, githubBranch)
assert.Equal(t, rm.VCSRepo.Identifier, githubIdentifier)
assert.Equal(t, rm.VCSRepo.IngressSubmodules, true)
assert.Equal(t, rm.VCSRepo.OAuthTokenID, oauthTokenTest.ID)
assert.Equal(t, rm.VCSRepo.ServiceProvider, string(ServiceProviderGithub))
assert.Regexp(t, fmt.Sprintf("^%s/webhooks/vcs/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$", regexp.QuoteMeta(DefaultConfig().Address)), rm.VCSRepo.WebhookURL)
if rm.VCSRepo.SourceDirectory != sourceDirectory {
t.Errorf("expected SourceDirectory %q, got %q", sourceDirectory, rm.VCSRepo.SourceDirectory)
}
if rm.VCSRepo.TagPrefix != tagPrefix {
t.Errorf("expected TagPrefix %q, got %q", tagPrefix, rm.VCSRepo.TagPrefix)
}
})
t.Run("without monorepo publishing", func(t *testing.T) {
tagPrefix := "v"
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
OrganizationName: String(orgTest.Name),
Identifier: String(githubIdentifier),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(githubIdentifier),
Branch: String(githubBranch),
TagPrefix: String(tagPrefix),
},
}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
require.NoError(t, err)
assert.NotEmpty(t, rm.ID)
assert.Equal(t, registryModuleName, rm.Name)
assert.Equal(t, registryModuleProvider, rm.Provider)
assert.Equal(t, rm.VCSRepo.Branch, githubBranch)
assert.Equal(t, rm.VCSRepo.Identifier, githubIdentifier)
assert.Equal(t, rm.VCSRepo.IngressSubmodules, true)
assert.Equal(t, rm.VCSRepo.OAuthTokenID, oauthTokenTest.ID)
assert.Equal(t, rm.VCSRepo.ServiceProvider, string(ServiceProviderGithub))
assert.Regexp(t, fmt.Sprintf("^%s/webhooks/vcs/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$", regexp.QuoteMeta(DefaultConfig().Address)), rm.VCSRepo.WebhookURL)
if rm.VCSRepo.SourceDirectory != "" {
t.Errorf("expected SourceDirectory %q, got %q", "", rm.VCSRepo.SourceDirectory)
}
if rm.VCSRepo.TagPrefix != tagPrefix {
t.Errorf("expected TagPrefix %q, got %q", tagPrefix, rm.VCSRepo.TagPrefix)
}
})
}
func TestRegistryModulesCreateMonorepoNonStandardName(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
// This test uses a repository like "private-modules" or "monorepo" that doesn't
// follow the terraform-- pattern, which would previously fail
// with "Name is invalid" error.
githubIdentifier := os.Getenv("GITHUB_REGISTRY_MODULE_IDENTIFIER")
if githubIdentifier == "" {
t.Skip("Export a valid GITHUB_REGISTRY_MODULE_IDENTIFIER before running this test")
}
githubBranch := os.Getenv("GITHUB_REGISTRY_MODULE_BRANCH")
if githubBranch == "" {
githubBranch = "main"
}
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthTokenTest, oauthTokenTestCleanup := createOAuthToken(t, client, orgTest)
t.Cleanup(oauthTokenTestCleanup)
t.Run("with explicit name and provider for monorepo with tags", func(t *testing.T) {
sourceDirectory := "modules/nestedA"
moduleName := "nestedA"
moduleProvider := "aws"
options := RegistryModuleCreateWithVCSConnectionOptions{
Name: String(moduleName),
Provider: String(moduleProvider),
VCSRepo: &RegistryModuleVCSRepoOptions{
OrganizationName: String(orgTest.Name),
Identifier: String(githubIdentifier),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(githubIdentifier),
SourceDirectory: String(sourceDirectory),
Tags: Bool(true),
},
}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
require.NoError(t, err)
assert.NotEmpty(t, rm.ID)
assert.Equal(t, moduleName, rm.Name)
assert.Equal(t, moduleProvider, rm.Provider)
assert.Equal(t, sourceDirectory, rm.VCSRepo.SourceDirectory)
assert.Equal(t, true, rm.VCSRepo.Tags)
})
t.Run("with explicit name and provider for monorepo with branch", func(t *testing.T) {
sourceDirectory := "modules/nestedB"
moduleName := "nestedB"
moduleProvider := "gcp"
options := RegistryModuleCreateWithVCSConnectionOptions{
Name: String(moduleName),
Provider: String(moduleProvider),
VCSRepo: &RegistryModuleVCSRepoOptions{
OrganizationName: String(orgTest.Name),
Identifier: String(githubIdentifier),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(githubIdentifier),
Branch: String(githubBranch),
SourceDirectory: String(sourceDirectory),
},
}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
require.NoError(t, err)
assert.NotEmpty(t, rm.ID)
assert.Equal(t, moduleName, rm.Name)
assert.Equal(t, moduleProvider, rm.Provider)
assert.Equal(t, sourceDirectory, rm.VCSRepo.SourceDirectory)
assert.Equal(t, githubBranch, rm.VCSRepo.Branch)
assert.Equal(t, false, rm.VCSRepo.Tags)
})
t.Run("with explicit name and provider for deeply nested path", func(t *testing.T) {
sourceDirectory := "terraform/modules/aws/compute"
moduleName := "compute"
moduleProvider := "aws"
options := RegistryModuleCreateWithVCSConnectionOptions{
Name: String(moduleName),
Provider: String(moduleProvider),
VCSRepo: &RegistryModuleVCSRepoOptions{
OrganizationName: String(orgTest.Name),
Identifier: String(githubIdentifier),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(githubIdentifier),
Branch: String(githubBranch),
SourceDirectory: String(sourceDirectory),
},
}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
require.NoError(t, err)
assert.NotEmpty(t, rm.ID)
assert.Equal(t, moduleName, rm.Name)
assert.Equal(t, moduleProvider, rm.Provider)
assert.Equal(t, sourceDirectory, rm.VCSRepo.SourceDirectory)
})
t.Run("with explicit name and provider for various providers", func(t *testing.T) {
testCases := []struct {
name string
moduleName string
moduleProvider string
sourceDirectory string
}{
{
name: "azurerm provider",
moduleName: "vnet",
moduleProvider: "azurerm",
sourceDirectory: "modules/azure-vnet",
},
{
name: "random provider",
moduleName: "pet",
moduleProvider: "random",
sourceDirectory: "modules/random-pet",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
options := RegistryModuleCreateWithVCSConnectionOptions{
Name: String(tc.moduleName),
Provider: String(tc.moduleProvider),
VCSRepo: &RegistryModuleVCSRepoOptions{
OrganizationName: String(orgTest.Name),
Identifier: String(githubIdentifier),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(githubIdentifier),
Branch: String(githubBranch),
SourceDirectory: String(tc.sourceDirectory),
},
}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
require.NoError(t, err)
assert.NotEmpty(t, rm.ID)
assert.Equal(t, tc.moduleName, rm.Name)
assert.Equal(t, tc.moduleProvider, rm.Provider)
assert.Equal(t, tc.sourceDirectory, rm.VCSRepo.SourceDirectory)
})
}
})
}
func TestRegistryModulesCreateBranchBasedWithVCSConnectionWithTesting(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
githubIdentifier := os.Getenv("GITHUB_REGISTRY_MODULE_IDENTIFIER")
if githubIdentifier == "" {
t.Skip("Export a valid GITHUB_REGISTRY_MODULE_IDENTIFIER before running this test")
}
repositoryName := strings.Split(githubIdentifier, "/")[1]
registryModuleProvider := strings.SplitN(repositoryName, "-", 3)[1]
registryModuleName := strings.SplitN(repositoryName, "-", 3)[2]
githubBranch := os.Getenv("GITHUB_REGISTRY_MODULE_BRANCH")
if githubBranch == "" {
githubBranch = "main"
}
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
oauthTokenTest, oauthTokenTestCleanup := createOAuthToken(t, client, orgTest)
defer oauthTokenTestCleanup()
t.Run("with valid options", func(t *testing.T) {
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
OrganizationName: String(orgTest.Name),
Identifier: String(githubIdentifier),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(githubIdentifier),
Branch: String(githubBranch),
},
TestConfig: &RegistryModuleTestConfigOptions{
TestsEnabled: Bool(true),
},
}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
require.NoError(t, err)
assert.NotEmpty(t, rm.ID)
assert.Equal(t, registryModuleName, rm.Name)
assert.Equal(t, registryModuleProvider, rm.Provider)
assert.Equal(t, githubBranch, rm.VCSRepo.Branch)
assert.Equal(t, false, rm.VCSRepo.Tags)
t.Run("tests are enabled", func(t *testing.T) {
assert.NotEmpty(t, rm.TestConfig)
assert.True(t, rm.TestConfig.TestsEnabled)
})
})
t.Run("with invalid options", func(t *testing.T) {
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
Identifier: String(githubIdentifier),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(githubIdentifier),
Branch: String(githubBranch),
},
}
_, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
require.Equal(t, err, ErrInvalidOrg)
t.Run("when the the module is not branch based and test are enabled", func(t *testing.T) {
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
Identifier: String(githubIdentifier),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(githubIdentifier),
},
TestConfig: &RegistryModuleTestConfigOptions{
TestsEnabled: Bool(true),
},
}
_, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
require.Equal(t, err, ErrRequiredBranchWhenTestsEnabled)
})
})
}
func TestRegistryModulesCreateWithGithubApp(t *testing.T) {
t.Parallel()
githubIdentifier := os.Getenv("GITHUB_REGISTRY_MODULE_IDENTIFIER")
if githubIdentifier == "" {
t.Skip("Export a valid GITHUB_REGISTRY_MODULE_IDENTIFIER before running this test")
}
gHAInstallationID := os.Getenv("GITHUB_APP_INSTALLATION_ID")
if gHAInstallationID == "" {
t.Skip("Export a valid GITHUB_APP_INSTALLATION_ID before running this test!")
}
repositoryName := strings.Split(githubIdentifier, "/")[1]
registryModuleProvider := strings.SplitN(repositoryName, "-", 3)[1]
registryModuleName := strings.SplitN(repositoryName, "-", 3)[2]
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
t.Run("with valid options", func(t *testing.T) {
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
Identifier: String(githubIdentifier),
DisplayIdentifier: String(githubIdentifier),
GHAInstallationID: String(gHAInstallationID),
OrganizationName: String(orgTest.Name),
},
}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
require.NoError(t, err)
assert.NotEmpty(t, rm.ID)
assert.Equal(t, registryModuleName, rm.Name)
assert.Equal(t, registryModuleProvider, rm.Provider)
assert.Equal(t, rm.VCSRepo.Branch, "")
assert.Equal(t, rm.VCSRepo.DisplayIdentifier, githubIdentifier)
assert.Equal(t, rm.VCSRepo.Identifier, githubIdentifier)
assert.Equal(t, rm.VCSRepo.IngressSubmodules, true)
assert.Equal(t, rm.VCSRepo.GHAInstallationID, gHAInstallationID)
assert.Equal(t, rm.VCSRepo.RepositoryHTTPURL, fmt.Sprintf("https://github.com/%s", githubIdentifier))
assert.Equal(t, rm.VCSRepo.ServiceProvider, string("github_app"))
t.Run("permissions are properly decoded", func(t *testing.T) {
assert.True(t, rm.Permissions.CanDelete)
assert.True(t, rm.Permissions.CanResync)
assert.True(t, rm.Permissions.CanRetry)
})
t.Run("relationships are properly decoded", func(t *testing.T) {
assert.Equal(t, orgTest.Name, rm.Organization.Name)
})
t.Run("timestamps are properly decoded", func(t *testing.T) {
assert.NotEmpty(t, rm.CreatedAt)
assert.NotEmpty(t, rm.UpdatedAt)
})
})
t.Run("with invalid options", func(t *testing.T) {
t.Run("without an github app installation ID", func(t *testing.T) {
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
Identifier: String(githubIdentifier),
DisplayIdentifier: String(githubIdentifier),
OrganizationName: String(orgTest.Name),
},
}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
assert.Nil(t, rm)
assert.Equal(t, err, ErrRequiredOauthTokenOrGithubAppInstallationID)
})
t.Run("without an org name", func(t *testing.T) {
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
Identifier: String(githubIdentifier),
GHAInstallationID: String(gHAInstallationID),
},
}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
assert.Nil(t, rm)
assert.Equal(t, err, ErrInvalidOrg)
})
})
t.Run("without options", func(t *testing.T) {
options := RegistryModuleCreateWithVCSConnectionOptions{}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
assert.Nil(t, rm)
assert.Equal(t, err, ErrRequiredVCSRepo)
})
}
func TestRegistryModulesRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
registryModuleTest, registryModuleTestCleanup := createRegistryModule(t, client, orgTest, PrivateRegistry)
defer registryModuleTestCleanup()
publicRegistryModuleTest, publicRegistryModuleTestCleanup := createRegistryModule(t, client, orgTest, PublicRegistry)
defer publicRegistryModuleTestCleanup()
t.Run("with valid name and provider", func(t *testing.T) {
rm, err := client.RegistryModules.Read(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: registryModuleTest.Name,
Provider: registryModuleTest.Provider,
})
require.NoError(t, err)
assert.Equal(t, registryModuleTest.ID, rm.ID)
t.Run("permissions are properly decoded", func(t *testing.T) {
assert.True(t, rm.Permissions.CanDelete)
assert.True(t, rm.Permissions.CanResync)
assert.True(t, rm.Permissions.CanRetry)
})
t.Run("timestamps are properly decoded", func(t *testing.T) {
assert.NotEmpty(t, rm.CreatedAt)
assert.NotEmpty(t, rm.UpdatedAt)
})
})
t.Run("with complete registry module ID fields for private module", func(t *testing.T) {
rm, err := client.RegistryModules.Read(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: registryModuleTest.Name,
Provider: registryModuleTest.Provider,
Namespace: orgTest.Name,
RegistryName: PrivateRegistry,
})
require.NoError(t, err)
require.NotEmpty(t, rm)
assert.Equal(t, registryModuleTest.ID, rm.ID)
t.Run("permissions are properly decoded", func(t *testing.T) {
require.NotEmpty(t, rm.Permissions)
assert.True(t, rm.Permissions.CanDelete)
assert.True(t, rm.Permissions.CanResync)
assert.True(t, rm.Permissions.CanRetry)
})
t.Run("timestamps are properly decoded", func(t *testing.T) {
assert.NotEmpty(t, rm.CreatedAt)
assert.NotEmpty(t, rm.UpdatedAt)
})
})
t.Run("with complete registry module ID fields for public module", func(t *testing.T) {
rm, err := client.RegistryModules.Read(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: publicRegistryModuleTest.Name,
Provider: publicRegistryModuleTest.Provider,
Namespace: publicRegistryModuleTest.Namespace,
RegistryName: PublicRegistry,
})
require.NoError(t, err)
require.NotEmpty(t, rm)
assert.Equal(t, publicRegistryModuleTest.ID, rm.ID)
t.Run("permissions are properly decoded", func(t *testing.T) {
require.NotEmpty(t, rm.Permissions)
assert.True(t, rm.Permissions.CanDelete)
assert.True(t, rm.Permissions.CanResync)
assert.True(t, rm.Permissions.CanRetry)
})
t.Run("timestamps are properly decoded", func(t *testing.T) {
assert.NotEmpty(t, rm.CreatedAt)
assert.NotEmpty(t, rm.UpdatedAt)
})
})
t.Run("with a unique ID field for private module", func(t *testing.T) {
rm, err := client.RegistryModules.Read(ctx, RegistryModuleID{
ID: registryModuleTest.ID,
})
require.NoError(t, err)
require.NotEmpty(t, rm)
assert.Equal(t, registryModuleTest.ID, rm.ID)
t.Run("permissions are properly decoded", func(t *testing.T) {
require.NotEmpty(t, rm.Permissions)
assert.True(t, rm.Permissions.CanDelete)
assert.True(t, rm.Permissions.CanResync)
assert.True(t, rm.Permissions.CanRetry)
})
t.Run("timestamps are properly decoded", func(t *testing.T) {
assert.NotEmpty(t, rm.CreatedAt)
assert.NotEmpty(t, rm.UpdatedAt)
})
})
t.Run("without a name", func(t *testing.T) {
rm, err := client.RegistryModules.Read(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: "",
Provider: registryModuleTest.Provider,
})
assert.Nil(t, rm)
assert.EqualError(t, err, ErrRequiredName.Error())
})
t.Run("with an invalid name", func(t *testing.T) {
rm, err := client.RegistryModules.Read(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: badIdentifier,
Provider: registryModuleTest.Provider,
})
assert.Nil(t, rm)
assert.EqualError(t, err, ErrInvalidName.Error())
})
t.Run("without a provider", func(t *testing.T) {
rm, err := client.RegistryModules.Read(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: registryModuleTest.Name,
Provider: "",
})
assert.Nil(t, rm)
assert.Equal(t, err, ErrRequiredProvider)
})
t.Run("with an invalid provider", func(t *testing.T) {
rm, err := client.RegistryModules.Read(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: registryModuleTest.Name,
Provider: badIdentifier,
})
assert.Nil(t, rm)
assert.Equal(t, err, ErrInvalidProvider)
})
t.Run("with an invalid registry name", func(t *testing.T) {
rm, err := client.RegistryModules.Read(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: registryModuleTest.Name,
Provider: registryModuleTest.Provider,
Namespace: orgTest.Name,
RegistryName: "PRIVATE",
})
assert.Nil(t, rm)
assert.Equal(t, err, ErrInvalidRegistryName)
})
t.Run("without a valid organization", func(t *testing.T) {
rm, err := client.RegistryModules.Read(ctx, RegistryModuleID{
Organization: badIdentifier,
Name: registryModuleTest.Name,
Provider: registryModuleTest.Provider,
})
assert.Nil(t, rm)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("without a valid namespace for public registry module", func(t *testing.T) {
rm, err := client.RegistryModules.Read(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: publicRegistryModuleTest.Name,
Provider: publicRegistryModuleTest.Provider,
RegistryName: PublicRegistry,
})
assert.Nil(t, rm)
assert.EqualError(t, err, ErrRequiredNamespace.Error())
})
t.Run("when the registry module does not exist", func(t *testing.T) {
rm, err := client.RegistryModules.Read(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: "nonexisting",
Provider: "nonexisting",
})
assert.Nil(t, rm)
assert.Error(t, err)
})
}
func TestRegistryModulesReadTerraformRegistryModule(t *testing.T) {
t.Parallel()
t.Skip("Skipping due to persistent failures - see TF-31172")
client := testClient(t)
ctx := context.Background()
r := require.New(t)
githubIdentifier := os.Getenv("GITHUB_REGISTRY_NO_CODE_MODULE_IDENTIFIER")
if githubIdentifier == "" {
t.Skip("Export a valid GITHUB_REGISTRY_NO_CODE_MODULE_IDENTIFIER before running this test")
}
// NOTE: These test cases use time.Sleep to wait for the module to be ready,
// an enhancement to these test cases would be to use a polling mechanism to
// check if the module is ready, and then time out if it is not ready after a
// certain amount of time.
t.Run("fetch module from private registry", func(t *testing.T) {
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
token, cleanupToken := createOAuthToken(t, client, orgTest)
defer cleanupToken()
rmOpts := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
OrganizationName: String(orgTest.Name),
Identifier: String(githubIdentifier),
Tags: Bool(true),
OAuthTokenID: String(token.ID),
DisplayIdentifier: String(githubIdentifier),
},
}
version := "1.0.0"
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, rmOpts)
r.NoError(err)
time.Sleep(time.Second * 10)
rmID := RegistryModuleID{
Organization: orgTest.Name,
Name: rm.Name,
Provider: rm.Provider,
Namespace: rm.Namespace,
RegistryName: rm.RegistryName,
}
tfm, err := client.RegistryModules.ReadTerraformRegistryModule(ctx, rmID, version)
r.NoError(err)
r.NotNil(tfm)
r.Equal(fmt.Sprintf("%s/%s/%s/%s", orgTest.Name, rm.Name, rm.Provider, version), tfm.ID)
r.Equal(rm.Name, tfm.Name)
r.Equal("A test Terraform module for use in CI pipelines", tfm.Description)
r.Equal(rm.Provider, tfm.Provider)
r.Equal(rm.Namespace, tfm.Namespace)
r.Equal(version, tfm.Version)
r.Equal("", tfm.Tag)
r.Equal(0, tfm.Downloads)
r.False(tfm.Verified)
r.NotNil(tfm.Root)
r.Equal(rm.Name, tfm.Root.Name)
r.Equal("", tfm.Root.Readme)
r.False(tfm.Root.Empty)
r.Len(tfm.Root.Inputs, 1)
r.Len(tfm.Root.Outputs, 1)
r.Len(tfm.Root.ProviderDependencies, 1)
r.Len(tfm.Root.Resources, 1)
})
t.Run("fetch module from public registry", func(t *testing.T) {
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
token, cleanupToken := createOAuthToken(t, client, orgTest)
defer cleanupToken()
rmOpts := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
OrganizationName: String(orgTest.Name),
Identifier: String(githubIdentifier),
Tags: Bool(true),
OAuthTokenID: String(token.ID),
DisplayIdentifier: String(githubIdentifier),
},
}
version := "1.0.0"
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, rmOpts)
r.NoError(err)
time.Sleep(time.Second * 10)
rmID := RegistryModuleID{
Organization: orgTest.Name,
Name: rm.Name,
Provider: rm.Provider,
Namespace: rm.Namespace,
RegistryName: rm.RegistryName,
}
tfm, err := client.RegistryModules.ReadTerraformRegistryModule(ctx, rmID, version)
r.NoError(err)
r.NotNil(tfm)
r.Equal(fmt.Sprintf("%s/%s/%s/%s", orgTest.Name, rm.Name, rm.Provider, version), tfm.ID)
r.Equal(rm.Name, tfm.Name)
r.Equal("A test Terraform module for use in CI pipelines", tfm.Description)
r.Equal(rm.Provider, tfm.Provider)
r.Equal(rm.Namespace, tfm.Namespace)
r.Equal(version, tfm.Version)
r.Equal("", tfm.Tag)
r.Equal(0, tfm.Downloads)
r.False(tfm.Verified)
r.NotNil(tfm.Root)
r.Equal(rm.Name, tfm.Root.Name)
r.Equal("", tfm.Root.Readme)
r.False(tfm.Root.Empty)
r.Len(tfm.Root.Inputs, 1)
r.Len(tfm.Root.Outputs, 1)
r.Len(tfm.Root.ProviderDependencies, 1)
r.Len(tfm.Root.Resources, 1)
})
}
func TestRegistryModulesDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
registryModuleTest, _ := createRegistryModule(t, client, orgTest, PrivateRegistry)
t.Run("with valid name", func(t *testing.T) {
err := client.RegistryModules.Delete(ctx, orgTest.Name, registryModuleTest.Name)
require.NoError(t, err)
rm, err := client.RegistryModules.Read(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: registryModuleTest.Name,
Provider: registryModuleTest.Provider,
})
assert.Nil(t, rm)
assert.Error(t, err)
})
t.Run("without a name", func(t *testing.T) {
err := client.RegistryModules.Delete(ctx, orgTest.Name, "")
assert.EqualError(t, err, ErrRequiredName.Error())
})
t.Run("with an invalid name", func(t *testing.T) {
err := client.RegistryModules.Delete(ctx, orgTest.Name, badIdentifier)
assert.EqualError(t, err, ErrInvalidName.Error())
})
t.Run("without a valid organization", func(t *testing.T) {
err := client.RegistryModules.Delete(ctx, badIdentifier, registryModuleTest.Name)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("when the registry module does not exist", func(t *testing.T) {
err := client.RegistryModules.Delete(ctx, orgTest.Name, "nonexisting")
assert.Error(t, err)
assert.Equal(t, ErrResourceNotFound, err)
})
}
func TestRegistryModulesDeleteByName(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
registryModuleTest, _ := createRegistryModule(t, client, orgTest, PrivateRegistry)
assert.NotNil(t, orgTest)
t.Run("with valid parameters", func(t *testing.T) {
err := client.RegistryModules.DeleteByName(ctx, RegistryModuleID{
Organization: orgTest.Name,
RegistryName: PrivateRegistry,
Namespace: registryModuleTest.Namespace,
Name: registryModuleTest.Name,
})
require.NoError(t, err)
})
t.Run("when the registry module does not exist", func(t *testing.T) {
err := client.RegistryModules.DeleteByName(ctx, RegistryModuleID{
Organization: orgTest.Name,
RegistryName: PrivateRegistry,
Namespace: registryModuleTest.Namespace,
Name: "",
})
assert.Error(t, err)
assert.Equal(t, err, ErrRequiredName)
})
t.Run("with invalid org", func(t *testing.T) {
err := client.RegistryModules.DeleteByName(ctx, RegistryModuleID{
Organization: badIdentifier,
RegistryName: PrivateRegistry,
Namespace: registryModuleTest.Namespace,
Name: registryModuleTest.Name,
})
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("with invalid registry name", func(t *testing.T) {
err := client.RegistryModules.DeleteByName(ctx, RegistryModuleID{
Organization: orgTest.Name,
RegistryName: badIdentifier,
Namespace: registryModuleTest.Namespace,
Name: registryModuleTest.Name,
})
assert.Error(t, err)
})
}
func TestRegistryModulesDeleteProvider(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
registryModuleTest, _ := createRegistryModule(t, client, orgTest, PrivateRegistry)
assert.NotNil(t, orgTest)
t.Run("with valid parameters", func(t *testing.T) {
err := client.RegistryModules.DeleteProvider(ctx, RegistryModuleID{
Organization: orgTest.Name,
RegistryName: PrivateRegistry,
Namespace: registryModuleTest.Organization.Name,
Name: registryModuleTest.Name,
Provider: registryModuleTest.Provider,
})
require.NoError(t, err)
})
t.Run("without a provider", func(t *testing.T) {
err := client.RegistryModules.DeleteProvider(ctx, RegistryModuleID{
Organization: orgTest.Name,
RegistryName: registryModuleTest.RegistryName,
Name: registryModuleTest.Name,
Namespace: registryModuleTest.Namespace,
Provider: "",
})
assert.Equal(t, err, ErrRequiredProvider)
})
t.Run("with an invalid provider", func(t *testing.T) {
err := client.RegistryModules.DeleteProvider(ctx, RegistryModuleID{
Organization: orgTest.Name,
RegistryName: registryModuleTest.RegistryName,
Name: registryModuleTest.Name,
Namespace: registryModuleTest.Namespace,
Provider: badIdentifier,
})
assert.Equal(t, err, ErrInvalidProvider)
})
t.Run("without a name", func(t *testing.T) {
err := client.RegistryModules.DeleteProvider(ctx, RegistryModuleID{
Organization: orgTest.Name,
RegistryName: registryModuleTest.RegistryName,
Namespace: registryModuleTest.Namespace,
Name: "",
Provider: registryModuleTest.Provider,
})
assert.EqualError(t, err, ErrRequiredName.Error())
})
t.Run("with an invalid name", func(t *testing.T) {
err := client.RegistryModules.DeleteProvider(ctx, RegistryModuleID{
Organization: orgTest.Name,
RegistryName: registryModuleTest.RegistryName,
Name: badIdentifier,
Namespace: registryModuleTest.Namespace,
Provider: registryModuleTest.Provider,
})
assert.EqualError(t, err, ErrInvalidName.Error())
})
t.Run("with invalid org", func(t *testing.T) {
err := client.RegistryModules.DeleteProvider(ctx, RegistryModuleID{
Organization: badIdentifier,
RegistryName: PrivateRegistry,
Namespace: "terraform-aws-modules",
Name: registryModuleTest.Name,
Provider: registryModuleTest.Provider,
})
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("without registry name", func(t *testing.T) {
err := client.RegistryModules.DeleteProvider(ctx, RegistryModuleID{
Organization: orgTest.Name,
RegistryName: badIdentifier,
Namespace: registryModuleTest.Namespace,
Name: registryModuleTest.Name,
Provider: registryModuleTest.Provider,
})
assert.Equal(t, ErrInvalidRegistryName, err)
})
t.Run("with invalid registry name", func(t *testing.T) {
err := client.RegistryModules.DeleteProvider(ctx, RegistryModuleID{
Organization: orgTest.Name,
RegistryName: badIdentifier,
Namespace: registryModuleTest.Namespace,
Name: registryModuleTest.Name,
Provider: registryModuleTest.Provider,
})
assert.Error(t, err)
})
t.Run("with namespace and when registry name is private", func(t *testing.T) {
err := client.RegistryModules.DeleteProvider(ctx, RegistryModuleID{
Organization: orgTest.Name,
RegistryName: PrivateRegistry,
Namespace: registryModuleTest.Namespace,
Name: registryModuleTest.Name,
Provider: registryModuleTest.Provider,
})
assert.Error(t, err)
})
}
func TestRegistryModulesDeleteVersion(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
registryModuleTest, registryModuleTestCleanup := createRegistryModuleWithVersion(t, client, orgTest)
defer registryModuleTestCleanup()
assert.NotNil(t, orgTest)
t.Run("create module version and delete with valid name and provider", func(t *testing.T) {
options := RegistryModuleCreateVersionOptions{
Version: String("1.2.3"),
}
mod, err := client.RegistryModules.CreateVersion(ctx, RegistryModuleID{
Organization: orgTest.Name,
RegistryName: PrivateRegistry,
Name: registryModuleTest.Name,
Namespace: registryModuleTest.Namespace,
Provider: registryModuleTest.Provider,
}, options)
require.NoError(t, err)
require.NotEmpty(t, mod.Version)
err = client.RegistryModules.DeleteVersion(ctx, RegistryModuleID{
Organization: orgTest.Name,
RegistryName: PrivateRegistry,
Name: registryModuleTest.Name,
Namespace: registryModuleTest.Namespace,
Provider: registryModuleTest.Provider,
}, mod.Version)
require.NoError(t, err)
})
t.Run("without registry name", func(t *testing.T) {
err := client.RegistryModules.DeleteVersion(ctx, RegistryModuleID{
Organization: orgTest.Name,
RegistryName: badIdentifier,
Namespace: registryModuleTest.Namespace,
Name: registryModuleTest.Name,
Provider: registryModuleTest.Provider,
}, registryModuleTest.VersionStatuses[0].Version)
assert.Equal(t, ErrInvalidRegistryName, err)
})
t.Run("with invalid registry name", func(t *testing.T) {
err := client.RegistryModules.DeleteVersion(ctx, RegistryModuleID{
Organization: orgTest.Name,
RegistryName: badIdentifier,
Namespace: registryModuleTest.Namespace,
Name: registryModuleTest.Name,
Provider: registryModuleTest.Provider,
}, registryModuleTest.VersionStatuses[0].Version)
assert.Error(t, err)
})
t.Run("without a name", func(t *testing.T) {
err := client.RegistryModules.DeleteVersion(ctx, RegistryModuleID{
Organization: orgTest.Name,
RegistryName: PrivateRegistry,
Namespace: registryModuleTest.Namespace,
Name: "",
Provider: registryModuleTest.Provider,
}, registryModuleTest.VersionStatuses[0].Version)
assert.EqualError(t, err, ErrRequiredName.Error())
})
t.Run("with an invalid name", func(t *testing.T) {
err := client.RegistryModules.DeleteVersion(ctx, RegistryModuleID{
Organization: orgTest.Name,
RegistryName: PrivateRegistry,
Name: badIdentifier,
Namespace: registryModuleTest.Namespace,
Provider: registryModuleTest.Provider,
}, registryModuleTest.VersionStatuses[0].Version)
assert.EqualError(t, err, ErrInvalidName.Error())
})
t.Run("without a provider", func(t *testing.T) {
err := client.RegistryModules.DeleteVersion(ctx, RegistryModuleID{
Organization: orgTest.Name,
RegistryName: PrivateRegistry,
Name: registryModuleTest.Name,
Namespace: registryModuleTest.Namespace,
Provider: "",
}, registryModuleTest.VersionStatuses[0].Version)
assert.Equal(t, err, ErrRequiredProvider)
})
t.Run("with an invalid provider", func(t *testing.T) {
err := client.RegistryModules.DeleteVersion(ctx, RegistryModuleID{
Organization: orgTest.Name,
RegistryName: PrivateRegistry,
Name: registryModuleTest.Name,
Namespace: registryModuleTest.Namespace,
Provider: badIdentifier,
}, registryModuleTest.VersionStatuses[0].Version)
assert.Equal(t, err, ErrInvalidProvider)
})
t.Run("without a version", func(t *testing.T) {
err := client.RegistryModules.DeleteVersion(ctx, RegistryModuleID{
Organization: orgTest.Name,
RegistryName: PrivateRegistry,
Name: registryModuleTest.Name,
Namespace: registryModuleTest.Namespace,
Provider: registryModuleTest.Provider,
}, "")
assert.Equal(t, err, ErrRequiredVersion)
})
t.Run("with an invalid version", func(t *testing.T) {
err := client.RegistryModules.DeleteVersion(ctx, RegistryModuleID{
Organization: orgTest.Name,
RegistryName: PrivateRegistry,
Name: registryModuleTest.Name,
Namespace: registryModuleTest.Namespace,
Provider: registryModuleTest.Provider,
}, badIdentifier)
assert.Equal(t, err, ErrInvalidVersion)
})
t.Run("without a valid organization", func(t *testing.T) {
err := client.RegistryModules.DeleteVersion(ctx, RegistryModuleID{
Organization: badIdentifier,
RegistryName: PrivateRegistry,
Name: registryModuleTest.Name,
Namespace: registryModuleTest.Namespace,
Provider: registryModuleTest.Provider,
}, registryModuleTest.VersionStatuses[0].Version)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("with prerelease and metadata version", func(t *testing.T) {
options := RegistryModuleCreateVersionOptions{
Version: String("1.2.3-alpha+feature"),
}
mod, err := client.RegistryModules.CreateVersion(ctx, RegistryModuleID{
Organization: orgTest.Name,
RegistryName: PrivateRegistry,
Name: registryModuleTest.Name,
Namespace: registryModuleTest.Namespace,
Provider: registryModuleTest.Provider,
}, options)
require.NoError(t, err)
require.NotEmpty(t, mod.Version)
err = client.RegistryModules.DeleteVersion(ctx, RegistryModuleID{
Organization: orgTest.Name,
RegistryName: PrivateRegistry,
Name: registryModuleTest.Name,
Namespace: registryModuleTest.Namespace,
Provider: registryModuleTest.Provider,
}, mod.Version)
require.NoError(t, err)
})
}
func TestRegistryModulesUpload(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
rm, _ := createRegistryModule(t, client, orgTest, PrivateRegistry)
optionsModuleVersion := RegistryModuleCreateVersionOptions{
Version: String("1.0.0"),
}
rmv, err := client.RegistryModules.CreateVersion(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: rm.Name,
Provider: rm.Provider,
}, optionsModuleVersion)
if err != nil {
t.Fatal(err)
}
t.Run("with valid upload URL", func(t *testing.T) {
err = client.RegistryModules.Upload(
ctx,
*rmv,
"test-fixtures/config-version",
)
require.NoError(t, err)
})
t.Run("with missing upload URL", func(t *testing.T) {
delete(rmv.Links, "upload")
err = client.RegistryModules.Upload(
ctx,
*rmv,
"test-fixtures/config-version",
)
assert.EqualError(t, err, "provided RegistryModuleVersion does not contain an upload link")
})
}
func TestRegistryModulesUploadTarGzip(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
rm, rmCleanup := createRegistryModule(t, client, orgTest, PrivateRegistry)
t.Cleanup(rmCleanup)
optionsModuleVersion := RegistryModuleCreateVersionOptions{
Version: String("1.0.0"),
}
rmv, err := client.RegistryModules.CreateVersion(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: rm.Name,
Provider: rm.Provider,
}, optionsModuleVersion)
require.NoError(t, err)
uploadURL, ok := rmv.Links["upload"].(string)
require.True(t, ok)
t.Run("with custom go-slug", func(t *testing.T) {
packer, err := slug.NewPacker(
slug.DereferenceSymlinks(),
slug.ApplyTerraformIgnore(),
)
require.NoError(t, err)
body := bytes.NewBuffer(nil)
_, err = packer.Pack("test-fixtures/config-version", body)
require.NoError(t, err)
err = client.RegistryModules.UploadTarGzip(ctx, uploadURL, body)
require.NoError(t, err)
})
t.Run("with custom tar archive", func(t *testing.T) {
archivePath := "test-fixtures/registry-module-archive.tar.gz"
createTarGzipArchive(t, []string{"test-fixtures/config-version/main.tf"}, archivePath)
archive, err := os.Open(archivePath)
require.NoError(t, err)
defer archive.Close()
err = client.RegistryModules.UploadTarGzip(ctx, uploadURL, archive)
require.NoError(t, err)
})
}
func TestRegistryModule_Unmarshal(t *testing.T) {
t.Parallel()
data := map[string]interface{}{
"data": map[string]interface{}{
"type": "registry-modules",
"id": "1",
"attributes": map[string]interface{}{
"name": "module",
"provider": "tfe",
"namespace": "org-abc",
"registry-name": "private",
"permissions": map[string]interface{}{
"can-delete": true,
"can-resync": true,
"can-retry": true,
},
"status": RegistryModuleStatusPending,
"vcs-repo": map[string]interface{}{
"branch": "main",
"display-identifier": "display",
"identifier": "identifier",
"ingress-submodules": true,
"oauth-token-id": "token",
"repository-http-url": "github.com",
"service-provider": "github",
"webhook-url": "https://app.terraform.io/webhooks/vcs/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
},
"version-statuses": []interface{}{
map[string]interface{}{
"version": "1.1.1",
"status": RegistryModuleVersionStatusPending,
"error": "no error",
},
},
},
},
}
byteData, err := json.Marshal(data)
require.NoError(t, err)
responseBody := bytes.NewReader(byteData)
rm := &RegistryModule{}
err = unmarshalResponse(responseBody, rm)
require.NoError(t, err)
assert.Equal(t, rm.ID, "1")
assert.Equal(t, rm.Name, "module")
assert.Equal(t, rm.Provider, "tfe")
assert.Equal(t, rm.Namespace, "org-abc")
assert.Equal(t, rm.RegistryName, PrivateRegistry)
assert.Equal(t, rm.Permissions.CanDelete, true)
assert.Equal(t, rm.Permissions.CanRetry, true)
assert.Equal(t, rm.Status, RegistryModuleStatusPending)
assert.Equal(t, rm.VCSRepo.Branch, "main")
assert.Equal(t, rm.VCSRepo.DisplayIdentifier, "display")
assert.Equal(t, rm.VCSRepo.Identifier, "identifier")
assert.Equal(t, rm.VCSRepo.IngressSubmodules, true)
assert.Equal(t, rm.VCSRepo.OAuthTokenID, "token")
assert.Equal(t, rm.VCSRepo.RepositoryHTTPURL, "github.com")
assert.Equal(t, rm.VCSRepo.ServiceProvider, "github")
assert.Equal(t, rm.VCSRepo.WebhookURL, "https://app.terraform.io/webhooks/vcs/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
assert.Equal(t, rm.Status, RegistryModuleStatusPending)
assert.Equal(t, rm.VersionStatuses[0].Version, "1.1.1")
assert.Equal(t, rm.VersionStatuses[0].Status, RegistryModuleVersionStatusPending)
assert.Equal(t, rm.VersionStatuses[0].Error, "no error")
}
func TestRegistryCreateWithVCSOptions_Marshal(t *testing.T) {
t.Parallel()
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/private-registry/modules#sample-payload
opts := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
Identifier: String("id"),
OAuthTokenID: String("token"),
DisplayIdentifier: String("display-id"),
},
}
reqBody, err := serializeRequestBody(&opts)
require.NoError(t, err)
req, err := retryablehttp.NewRequest("POST", "url", reqBody)
require.NoError(t, err)
bodyBytes, err := req.BodyBytes()
require.NoError(t, err)
expectedBody := `{"data":{"type":"registry-modules","attributes":{"vcs-repo":{"identifier":"id","oauth-token-id":"token","display-identifier":"display-id"}}}}
`
assert.Equal(t, expectedBody, string(bodyBytes))
}
func TestRegistryModulesUpdate_AgentExecutionValidation(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
githubIdentifier := os.Getenv("GITHUB_REGISTRY_MODULE_IDENTIFIER")
if githubIdentifier == "" {
t.Skip("Export a valid GITHUB_REGISTRY_MODULE_IDENTIFIER before running this test")
}
githubBranch := os.Getenv("GITHUB_REGISTRY_MODULE_BRANCH")
if githubBranch == "" {
githubBranch = "main"
}
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
agentPool, agentPoolCleanup := createAgentPool(t, client, orgTest)
defer agentPoolCleanup()
oauthTokenTest, oauthTokenTestCleanup := createOAuthToken(t, client, orgTest)
defer oauthTokenTestCleanup()
// Create a VCS-connected registry module with tests enabled for testing updates
createOptions := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
OrganizationName: String(orgTest.Name),
Identifier: String(githubIdentifier),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(githubIdentifier),
Branch: String(githubBranch),
},
TestConfig: &RegistryModuleTestConfigOptions{
TestsEnabled: Bool(true),
},
}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, createOptions)
require.NoError(t, err)
assert.NotEmpty(t, rm.ID)
moduleID := RegistryModuleID{
Organization: orgTest.Name,
Name: rm.Name,
Provider: rm.Provider,
Namespace: rm.Namespace,
RegistryName: rm.RegistryName,
}
// Cleanup the created module
defer func() {
if err := client.RegistryModules.Delete(ctx, orgTest.Name, rm.Name); err != nil {
t.Logf("Error deleting registry module: %v", err)
}
}()
t.Run("errors when remote execution mode has agent pool ID", func(t *testing.T) {
updateOptions := RegistryModuleUpdateOptions{
TestConfig: &RegistryModuleTestConfigOptions{
TestsEnabled: Bool(true),
AgentExecutionMode: AgentExecutionModePtr(AgentExecutionModeRemote),
AgentPoolID: String(agentPool.ID),
},
}
_, err := client.RegistryModules.Update(ctx, moduleID, updateOptions)
assert.Error(t, err)
assert.Equal(t, ErrAgentPoolNotRequiredForRemoteExecution, err)
})
t.Run("succeeds when agent execution mode has agent pool ID", func(t *testing.T) {
updateOptions := RegistryModuleUpdateOptions{
TestConfig: &RegistryModuleTestConfigOptions{
TestsEnabled: Bool(true),
AgentExecutionMode: AgentExecutionModePtr(AgentExecutionModeAgent),
AgentPoolID: String(agentPool.ID),
},
}
updatedRM, err := client.RegistryModules.Update(ctx, moduleID, updateOptions)
require.NoError(t, err)
assert.NotNil(t, updatedRM)
assert.NotNil(t, updatedRM.TestConfig)
assert.True(t, updatedRM.TestConfig.TestsEnabled)
// Verify that AgentExecutionMode and AgentPoolID are returned correctly
assert.NotNil(t, updatedRM.TestConfig.AgentExecutionMode)
assert.Equal(t, string(AgentExecutionModeAgent), *updatedRM.TestConfig.AgentExecutionMode)
assert.NotNil(t, updatedRM.TestConfig.AgentPoolID)
assert.Equal(t, agentPool.ID, *updatedRM.TestConfig.AgentPoolID)
})
t.Run("succeeds when remote execution mode has no agent pool ID", func(t *testing.T) {
updateOptions := RegistryModuleUpdateOptions{
TestConfig: &RegistryModuleTestConfigOptions{
TestsEnabled: Bool(true),
AgentExecutionMode: AgentExecutionModePtr(AgentExecutionModeRemote),
},
}
updatedRM, err := client.RegistryModules.Update(ctx, moduleID, updateOptions)
require.NoError(t, err)
assert.NotNil(t, updatedRM)
assert.NotNil(t, updatedRM.TestConfig)
assert.True(t, updatedRM.TestConfig.TestsEnabled)
// Verify that AgentExecutionMode is returned correctly and AgentPoolID is nil
assert.NotNil(t, updatedRM.TestConfig.AgentExecutionMode)
assert.Equal(t, string(AgentExecutionModeRemote), *updatedRM.TestConfig.AgentExecutionMode)
assert.Nil(t, updatedRM.TestConfig.AgentPoolID)
})
}
func TestRegistryModulesCreateWithVCSConnection_AgentExecutionValidation(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
githubIdentifier := os.Getenv("GITHUB_REGISTRY_MODULE_IDENTIFIER")
if githubIdentifier == "" {
t.Skip("Export a valid GITHUB_REGISTRY_MODULE_IDENTIFIER before running this test")
}
githubBranch := os.Getenv("GITHUB_REGISTRY_MODULE_BRANCH")
if githubBranch == "" {
githubBranch = "main"
}
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
agentPool, agentPoolCleanup := createAgentPool(t, client, orgTest)
defer agentPoolCleanup()
oauthTokenTest, oauthTokenTestCleanup := createOAuthToken(t, client, orgTest)
defer oauthTokenTestCleanup()
t.Run("errors when remote execution mode has agent pool ID", func(t *testing.T) {
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
OrganizationName: String(orgTest.Name),
Identifier: String(githubIdentifier),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(githubIdentifier),
Branch: String(githubBranch),
},
TestConfig: &RegistryModuleTestConfigOptions{
TestsEnabled: Bool(true),
AgentExecutionMode: AgentExecutionModePtr(AgentExecutionModeRemote),
AgentPoolID: String(agentPool.ID),
},
}
_, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
assert.Error(t, err)
assert.Equal(t, ErrAgentPoolNotRequiredForRemoteExecution, err)
})
t.Run("succeeds when agent execution mode has agent pool ID", func(t *testing.T) {
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
OrganizationName: String(orgTest.Name),
Identifier: String(githubIdentifier),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(githubIdentifier),
Branch: String(githubBranch),
},
TestConfig: &RegistryModuleTestConfigOptions{
TestsEnabled: Bool(true),
AgentExecutionMode: AgentExecutionModePtr(AgentExecutionModeAgent),
AgentPoolID: String(agentPool.ID),
},
}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
require.NoError(t, err)
assert.NotEmpty(t, rm.ID)
assert.NotNil(t, rm.TestConfig)
assert.True(t, rm.TestConfig.TestsEnabled)
// Verify that AgentExecutionMode and AgentPoolID are returned correctly
assert.NotNil(t, rm.TestConfig.AgentExecutionMode)
assert.Equal(t, string(AgentExecutionModeAgent), *rm.TestConfig.AgentExecutionMode)
assert.NotNil(t, rm.TestConfig.AgentPoolID)
assert.Equal(t, agentPool.ID, *rm.TestConfig.AgentPoolID)
// Cleanup the created module
defer func() {
if err := client.RegistryModules.Delete(ctx, orgTest.Name, rm.Name); err != nil {
t.Logf("Error deleting registry module: %v", err)
}
}()
})
t.Run("succeeds when remote execution mode has no agent pool ID", func(t *testing.T) {
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
OrganizationName: String(orgTest.Name),
Identifier: String(githubIdentifier),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(githubIdentifier),
Branch: String(githubBranch),
},
TestConfig: &RegistryModuleTestConfigOptions{
TestsEnabled: Bool(true),
AgentExecutionMode: AgentExecutionModePtr(AgentExecutionModeRemote),
},
}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
require.NoError(t, err)
assert.NotEmpty(t, rm.ID)
assert.NotNil(t, rm.TestConfig)
assert.True(t, rm.TestConfig.TestsEnabled)
// Verify that AgentExecutionMode is returned correctly and AgentPoolID is nil
assert.NotNil(t, rm.TestConfig.AgentExecutionMode)
assert.Equal(t, string(AgentExecutionModeRemote), *rm.TestConfig.AgentExecutionMode)
assert.Nil(t, rm.TestConfig.AgentPoolID)
// Cleanup the created module
defer func() {
if err := client.RegistryModules.Delete(ctx, orgTest.Name, rm.Name); err != nil {
t.Logf("Error deleting registry module: %v", err)
}
}()
})
}
================================================
FILE: registry_module_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestRegistryModules_Update_AgentExecutionValidation(t *testing.T) {
t.Parallel()
// Create a test server for API calls
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// This shouldn't be called for validation errors, but provide a response just in case
w.WriteHeader(http.StatusOK)
}))
defer testServer.Close()
// Create a client pointing to the test server
client, err := NewClient(&Config{
Address: testServer.URL,
Token: "fake-token",
})
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
t.Run("errors when remote execution mode has agent pool ID", func(t *testing.T) {
moduleID := RegistryModuleID{
Organization: "test-org",
Name: "test-module",
Provider: "aws",
Namespace: "test-namespace",
RegistryName: PrivateRegistry,
}
options := RegistryModuleUpdateOptions{
TestConfig: &RegistryModuleTestConfigOptions{
AgentExecutionMode: AgentExecutionModePtr(AgentExecutionModeRemote),
AgentPoolID: String("apool-123"),
},
}
rm, err := client.RegistryModules.Update(ctx, moduleID, options)
assert.Error(t, err)
assert.Equal(t, ErrAgentPoolNotRequiredForRemoteExecution, err)
assert.Nil(t, rm)
})
t.Run("succeeds when agent execution mode has agent pool ID", func(t *testing.T) {
moduleID := RegistryModuleID{
Organization: "test-org",
Name: "test-module",
Provider: "aws",
Namespace: "test-namespace",
RegistryName: PrivateRegistry,
}
options := RegistryModuleUpdateOptions{
TestConfig: &RegistryModuleTestConfigOptions{
AgentExecutionMode: AgentExecutionModePtr(AgentExecutionModeAgent),
AgentPoolID: String("apool-123"),
},
}
// This test only validates that the validation logic doesn't fail
// The actual API call will fail since we're using a test client,
// but that's expected and not what we're testing here
_, err := client.RegistryModules.Update(ctx, moduleID, options)
// We expect some error (likely network/API related), but NOT our validation error
if err != nil {
assert.NotEqual(t, ErrAgentPoolNotRequiredForRemoteExecution, err)
}
})
t.Run("succeeds when remote execution mode has no agent pool ID", func(t *testing.T) {
moduleID := RegistryModuleID{
Organization: "test-org",
Name: "test-module",
Provider: "aws",
Namespace: "test-namespace",
RegistryName: PrivateRegistry,
}
options := RegistryModuleUpdateOptions{
TestConfig: &RegistryModuleTestConfigOptions{
AgentExecutionMode: AgentExecutionModePtr(AgentExecutionModeRemote),
AgentPoolID: nil,
},
}
// This test only validates that the validation logic doesn't fail
_, err := client.RegistryModules.Update(ctx, moduleID, options)
// We expect some error (likely network/API related), but NOT our validation error
if err != nil {
assert.NotEqual(t, ErrAgentPoolNotRequiredForRemoteExecution, err)
}
})
}
func TestRegistryModules_CreateWithVCSConnection_AgentExecutionValidation(t *testing.T) {
t.Parallel()
// Create a test server for API calls
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// This shouldn't be called for validation errors, but provide a response just in case
w.WriteHeader(http.StatusOK)
}))
defer testServer.Close()
// Create a client pointing to the test server
client, err := NewClient(&Config{
Address: testServer.URL,
Token: "fake-token",
})
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
t.Run("errors when remote execution mode has agent pool ID", func(t *testing.T) {
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
Identifier: String("test/repo"),
OAuthTokenID: String("ot-123"),
DisplayIdentifier: String("test/repo"),
},
TestConfig: &RegistryModuleTestConfigOptions{
AgentExecutionMode: AgentExecutionModePtr(AgentExecutionModeRemote),
AgentPoolID: String("apool-123"),
},
}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
assert.Error(t, err)
assert.Equal(t, ErrAgentPoolNotRequiredForRemoteExecution, err)
assert.Nil(t, rm)
})
t.Run("succeeds when agent execution mode has agent pool ID", func(t *testing.T) {
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
Identifier: String("test/repo"),
OAuthTokenID: String("ot-123"),
DisplayIdentifier: String("test/repo"),
},
TestConfig: &RegistryModuleTestConfigOptions{
AgentExecutionMode: AgentExecutionModePtr(AgentExecutionModeAgent),
AgentPoolID: String("apool-123"),
},
}
// This test only validates that the validation logic doesn't fail
_, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
// We expect some error (likely network/API related), but NOT our validation error
if err != nil {
assert.NotEqual(t, ErrAgentPoolNotRequiredForRemoteExecution, err)
}
})
t.Run("succeeds when remote execution mode has no agent pool ID", func(t *testing.T) {
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
Identifier: String("test/repo"),
OAuthTokenID: String("ot-123"),
DisplayIdentifier: String("test/repo"),
},
TestConfig: &RegistryModuleTestConfigOptions{
AgentExecutionMode: AgentExecutionModePtr(AgentExecutionModeRemote),
AgentPoolID: nil,
},
}
// This test only validates that the validation logic doesn't fail
_, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
// We expect some error (likely network/API related), but NOT our validation error
if err != nil {
assert.NotEqual(t, ErrAgentPoolNotRequiredForRemoteExecution, err)
}
})
t.Run("succeeds when TestConfig is nil", func(t *testing.T) {
options := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
Identifier: String("test/repo"),
OAuthTokenID: String("ot-123"),
DisplayIdentifier: String("test/repo"),
},
TestConfig: nil,
}
// This test only validates that the validation logic doesn't fail
_, err := client.RegistryModules.CreateWithVCSConnection(ctx, options)
// We expect some error (likely network/API related), but NOT our validation error
if err != nil {
assert.NotEqual(t, ErrAgentPoolNotRequiredForRemoteExecution, err)
}
})
}
================================================
FILE: registry_no_code_module.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ RegistryNoCodeModules = (*registryNoCodeModules)(nil)
// RegistryNoCodeModules describes all the registry no-code module related methods that the Terraform
// Enterprise API supports.
//
// TFE API docs: (TODO: Add link to API docs)
type RegistryNoCodeModules interface {
// Create a registry no-code module
// **Note: This API is still in BETA and subject to change.**
Create(ctx context.Context, organization string, options RegistryNoCodeModuleCreateOptions) (*RegistryNoCodeModule, error)
// Read a registry no-code module
// **Note: This API is still in BETA and subject to change.**
Read(ctx context.Context, noCodeModuleID string, options *RegistryNoCodeModuleReadOptions) (*RegistryNoCodeModule, error)
// ReadVariables returns the variables for a version of a no-code module
// **Note: This API is still in BETA and subject to change.**
ReadVariables(ctx context.Context, noCodeModuleID, noCodeModuleVersion string, options *RegistryNoCodeModuleReadVariablesOptions) (*RegistryModuleVariableList, error)
// Update a registry no-code module
// **Note: This API is still in BETA and subject to change.**
Update(ctx context.Context, noCodeModuleID string, options RegistryNoCodeModuleUpdateOptions) (*RegistryNoCodeModule, error)
// Delete a registry no-code module
// **Note: This API is still in BETA and subject to change.**
Delete(ctx context.Context, ID string) error
// CreateWorkspace creates a workspace using a no-code module.
CreateWorkspace(ctx context.Context, noCodeModuleID string, options *RegistryNoCodeModuleCreateWorkspaceOptions) (*Workspace, error)
// UpgradeWorkspace initiates an upgrade of an existing no-code module workspace.
UpgradeWorkspace(ctx context.Context, noCodeModuleID string, workspaceID string, options *RegistryNoCodeModuleUpgradeWorkspaceOptions) (*WorkspaceUpgrade, error)
}
// RegistryModuleVariableList is a list of registry module variables.
// **Note: This API is still in BETA and subject to change.**
type RegistryModuleVariableList struct {
Items []*RegistryModuleVariable
// NOTE: At the time of authoring this comment, the API endpoint to fetch
// registry module variables does not support pagination. This field is
// included to satisfy jsonapi unmarshaler implementation here:
// https://github.com/hashicorp/go-tfe/blob/3d29602707fa4b10469d1a02685644bd159d3ccc/tfe.go#L859
*Pagination
}
// RegistryModuleVariable represents a registry module variable.
type RegistryModuleVariable struct {
// ID is the ID of the variable.
ID string `jsonapi:"primary,registry-module-variables"`
// Name is the name of the variable.
Name string `jsonapi:"attr,name"`
// VariableType is the type of the variable.
VariableType string `jsonapi:"attr,type"`
// Description is the description of the variable.
Description string `jsonapi:"attr,description"`
// Required is a boolean indicating if the variable is required.
Required bool `jsonapi:"attr,required"`
// Sensitive is a boolean indicating if the variable is sensitive.
Sensitive bool `jsonapi:"attr,sensitive"`
// Options is a slice of strings representing the options for the variable.
Options []string `jsonapi:"attr,options"`
// HasGlobal is a boolean indicating if the variable is global.
HasGlobal bool `jsonapi:"attr,has-global"`
}
type RegistryNoCodeModuleCreateWorkspaceOptions struct {
Type string `jsonapi:"primary,no-code-module-workspace"`
// Name is the name of the workspace, which can only include letters,
// numbers, and _. This will be used as an identifier and must be unique in
// the organization.
Name string `jsonapi:"attr,name"`
// Description is a description for the workspace.
Description *string `jsonapi:"attr,description,omitempty"`
AutoApply *bool `jsonapi:"attr,auto-apply,omitempty"`
// Project is the associated project with the workspace. If not provided,
// default project of the organization will be assigned to the workspace.
Project *Project `jsonapi:"relation,project,omitempty"`
// Variables is the slice of variables to be configured for the no-code
// workspace.
Variables []*Variable `jsonapi:"relation,vars,omitempty"`
// SourceName is the name of the source of the workspace.
SourceName *string `jsonapi:"attr,source-name,omitempty"`
// SourceUrl is the URL of the source of the workspace.
SourceURL *string `jsonapi:"attr,source-url,omitempty"`
// ExecutionMode is the execution mode of the workspace.
ExecutionMode *string `jsonapi:"attr,execution-mode,omitempty"`
// AgentPoolId is the ID of the agent pool to use for the workspace.
// This is required when execution mode is set to "agent".
// This must not be specified when execution mode is set to "remote".
AgentPoolID *string `jsonapi:"attr,agent-pool-id,omitempty"`
}
type RegistryNoCodeModuleUpgradeWorkspaceOptions struct {
Type string `jsonapi:"primary,no-code-module-workspace"`
// Variables is the slice of variables to be configured for the no-code
// workspace.
Variables []*Variable `jsonapi:"relation,vars,omitempty"`
}
// registryNoCodeModules implements RegistryNoCodeModules.
type registryNoCodeModules struct {
client *Client
}
// RegistryNoCodeModule represents a registry no-code module
type RegistryNoCodeModule struct {
ID string `jsonapi:"primary,no-code-modules"`
VersionPin string `jsonapi:"attr,version-pin"`
Enabled bool `jsonapi:"attr,enabled"`
// Relations
Organization *Organization `jsonapi:"relation,organization"`
RegistryModule *RegistryModule `jsonapi:"relation,registry-module"`
VariableOptions []*NoCodeVariableOption `jsonapi:"relation,variable-options"`
}
// NoCodeVariableOption represents a registry no-code module variable and its
// options.
type NoCodeVariableOption struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
Type string `jsonapi:"primary,variable-options"`
// Required: The variable name
VariableName string `jsonapi:"attr,variable-name"`
// Required: The variable type
VariableType string `jsonapi:"attr,variable-type"`
// Optional: The options for the variable
Options []string `jsonapi:"attr,options"`
}
// RegistryNoCodeModuleCreateOptions is used when creating a registry no-code module
type RegistryNoCodeModuleCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,no-code-modules"`
// Required: the registry module to use for the no-code module (only the ID is used)
RegistryModule *RegistryModule `jsonapi:"relation,registry-module"`
// Optional: whether no-code is enabled for the module
Enabled *bool `jsonapi:"attr,enabled,omitempty"`
// Optional: the version pin for the module. valid values are "latest" or a semver string
VersionPin string `jsonapi:"attr,version-pin,omitempty"`
// Optional: the variable options for the registry module
VariableOptions []*NoCodeVariableOption `jsonapi:"relation,variable-options,omitempty"`
}
// RegistryNoCodeModuleIncludeOpt represents the available options for include query params.
type RegistryNoCodeModuleIncludeOpt string
var (
// RegistryNoCodeIncludeVariableOptions is used to include variable options in the response
RegistryNoCodeIncludeVariableOptions RegistryNoCodeModuleIncludeOpt = "variable-options"
)
// RegistryNoCodeModuleReadOptions is used when reading a registry no-code module
type RegistryNoCodeModuleReadOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-updating
Type string `jsonapi:"primary,no-code-modules"`
// Optional: Include is used to specify the related resources to include in the response.
Include []RegistryNoCodeModuleIncludeOpt `url:"include,omitempty"`
}
// RegistryNoCodeModuleReadVariablesOptions is used when reading the variables
// for a no-code module.
type RegistryNoCodeModuleReadVariablesOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-updating
Type string `jsonapi:"primary,no-code-modules"`
}
// RegistryNoCodeModuleUpdateOptions is used when updating a registry no-code module
type RegistryNoCodeModuleUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-updating
Type string `jsonapi:"primary,no-code-modules"`
// Required: the registry module to use for the no-code module (only the ID is used)
RegistryModule *RegistryModule `jsonapi:"relation,registry-module"`
// Optional: the version pin for the module. valid values are "latest" or a semver string
VersionPin string `jsonapi:"attr,version-pin,omitempty"`
// Optional: whether no-code is enabled for the module
Enabled *bool `jsonapi:"attr,enabled,omitempty"`
// Optional: are the variable options for the module
VariableOptions []*NoCodeVariableOption `jsonapi:"relation,variable-options,omitempty"`
}
// WorkspaceUpgrade contains the data returned by the no-code workspace upgrade
// API endpoint.
type WorkspaceUpgrade struct {
// Status is the status of the run of the upgrade
Status string `jsonapi:"attr,status"`
// PlanURL is the URL to the plan of the upgrade
PlanURL string `jsonapi:"attr,plan-url"`
// Message is the message returned by the API when an upgrade is not available.
Message string `jsonapi:"attr,message"`
}
// Create a new registry no-code module
func (r *registryNoCodeModules) Create(ctx context.Context, organization string, options RegistryNoCodeModuleCreateOptions) (*RegistryNoCodeModule, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("organizations/%s/no-code-modules", url.PathEscape(organization))
req, err := r.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
rm := &RegistryNoCodeModule{}
err = req.Do(ctx, rm)
if err != nil {
return nil, err
}
return rm, nil
}
// Read a registry no-code module
func (r *registryNoCodeModules) Read(ctx context.Context, noCodeModuleID string, options *RegistryNoCodeModuleReadOptions) (*RegistryNoCodeModule, error) {
if !validStringID(&noCodeModuleID) {
return nil, ErrInvalidModuleID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("no-code-modules/%s", url.PathEscape(noCodeModuleID))
req, err := r.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
rm := &RegistryNoCodeModule{}
err = req.Do(ctx, rm)
if err != nil {
return nil, err
}
return rm, nil
}
// ReadVariables retrieves the no-code variable options for a version of a
// module.
func (r *registryNoCodeModules) ReadVariables(
ctx context.Context,
noCodeModuleID, noCodeModuleVersion string,
options *RegistryNoCodeModuleReadVariablesOptions,
) (*RegistryModuleVariableList, error) {
if !validStringID(&noCodeModuleID) {
return nil, ErrInvalidModuleID
}
if !validVersion(noCodeModuleVersion) {
return nil, ErrInvalidVersion
}
u := fmt.Sprintf(
"no-code-modules/%s/versions/%s/module-variables",
url.PathEscape(noCodeModuleID),
url.PathEscape(noCodeModuleVersion),
)
req, err := r.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
resp := &RegistryModuleVariableList{}
err = req.Do(ctx, resp)
if err != nil {
return nil, err
}
return resp, nil
}
// Update a registry no-code module
func (r *registryNoCodeModules) Update(ctx context.Context, noCodeModuleID string, options RegistryNoCodeModuleUpdateOptions) (*RegistryNoCodeModule, error) {
if !validString(&noCodeModuleID) {
return nil, ErrInvalidModuleID
}
if !validStringID(&noCodeModuleID) {
return nil, ErrInvalidModuleID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("no-code-modules/%s", url.PathEscape(noCodeModuleID))
req, err := r.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
rm := &RegistryNoCodeModule{}
err = req.Do(ctx, rm)
if err != nil {
return nil, err
}
return rm, nil
}
// Delete is used to delete the registry no-code module
func (r *registryNoCodeModules) Delete(ctx context.Context, noCodeModuleID string) error {
if !validStringID(&noCodeModuleID) {
return ErrInvalidModuleID
}
u := fmt.Sprintf("no-code-modules/%s", url.PathEscape(noCodeModuleID))
req, err := r.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// CreateWorkspace creates a no-code workspace using a no-code module.
func (r *registryNoCodeModules) CreateWorkspace(
ctx context.Context,
noCodeModuleID string,
options *RegistryNoCodeModuleCreateWorkspaceOptions,
) (*Workspace, error) {
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("no-code-modules/%s/workspaces", url.PathEscape(noCodeModuleID))
req, err := r.client.NewRequest("POST", u, options)
if err != nil {
return nil, err
}
w := &Workspace{}
err = req.Do(ctx, w)
if err != nil {
return nil, err
}
return w, nil
}
// UpgradeWorkspace initiates an upgrade of an existing no-code module workspace.
func (r *registryNoCodeModules) UpgradeWorkspace(
ctx context.Context,
noCodeModuleID string,
workspaceID string,
options *RegistryNoCodeModuleUpgradeWorkspaceOptions,
) (*WorkspaceUpgrade, error) {
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("no-code-modules/%s/workspaces/%s/upgrade",
url.PathEscape(noCodeModuleID),
workspaceID,
)
req, err := r.client.NewRequest("POST", u, options)
if err != nil {
return nil, err
}
wu := &WorkspaceUpgrade{}
err = req.Do(ctx, wu)
if err != nil {
return nil, err
}
return wu, nil
}
func (o RegistryNoCodeModuleCreateOptions) valid() error {
if o.RegistryModule == nil || o.RegistryModule.ID == "" {
return ErrRequiredRegistryModule
}
return nil
}
func (o *RegistryNoCodeModuleUpdateOptions) valid() error {
if o == nil {
return nil // nothing to validate
}
if o.RegistryModule == nil || o.RegistryModule.ID == "" {
return ErrRequiredRegistryModule
}
return nil
}
func (o *RegistryNoCodeModuleReadOptions) valid() error {
return nil
}
func (o *RegistryNoCodeModuleCreateWorkspaceOptions) valid() error {
if !validString(&o.Name) {
return ErrRequiredName
}
return nil
}
func (o *RegistryNoCodeModuleUpgradeWorkspaceOptions) valid() error {
return nil
}
================================================
FILE: registry_no_code_module_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRegistryNoCodeModulesCreate(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
t.Run("with valid options", func(t *testing.T) {
t.Run("with no version given", func(t *testing.T) {
registryModuleTest, registryModuleTestCleanup := createRegistryModule(t, client, orgTest, PrivateRegistry)
defer registryModuleTestCleanup()
options := RegistryModuleCreateVersionOptions{
Version: String("1.2.3"),
}
rmv, err := client.RegistryModules.CreateVersion(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: registryModuleTest.Name,
Provider: registryModuleTest.Provider,
}, options)
require.NoError(t, err)
require.NotEmpty(t, rmv.Version)
ncOptions := RegistryNoCodeModuleCreateOptions{
RegistryModule: registryModuleTest,
}
noCodeModule, err := client.RegistryNoCodeModules.Create(ctx, orgTest.Name, ncOptions)
require.NoError(t, err)
assert.NotEmpty(t, noCodeModule.ID)
require.NotEmpty(t, noCodeModule.Organization)
assert.True(t, noCodeModule.Enabled)
require.NotEmpty(t, noCodeModule.RegistryModule)
assert.Equal(t, orgTest.Name, noCodeModule.Organization.Name)
assert.Equal(t, registryModuleTest.ID, noCodeModule.RegistryModule.ID)
})
t.Run("with version pin given", func(t *testing.T) {
registryModuleTest, _ := createRegistryModule(t, client, orgTest, PrivateRegistry)
options := RegistryModuleCreateVersionOptions{
Version: String("1.2.3"),
}
rmv, err := client.RegistryModules.CreateVersion(ctx, RegistryModuleID{
Organization: orgTest.Name,
Name: registryModuleTest.Name,
Provider: registryModuleTest.Provider,
}, options)
require.NoError(t, err)
require.NotEmpty(t, rmv.Version)
ncOptions := RegistryNoCodeModuleCreateOptions{
VersionPin: "1.2.3",
RegistryModule: registryModuleTest,
}
noCodeModule, err := client.RegistryNoCodeModules.Create(ctx, orgTest.Name, ncOptions)
require.NoError(t, err)
assert.NotEmpty(t, noCodeModule.ID)
require.NotEmpty(t, noCodeModule.Organization)
require.NotEmpty(t, noCodeModule.RegistryModule)
assert.True(t, noCodeModule.Enabled)
assert.Equal(t, ncOptions.VersionPin, noCodeModule.VersionPin)
assert.Equal(t, orgTest.Name, noCodeModule.Organization.Name)
assert.Equal(t, registryModuleTest.ID, noCodeModule.RegistryModule.ID)
})
t.Run("with enabled set to false", func(t *testing.T) {
registryModuleTest, _ := createRegistryModuleWithVersion(t, client, orgTest)
ncOptions := RegistryNoCodeModuleCreateOptions{
RegistryModule: registryModuleTest,
Enabled: Bool(false),
}
noCodeModule, err := client.RegistryNoCodeModules.Create(ctx, orgTest.Name, ncOptions)
require.NoError(t, err)
assert.NotEmpty(t, noCodeModule.ID)
require.NotEmpty(t, noCodeModule.Organization)
require.NotEmpty(t, noCodeModule.RegistryModule)
assert.False(t, noCodeModule.Enabled)
assert.Equal(t, ncOptions.VersionPin, noCodeModule.VersionPin)
assert.Equal(t, orgTest.Name, noCodeModule.Organization.Name)
assert.Equal(t, registryModuleTest.ID, noCodeModule.RegistryModule.ID)
})
})
t.Run("with invalid options", func(t *testing.T) {
registryModuleTest, registryModuleTestCleanup := createRegistryModule(t, client, orgTest, PrivateRegistry)
defer registryModuleTestCleanup()
t.Run("with version pinned to one that does not exist", func(t *testing.T) {
options := RegistryNoCodeModuleCreateOptions{
VersionPin: "1.2.5",
RegistryModule: registryModuleTest,
}
noCodeModule, err := client.RegistryNoCodeModules.Create(ctx, orgTest.Name, options)
require.Error(t, err)
require.Nil(t, noCodeModule)
})
})
t.Run("with variable options", func(t *testing.T) {
registryModuleTest, registryModuleTestCleanup := createRegistryModule(t, client, orgTest, PrivateRegistry)
defer registryModuleTestCleanup()
options := RegistryNoCodeModuleCreateOptions{
RegistryModule: registryModuleTest,
VariableOptions: []*NoCodeVariableOption{
{
VariableName: "var1",
VariableType: "string",
Options: []string{"option1", "option2"},
},
{
VariableName: "my_var",
VariableType: "string",
Options: []string{"my_option1", "my_option2"},
},
},
}
noCodeModule, err := client.RegistryNoCodeModules.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.NotEmpty(t, noCodeModule.ID)
require.NotEmpty(t, noCodeModule.Organization)
require.NotEmpty(t, noCodeModule.RegistryModule)
require.True(t, noCodeModule.Enabled)
assert.Equal(t, orgTest.Name, noCodeModule.Organization.Name)
assert.Equal(t, registryModuleTest.ID, noCodeModule.RegistryModule.ID)
assert.Equal(t, len(options.VariableOptions), len(noCodeModule.VariableOptions))
})
}
func TestRegistryNoCodeModulesRead(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
registryModuleTest, registryModuleTestCleanup := createRegistryModule(t, client, orgTest, PrivateRegistry)
defer registryModuleTestCleanup()
t.Run("with valid ID", func(t *testing.T) {
noCodeModule, noCodeModuleCleanup := createNoCodeRegistryModule(t, client, orgTest.Name, registryModuleTest, nil)
defer noCodeModuleCleanup()
ncm, err := client.RegistryNoCodeModules.Read(ctx, noCodeModule.ID, nil)
require.NoError(t, err)
assert.Equal(t, noCodeModule.ID, ncm.ID)
assert.True(t, noCodeModule.Enabled)
assert.Equal(t, noCodeModule.Organization.Name, ncm.Organization.Name)
assert.Equal(t, noCodeModule.RegistryModule.ID, ncm.RegistryModule.ID)
})
t.Run("when the variable-options is included in the params", func(t *testing.T) {
varOpts := []*NoCodeVariableOption{
{
VariableName: "var1",
VariableType: "string",
Options: []string{"option1", "option2"},
},
{
VariableName: "my_var",
VariableType: "string",
Options: []string{"my_option1", "my_option2"},
},
}
noCodeModule, noCodeModuleCleanup := createNoCodeRegistryModule(t, client, orgTest.Name, registryModuleTest, varOpts)
defer noCodeModuleCleanup()
ncm, err := client.RegistryNoCodeModules.Read(ctx, noCodeModule.ID, &RegistryNoCodeModuleReadOptions{
Include: []RegistryNoCodeModuleIncludeOpt{RegistryNoCodeIncludeVariableOptions},
})
require.NoError(t, err)
assert.Equal(t, noCodeModule.ID, ncm.ID)
assert.True(t, noCodeModule.Enabled)
assert.Equal(t, noCodeModule.Organization.Name, ncm.Organization.Name)
assert.Equal(t, noCodeModule.RegistryModule.ID, ncm.RegistryModule.ID)
assert.Equal(t, len(varOpts), len(ncm.VariableOptions))
for i, opt := range varOpts {
assert.Equal(t, opt.VariableName, ncm.VariableOptions[i].VariableName)
assert.Equal(t, opt.VariableType, ncm.VariableOptions[i].VariableType)
assert.Equal(t, opt.Options, ncm.VariableOptions[i].Options)
}
})
t.Run("when the id does not exist", func(t *testing.T) {
ncm, err := client.RegistryNoCodeModules.Read(ctx, "non-existing", nil)
assert.Nil(t, ncm)
assert.Equal(t, err, ErrResourceNotFound)
})
}
// TestRegistryNoCodeModuleReadVariables tests the ReadVariables method of the
// RegistryNoCodeModules service.
//
// This test requires that the environment variable "GITHUB_REGISTRY_NO_CODE_MODULE_IDENTIFIER" is set
// with the ID of an existing no-code module that has variables.
func TestRegistryNoCodeModulesReadVariables(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
r := require.New(t)
ncmID := os.Getenv("GITHUB_REGISTRY_NO_CODE_MODULE_IDENTIFIER")
if ncmID == "" {
t.Skip("Export a valid GITHUB_REGISTRY_NO_CODE_MODULE_IDENTIFIER before running this test")
}
ncm, err := client.RegistryNoCodeModules.Read(ctx, ncmID, nil)
r.NoError(err)
r.NotNil(ncm)
t.Run("happy path", func(t *testing.T) {
vars, err := client.RegistryNoCodeModules.ReadVariables(ctx, ncm.ID, ncm.VersionPin, &RegistryNoCodeModuleReadVariablesOptions{})
r.NoError(err)
r.NotNil(vars)
r.NotEmpty(vars)
})
}
func TestRegistryNoCodeModulesUpdate(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
registryModuleTest, registryModuleTestCleanup := createRegistryModule(t, client, orgTest, PrivateRegistry)
defer registryModuleTestCleanup()
t.Run("update no-code registry module", func(t *testing.T) {
noCodeModule, noCodeModuleCleanup := createNoCodeRegistryModule(t, client, orgTest.Name, registryModuleTest, nil)
defer noCodeModuleCleanup()
assert.True(t, noCodeModule.Enabled)
options := RegistryNoCodeModuleUpdateOptions{
RegistryModule: &RegistryModule{ID: registryModuleTest.ID},
Enabled: Bool(false),
}
updated, err := client.RegistryNoCodeModules.Update(ctx, noCodeModule.ID, options)
require.NoError(t, err)
assert.False(t, updated.Enabled)
})
t.Run("no changes when no options are set", func(t *testing.T) {
noCodeModule, noCodeModuleCleanup := createNoCodeRegistryModule(t, client, orgTest.Name, registryModuleTest, nil)
defer noCodeModuleCleanup()
options := RegistryNoCodeModuleUpdateOptions{
RegistryModule: &RegistryModule{ID: registryModuleTest.ID},
}
updated, err := client.RegistryNoCodeModules.Update(ctx, noCodeModule.ID, options)
require.NoError(t, err)
assert.Equal(t, *noCodeModule, *updated)
})
}
func TestRegistryNoCodeModulesDelete(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
registryModuleTest, registryModuleTestCleanup := createRegistryModule(t, client, orgTest, PrivateRegistry)
defer registryModuleTestCleanup()
t.Run("with valid ID", func(t *testing.T) {
noCodeModule, _ := createNoCodeRegistryModule(t, client, orgTest.Name, registryModuleTest, nil)
err := client.RegistryNoCodeModules.Delete(ctx, noCodeModule.ID)
require.NoError(t, err)
rm, err := client.RegistryNoCodeModules.Read(ctx, noCodeModule.ID, nil)
assert.Nil(t, rm)
assert.Error(t, err)
})
t.Run("without an ID", func(t *testing.T) {
err := client.RegistryNoCodeModules.Delete(ctx, "")
assert.EqualError(t, err, ErrInvalidModuleID.Error())
})
t.Run("with an invalid ID", func(t *testing.T) {
err := client.RegistryNoCodeModules.Delete(ctx, "invalid")
assert.EqualError(t, err, ErrResourceNotFound.Error())
})
}
func createNoCodeRegistryModule(t *testing.T, client *Client, orgName string, rm *RegistryModule, variables []*NoCodeVariableOption) (*RegistryNoCodeModule, func()) {
options := RegistryNoCodeModuleCreateOptions{
RegistryModule: rm,
VariableOptions: variables,
}
ctx := context.Background()
ncm, err := client.RegistryNoCodeModules.Create(ctx, orgName, options)
require.NoError(t, err)
require.NotEmpty(t, ncm)
return ncm, func() {
if err := client.RegistryNoCodeModules.Delete(ctx, ncm.ID); err != nil {
t.Errorf("Error destroying no-code registry module! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"NoCode Module: %s\nError: %s", ncm.ID, err)
}
}
}
func TestRegistryNoCodeModulesCreateWorkspace(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
r := require.New(t)
// create an org that will be deleted later. the wskp will live here
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
org, err := client.Organizations.Read(ctx, orgTest.Name)
r.NoError(err)
r.NotNil(org)
githubIdentifier := os.Getenv("GITHUB_REGISTRY_NO_CODE_MODULE_IDENTIFIER")
if githubIdentifier == "" {
t.Skip("Export a valid GITHUB_REGISTRY_NO_CODE_MODULE_IDENTIFIER before running this test")
}
token, cleanupToken := createOAuthToken(t, client, org)
defer cleanupToken()
rmOpts := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
OrganizationName: String(org.Name),
Identifier: String(githubIdentifier),
Tags: Bool(true),
OAuthTokenID: String(token.ID),
DisplayIdentifier: String(githubIdentifier),
},
}
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, rmOpts)
r.NoError(err)
// 1. create the registry module
// 2. create the no-code module, with the registry module
// 3. use the ID to create the workspace
ncm, err := client.RegistryNoCodeModules.Create(ctx, org.Name, RegistryNoCodeModuleCreateOptions{
RegistryModule: rm,
Enabled: Bool(true),
VariableOptions: nil,
})
r.NoError(err)
r.NotNil(ncm)
// We sleep for 10 seconds to let the module finish getting ready
time.Sleep(time.Second * 10)
t.Run("test creating a workspace via a no-code module", func(t *testing.T) {
wn := fmt.Sprintf("foo-%s", randomString(t))
sn := "my-app"
su := "http://my-app.com"
w, err := client.RegistryNoCodeModules.CreateWorkspace(
ctx,
ncm.ID,
&RegistryNoCodeModuleCreateWorkspaceOptions{
Name: wn,
SourceName: String(sn),
SourceURL: String(su),
ExecutionMode: String("remote"),
},
)
r.NoError(err)
r.Equal(wn, w.Name)
r.Equal(sn, w.SourceName)
r.Equal(su, w.SourceURL)
r.Equal("remote", w.ExecutionMode)
})
t.Run("fail to create a workspace with a bad module ID", func(t *testing.T) {
wn := fmt.Sprintf("foo-%s", randomString(t))
_, err = client.RegistryNoCodeModules.CreateWorkspace(
ctx,
"codeno-abc123XYZ",
&RegistryNoCodeModuleCreateWorkspaceOptions{
Name: wn,
},
)
r.Error(err)
})
}
func TestRegistryNoCodeModuleWorkspaceUpgrade(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
r := require.New(t)
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
org, err := client.Organizations.Read(ctx, orgTest.Name)
r.NoError(err)
r.NotNil(org)
githubIdentifier := os.Getenv("GITHUB_REGISTRY_NO_CODE_MODULE_IDENTIFIER")
if githubIdentifier == "" {
t.Skip("Export a valid GITHUB_REGISTRY_NO_CODE_MODULE_IDENTIFIER before running this test")
}
token, cleanupToken := createOAuthToken(t, client, org)
defer cleanupToken()
rmOpts := RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
OrganizationName: String(org.Name),
Identifier: String(githubIdentifier),
Tags: Bool(true),
OAuthTokenID: String(token.ID),
DisplayIdentifier: String(githubIdentifier),
},
InitialVersion: String("1.0.0"),
}
// create the module
rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, rmOpts)
r.NoError(err)
// create the no-code module
ncm, err := client.RegistryNoCodeModules.Create(ctx, org.Name, RegistryNoCodeModuleCreateOptions{
RegistryModule: rm,
Enabled: Bool(true),
VariableOptions: nil,
})
r.NoError(err)
r.NotNil(ncm)
// We sleep for 10 seconds to let the module finish getting ready
time.Sleep(time.Second * 10)
// update the module's pinned version to be 1.0.0
// NOTE: This is done here as an update instead of at create time, because
// that results in the following error:
// Validation failed: Provided version pin is not equal to latest or provided
// string does not represent an existing version of the module.
uncm, err := client.RegistryNoCodeModules.Update(ctx, ncm.ID, RegistryNoCodeModuleUpdateOptions{
RegistryModule: rm,
VersionPin: "1.0.0",
})
r.NoError(err)
r.NotNil(uncm)
// create a workspace, which will be attempted to be updated during the test
wn := fmt.Sprintf("foo-%s", randomString(t))
sn := "my-app"
su := "http://my-app.com"
w, err := client.RegistryNoCodeModules.CreateWorkspace(
ctx,
uncm.ID,
&RegistryNoCodeModuleCreateWorkspaceOptions{
Name: wn,
SourceName: String(sn),
SourceURL: String(su),
},
)
r.NoError(err)
r.NotNil(w)
// update the module's pinned version
uncm, err = client.RegistryNoCodeModules.Update(ctx, ncm.ID, RegistryNoCodeModuleUpdateOptions{
VersionPin: "1.0.1",
})
r.NoError(err)
r.NotNil(uncm)
t.Run("test upgrading a workspace via a no-code module", func(t *testing.T) {
wu, err := client.RegistryNoCodeModules.UpgradeWorkspace(
ctx,
ncm.ID,
w.ID,
&RegistryNoCodeModuleUpgradeWorkspaceOptions{},
)
r.NoError(err)
r.NotNil(wu)
r.NotEmpty(wu.Status)
r.NotEmpty(wu.PlanURL)
})
t.Run("fail to upgrade workspace with invalid no-code module", func(t *testing.T) {
_, err = client.RegistryNoCodeModules.UpgradeWorkspace(
ctx,
ncm.ID+"-invalid",
w.ID,
&RegistryNoCodeModuleUpgradeWorkspaceOptions{},
)
r.Error(err)
})
t.Run("fail to upgrade workspace with invalid workspace ID", func(t *testing.T) {
_, err = client.RegistryNoCodeModules.UpgradeWorkspace(
ctx,
ncm.ID,
w.ID+"-invalid",
&RegistryNoCodeModuleUpgradeWorkspaceOptions{},
)
r.Error(err)
})
}
================================================
FILE: registry_provider.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ RegistryProviders = (*registryProviders)(nil)
// RegistryProviders describes all the registry provider-related methods that the Terraform
// Enterprise API supports.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/private-registry/providers
type RegistryProviders interface {
// List all the providers within an organization.
List(ctx context.Context, organization string, options *RegistryProviderListOptions) (*RegistryProviderList, error)
// Create a registry provider.
Create(ctx context.Context, organization string, options RegistryProviderCreateOptions) (*RegistryProvider, error)
// Read a registry provider.
Read(ctx context.Context, providerID RegistryProviderID, options *RegistryProviderReadOptions) (*RegistryProvider, error)
// Delete a registry provider.
Delete(ctx context.Context, providerID RegistryProviderID) error
}
// registryProviders implements RegistryProviders.
type registryProviders struct {
client *Client
}
// RegistryName represents which registry is being targeted
type RegistryName string
// List of available registry names
const (
PrivateRegistry RegistryName = "private"
PublicRegistry RegistryName = "public"
)
// RegistryProviderIncludeOps represents which jsonapi include can be used with registry providers
type RegistryProviderIncludeOps string
// List of available includes
const (
RegistryProviderVersionsInclude RegistryProviderIncludeOps = "registry-provider-versions"
)
// RegistryProvider represents a registry provider
type RegistryProvider struct {
ID string `jsonapi:"primary,registry-providers"`
Name string `jsonapi:"attr,name"`
Namespace string `jsonapi:"attr,namespace"`
CreatedAt string `jsonapi:"attr,created-at,iso8601"`
UpdatedAt string `jsonapi:"attr,updated-at,iso8601"`
RegistryName RegistryName `jsonapi:"attr,registry-name"`
Permissions RegistryProviderPermissions `jsonapi:"attr,permissions"`
// Relations
Organization *Organization `jsonapi:"relation,organization"`
RegistryProviderVersions []*RegistryProviderVersion `jsonapi:"relation,registry-provider-versions"`
// Links
Links map[string]interface{} `jsonapi:"links,omitempty"`
}
type RegistryProviderPermissions struct {
CanDelete bool `jsonapi:"attr,can-delete"`
}
type RegistryProviderListOptions struct {
ListOptions
// Optional: A query string to filter by registry_name
RegistryName RegistryName `url:"filter[registry_name],omitempty"`
// Optional: A query string to filter by organization
OrganizationName string `url:"filter[organization_name],omitempty"`
// Optional: A query string to do a fuzzy search
Search string `url:"q,omitempty"`
// Optional: Include related jsonapi relationships
Include *[]RegistryProviderIncludeOps `url:"include,omitempty"`
}
type RegistryProviderList struct {
*Pagination
Items []*RegistryProvider
}
// RegistryProviderID is the multi key ID for addressing a provider
type RegistryProviderID struct {
OrganizationName string
RegistryName RegistryName
Namespace string
Name string
}
// RegistryProviderCreateOptions is used when creating a registry provider
type RegistryProviderCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,registry-providers"`
// Required: The name of the registry provider
Name string `jsonapi:"attr,name"`
// Required: The namespace of the provider. For private providers, this is the same as the organization name
Namespace string `jsonapi:"attr,namespace"`
// Required: Whether this is a publicly maintained provider or private. Must be either public or private.
RegistryName RegistryName `jsonapi:"attr,registry-name"`
}
type RegistryProviderReadOptions struct {
// Optional: Include related jsonapi relationships
Include []RegistryProviderIncludeOps `url:"include,omitempty"`
}
func (r *registryProviders) List(ctx context.Context, organization string, options *RegistryProviderListOptions) (*RegistryProviderList, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("organizations/%s/registry-providers", url.PathEscape(organization))
req, err := r.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
pl := &RegistryProviderList{}
err = req.Do(ctx, pl)
if err != nil {
return nil, err
}
return pl, nil
}
func (r *registryProviders) Create(ctx context.Context, organization string, options RegistryProviderCreateOptions) (*RegistryProvider, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf(
"organizations/%s/registry-providers",
url.PathEscape(organization),
)
req, err := r.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
prv := &RegistryProvider{}
err = req.Do(ctx, prv)
if err != nil {
return nil, err
}
return prv, nil
}
func (r *registryProviders) Read(ctx context.Context, providerID RegistryProviderID, options *RegistryProviderReadOptions) (*RegistryProvider, error) {
if err := providerID.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf(
"organizations/%s/registry-providers/%s/%s/%s",
url.PathEscape(providerID.OrganizationName),
url.PathEscape(string(providerID.RegistryName)),
url.PathEscape(providerID.Namespace),
url.PathEscape(providerID.Name),
)
req, err := r.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
prv := &RegistryProvider{}
err = req.Do(ctx, prv)
if err != nil {
return nil, err
}
return prv, nil
}
func (r *registryProviders) Delete(ctx context.Context, providerID RegistryProviderID) error {
if err := providerID.valid(); err != nil {
return err
}
u := fmt.Sprintf(
"organizations/%s/registry-providers/%s/%s/%s",
url.PathEscape(providerID.OrganizationName),
url.PathEscape(string(providerID.RegistryName)),
url.PathEscape(providerID.Namespace),
url.PathEscape(providerID.Name),
)
req, err := r.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o RegistryProviderCreateOptions) valid() error {
if !validStringID(&o.Name) {
return ErrInvalidName
}
if !validStringID(&o.Namespace) {
return ErrInvalidNamespace
}
return nil
}
func (id RegistryProviderID) valid() error {
if !validStringID(&id.OrganizationName) {
return ErrInvalidOrg
}
if !validStringID(&id.Name) {
return ErrInvalidName
}
if !validStringID(&id.Namespace) {
return ErrInvalidNamespace
}
if !validStringID((*string)(&id.RegistryName)) {
return ErrInvalidRegistryName
}
return nil
}
func (o *RegistryProviderListOptions) valid() error {
return nil
}
================================================
FILE: registry_provider_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRegistryProvidersList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
t.Run("with providers", func(t *testing.T) {
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
createN := 10
providers := make([]*RegistryProvider, 0)
// These providers will be destroyed when the org is cleaned up
for i := 0; i < createN; i++ {
// Create public providers
providerTest, _ := createRegistryProvider(t, client, orgTest, PublicRegistry)
providers = append(providers, providerTest)
}
for i := 0; i < createN; i++ {
// Create private providers
providerTest, _ := createRegistryProvider(t, client, orgTest, PrivateRegistry)
providers = append(providers, providerTest)
}
providerN := len(providers)
publicProviderN := createN
t.Run("returns all providers", func(t *testing.T) {
returnedProviders, err := client.RegistryProviders.List(ctx, orgTest.Name, &RegistryProviderListOptions{
ListOptions: ListOptions{
PageNumber: 0,
PageSize: providerN,
},
})
require.NoError(t, err)
assert.NotEmpty(t, returnedProviders.Items)
assert.Equal(t, providerN, returnedProviders.TotalCount)
assert.Equal(t, 1, returnedProviders.TotalPages)
})
t.Run("with list options", func(t *testing.T) {
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
rpl, err := client.RegistryProviders.List(ctx, orgTest.Name, &RegistryProviderListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, rpl.Items)
assert.Equal(t, 999, rpl.CurrentPage)
assert.Equal(t, 20, rpl.TotalCount)
})
t.Run("filters on registry name", func(t *testing.T) {
returnedProviders, err := client.RegistryProviders.List(ctx, orgTest.Name, &RegistryProviderListOptions{
RegistryName: PublicRegistry,
ListOptions: ListOptions{
PageNumber: 0,
PageSize: providerN,
},
})
require.NoError(t, err)
assert.NotEmpty(t, returnedProviders.Items)
assert.Equal(t, publicProviderN, returnedProviders.TotalCount)
assert.Equal(t, 1, returnedProviders.TotalPages)
for _, rp := range returnedProviders.Items {
foundProvider := false
for _, p := range providers {
if rp.ID == p.ID {
foundProvider = true
break
}
}
assert.Equal(t, PublicRegistry, rp.RegistryName)
assert.True(t, foundProvider, "Expected to find provider %s but did not:\nexpected:\n%v\nreturned\n%v", rp.ID, providers, returnedProviders)
}
})
t.Run("searches", func(t *testing.T) {
expectedProvider := providers[0]
returnedProviders, err := client.RegistryProviders.List(ctx, orgTest.Name, &RegistryProviderListOptions{
Search: expectedProvider.Name,
})
require.NoError(t, err)
assert.NotEmpty(t, returnedProviders.Items)
assert.Equal(t, 1, returnedProviders.TotalCount)
assert.Equal(t, 1, returnedProviders.TotalPages)
foundProvider := returnedProviders.Items[0]
assert.Equal(t, foundProvider.ID, expectedProvider.ID, "Expected to find provider %s but did not:\nexpected:\n%v\nreturned\n%v", expectedProvider.ID, providers, returnedProviders)
})
})
t.Run("without providers", func(t *testing.T) {
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
providers, err := client.RegistryProviders.List(ctx, orgTest.Name, nil)
require.NoError(t, err)
assert.Empty(t, providers.Items)
assert.Equal(t, 0, providers.TotalCount)
assert.Equal(t, 0, providers.TotalPages)
})
t.Run("with include provider versions", func(t *testing.T) {
version1, version1Cleanup := createRegistryProviderVersion(t, client, nil)
defer version1Cleanup()
provider := version1.RegistryProvider
version2, version2Cleanup := createRegistryProviderVersion(t, client, provider)
defer version2Cleanup()
versions := []*RegistryProviderVersion{version1, version2}
options := RegistryProviderListOptions{
Include: &[]RegistryProviderIncludeOps{
RegistryProviderVersionsInclude,
},
}
providersRead, err := client.RegistryProviders.List(ctx, provider.Organization.Name, &options)
require.NoError(t, err)
require.NotEmpty(t, providersRead.Items)
providerRead := providersRead.Items[0]
assert.Equal(t, providerRead.ID, provider.ID)
assert.Equal(t, len(versions), len(providerRead.RegistryProviderVersions))
foundVersion := &RegistryProviderVersion{}
for _, v := range providerRead.RegistryProviderVersions {
for i := 0; i < len(versions); i++ {
if v.ID == versions[i].ID {
foundVersion = versions[i]
break
}
}
assert.True(t, foundVersion.ID != "", "Expected to find versions: %v but did not", versions)
assert.Equal(t, v.Version, foundVersion.Version)
}
})
}
func TestRegistryProvidersCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
t.Run("with valid options", func(t *testing.T) {
publicProviderOptions := RegistryProviderCreateOptions{
Name: "provider_name",
Namespace: "public_namespace",
RegistryName: PublicRegistry,
}
privateProviderOptions := RegistryProviderCreateOptions{
Name: "provider_name",
Namespace: orgTest.Name,
RegistryName: PrivateRegistry,
}
registryOptions := []RegistryProviderCreateOptions{publicProviderOptions, privateProviderOptions}
for _, options := range registryOptions {
testName := fmt.Sprintf("with %s provider", options.RegistryName)
t.Run(testName, func(t *testing.T) {
prv, err := client.RegistryProviders.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.NotEmpty(t, prv.ID)
assert.Equal(t, options.Name, prv.Name)
assert.Equal(t, options.Namespace, prv.Namespace)
assert.Equal(t, options.RegistryName, prv.RegistryName)
t.Run("permissions are properly decoded", func(t *testing.T) {
assert.True(t, prv.Permissions.CanDelete)
})
t.Run("relationships are properly decoded", func(t *testing.T) {
assert.Equal(t, orgTest.Name, prv.Organization.Name)
})
t.Run("timestamps are properly decoded", func(t *testing.T) {
assert.NotEmpty(t, prv.CreatedAt)
assert.NotEmpty(t, prv.UpdatedAt)
})
})
}
})
t.Run("with invalid options", func(t *testing.T) {
t.Run("without a name", func(t *testing.T) {
options := RegistryProviderCreateOptions{
Namespace: "namespace",
RegistryName: PublicRegistry,
}
rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options)
assert.Nil(t, rm)
assert.EqualError(t, err, ErrInvalidName.Error())
})
t.Run("with an invalid name", func(t *testing.T) {
options := RegistryProviderCreateOptions{
Name: "invalid name",
Namespace: "namespace",
RegistryName: PublicRegistry,
}
rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options)
assert.Nil(t, rm)
assert.EqualError(t, err, ErrInvalidName.Error())
})
t.Run("without a namespace", func(t *testing.T) {
options := RegistryProviderCreateOptions{
Name: "name",
RegistryName: PublicRegistry,
}
rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options)
assert.Nil(t, rm)
assert.EqualError(t, err, ErrInvalidNamespace.Error())
})
t.Run("with an invalid namespace", func(t *testing.T) {
options := RegistryProviderCreateOptions{
Name: "name",
Namespace: "invalid namespace",
RegistryName: PublicRegistry,
}
rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options)
assert.Nil(t, rm)
assert.EqualError(t, err, ErrInvalidNamespace.Error())
})
t.Run("without a registry-name", func(t *testing.T) {
options := RegistryProviderCreateOptions{
Name: "name",
Namespace: "namespace",
}
rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options)
assert.Nil(t, rm)
// This error is returned by the API
assert.EqualError(t, err, "invalid attribute\n\nRegistry name can't be blank\ninvalid attribute\n\nRegistry name is not included in the list")
})
t.Run("with an invalid registry-name", func(t *testing.T) {
options := RegistryProviderCreateOptions{
Name: "name",
Namespace: "namespace",
RegistryName: "invalid",
}
rm, err := client.RegistryProviders.Create(ctx, orgTest.Name, options)
assert.Nil(t, rm)
// This error is returned by the API
assert.EqualError(t, err, "invalid attribute\n\nRegistry name is not included in the list")
})
})
t.Run("without a valid organization", func(t *testing.T) {
options := RegistryProviderCreateOptions{
Name: "name",
Namespace: "namespace",
RegistryName: PublicRegistry,
}
rm, err := client.RegistryProviders.Create(ctx, badIdentifier, options)
assert.Nil(t, rm)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
func TestRegistryProvidersRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
type ProviderContext struct {
RegistryName RegistryName
}
providerContexts := []ProviderContext{
{
RegistryName: PublicRegistry,
},
{
RegistryName: PrivateRegistry,
},
}
for _, prvCtx := range providerContexts {
testName := fmt.Sprintf("with %s provider", prvCtx.RegistryName)
t.Run(testName, func(t *testing.T) {
t.Run("with valid provider", func(t *testing.T) {
registryProviderTest, providerTestCleanup := createRegistryProvider(t, client, orgTest, prvCtx.RegistryName)
defer providerTestCleanup()
id := RegistryProviderID{
OrganizationName: orgTest.Name,
RegistryName: registryProviderTest.RegistryName,
Namespace: registryProviderTest.Namespace,
Name: registryProviderTest.Name,
}
prv, err := client.RegistryProviders.Read(ctx, id, nil)
require.NoError(t, err)
assert.NotEmpty(t, prv.ID)
assert.Equal(t, registryProviderTest.Name, prv.Name)
assert.Equal(t, registryProviderTest.Namespace, prv.Namespace)
assert.Equal(t, registryProviderTest.RegistryName, prv.RegistryName)
t.Run("permissions are properly decoded", func(t *testing.T) {
assert.True(t, prv.Permissions.CanDelete)
})
t.Run("relationships are properly decoded", func(t *testing.T) {
assert.Equal(t, orgTest.Name, prv.Organization.Name)
})
t.Run("timestamps are properly decoded", func(t *testing.T) {
assert.NotEmpty(t, prv.CreatedAt)
assert.NotEmpty(t, prv.UpdatedAt)
})
})
t.Run("when the registry provider does not exist", func(t *testing.T) {
id := RegistryProviderID{
OrganizationName: orgTest.Name,
RegistryName: prvCtx.RegistryName,
Namespace: "nonexistent",
Name: "nonexistent",
}
_, err := client.RegistryProviders.Read(ctx, id, nil)
assert.Error(t, err)
// Local HCP Terraform or Terraform Enterprise will return a forbidden here when HCP Terraform or Terraform Enterprise is in development mode
// In non development mode this returns a 404
assert.Equal(t, ErrResourceNotFound, err)
})
})
}
t.Run("populates version relationships", func(t *testing.T) {
version1, version1Cleanup := createRegistryProviderVersion(t, client, nil)
defer version1Cleanup()
provider := version1.RegistryProvider
version2, version2Cleanup := createRegistryProviderVersion(t, client, provider)
defer version2Cleanup()
versions := []*RegistryProviderVersion{version1, version2}
id := RegistryProviderID{
OrganizationName: provider.Organization.Name,
RegistryName: provider.RegistryName,
Namespace: provider.Namespace,
Name: provider.Name,
}
options := RegistryProviderReadOptions{
Include: []RegistryProviderIncludeOps{
RegistryProviderVersionsInclude,
},
}
providerRead, err := client.RegistryProviders.Read(ctx, id, &options)
require.NoError(t, err)
require.NotEmpty(t, providerRead.RegistryProviderVersions)
assert.Equal(t, providerRead.ID, provider.ID)
assert.Equal(t, len(versions), len(providerRead.RegistryProviderVersions))
foundVersion := &RegistryProviderVersion{}
for _, v := range providerRead.RegistryProviderVersions {
for i := 0; i < len(versions); i++ {
if v.ID == versions[i].ID {
foundVersion = versions[i]
break
}
}
assert.True(t, foundVersion.ID != "", "Expected to find versions: %v but did not", versions)
assert.Equal(t, v.Version, foundVersion.Version)
}
})
}
func TestRegistryProvidersDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
type ProviderContext struct {
RegistryName RegistryName
}
providerContexts := []ProviderContext{
{
RegistryName: PublicRegistry,
},
{
RegistryName: PrivateRegistry,
},
}
for _, prvCtx := range providerContexts {
testName := fmt.Sprintf("with %s provider", prvCtx.RegistryName)
t.Run(testName, func(t *testing.T) {
t.Run("with valid provider", func(t *testing.T) {
registryProviderTest, _ := createRegistryProvider(t, client, orgTest, prvCtx.RegistryName)
id := RegistryProviderID{
OrganizationName: orgTest.Name,
RegistryName: registryProviderTest.RegistryName,
Namespace: registryProviderTest.Namespace,
Name: registryProviderTest.Name,
}
err := client.RegistryProviders.Delete(ctx, id)
require.NoError(t, err)
prv, err := client.RegistryProviders.Read(ctx, id, nil)
assert.Nil(t, prv)
assert.Error(t, err)
})
t.Run("when the registry provider does not exist", func(t *testing.T) {
id := RegistryProviderID{
OrganizationName: orgTest.Name,
RegistryName: prvCtx.RegistryName,
Namespace: "nonexistent",
Name: "nonexistent",
}
err := client.RegistryProviders.Delete(ctx, id)
assert.Error(t, err)
// Local HCP Terraform or Terraform Enterprise will return a forbidden here when HCP Terraform or Terraform Enterprise is in development mode
// In non development mode this returns a 404
assert.Equal(t, ErrResourceNotFound, err)
})
})
}
}
func TestRegistryProvidersIDValidation(t *testing.T) {
t.Parallel()
orgName := "orgName"
registryName := PublicRegistry
t.Run("valid", func(t *testing.T) {
id := RegistryProviderID{
OrganizationName: orgName,
RegistryName: registryName,
Namespace: "namespace",
Name: "name",
}
require.NoError(t, id.valid())
})
t.Run("without a name", func(t *testing.T) {
id := RegistryProviderID{
OrganizationName: orgName,
RegistryName: registryName,
Namespace: "namespace",
Name: "",
}
assert.EqualError(t, id.valid(), ErrInvalidName.Error())
})
t.Run("with an invalid name", func(t *testing.T) {
id := RegistryProviderID{
OrganizationName: orgName,
RegistryName: registryName,
Namespace: "namespace",
Name: badIdentifier,
}
assert.EqualError(t, id.valid(), ErrInvalidName.Error())
})
t.Run("without a namespace", func(t *testing.T) {
id := RegistryProviderID{
OrganizationName: orgName,
RegistryName: registryName,
Namespace: "",
Name: "name",
}
assert.EqualError(t, id.valid(), ErrInvalidNamespace.Error())
})
t.Run("with an invalid namespace", func(t *testing.T) {
id := RegistryProviderID{
OrganizationName: orgName,
RegistryName: registryName,
Namespace: badIdentifier,
Name: "name",
}
assert.EqualError(t, id.valid(), ErrInvalidNamespace.Error())
})
t.Run("without a registry-name", func(t *testing.T) {
id := RegistryProviderID{
OrganizationName: orgName,
RegistryName: "",
Namespace: "namespace",
Name: "name",
}
assert.EqualError(t, id.valid(), ErrInvalidRegistryName.Error())
})
t.Run("without a valid organization", func(t *testing.T) {
id := RegistryProviderID{
OrganizationName: badIdentifier,
RegistryName: registryName,
Namespace: "namespace",
Name: "name",
}
assert.EqualError(t, id.valid(), ErrInvalidOrg.Error())
})
}
================================================
FILE: registry_provider_platform.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation
var _ RegistryProviderPlatforms = (*registryProviderPlatforms)(nil)
// RegistryProviderPlatforms describes the registry provider platform methods supported by the Terraform Enterprise API.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/private-registry/provider-versions-platforms#private-provider-versions-and-platforms-api
type RegistryProviderPlatforms interface {
// Create a provider platform for an organization
Create(ctx context.Context, versionID RegistryProviderVersionID, options RegistryProviderPlatformCreateOptions) (*RegistryProviderPlatform, error)
// List all provider platforms for a single version
List(ctx context.Context, versionID RegistryProviderVersionID, options *RegistryProviderPlatformListOptions) (*RegistryProviderPlatformList, error)
// Read a provider platform by ID
Read(ctx context.Context, platformID RegistryProviderPlatformID) (*RegistryProviderPlatform, error)
// Delete a provider platform
Delete(ctx context.Context, platformID RegistryProviderPlatformID) error
}
// registryProviders implements RegistryProviders
type registryProviderPlatforms struct {
client *Client
}
// RegistryProviderPlatform represents a registry provider platform
type RegistryProviderPlatform struct {
ID string `jsonapi:"primary,registry-provider-platforms"`
OS string `jsonapi:"attr,os"`
Arch string `jsonapi:"attr,arch"`
Filename string `jsonapi:"attr,filename"`
Shasum string `jsonapi:"attr,shasum"`
ProviderBinaryUploaded bool `jsonapi:"attr,provider-binary-uploaded"`
// Relations
RegistryProviderVersion *RegistryProviderVersion `jsonapi:"relation,registry-provider-version"`
// Links
Links map[string]interface{} `jsonapi:"links,omitempty"`
}
// RegistryProviderPlatformID is the multi key ID for identifying a provider platform
type RegistryProviderPlatformID struct {
RegistryProviderVersionID
OS string
Arch string
}
// RegistryProviderPlatformCreateOptions represents the set of options for creating a registry provider platform
type RegistryProviderPlatformCreateOptions struct {
// Required: A valid operating system string
OS string `jsonapi:"attr,os"`
// Required: A valid architecture string
Arch string `jsonapi:"attr,arch"`
// Required: A valid shasum string
Shasum string `jsonapi:"attr,shasum"`
// Required: A valid filename string
Filename string `jsonapi:"attr,filename"`
}
type RegistryProviderPlatformList struct {
*Pagination
Items []*RegistryProviderPlatform
}
type RegistryProviderPlatformListOptions struct {
ListOptions
}
// Create a new registry provider platform
func (r *registryProviderPlatforms) Create(ctx context.Context, versionID RegistryProviderVersionID, options RegistryProviderPlatformCreateOptions) (*RegistryProviderPlatform, error) {
if err := versionID.valid(); err != nil {
return nil, err
}
if err := options.valid(); err != nil {
return nil, err
}
// POST /organizations/:organization_name/registry-providers/:registry_name/:namespace/:name/versions/:version/platforms
u := fmt.Sprintf(
"organizations/%s/registry-providers/%s/%s/%s/versions/%s/platforms",
url.PathEscape(versionID.OrganizationName),
url.PathEscape(string(versionID.RegistryName)),
url.PathEscape(versionID.Namespace),
url.PathEscape(versionID.Name),
url.PathEscape(versionID.Version),
)
req, err := r.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
rpp := &RegistryProviderPlatform{}
err = req.Do(ctx, rpp)
if err != nil {
return nil, err
}
return rpp, nil
}
// List all provider platforms for a single version
func (r *registryProviderPlatforms) List(ctx context.Context, versionID RegistryProviderVersionID, options *RegistryProviderPlatformListOptions) (*RegistryProviderPlatformList, error) {
if err := versionID.valid(); err != nil {
return nil, err
}
if err := options.valid(); err != nil {
return nil, err
}
// GET /organizations/:organization_name/registry-providers/:registry_name/:namespace/:name/versions/:version/platforms
u := fmt.Sprintf(
"organizations/%s/registry-providers/%s/%s/%s/versions/%s/platforms",
url.PathEscape(versionID.OrganizationName),
url.PathEscape(string(versionID.RegistryName)),
url.PathEscape(versionID.Namespace),
url.PathEscape(versionID.Name),
url.PathEscape(versionID.Version),
)
req, err := r.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
ppl := &RegistryProviderPlatformList{}
err = req.Do(ctx, ppl)
if err != nil {
return nil, err
}
return ppl, nil
}
// Read is used to read an organization's example by ID
func (r *registryProviderPlatforms) Read(ctx context.Context, platformID RegistryProviderPlatformID) (*RegistryProviderPlatform, error) {
if err := platformID.valid(); err != nil {
return nil, err
}
// GET /organizations/:organization_name/registry-providers/:registry_name/:namespace/:name/versions/:version/platforms/:os/:arch
u := fmt.Sprintf(
"organizations/%s/registry-providers/%s/%s/%s/versions/%s/platforms/%s/%s",
url.PathEscape(platformID.OrganizationName),
url.PathEscape(string(platformID.RegistryName)),
url.PathEscape(platformID.Namespace),
url.PathEscape(platformID.Name),
url.PathEscape(platformID.Version),
url.PathEscape(platformID.OS),
url.PathEscape(platformID.Arch),
)
req, err := r.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
rpp := &RegistryProviderPlatform{}
err = req.Do(ctx, rpp)
if err != nil {
return nil, err
}
return rpp, nil
}
// Delete a registry provider platform
func (r *registryProviderPlatforms) Delete(ctx context.Context, platformID RegistryProviderPlatformID) error {
if err := platformID.valid(); err != nil {
return err
}
// DELETE /organizations/:organization_name/registry-providers/:registry_name/:namespace/:name/versions/:version/platforms/:os/:arch
u := fmt.Sprintf(
"organizations/%s/registry-providers/%s/%s/%s/versions/%s/platforms/%s/%s",
url.PathEscape(platformID.OrganizationName),
url.PathEscape(string(platformID.RegistryName)),
url.PathEscape(platformID.Namespace),
url.PathEscape(platformID.Name),
url.PathEscape(platformID.Version),
url.PathEscape(platformID.OS),
url.PathEscape(platformID.Arch),
)
req, err := r.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (id RegistryProviderPlatformID) valid() error {
if err := id.RegistryProviderID.valid(); err != nil {
return err
}
if !validString(&id.OS) {
return ErrInvalidOS
}
if !validString(&id.Arch) {
return ErrInvalidArch
}
return nil
}
func (o RegistryProviderPlatformCreateOptions) valid() error {
if !validString(&o.OS) {
return ErrRequiredOS
}
if !validString(&o.Arch) {
return ErrRequiredArch
}
if !validStringID(&o.Shasum) {
return ErrRequiredShasum
}
if !validStringID(&o.Filename) {
return ErrRequiredFilename
}
return nil
}
func (o *RegistryProviderPlatformListOptions) valid() error {
return nil
}
================================================
FILE: registry_provider_platform_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRegistryProviderPlatformsCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
provider, providerTestCleanup := createRegistryProvider(t, client, nil, PrivateRegistry)
defer providerTestCleanup()
version, versionCleanup := createRegistryProviderVersion(t, client, provider)
defer versionCleanup()
versionID := RegistryProviderVersionID{
RegistryProviderID: RegistryProviderID{
OrganizationName: provider.Organization.Name,
RegistryName: provider.RegistryName,
Namespace: provider.Namespace,
Name: provider.Name,
},
Version: version.Version,
}
t.Run("with valid options", func(t *testing.T) {
options := RegistryProviderPlatformCreateOptions{
OS: "linux",
Arch: "amd64",
Shasum: "shasum",
Filename: "filename",
}
rpp, err := client.RegistryProviderPlatforms.Create(ctx, versionID, options)
require.NoError(t, err)
assert.NotEmpty(t, rpp.ID)
assert.Equal(t, options.OS, rpp.OS)
assert.Equal(t, options.Arch, rpp.Arch)
assert.Equal(t, options.Shasum, rpp.Shasum)
assert.Equal(t, options.Filename, rpp.Filename)
assert.False(t, rpp.ProviderBinaryUploaded)
t.Run("relationships are properly decoded", func(t *testing.T) {
assert.Equal(t, version.ID, rpp.RegistryProviderVersion.ID)
})
t.Run("attributes are properly decoded", func(t *testing.T) {
assert.NotEmpty(t, rpp.Arch)
assert.NotEmpty(t, rpp.OS)
assert.NotEmpty(t, rpp.Shasum)
assert.NotEmpty(t, rpp.Filename)
})
})
t.Run("with invalid options", func(t *testing.T) {
t.Run("without an OS", func(t *testing.T) {
options := RegistryProviderPlatformCreateOptions{
OS: "",
Arch: "amd64",
Shasum: "shasum",
Filename: "filename",
}
sadRpp, err := client.RegistryProviderPlatforms.Create(ctx, versionID, options)
assert.Nil(t, sadRpp)
assert.EqualError(t, err, ErrRequiredOS.Error())
})
t.Run("without an arch", func(t *testing.T) {
options := RegistryProviderPlatformCreateOptions{
OS: "os",
Arch: "",
Shasum: "shasum",
Filename: "filename",
}
sadRpp, err := client.RegistryProviderPlatforms.Create(ctx, versionID, options)
assert.Nil(t, sadRpp)
assert.EqualError(t, err, ErrRequiredArch.Error())
})
t.Run("without a shasum", func(t *testing.T) {
options := RegistryProviderPlatformCreateOptions{
OS: "linux",
Arch: "amd64",
Shasum: "",
Filename: "filename",
}
sadRpp, err := client.RegistryProviderPlatforms.Create(ctx, versionID, options)
assert.Nil(t, sadRpp)
assert.EqualError(t, err, ErrRequiredShasum.Error())
})
t.Run("without a filename", func(t *testing.T) {
options := RegistryProviderPlatformCreateOptions{
OS: "linux",
Arch: "amd64",
Shasum: "shasum",
Filename: "",
}
sadRpp, err := client.RegistryProviderPlatforms.Create(ctx, versionID, options)
assert.Nil(t, sadRpp)
assert.EqualError(t, err, ErrRequiredFilename.Error())
})
t.Run("with a public provider", func(t *testing.T) {
options := RegistryProviderPlatformCreateOptions{
OS: "linux",
Arch: "amd64",
Shasum: "shasum",
Filename: "filename",
}
versionID = RegistryProviderVersionID{
RegistryProviderID: RegistryProviderID{
OrganizationName: provider.Organization.Name,
RegistryName: PublicRegistry,
Namespace: provider.Namespace,
Name: provider.Name,
},
Version: version.Version,
}
rm, err := client.RegistryProviderPlatforms.Create(ctx, versionID, options)
assert.Nil(t, rm)
assert.EqualError(t, err, ErrRequiredPrivateRegistry.Error())
})
t.Run("without a valid registry provider version id", func(t *testing.T) {
options := RegistryProviderPlatformCreateOptions{
OS: "linux",
Arch: "amd64",
Shasum: "shasum",
Filename: "filename",
}
versionID = RegistryProviderVersionID{
RegistryProviderID: RegistryProviderID{
OrganizationName: badIdentifier,
RegistryName: provider.RegistryName,
Namespace: provider.Namespace,
Name: provider.Name,
},
Version: version.Version,
}
rm, err := client.RegistryProviderPlatforms.Create(ctx, versionID, options)
assert.Nil(t, rm)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
})
}
func TestRegistryProviderPlatformsDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
provider, providerCleanup := createRegistryProvider(t, client, nil, PrivateRegistry)
defer providerCleanup()
version, versionCleanup := createRegistryProviderVersion(t, client, provider)
defer versionCleanup()
versionID := RegistryProviderVersionID{
RegistryProviderID: RegistryProviderID{
OrganizationName: provider.Organization.Name,
RegistryName: provider.RegistryName,
Namespace: provider.Namespace,
Name: provider.Name,
},
Version: version.Version,
}
t.Run("with a valid version", func(t *testing.T) {
platform, _ := createRegistryProviderPlatform(t, client, provider, version, "", "")
platformID := RegistryProviderPlatformID{
RegistryProviderVersionID: versionID,
OS: platform.OS,
Arch: platform.Arch,
}
err := client.RegistryProviderPlatforms.Delete(ctx, platformID)
require.NoError(t, err)
})
t.Run("with a non-existent version", func(t *testing.T) {
platformID := RegistryProviderPlatformID{
RegistryProviderVersionID: versionID,
OS: "linux",
Arch: "amd64",
}
err := client.RegistryProviderPlatforms.Delete(ctx, platformID)
assert.Error(t, err)
})
}
func TestRegistryProviderPlatformsRead(t *testing.T) {
t.Parallel()
t.Skip()
client := testClient(t)
ctx := context.Background()
provider, providerCleanup := createRegistryProvider(t, client, nil, PrivateRegistry)
defer providerCleanup()
providerID := RegistryProviderID{
OrganizationName: provider.Organization.Name,
Namespace: provider.Namespace,
Name: provider.Name,
RegistryName: provider.RegistryName,
}
version, versionCleanup := createRegistryProviderVersion(t, client, provider)
defer versionCleanup()
versionID := RegistryProviderVersionID{
RegistryProviderID: providerID,
Version: version.Version,
}
platform, platformCleanup := createRegistryProviderPlatform(t, client, provider, version, "", "")
defer platformCleanup()
t.Run("with valid platform", func(t *testing.T) {
platformID := RegistryProviderPlatformID{
RegistryProviderVersionID: versionID,
OS: platform.OS,
Arch: platform.Arch,
}
readPlatform, err := client.RegistryProviderPlatforms.Read(ctx, platformID)
require.NoError(t, err)
assert.Equal(t, platformID.OS, readPlatform.OS)
assert.Equal(t, platformID.Arch, readPlatform.Arch)
assert.Equal(t, platform.Filename, readPlatform.Filename)
assert.Equal(t, platform.Shasum, readPlatform.Shasum)
assert.Equal(t, platform.ProviderBinaryUploaded, readPlatform.ProviderBinaryUploaded)
t.Run("relationships are properly decoded", func(t *testing.T) {
assert.Equal(t, platform.RegistryProviderVersion.ID, readPlatform.RegistryProviderVersion.ID)
})
t.Run("includes provider binary upload link", func(t *testing.T) {
expectedLinks := []string{
"provider-binary-upload",
}
for _, l := range expectedLinks {
_, ok := readPlatform.Links[l].(string)
assert.True(t, ok, "Expect upload link: %s", l)
}
})
})
t.Run("with non-existent os", func(t *testing.T) {
platformID := RegistryProviderPlatformID{
RegistryProviderVersionID: versionID,
OS: "DoesNotExist",
Arch: platform.Arch,
}
_, err := client.RegistryProviderPlatforms.Read(ctx, platformID)
assert.Error(t, err)
})
t.Run("with non-existent arch", func(t *testing.T) {
platformID := RegistryProviderPlatformID{
RegistryProviderVersionID: versionID,
OS: platform.OS,
Arch: "DoesNotExist",
}
_, err := client.RegistryProviderPlatforms.Read(ctx, platformID)
assert.Error(t, err)
})
}
func TestRegistryProviderPlatformsList(t *testing.T) {
t.Parallel()
t.Skip()
client := testClient(t)
ctx := context.Background()
t.Run("with platforms", func(t *testing.T) {
provider, providerCleanup := createRegistryProvider(t, client, nil, PrivateRegistry)
defer providerCleanup()
version, versionCleanup := createRegistryProviderVersion(t, client, provider)
defer versionCleanup()
osl := []string{"linux", "darwin", "windows"}
archl := []string{"amd64", "arm64", "amd64"}
platforms := make([]*RegistryProviderPlatform, 0)
for i, os := range osl {
platform, _ := createRegistryProviderPlatform(t, client, provider, version, os, archl[i])
platforms = append(platforms, platform)
}
numPlatforms := len(platforms)
providerID := RegistryProviderID{
OrganizationName: provider.Organization.Name,
Namespace: provider.Namespace,
Name: provider.Name,
RegistryName: provider.RegistryName,
}
versionID := RegistryProviderVersionID{
RegistryProviderID: providerID,
Version: version.Version,
}
t.Run("with no list options", func(t *testing.T) {
returnedPlatforms, err := client.RegistryProviderPlatforms.List(ctx, versionID, nil)
require.NoError(t, err)
require.Len(t, returnedPlatforms.Items, numPlatforms)
assert.Equal(t, 1, returnedPlatforms.TotalPages)
for _, rp := range returnedPlatforms.Items {
foundPlatform := false
for _, p := range platforms {
if rp.ID == p.ID {
foundPlatform = true
break
}
}
assert.True(t, foundPlatform, "Expected to find platform %s but did not:\nexpected:\n%v\nreturned\n%v", rp.ID, platforms, returnedPlatforms)
}
})
t.Run("with list options", func(t *testing.T) {
returnedPlatforms, err := client.RegistryProviderPlatforms.List(ctx, versionID, &RegistryProviderPlatformListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
require.Len(t, returnedPlatforms.Items, 0)
assert.Equal(t, 999, returnedPlatforms.CurrentPage)
assert.Equal(t, numPlatforms, returnedPlatforms.TotalCount)
})
})
t.Run("without platforms", func(t *testing.T) {
provider, providerCleanup := createRegistryProvider(t, client, nil, PrivateRegistry)
defer providerCleanup()
version, versionCleanup := createRegistryProviderVersion(t, client, provider)
defer versionCleanup()
versionID := RegistryProviderVersionID{
RegistryProviderID: RegistryProviderID{
OrganizationName: provider.Organization.Name,
Namespace: provider.Namespace,
Name: provider.Name,
RegistryName: provider.RegistryName,
},
Version: version.Version,
}
platforms, err := client.RegistryProviderPlatforms.List(ctx, versionID, nil)
require.NoError(t, err)
assert.Empty(t, platforms.Items)
assert.Equal(t, 0, platforms.TotalCount)
assert.Equal(t, 0, platforms.TotalPages)
})
}
================================================
FILE: registry_provider_version.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ RegistryProviderVersions = (*registryProviderVersions)(nil)
// RegistryProviderVersions describes the registry provider version methods that
// the Terraform Enterprise API supports.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/private-registry/provider-versions-platforms
type RegistryProviderVersions interface {
// List all versions for a single provider.
List(ctx context.Context, providerID RegistryProviderID, options *RegistryProviderVersionListOptions) (*RegistryProviderVersionList, error)
// Create a registry provider version.
Create(ctx context.Context, providerID RegistryProviderID, options RegistryProviderVersionCreateOptions) (*RegistryProviderVersion, error)
// Read a registry provider version.
Read(ctx context.Context, versionID RegistryProviderVersionID) (*RegistryProviderVersion, error)
// Delete a registry provider version.
Delete(ctx context.Context, versionID RegistryProviderVersionID) error
}
// registryProvidersVersions implements RegistryProvidersVersions
type registryProviderVersions struct {
client *Client
}
// RegistryProviderVersion represents a registry provider version
type RegistryProviderVersion struct {
ID string `jsonapi:"primary,registry-provider-versions"`
Version string `jsonapi:"attr,version"`
CreatedAt string `jsonapi:"attr,created-at,iso8601"`
UpdatedAt string `jsonapi:"attr,updated-at,iso8601"`
KeyID string `jsonapi:"attr,key-id"`
Protocols []string `jsonapi:"attr,protocols"`
Permissions RegistryProviderVersionPermissions `jsonapi:"attr,permissions"`
ShasumsUploaded bool `jsonapi:"attr,shasums-uploaded"`
ShasumsSigUploaded bool `jsonapi:"attr,shasums-sig-uploaded"`
// Relations
RegistryProvider *RegistryProvider `jsonapi:"relation,registry-provider"`
RegistryProviderPlatforms []*RegistryProviderPlatform `jsonapi:"relation,platforms"`
// Links
Links map[string]interface{} `jsonapi:"links,omitempty"`
}
// RegistryProviderVersionID is the multi key ID for addressing a version provider
type RegistryProviderVersionID struct {
RegistryProviderID
Version string
}
type RegistryProviderVersionPermissions struct {
CanDelete bool `jsonapi:"attr,can-delete"`
CanUploadAsset bool `jsonapi:"attr,can-upload-asset"`
}
type RegistryProviderVersionList struct {
*Pagination
Items []*RegistryProviderVersion
}
type RegistryProviderVersionListOptions struct {
ListOptions
}
type RegistryProviderVersionCreateOptions struct {
// Required: A valid semver version string.
Version string `jsonapi:"attr,version"`
// Required: A valid gpg-key string.
KeyID string `jsonapi:"attr,key-id"`
// Required: An array of Terraform provider API versions that this version supports.
Protocols []string `jsonapi:"attr,protocols"`
}
// List registry provider versions
func (r *registryProviderVersions) List(ctx context.Context, providerID RegistryProviderID, options *RegistryProviderVersionListOptions) (*RegistryProviderVersionList, error) {
if err := providerID.valid(); err != nil {
return nil, err
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf(
"organizations/%s/registry-providers/%s/%s/%s/versions",
url.PathEscape(providerID.OrganizationName),
url.PathEscape(string(providerID.RegistryName)),
url.PathEscape(providerID.Namespace),
url.PathEscape(providerID.Name),
)
req, err := r.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
pvl := &RegistryProviderVersionList{}
err = req.Do(ctx, pvl)
if err != nil {
return nil, err
}
return pvl, nil
}
// Create a registry provider version
func (r *registryProviderVersions) Create(ctx context.Context, providerID RegistryProviderID, options RegistryProviderVersionCreateOptions) (*RegistryProviderVersion, error) {
if err := providerID.valid(); err != nil {
return nil, err
}
if providerID.RegistryName != PrivateRegistry {
return nil, ErrRequiredPrivateRegistry
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf(
"organizations/%s/registry-providers/%s/%s/%s/versions",
url.PathEscape(providerID.OrganizationName),
url.PathEscape(string(providerID.RegistryName)),
url.PathEscape(providerID.Namespace),
url.PathEscape(providerID.Name),
)
req, err := r.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
prvv := &RegistryProviderVersion{}
err = req.Do(ctx, prvv)
if err != nil {
return nil, err
}
return prvv, nil
}
// Read a registry provider version
func (r *registryProviderVersions) Read(ctx context.Context, versionID RegistryProviderVersionID) (*RegistryProviderVersion, error) {
if err := versionID.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf(
"organizations/%s/registry-providers/%s/%s/%s/versions/%s",
url.PathEscape(versionID.OrganizationName),
url.PathEscape(string(versionID.RegistryName)),
url.PathEscape(versionID.Namespace),
url.PathEscape(versionID.Name),
url.PathEscape(versionID.Version),
)
req, err := r.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
prvv := &RegistryProviderVersion{}
err = req.Do(ctx, prvv)
if err != nil {
return nil, err
}
return prvv, nil
}
// Delete a registry provider version
func (r *registryProviderVersions) Delete(ctx context.Context, versionID RegistryProviderVersionID) error {
if err := versionID.valid(); err != nil {
return err
}
u := fmt.Sprintf(
"organizations/%s/registry-providers/%s/%s/%s/versions/%s",
url.PathEscape(versionID.OrganizationName),
url.PathEscape(string(versionID.RegistryName)),
url.PathEscape(versionID.Namespace),
url.PathEscape(versionID.Name),
url.PathEscape(versionID.Version),
)
req, err := r.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// ShasumsUploadURL returns the upload URL to upload shasums if one is available
func (v *RegistryProviderVersion) ShasumsUploadURL() (string, error) {
uploadURL, ok := v.Links["shasums-upload"].(string)
if !ok {
return uploadURL, fmt.Errorf("the Registry Provider Version does not contain a shasums upload link")
}
if uploadURL == "" {
return uploadURL, fmt.Errorf("the Registry Provider Version shasums upload URL is empty")
}
return uploadURL, nil
}
// ShasumsSigUploadURL returns the URL to upload a shasums sig
func (v *RegistryProviderVersion) ShasumsSigUploadURL() (string, error) {
uploadURL, ok := v.Links["shasums-sig-upload"].(string)
if !ok {
return uploadURL, fmt.Errorf("the Registry Provider Version does not contain a shasums sig upload link")
}
if uploadURL == "" {
return uploadURL, fmt.Errorf("the Registry Provider Version shasums sig upload URL is empty")
}
return uploadURL, nil
}
// ShasumsDownloadURL returns the URL to download the shasums for the registry version
func (v *RegistryProviderVersion) ShasumsDownloadURL() (string, error) {
downloadURL, ok := v.Links["shasums-download"].(string)
if !ok {
return downloadURL, fmt.Errorf("the Registry Provider Version does not contain a shasums download link")
}
if downloadURL == "" {
return downloadURL, fmt.Errorf("the Registry Provider Version shasums download URL is empty")
}
return downloadURL, nil
}
// ShasumsSigDownloadURL returns the URL to download the shasums sig for the registry version
func (v *RegistryProviderVersion) ShasumsSigDownloadURL() (string, error) {
downloadURL, ok := v.Links["shasums-sig-download"].(string)
if !ok {
return downloadURL, fmt.Errorf("the Registry Provider Version does not contain a shasums sig download link")
}
if downloadURL == "" {
return downloadURL, fmt.Errorf("the Registry Provider Version shasums sig download URL is empty")
}
return downloadURL, nil
}
func (id RegistryProviderVersionID) valid() error {
if !validStringID(&id.Version) {
return ErrInvalidVersion
}
if id.RegistryName != PrivateRegistry {
return ErrRequiredPrivateRegistry
}
if err := id.RegistryProviderID.valid(); err != nil {
return err
}
return nil
}
func (o *RegistryProviderVersionListOptions) valid() error {
return nil
}
func (o RegistryProviderVersionCreateOptions) valid() error {
if !validStringID(&o.Version) {
return ErrInvalidVersion
}
if !validStringID(&o.KeyID) {
return ErrInvalidKeyID
}
return nil
}
================================================
FILE: registry_provider_version_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRegistryProviderVersionsIDValidation(t *testing.T) {
t.Parallel()
version := "1.0.0"
validRegistryProviderID := RegistryProviderID{
OrganizationName: "orgName",
RegistryName: PrivateRegistry,
Namespace: "namespace",
Name: "name",
}
invalidRegistryProviderID := RegistryProviderID{
OrganizationName: badIdentifier,
RegistryName: PrivateRegistry,
Namespace: "namespace",
Name: "name",
}
publicRegistryProviderID := RegistryProviderID{
OrganizationName: "orgName",
RegistryName: PublicRegistry,
Namespace: "namespace",
Name: "name",
}
t.Run("valid", func(t *testing.T) {
id := RegistryProviderVersionID{
Version: version,
RegistryProviderID: validRegistryProviderID,
}
require.NoError(t, id.valid())
})
t.Run("without a version", func(t *testing.T) {
id := RegistryProviderVersionID{
Version: "",
RegistryProviderID: validRegistryProviderID,
}
assert.EqualError(t, id.valid(), ErrInvalidVersion.Error())
})
t.Run("without a key-id", func(t *testing.T) {
id := RegistryProviderVersionID{
Version: "",
RegistryProviderID: validRegistryProviderID,
}
assert.EqualError(t, id.valid(), ErrInvalidVersion.Error())
})
t.Run("invalid version", func(t *testing.T) {
t.Skip("This is skipped as we don't actually validate version is a valid semver - the registry does this validation")
id := RegistryProviderVersionID{
Version: "foo",
RegistryProviderID: validRegistryProviderID,
}
assert.EqualError(t, id.valid(), ErrInvalidVersion.Error())
})
t.Run("invalid registry for parent provider", func(t *testing.T) {
id := RegistryProviderVersionID{
Version: version,
RegistryProviderID: publicRegistryProviderID,
}
assert.EqualError(t, id.valid(), ErrRequiredPrivateRegistry.Error())
})
t.Run("without a valid registry provider id", func(t *testing.T) {
// this is a proxy for all permutations of an invalid registry provider id
// it is assumed that validity of the registry provider id is delegated to its own valid method
id := RegistryProviderVersionID{
Version: version,
RegistryProviderID: invalidRegistryProviderID,
}
assert.EqualError(t, id.valid(), ErrInvalidOrg.Error())
})
}
func TestRegistryProviderVersionsCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
providerTest, providerTestCleanup := createRegistryProvider(t, client, nil, PrivateRegistry)
defer providerTestCleanup()
providerID := RegistryProviderID{
OrganizationName: providerTest.Organization.Name,
RegistryName: providerTest.RegistryName,
Namespace: providerTest.Namespace,
Name: providerTest.Name,
}
t.Run("with valid options", func(t *testing.T) {
options := RegistryProviderVersionCreateOptions{
Version: "1.0.0",
KeyID: "abcdefg",
}
prvv, err := client.RegistryProviderVersions.Create(ctx, providerID, options)
require.NoError(t, err)
assert.NotEmpty(t, prvv.ID)
assert.Equal(t, options.Version, prvv.Version)
assert.Equal(t, options.KeyID, prvv.KeyID)
t.Run("relationships are properly decoded", func(t *testing.T) {
assert.Equal(t, providerTest.ID, prvv.RegistryProvider.ID)
})
t.Run("timestamps are properly decoded", func(t *testing.T) {
assert.NotEmpty(t, prvv.CreatedAt)
assert.NotEmpty(t, prvv.UpdatedAt)
})
t.Run("includes upload links", func(t *testing.T) {
_, err := prvv.ShasumsUploadURL()
require.NoError(t, err)
_, err = prvv.ShasumsSigUploadURL()
require.NoError(t, err)
expectedLinks := []string{
"shasums-upload",
"shasums-sig-upload",
}
for _, l := range expectedLinks {
_, ok := prvv.Links[l].(string)
assert.True(t, ok, "Expect upload link: %s", l)
}
})
t.Run("doesn't include download links", func(t *testing.T) {
_, err := prvv.ShasumsDownloadURL()
assert.Error(t, err)
_, err = prvv.ShasumsSigDownloadURL()
assert.Error(t, err)
})
})
t.Run("with invalid options", func(t *testing.T) {
t.Run("without a version", func(t *testing.T) {
options := RegistryProviderVersionCreateOptions{
Version: "",
KeyID: "abcdefg",
}
rm, err := client.RegistryProviderVersions.Create(ctx, providerID, options)
assert.Nil(t, rm)
assert.EqualError(t, err, ErrInvalidVersion.Error())
})
t.Run("without a key-id", func(t *testing.T) {
options := RegistryProviderVersionCreateOptions{
Version: "1.0.0",
KeyID: "",
}
rm, err := client.RegistryProviderVersions.Create(ctx, providerID, options)
assert.Nil(t, rm)
assert.EqualError(t, err, ErrInvalidKeyID.Error())
})
t.Run("with a public provider", func(t *testing.T) {
options := RegistryProviderVersionCreateOptions{
Version: "1.0.0",
KeyID: "abcdefg",
}
providerID := RegistryProviderID{
OrganizationName: providerTest.Organization.Name,
RegistryName: PublicRegistry,
Namespace: providerTest.Namespace,
Name: providerTest.Name,
}
rm, err := client.RegistryProviderVersions.Create(ctx, providerID, options)
assert.Nil(t, rm)
assert.EqualError(t, err, ErrRequiredPrivateRegistry.Error())
})
t.Run("without a valid provider id", func(t *testing.T) {
options := RegistryProviderVersionCreateOptions{
Version: "1.0.0",
KeyID: "abcdefg",
}
providerID := RegistryProviderID{
OrganizationName: badIdentifier,
RegistryName: providerTest.RegistryName,
Namespace: providerTest.Namespace,
Name: providerTest.Name,
}
rm, err := client.RegistryProviderVersions.Create(ctx, providerID, options)
assert.Nil(t, rm)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
})
}
func TestRegistryProviderVersionsList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
t.Run("with versions", func(t *testing.T) {
provider, providerCleanup := createRegistryProvider(t, client, nil, PrivateRegistry)
defer providerCleanup()
createN := 10
versions := make([]*RegistryProviderVersion, 0)
// these providers will be destroyed when the org is cleaned up
for i := 0; i < createN; i++ {
version, _ := createRegistryProviderVersion(t, client, provider)
versions = append(versions, version)
}
versionN := len(versions)
providerID := RegistryProviderID{
OrganizationName: provider.Organization.Name,
Namespace: provider.Namespace,
Name: provider.Name,
RegistryName: provider.RegistryName,
}
t.Run("returns all versions", func(t *testing.T) {
returnedVersions, err := client.RegistryProviderVersions.List(ctx, providerID, &RegistryProviderVersionListOptions{
ListOptions: ListOptions{
PageNumber: 0,
PageSize: versionN,
},
})
require.NoError(t, err)
require.NotEmpty(t, returnedVersions.Items)
assert.Equal(t, versionN, returnedVersions.TotalCount)
assert.Equal(t, 1, returnedVersions.TotalPages)
for _, rv := range returnedVersions.Items {
foundVersion := false
for _, v := range versions {
if rv.ID == v.ID {
foundVersion = true
break
}
}
assert.True(t, foundVersion, "Expected to find version %s but did not:\nexpected:\n%v\nreturned\n%v", rv.ID, versions, returnedVersions)
}
})
t.Run("returns pages", func(t *testing.T) {
pageN := 2
pageSize := versionN / pageN
for page := 0; page < pageN; page++ {
testName := fmt.Sprintf("returns page %d of versions", page)
t.Run(testName, func(t *testing.T) {
returnedVersions, err := client.RegistryProviderVersions.List(ctx, providerID, &RegistryProviderVersionListOptions{
ListOptions: ListOptions{
PageNumber: page,
PageSize: pageSize,
},
})
require.NoError(t, err)
require.NotEmpty(t, returnedVersions.Items)
assert.Equal(t, versionN, returnedVersions.TotalCount)
assert.Equal(t, pageN, returnedVersions.TotalPages)
assert.Equal(t, pageSize, len(returnedVersions.Items))
for _, rv := range returnedVersions.Items {
foundVersion := false
for _, v := range versions {
if rv.ID == v.ID {
foundVersion = true
break
}
}
assert.True(t, foundVersion, "Expected to find version %s but did not:\nexpected:\n%v\nreturned\n%v", rv.ID, versions, returnedVersions)
}
})
}
})
})
t.Run("without versions", func(t *testing.T) {
provider, providerCleanup := createRegistryProvider(t, client, nil, PrivateRegistry)
defer providerCleanup()
providerID := RegistryProviderID{
OrganizationName: provider.Organization.Name,
Namespace: provider.Namespace,
Name: provider.Name,
RegistryName: provider.RegistryName,
}
versions, err := client.RegistryProviderVersions.List(ctx, providerID, nil)
require.NoError(t, err)
assert.Empty(t, versions.Items)
assert.Equal(t, 0, versions.TotalCount)
assert.Equal(t, 0, versions.TotalPages)
})
}
func TestRegistryProviderVersionsDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
provider, providerCleanup := createRegistryProvider(t, client, nil, PrivateRegistry)
defer providerCleanup()
t.Run("with valid version", func(t *testing.T) {
version, _ := createRegistryProviderVersion(t, client, provider)
versionID := RegistryProviderVersionID{
RegistryProviderID: RegistryProviderID{
OrganizationName: version.RegistryProvider.Organization.Name,
RegistryName: version.RegistryProvider.RegistryName,
Namespace: version.RegistryProvider.Namespace,
Name: version.RegistryProvider.Name,
},
Version: version.Version,
}
err := client.RegistryProviderVersions.Delete(ctx, versionID)
require.NoError(t, err)
})
t.Run("with non existing version", func(t *testing.T) {
versionID := RegistryProviderVersionID{
RegistryProviderID: RegistryProviderID{
OrganizationName: provider.Organization.Name,
RegistryName: provider.RegistryName,
Namespace: provider.Namespace,
Name: provider.Name,
},
Version: "1.0.0",
}
err := client.RegistryProviderVersions.Delete(ctx, versionID)
assert.Error(t, err)
})
}
func TestRegistryProviderVersionsRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
t.Run("with valid version", func(t *testing.T) {
version, versionCleanup := createRegistryProviderVersion(t, client, nil)
defer versionCleanup()
versionID := RegistryProviderVersionID{
RegistryProviderID: RegistryProviderID{
OrganizationName: version.RegistryProvider.Organization.Name,
RegistryName: version.RegistryProvider.RegistryName,
Namespace: version.RegistryProvider.Namespace,
Name: version.RegistryProvider.Name,
},
Version: version.Version,
}
readVersion, err := client.RegistryProviderVersions.Read(ctx, versionID)
require.NoError(t, err)
assert.Equal(t, version.ID, readVersion.ID)
assert.Equal(t, version.Version, readVersion.Version)
assert.Equal(t, version.KeyID, readVersion.KeyID)
t.Run("relationships are properly decoded", func(t *testing.T) {
assert.Equal(t, version.RegistryProvider.ID, readVersion.RegistryProvider.ID)
})
t.Run("timestamps are properly decoded", func(t *testing.T) {
assert.NotEmpty(t, readVersion.CreatedAt)
assert.NotEmpty(t, readVersion.UpdatedAt)
})
t.Run("includes upload links", func(t *testing.T) {
expectedLinks := []string{
"shasums-upload",
"shasums-sig-upload",
}
for _, l := range expectedLinks {
_, ok := readVersion.Links[l].(string)
assert.True(t, ok, "Expect upload link: %s", l)
}
})
})
t.Run("with non existing version", func(t *testing.T) {
provider, providerCleanup := createRegistryProvider(t, client, nil, PrivateRegistry)
defer providerCleanup()
versionID := RegistryProviderVersionID{
RegistryProviderID: RegistryProviderID{
OrganizationName: provider.Organization.Name,
RegistryName: provider.RegistryName,
Namespace: provider.Namespace,
Name: provider.Name,
},
Version: "1.0.0",
}
_, err := client.RegistryProviderVersions.Read(ctx, versionID)
assert.Error(t, err)
})
}
================================================
FILE: request.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
retryablehttp "github.com/hashicorp/go-retryablehttp"
"golang.org/x/time/rate"
)
// ClientRequest encapsulates a request sent by the Client
type ClientRequest struct {
retryableRequest *retryablehttp.Request
http *retryablehttp.Client
limiter *rate.Limiter
// Header are the headers that will be sent in this request
Header http.Header
}
func (r ClientRequest) Do(ctx context.Context, model interface{}) error {
// Wait will block until the limiter can obtain a new token
// or returns an error if the given context is canceled.
if r.limiter != nil {
if err := r.limiter.Wait(ctx); err != nil {
return err
}
}
// If the caller provided a response header hook then we'll call it
// once we have a response.
respHeaderHook, err := contextResponseHeaderHook(ctx)
if err != nil {
return err
}
// Add the context to the request.
reqWithCxt := r.retryableRequest.WithContext(ctx)
// Execute the request and check the response.
resp, err := r.http.Do(reqWithCxt)
if resp != nil {
// We call the callback whenever there's any sort of response,
// even if it's returned in conjunction with an error.
respHeaderHook(resp.StatusCode, resp.Header)
}
if err != nil {
// If we got an error, and the context has been canceled,
// the context's error is probably more useful.
select {
case <-ctx.Done():
return ctx.Err()
default:
return err
}
}
defer resp.Body.Close() //nolint:errcheck
// Basic response checking.
if err := checkResponseCode(resp); err != nil {
return err
}
// Return here if decoding the response isn't needed.
if model == nil {
return nil
}
// If v implements io.Writer, write the raw response body.
if w, ok := model.(io.Writer); ok {
_, err := io.Copy(w, resp.Body)
return err
}
return unmarshalResponse(resp.Body, model)
}
// DoJSON is similar to Do except that it should be used when a plain JSON response is expected
// as opposed to json-api.
func (r *ClientRequest) DoJSON(ctx context.Context, model any) error {
// Wait will block until the limiter can obtain a new token
// or returns an error if the given context is canceled.
if r.limiter != nil {
if err := r.limiter.Wait(ctx); err != nil {
return err
}
}
// Add the context to the request.
contextReq := r.retryableRequest.WithContext(ctx)
// If the caller provided a response header hook then we'll call it
// once we have a response.
respHeaderHook, err := contextResponseHeaderHook(ctx)
if err != nil {
return err
}
// Execute the request and check the response.
resp, err := r.http.Do(contextReq)
if resp != nil {
// We call the callback whenever there's any sort of response,
// even if it's returned in conjunction with an error.
respHeaderHook(resp.StatusCode, resp.Header)
}
if err != nil {
// If we got an error, and the context has been canceled,
// the context's error is probably more useful.
select {
case <-ctx.Done():
return ctx.Err()
default:
return err
}
}
defer resp.Body.Close() //nolint:errcheck
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
return fmt.Errorf("error HTTP response: %d", resp.StatusCode)
} else if resp.StatusCode == 304 {
// Got a "Not Modified" response, but we can't return a model because there is no response body.
// This is necessary to support the IPRanges endpoint, which has the peculiar behavior
// of not returning content but allowing a 304 response by optionally sending an
// If-Modified-Since header.
return nil
}
// Return here if decoding the response isn't needed.
if model == nil {
return nil
}
// If v implements io.Writer, write the raw response body.
if w, ok := model.(io.Writer); ok {
_, err := io.Copy(w, resp.Body)
return err
}
return json.NewDecoder(resp.Body).Decode(model)
}
// DoRaw exposes the underlying io.ReadCloser for the response body.
// The caller is responsible for closing the ReadCloser and unmarshaling the
// results.
func (r *ClientRequest) DoRaw(ctx context.Context) (io.ReadCloser, error) {
// Wait will block until the limiter can obtain a new token
// or returns an error if the given context is canceled.
if r.limiter != nil {
if err := r.limiter.Wait(ctx); err != nil {
return nil, err
}
}
// Add the context to the request.
contextReq := r.retryableRequest.WithContext(ctx)
// If the caller provided a response header hook then we'll call it
// once we have a response.
respHeaderHook, err := contextResponseHeaderHook(ctx)
if err != nil {
return nil, err
}
// Execute the request and check the response.
resp, err := r.http.Do(contextReq)
if resp != nil {
// We call the callback whenever there's any sort of response,
// even if it's returned in conjunction with an error.
respHeaderHook(resp.StatusCode, resp.Header)
}
if err != nil {
// If we got an error, and the context has been canceled,
// the context's error is probably more useful.
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
return nil, err
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
// Close the body here since we won't be returning it to the caller.
resp.Body.Close() //nolint:errcheck
return nil, fmt.Errorf("error HTTP response: %d", resp.StatusCode)
} else if resp.StatusCode == 304 {
// Got a "Not Modified" response, but we can't return a model because there is no response body.
// This is necessary to support the IPRanges endpoint, which has the peculiar behavior
// of not returning content but allowing a 304 response by optionally sending an
// If-Modified-Since header.
return nil, nil
}
return resp.Body, nil
}
================================================
FILE: request_hooks.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/http"
)
// ContextWithResponseHeaderHook returns a context that will, if passed to
// [ClientRequest.Do] or to any of the wrapper methods that call it, arrange
// for the given callback to be called with the headers from the raw HTTP
// response.
//
// This is intended for allowing callers to respond to out-of-band metadata
// such as cache-control-related headers, rate limiting headers, etc. Hooks
// must not modify the given [http.Header] or otherwise attempt to change how
// the response is handled by [ClientRequest.Do].
//
// If the given context already has a response header hook then the returned
// context will call both the existing hook and the newly-provided one, with
// the newer being called first.
func ContextWithResponseHeaderHook(parentCtx context.Context, cb func(status int, header http.Header)) context.Context {
// If the given context already has a notification callback then we'll
// arrange to notify both the previous and the new one. This is not
// a super efficient way to achieve that but we expect it to be rare
// for there to be more than one or two hooks associated with a particular
// request, so it's not warranted to optimize this further.
existingI := parentCtx.Value(contextResponseHeaderHookKey)
finalCb := cb
if existingI != nil {
existing, ok := existingI.(func(int, http.Header))
// This explicit check-and-panic is redundant but required by our linter.
if !ok {
panic(fmt.Sprintf("context has response header hook of invalid type %T", existingI))
}
finalCb = func(status int, header http.Header) {
cb(status, header)
existing(status, header)
}
}
return context.WithValue(parentCtx, contextResponseHeaderHookKey, finalCb)
}
func contextResponseHeaderHook(ctx context.Context) (func(int, http.Header), error) {
cbI := ctx.Value(contextResponseHeaderHookKey)
if cbI == nil {
// Stub callback that does absolutely nothing, then.
return func(int, http.Header) {}, nil
}
cb, ok := cbI.(func(int, http.Header))
if !ok {
return nil, fmt.Errorf("context has response header hook of invalid type %T", cbI)
}
return cb, nil
}
// contextResponseHeaderHookKey is the type of the internal key used to store
// the callback for [ContextWithResponseHeaderHook] inside a [context.Context]
// object.
type contextResponseHeaderHookKeyType struct{}
// contextResponseHeaderHookKey is the internal key used to store the callback
// for [ContextWithResponseHeaderHook] inside a [context.Context] object.
var contextResponseHeaderHookKey contextResponseHeaderHookKeyType
================================================
FILE: request_hooks_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"net/http"
"net/http/httptest"
"testing"
)
func TestContextWithResponseHeaderHook(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("x-thingy", "boop")
w.WriteHeader(http.StatusNoContent)
}))
defer server.Close()
cfg := &Config{
Address: server.URL,
BasePath: "/anything",
Token: "placeholder",
}
client, err := NewClient(cfg)
if err != nil {
t.Fatal(err)
}
called := false
var gotStatus int
var gotHeader http.Header
ctx := ContextWithResponseHeaderHook(context.Background(), func(status int, header http.Header) {
called = true
gotStatus = status
gotHeader = header
})
req, err := client.NewRequest("GET", "boop", nil)
if err != nil {
t.Fatal(err)
}
err = req.Do(ctx, nil)
if err != nil {
t.Fatal(err)
}
if !called {
t.Fatal("hook was not called")
}
if got, want := gotStatus, http.StatusNoContent; got != want {
t.Fatalf("wrong response status: got %d, want %d", got, want)
}
if got, want := gotHeader.Get("x-thingy"), "boop"; got != want {
t.Fatalf("wrong value for x-thingy field: got %q, want %q", got, want)
}
}
================================================
FILE: request_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
retryablehttp "github.com/hashicorp/go-retryablehttp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type fixtureBody struct {
ID string `json:"id"`
Name string `json:"name"`
Method string `json:"method"`
}
func newTestRequest(r *retryablehttp.Request) ClientRequest {
header := make(http.Header)
header.Add("TestHeader", "test-header-value")
return ClientRequest{
retryableRequest: r,
http: retryablehttp.NewClient(),
Header: header,
}
}
func TestClientRequest_DoJSON(t *testing.T) {
t.Parallel()
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fakeBody := map[string]any{
"id": "example",
"name": "fixture",
"method": r.Method,
}
fakeBodyRaw, err := json.Marshal(fakeBody)
require.NoError(t, err)
if strings.HasSuffix(r.URL.String(), "/ok_request") {
w.Header().Set("content-type", "application/json")
w.Header().Set("content-length", strconv.FormatInt(int64(len(fakeBodyRaw)), 10))
w.WriteHeader(http.StatusOK)
_, err = w.Write(fakeBodyRaw)
require.NoError(t, err)
} else if strings.HasSuffix(r.URL.String(), "/bad_request") {
w.WriteHeader(http.StatusBadRequest)
} else if strings.HasSuffix(r.URL.String(), "/created_request") {
w.WriteHeader(http.StatusCreated)
} else if strings.HasSuffix(r.URL.String(), "/not_modified_request") {
w.WriteHeader(http.StatusNotModified)
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
t.Cleanup(func() {
testServer.Close()
})
t.Run("Success 200 responses", func(t *testing.T) {
r, err := retryablehttp.NewRequest("PUT", fmt.Sprintf("%s/ok_request", testServer.URL), nil)
require.NoError(t, err)
ctx := context.Background()
request := newTestRequest(r)
putResponseBody := &fixtureBody{}
err = request.DoJSON(ctx, putResponseBody)
require.NoError(t, err)
assert.Equal(t, "example", putResponseBody.ID)
assert.Equal(t, "fixture", putResponseBody.Name)
assert.Equal(t, "PUT", putResponseBody.Method)
})
t.Run("Success response with no body", func(t *testing.T) {
r, err := retryablehttp.NewRequest("POST", fmt.Sprintf("%s/created_request", testServer.URL), nil)
require.NoError(t, err)
ctx := context.Background()
request := newTestRequest(r)
err = request.DoJSON(ctx, nil)
require.NoError(t, err)
})
t.Run("Not Modified response", func(t *testing.T) {
r, err := retryablehttp.NewRequest("POST", fmt.Sprintf("%s/not_modified_request", testServer.URL), nil)
require.NoError(t, err)
ctx := context.Background()
request := newTestRequest(r)
postResponseBody := &fixtureBody{}
err = request.DoJSON(ctx, postResponseBody)
require.NoError(t, err)
assert.Empty(t, postResponseBody.Method)
assert.Empty(t, postResponseBody.ID)
})
t.Run("Bad 400 responses", func(t *testing.T) {
r, err := retryablehttp.NewRequest("POST", fmt.Sprintf("%s/bad_request", testServer.URL), nil)
require.NoError(t, err)
ctx := context.Background()
request := newTestRequest(r)
postResponseBody := &fixtureBody{}
err = request.DoJSON(ctx, postResponseBody)
// body is empty (no response)
assert.Empty(t, postResponseBody.Method)
assert.Empty(t, postResponseBody.ID)
assert.EqualError(t, err, "error HTTP response: 400")
})
}
================================================
FILE: reserved_tag_key.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ ReservedTagKeys = (*reservedTagKeys)(nil)
// ReservedTagKeys describes all the reserved tag key endpoints that the
// Terraform Enterprise API supports.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/reserved-tag-keys
type ReservedTagKeys interface {
// List all the reserved tag keys for the given organization.
List(ctx context.Context, organization string, options *ReservedTagKeyListOptions) (*ReservedTagKeyList, error)
// Create a new reserved tag key for the given organization.
Create(ctx context.Context, organization string, options ReservedTagKeyCreateOptions) (*ReservedTagKey, error)
// Update the reserved tag key with the given ID.
Update(ctx context.Context, reservedTagKeyID string, options ReservedTagKeyUpdateOptions) (*ReservedTagKey, error)
// Delete the reserved tag key with the given ID.
Delete(ctx context.Context, reservedTagKeyID string) error
}
// reservedTagKeys implements ReservedTagKeys.
type reservedTagKeys struct {
client *Client
}
// ReservedTagKeyList represents a list of reserved tag keys.
type ReservedTagKeyList struct {
*Pagination
Items []*ReservedTagKey
}
// ReservedTagKey represents a Terraform Enterprise reserved tag key.
type ReservedTagKey struct {
ID string `jsonapi:"primary,reserved-tag-keys"`
Key string `jsonapi:"attr,key"`
DisableOverrides bool `jsonapi:"attr,disable-overrides"`
CreatedAt time.Time `jsonapi:"attr,created_at,iso8601"`
UpdatedAt time.Time `jsonapi:"attr,updated_at,iso8601"`
}
// ReservedTagKeyListOptions represents the options for listing reserved tag
// keys.
type ReservedTagKeyListOptions struct {
ListOptions
}
// List all the reserved tag keys for the given organization.
func (s *reservedTagKeys) List(ctx context.Context, organization string, options *ReservedTagKeyListOptions) (*ReservedTagKeyList, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf("organizations/%s/reserved-tag-keys", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
rl := &ReservedTagKeyList{}
err = req.Do(ctx, rl)
if err != nil {
return nil, err
}
return rl, nil
}
// ReservedTagKeyCreateOptions represents the options for creating a
// reserved tag key.
type ReservedTagKeyCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,reserved-tag-keys"`
// Required: The reserved tag key's key string.
Key string `jsonapi:"attr,key"`
// Optional: When true, project tag bindings that match this reserved tag key can not
// be overridden at the workspace level.
DisableOverrides *bool `jsonapi:"attr,disable-overrides,omitempty"`
}
func (o ReservedTagKeyCreateOptions) valid() error {
if !validString(&o.Key) {
return ErrRequiredKey
}
return nil
}
// Create a reserved tag key.
func (s *reservedTagKeys) Create(ctx context.Context, organization string, options ReservedTagKeyCreateOptions) (*ReservedTagKey, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("organizations/%s/reserved-tag-keys", url.PathEscape(organization))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
r := &ReservedTagKey{}
err = req.Do(ctx, r)
if err != nil {
return nil, err
}
return r, nil
}
// ReservedTagKeyUpdateOptions represents the options for updating a
// reserved tag key.
type ReservedTagKeyUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,reserved-tag-keys"`
// Optional: The reserved tag key's key string.
Key *string `jsonapi:"attr,key,omitempty"`
// Optional: When true, project tag bindings that match this reserved tag key can not
// be overridden at the workspace level.
DisableOverrides *bool `jsonapi:"attr,disable-overrides,omitempty"`
}
// Update the reserved tag key with the given ID.
func (s *reservedTagKeys) Update(ctx context.Context, reservedTagKeyID string, options ReservedTagKeyUpdateOptions) (*ReservedTagKey, error) {
if !validStringID(&reservedTagKeyID) {
return nil, ErrInvalidReservedTagKeyID
}
u := fmt.Sprintf("reserved-tag-keys/%s", url.PathEscape(reservedTagKeyID))
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
r := &ReservedTagKey{}
err = req.Do(ctx, r)
if err != nil {
return nil, err
}
return r, nil
}
// Delete the reserved tag key with the given ID.
func (s *reservedTagKeys) Delete(ctx context.Context, reservedTagKeyID string) error {
if !validStringID(&reservedTagKeyID) {
return ErrInvalidReservedTagKeyID
}
u := fmt.Sprintf("reserved-tag-keys/%s", url.PathEscape(reservedTagKeyID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
================================================
FILE: reserved_tag_key_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestReservedTagKeysList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
rtkTest1, rtkTestCleanup := createReservedTagKey(t, client, orgTest,
ReservedTagKeyCreateOptions{
Key: randomString(t),
})
defer rtkTestCleanup()
rtkTest2, rtkTestCleanup := createReservedTagKey(t, client, orgTest,
ReservedTagKeyCreateOptions{
Key: randomString(t),
})
defer rtkTestCleanup()
rtks, err := client.ReservedTagKeys.List(ctx, orgTest.Name, nil)
require.NoError(t, err)
require.Len(t, rtks.Items, 2)
t.Run("without list options", func(t *testing.T) {
pl, err := client.ReservedTagKeys.List(ctx, orgTest.Name, nil)
require.NoError(t, err)
assert.Contains(t, pl.Items, rtkTest1)
assert.Equal(t, 1, pl.CurrentPage)
assert.Equal(t, 2, pl.TotalCount)
})
t.Run("with pagination list options", func(t *testing.T) {
rtks, err := client.ReservedTagKeys.List(ctx, orgTest.Name, &ReservedTagKeyListOptions{
ListOptions: ListOptions{
PageNumber: 1,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Contains(t, rtks.Items, rtkTest1)
assert.Contains(t, rtks.Items, rtkTest2)
assert.Equal(t, 2, len(rtks.Items))
})
t.Run("without a valid organization", func(t *testing.T) {
pl, err := client.ReservedTagKeys.List(ctx, badIdentifier, nil)
assert.Nil(t, pl)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
func TestReservedTagKeysCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t)
t.Run("with valid options", func(t *testing.T) {
options := ReservedTagKeyCreateOptions{
Key: randomString(t),
DisableOverrides: Bool(true),
}
rtk, err := client.ReservedTagKeys.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
rtks, err := client.ReservedTagKeys.List(ctx, orgTest.Name, nil)
require.NoError(t, err)
require.Len(t, rtks.Items, 1)
assert.NotEmpty(t, rtk.ID)
assert.Equal(t, options.Key, rtk.Key)
assert.Equal(t, *options.DisableOverrides, rtk.DisableOverrides)
})
t.Run("when key has already been taken", func(t *testing.T) {
rtkExisting, rtkTestCleanup := createReservedTagKey(t, client, orgTest, ReservedTagKeyCreateOptions{
Key: randomString(t),
DisableOverrides: Bool(true),
})
t.Cleanup(rtkTestCleanup)
rtks, err := client.ReservedTagKeys.List(ctx, orgTest.Name, nil)
assert.NoError(t, err)
assert.Len(t, rtks.Items, 2)
rtk, err := client.ReservedTagKeys.Create(ctx, orgTest.Name, ReservedTagKeyCreateOptions{
Key: rtkExisting.Key,
})
assert.Nil(t, rtk)
assert.Contains(t, err.Error(), "invalid attribute\n\nKey has already been taken")
})
t.Run("when options is missing key", func(t *testing.T) {
w, err := client.ReservedTagKeys.Create(ctx, orgTest.Name, ReservedTagKeyCreateOptions{
DisableOverrides: Bool(true),
})
assert.Nil(t, w)
assert.EqualError(t, err, "key is required")
})
t.Run("when options has an invalid key", func(t *testing.T) {
rtk, err := client.ReservedTagKeys.Create(ctx, orgTest.Name, ReservedTagKeyCreateOptions{
Key: badIdentifier,
})
assert.Nil(t, rtk)
assert.Contains(t, err.Error(), "invalid attribute\n\nKey is invalid")
})
t.Run("when options has an invalid organization", func(t *testing.T) {
rtk, err := client.ReservedTagKeys.Create(ctx, badIdentifier, ReservedTagKeyCreateOptions{
Key: randomString(t),
})
assert.Nil(t, rtk)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("when organization does not exist", func(t *testing.T) {
rtk, err := client.ReservedTagKeys.Create(ctx, "nonexistent", ReservedTagKeyCreateOptions{
Key: randomString(t),
})
assert.Nil(t, rtk)
assert.EqualError(t, err, ErrResourceNotFound.Error())
})
}
func TestReservedTagKeysUpdate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
rtkExisting, rtkTestCleanup := createReservedTagKey(t, client, orgTest, ReservedTagKeyCreateOptions{
Key: randomString(t),
DisableOverrides: Bool(true),
})
t.Cleanup(rtkTestCleanup)
rtks, err := client.ReservedTagKeys.List(ctx, orgTest.Name, nil)
require.NoError(t, err)
require.Len(t, rtks.Items, 1)
t.Run("with valid options", func(t *testing.T) {
rtkAfter, err := client.ReservedTagKeys.Update(ctx, rtkExisting.ID, ReservedTagKeyUpdateOptions{
Key: String(randomString(t)),
DisableOverrides: Bool(false),
})
require.NoError(t, err)
assert.Equal(t, rtkExisting.ID, rtkAfter.ID)
assert.NotEqual(t, rtkExisting.Key, rtkAfter.Key)
assert.NotEqual(t, rtkExisting.DisableOverrides, rtkAfter.DisableOverrides)
})
t.Run("when updating with invalid key", func(t *testing.T) {
rtkAfter, err := client.ReservedTagKeys.Update(ctx, rtkExisting.ID, ReservedTagKeyUpdateOptions{
Key: String(badIdentifier),
})
assert.Error(t, err)
assert.Nil(t, rtkAfter)
assert.Contains(t, err.Error(), "invalid attribute\n\nKey is invalid")
})
t.Run("when key has already been taken", func(t *testing.T) {
rtkOther, rtkTestCleanup := createReservedTagKey(t, client, orgTest, ReservedTagKeyCreateOptions{
Key: randomString(t),
DisableOverrides: Bool(true),
})
t.Cleanup(rtkTestCleanup)
rtks, err := client.ReservedTagKeys.List(ctx, orgTest.Name, nil)
assert.NoError(t, err)
assert.Len(t, rtks.Items, 2)
rtkAfter, err := client.ReservedTagKeys.Update(ctx, rtkExisting.ID, ReservedTagKeyUpdateOptions{
Key: String(rtkOther.Key),
})
require.Error(t, err)
assert.Nil(t, rtkAfter)
assert.Contains(t, err.Error(), "invalid attribute\n\nKey has already been taken")
})
t.Run("without a valid reserved tag key ID", func(t *testing.T) {
rtkAfter, err := client.ReservedTagKeys.Update(ctx, badIdentifier, ReservedTagKeyUpdateOptions{
Key: String(randomString(t)),
})
assert.Nil(t, rtkAfter)
assert.EqualError(t, err, ErrInvalidReservedTagKeyID.Error())
})
t.Run("when the reserved tag key does not exist", func(t *testing.T) {
rtkAfter, err := client.ReservedTagKeys.Update(ctx, "nonexistent", ReservedTagKeyUpdateOptions{
Key: String(randomString(t)),
})
assert.Nil(t, rtkAfter)
assert.EqualError(t, err, ErrResourceNotFound.Error())
})
}
func TestReservedTagKeysDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
rtkBefore, rtkTestCleanup := createReservedTagKey(t, client, orgTest, ReservedTagKeyCreateOptions{
Key: randomString(t),
DisableOverrides: Bool(true),
})
t.Cleanup(rtkTestCleanup)
rtks, err := client.ReservedTagKeys.List(ctx, orgTest.Name, nil)
assert.NoError(t, err)
assert.Len(t, rtks.Items, 1)
t.Run("when the request is valid", func(t *testing.T) {
err := client.ReservedTagKeys.Delete(ctx, rtkBefore.ID)
require.NoError(t, err)
rtks, err = client.ReservedTagKeys.List(ctx, orgTest.Name, nil)
assert.NoError(t, err)
assert.Len(t, rtks.Items, 0)
})
t.Run("when the reserved tag key does not exist", func(t *testing.T) {
err := client.ReservedTagKeys.Delete(ctx, "nonexistent")
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the reserved tag key ID is invalid", func(t *testing.T) {
err := client.ReservedTagKeys.Delete(ctx, badIdentifier)
assert.EqualError(t, err, ErrInvalidReservedTagKeyID.Error())
})
}
func createReservedTagKey(t *testing.T, client *Client, org *Organization, opts ReservedTagKeyCreateOptions) (*ReservedTagKey, func()) {
t.Helper()
rtk, err := client.ReservedTagKeys.Create(context.Background(), org.Name, opts)
require.NoError(t, err)
cleanup := func() {
err := client.ReservedTagKeys.Delete(context.Background(), rtk.ID)
if err != nil && err == ErrResourceNotFound {
// It's already been deleted
return
}
require.NoError(t, err)
}
return rtk, cleanup
}
================================================
FILE: run.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ Runs = (*runs)(nil)
// Runs describes all the run related methods that the Terraform Enterprise
// API supports.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run
type Runs interface {
// List all the runs of the given workspace.
List(ctx context.Context, workspaceID string, options *RunListOptions) (*RunList, error)
// List all the runs of the given organization.
ListForOrganization(ctx context.Context, organization string, options *RunListForOrganizationOptions) (*OrganizationRunList, error)
// Create a new run with the given options.
Create(ctx context.Context, options RunCreateOptions) (*Run, error)
// Read a run by its ID.
Read(ctx context.Context, runID string) (*Run, error)
// ReadWithOptions reads a run by its ID using the options supplied
ReadWithOptions(ctx context.Context, runID string, options *RunReadOptions) (*Run, error)
// Apply a run by its ID.
Apply(ctx context.Context, runID string, options RunApplyOptions) error
// Cancel a run by its ID.
Cancel(ctx context.Context, runID string, options RunCancelOptions) error
// Force-cancel a run by its ID.
ForceCancel(ctx context.Context, runID string, options RunForceCancelOptions) error
// Force execute a run by its ID.
ForceExecute(ctx context.Context, runID string) error
// Discard a run by its ID.
Discard(ctx context.Context, runID string, options RunDiscardOptions) error
}
// runs implements Runs.
type runs struct {
client *Client
}
// RunStatus represents a run state.
type RunStatus string
// List all available run statuses.
const (
RunApplied RunStatus = "applied"
RunApplying RunStatus = "applying"
RunApplyQueued RunStatus = "apply_queued"
RunCanceled RunStatus = "canceled"
RunConfirmed RunStatus = "confirmed"
RunCostEstimated RunStatus = "cost_estimated"
RunCostEstimating RunStatus = "cost_estimating"
RunDiscarded RunStatus = "discarded"
RunErrored RunStatus = "errored"
RunFetching RunStatus = "fetching"
RunFetchingCompleted RunStatus = "fetching_completed"
RunPending RunStatus = "pending"
RunPlanned RunStatus = "planned"
RunPlannedAndFinished RunStatus = "planned_and_finished"
RunPlannedAndSaved RunStatus = "planned_and_saved"
RunPlanning RunStatus = "planning"
RunPlanQueued RunStatus = "plan_queued"
RunPolicyChecked RunStatus = "policy_checked"
RunPolicyChecking RunStatus = "policy_checking"
RunPolicyOverride RunStatus = "policy_override"
RunPolicySoftFailed RunStatus = "policy_soft_failed"
RunPostPlanAwaitingDecision RunStatus = "post_plan_awaiting_decision"
RunPostPlanCompleted RunStatus = "post_plan_completed"
RunPostPlanRunning RunStatus = "post_plan_running"
RunPreApplyRunning RunStatus = "pre_apply_running"
RunPreApplyCompleted RunStatus = "pre_apply_completed"
RunPrePlanCompleted RunStatus = "pre_plan_completed"
RunPrePlanRunning RunStatus = "pre_plan_running"
RunQueuing RunStatus = "queuing"
RunQueuingApply RunStatus = "queuing_apply"
)
// RunSource represents a source type of a run.
type RunSource string
// List all available run sources.
const (
RunSourceAPI RunSource = "tfe-api"
RunSourceConfigurationVersion RunSource = "tfe-configuration-version"
RunSourceUI RunSource = "tfe-ui"
)
// RunOperation represents an operation type of run.
type RunOperation string
// List all available run operations.
const (
RunOperationPlanApply RunOperation = "plan_and_apply"
RunOperationPlanOnly RunOperation = "plan_only"
RunOperationRefreshOnly RunOperation = "refresh_only"
RunOperationDestroy RunOperation = "destroy"
RunOperationEmptyApply RunOperation = "empty_apply"
RunOperationSavePlan RunOperation = "save_plan"
)
// RunList represents a list of runs.
type RunList struct {
*Pagination
Items []*Run
}
// OrganizationRunList represents a list of runs across an organization. It
// differs from the RunList in that it does not include a TotalCount of records
// in the pagination details
type OrganizationRunList struct {
*PaginationNextPrev
Items []*Run
}
// Run represents a Terraform Enterprise run.
type Run struct {
ID string `jsonapi:"primary,runs"`
Actions *RunActions `jsonapi:"attr,actions"`
AutoApply bool `jsonapi:"attr,auto-apply,omitempty"`
AllowConfigGeneration *bool `jsonapi:"attr,allow-config-generation,omitempty"`
AllowEmptyApply bool `jsonapi:"attr,allow-empty-apply"`
CanceledAt time.Time `jsonapi:"attr,canceled-at,iso8601"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
ForceCancelAvailableAt time.Time `jsonapi:"attr,force-cancel-available-at,iso8601"`
HasChanges bool `jsonapi:"attr,has-changes"`
IsDestroy bool `jsonapi:"attr,is-destroy"`
InvokeActionAddrs []string `jsonapi:"attr,invoke-action-addrs,omitempty"`
Message string `jsonapi:"attr,message"`
Permissions *RunPermissions `jsonapi:"attr,permissions"`
PolicyPaths []string `jsonapi:"attr,policy-paths,omitempty"`
PositionInQueue int `jsonapi:"attr,position-in-queue"`
PlanOnly bool `jsonapi:"attr,plan-only"`
Refresh bool `jsonapi:"attr,refresh"`
RefreshOnly bool `jsonapi:"attr,refresh-only"`
ReplaceAddrs []string `jsonapi:"attr,replace-addrs,omitempty"`
SavePlan bool `jsonapi:"attr,save-plan,omitempty"`
Source RunSource `jsonapi:"attr,source"`
Status RunStatus `jsonapi:"attr,status"`
StatusTimestamps *RunStatusTimestamps `jsonapi:"attr,status-timestamps"`
TargetAddrs []string `jsonapi:"attr,target-addrs,omitempty"`
TerraformVersion string `jsonapi:"attr,terraform-version"`
TriggerReason string `jsonapi:"attr,trigger-reason"`
Variables []*RunVariableAttr `jsonapi:"attr,variables"`
// Relations
Apply *Apply `jsonapi:"relation,apply"`
ConfigurationVersion *ConfigurationVersion `jsonapi:"relation,configuration-version"`
CostEstimate *CostEstimate `jsonapi:"relation,cost-estimate"`
CreatedBy *User `jsonapi:"relation,created-by"`
ConfirmedBy *User `jsonapi:"relation,confirmed-by"`
Plan *Plan `jsonapi:"relation,plan"`
PolicyChecks []*PolicyCheck `jsonapi:"relation,policy-checks"`
RunEvents []*RunEvent `jsonapi:"relation,run-events"`
TaskStages []*TaskStage `jsonapi:"relation,task-stages,omitempty"`
Workspace *Workspace `jsonapi:"relation,workspace"`
Comments []*Comment `jsonapi:"relation,comments"`
}
// RunActions represents the run actions.
type RunActions struct {
IsCancelable bool `jsonapi:"attr,is-cancelable"`
IsConfirmable bool `jsonapi:"attr,is-confirmable"`
IsDiscardable bool `jsonapi:"attr,is-discardable"`
IsForceCancelable bool `jsonapi:"attr,is-force-cancelable"`
}
// RunPermissions represents the run permissions.
type RunPermissions struct {
CanApply bool `jsonapi:"attr,can-apply"`
CanCancel bool `jsonapi:"attr,can-cancel"`
CanDiscard bool `jsonapi:"attr,can-discard"`
CanForceCancel bool `jsonapi:"attr,can-force-cancel"`
CanForceExecute bool `jsonapi:"attr,can-force-execute"`
}
// RunStatusTimestamps holds the timestamps for individual run statuses.
type RunStatusTimestamps struct {
AppliedAt time.Time `jsonapi:"attr,applied-at,rfc3339"`
ApplyingAt time.Time `jsonapi:"attr,applying-at,rfc3339"`
ApplyQueuedAt time.Time `jsonapi:"attr,apply-queued-at,rfc3339"`
CanceledAt time.Time `jsonapi:"attr,canceled-at,rfc3339"`
ConfirmedAt time.Time `jsonapi:"attr,confirmed-at,rfc3339"`
CostEstimatedAt time.Time `jsonapi:"attr,cost-estimated-at,rfc3339"`
CostEstimatingAt time.Time `jsonapi:"attr,cost-estimating-at,rfc3339"`
DiscardedAt time.Time `jsonapi:"attr,discarded-at,rfc3339"`
ErroredAt time.Time `jsonapi:"attr,errored-at,rfc3339"`
FetchedAt time.Time `jsonapi:"attr,fetched-at,rfc3339"`
FetchingAt time.Time `jsonapi:"attr,fetching-at,rfc3339"`
ForceCanceledAt time.Time `jsonapi:"attr,force-canceled-at,rfc3339"`
PlannedAndFinishedAt time.Time `jsonapi:"attr,planned-and-finished-at,rfc3339"`
PlannedAndSavedAt time.Time `jsonapi:"attr,planned-and-saved-at,rfc3339"`
PlannedAt time.Time `jsonapi:"attr,planned-at,rfc3339"`
PlanningAt time.Time `jsonapi:"attr,planning-at,rfc3339"`
PlanQueueableAt time.Time `jsonapi:"attr,plan-queueable-at,rfc3339"`
PlanQueuedAt time.Time `jsonapi:"attr,plan-queued-at,rfc3339"`
PolicyCheckedAt time.Time `jsonapi:"attr,policy-checked-at,rfc3339"`
PolicySoftFailedAt time.Time `jsonapi:"attr,policy-soft-failed-at,rfc3339"`
PostPlanCompletedAt time.Time `jsonapi:"attr,post-plan-completed-at,rfc3339"`
PostPlanRunningAt time.Time `jsonapi:"attr,post-plan-running-at,rfc3339"`
PrePlanCompletedAt time.Time `jsonapi:"attr,pre-plan-completed-at,rfc3339"`
PrePlanRunningAt time.Time `jsonapi:"attr,pre-plan-running-at,rfc3339"`
QueuingAt time.Time `jsonapi:"attr,queuing-at,rfc3339"`
}
// RunIncludeOpt represents the available options for include query params.
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#available-related-resources
type RunIncludeOpt string
const (
RunPlan RunIncludeOpt = "plan"
RunApply RunIncludeOpt = "apply"
RunCreatedBy RunIncludeOpt = "created_by"
RunCostEstimate RunIncludeOpt = "cost_estimate"
RunConfigVer RunIncludeOpt = "configuration_version"
RunConfigVerIngress RunIncludeOpt = "configuration_version.ingress_attributes"
RunWorkspace RunIncludeOpt = "workspace"
RunTaskStages RunIncludeOpt = "task_stages"
)
// RunListOptions represents the options for listing runs.
type RunListOptions struct {
ListOptions
// Optional: Searches runs that matches the supplied VCS username.
User string `url:"search[user],omitempty"`
// Optional: Searches runs that matches the supplied commit sha.
Commit string `url:"search[commit],omitempty"`
// Optional: Searches runs that matches the supplied VCS username, commit sha, run_id, and run message.
// The presence of search[commit] or search[user] takes priority over this parameter and will be omitted.
Search string `url:"search[basic],omitempty"`
// Optional: Comma-separated list of acceptable run statuses.
// Options are listed at https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#run-states,
// or as constants with the RunStatus string type.
Status string `url:"filter[status],omitempty"`
// Optional: Comma-separated list of acceptable run sources.
// Options are listed at https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#run-sources,
// or as constants with the RunSource string type.
Source string `url:"filter[source],omitempty"`
// Optional: Comma-separated list of acceptable run operation types.
// Options are listed at https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#run-operations,
// or as constants with the RunOperation string type.
Operation string `url:"filter[operation],omitempty"`
// Optional: A list of relations to include. See available resources:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#available-related-resources
Include []RunIncludeOpt `url:"include,omitempty"`
}
// RunListForOrganizationOptions represents the options for listing runs for an organization.
type RunListForOrganizationOptions struct {
ListOptions
// Optional: Searches runs that matches the supplied VCS username.
User string `url:"search[user],omitempty"`
// Optional: Searches runs that matches the supplied commit sha.
Commit string `url:"search[commit],omitempty"`
// Optional: Searches for runs that match the VCS username, commit sha, run_id, or run message your specify.
// The presence of search[commit] or search[user] takes priority over this parameter and will be omitted.
Basic string `url:"search[basic],omitempty"`
// Optional: Comma-separated list of acceptable run statuses.
// Options are listed at https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#run-states,
// or as constants with the RunStatus string type.
Status string `url:"filter[status],omitempty"`
// Optional: Comma-separated list of acceptable run sources.
// Options are listed at https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#run-sources,
// or as constants with the RunSource string type.
Source string `url:"filter[source],omitempty"`
// Optional: Comma-separated list of acceptable run operation types.
// Options are listed at https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#run-operations,
// or as constants with the RunOperation string type.
Operation string `url:"filter[operation],omitempty"`
// Optional: Comma-separated list of agent pool names.
AgentPoolNames string `url:"filter[agent_pool_names],omitempty"`
// Optional: Comma-separated list of run status groups.
StatusGroup string `url:"filter[status_group],omitempty"`
// Optional: Comma-separated list of run timeframe.
Timeframe string `url:"filter[timeframe],omitempty"`
// Optional: Comma-separated list of workspace names. The result lists runs that belong to one of the workspaces your specify.
WorkspaceNames string `url:"filter[workspace_names],omitempty"`
// Optional: A list of relations to include. See available resources:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#available-related-resources
Include []RunIncludeOpt `url:"include,omitempty"`
}
// RunReadOptions represents the options for reading a run.
type RunReadOptions struct {
// Optional: A list of relations to include. See available resources:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#available-related-resources
Include []RunIncludeOpt `url:"include,omitempty"`
}
// RunCreateOptions represents the options for creating a new run.
type RunCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,runs"`
// AllowConfigGeneration specifies whether generated resource configuration may be created as a side
// effect of an import block in this run. Setting this does not mean that configuration _will_ be generated,
// only that it can be.
AllowConfigGeneration *bool `jsonapi:"attr,allow-config-generation,omitempty"`
// AllowEmptyApply specifies whether Terraform can apply the run even when the plan contains no changes.
// Often used to upgrade state after upgrading a workspace to a new terraform version.
AllowEmptyApply *bool `jsonapi:"attr,allow-empty-apply,omitempty"`
// TerraformVersion specifies the Terraform version to use in this run.
// Only valid for plan-only runs; must be a valid Terraform version available to the organization.
TerraformVersion *string `jsonapi:"attr,terraform-version,omitempty"`
// PlanOnly specifies if this is a speculative, plan-only run that Terraform cannot apply.
// Often used in conjunction with terraform-version in order to test whether an upgrade would succeed.
PlanOnly *bool `jsonapi:"attr,plan-only,omitempty"`
// Specifies if this plan is a destroy plan, which will destroy all
// provisioned resources.
IsDestroy *bool `jsonapi:"attr,is-destroy,omitempty"`
// Refresh determines if the run should
// update the state prior to checking for differences
Refresh *bool `jsonapi:"attr,refresh,omitempty"`
// RefreshOnly determines whether the run should ignore config changes
// and refresh the state only
RefreshOnly *bool `jsonapi:"attr,refresh-only,omitempty"`
// SavePlan determines whether this should be a saved-plan run. Saved-plan
// runs perform their plan and checks immediately, but won't lock the
// workspace and become its current run until they are confirmed for apply.
SavePlan *bool `jsonapi:"attr,save-plan,omitempty"`
// Specifies the message to be associated with this run.
Message *string `jsonapi:"attr,message,omitempty"`
// Specifies the configuration version to use for this run. If the
// configuration version object is omitted, the run will be created using the
// workspace's latest configuration version.
ConfigurationVersion *ConfigurationVersion `jsonapi:"relation,configuration-version"`
// Specifies the workspace where the run will be executed.
Workspace *Workspace `jsonapi:"relation,workspace"`
// If non-empty, requests that Terraform should create a plan including
// actions only for the given objects (specified using resource address
// syntax) and the objects they depend on.
//
// This capability is provided for exceptional circumstances only, such as
// recovering from mistakes or working around existing Terraform
// limitations. Terraform will generally mention the -target command line
// option in its error messages describing situations where setting this
// argument may be appropriate. This argument should not be used as part
// of routine workflow and Terraform will emit warnings reminding about
// this whenever this property is set.
TargetAddrs []string `jsonapi:"attr,target-addrs,omitempty"`
// If non-empty, requests that Terraform create a plan that replaces
// (destroys and then re-creates) the objects specified by the given
// resource addresses.
ReplaceAddrs []string `jsonapi:"attr,replace-addrs,omitempty"`
// PolicyPaths is a list of relative directory paths that point to policy
// configuration files.
//
// **Note: This field is in BETA and subject to change.**
PolicyPaths []string `jsonapi:"attr,policy-paths,omitempty"`
// AutoApply determines if the run should be applied automatically without
// user confirmation. It defaults to the Workspace.AutoApply setting.
AutoApply *bool `jsonapi:"attr,auto-apply,omitempty"`
// Variables allows you to specify terraform input variables for
// a particular run, prioritized over variables defined on the workspace.
Variables []*RunVariable `jsonapi:"attr,variables,omitempty"`
// Action Addresses to invoke.
InvokeActionAddrs []string `jsonapi:"attr,invoke-action-addrs,omitempty"`
}
// RunApplyOptions represents the options for applying a run.
type RunApplyOptions struct {
// An optional comment about the run.
Comment *string `json:"comment,omitempty"`
}
// RunCancelOptions represents the options for canceling a run.
type RunCancelOptions struct {
// An optional explanation for why the run was canceled.
Comment *string `json:"comment,omitempty"`
}
type RunVariableAttr struct {
Key string `jsonapi:"attr,key"`
Value string `jsonapi:"attr,value"`
}
// RunVariableAttr represents a variable that can be applied to a run. All values must be expressed as an HCL literal
// in the same syntax you would use when writing terraform code. See https://developer.hashicorp.com/terraform/language/expressions/types#types
// for more details.
type RunVariable struct {
Key string `json:"key"`
Value string `json:"value"`
}
// RunForceCancelOptions represents the options for force-canceling a run.
type RunForceCancelOptions struct {
// An optional comment explaining the reason for the force-cancel.
Comment *string `json:"comment,omitempty"`
}
// RunDiscardOptions represents the options for discarding a run.
type RunDiscardOptions struct {
// An optional explanation for why the run was discarded.
Comment *string `json:"comment,omitempty"`
}
// List all the runs of the given workspace.
func (s *runs) List(ctx context.Context, workspaceID string, options *RunListOptions) (*RunList, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("workspaces/%s/runs", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
rl := &RunList{}
err = req.Do(ctx, rl)
if err != nil {
return nil, err
}
return rl, nil
}
// List all the runs of the given workspace.
func (s *runs) ListForOrganization(ctx context.Context, organization string, options *RunListForOrganizationOptions) (*OrganizationRunList, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("organizations/%s/runs", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
rl := &OrganizationRunList{}
err = req.Do(ctx, rl)
if err != nil {
return nil, err
}
return rl, nil
}
// Create a new run with the given options.
func (s *runs) Create(ctx context.Context, options RunCreateOptions) (*Run, error) {
if err := options.valid(); err != nil {
return nil, err
}
req, err := s.client.NewRequest("POST", "runs", &options)
if err != nil {
return nil, err
}
r := &Run{}
err = req.Do(ctx, r)
if err != nil {
return nil, err
}
return r, nil
}
// Read a run by its ID.
func (s *runs) Read(ctx context.Context, runID string) (*Run, error) {
return s.ReadWithOptions(ctx, runID, nil)
}
// Read a run by its ID with the given options.
func (s *runs) ReadWithOptions(ctx context.Context, runID string, options *RunReadOptions) (*Run, error) {
if !validStringID(&runID) {
return nil, ErrInvalidRunID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("runs/%s", url.PathEscape(runID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
r := &Run{}
err = req.Do(ctx, r)
if err != nil {
return nil, err
}
return r, nil
}
// Apply a run by its ID.
func (s *runs) Apply(ctx context.Context, runID string, options RunApplyOptions) error {
if !validStringID(&runID) {
return ErrInvalidRunID
}
u := fmt.Sprintf("runs/%s/actions/apply", url.PathEscape(runID))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// Cancel a run by its ID.
func (s *runs) Cancel(ctx context.Context, runID string, options RunCancelOptions) error {
if !validStringID(&runID) {
return ErrInvalidRunID
}
u := fmt.Sprintf("runs/%s/actions/cancel", url.PathEscape(runID))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// ForceCancel is used to forcefully cancel a run by its ID.
func (s *runs) ForceCancel(ctx context.Context, runID string, options RunForceCancelOptions) error {
if !validStringID(&runID) {
return ErrInvalidRunID
}
u := fmt.Sprintf("runs/%s/actions/force-cancel", url.PathEscape(runID))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// ForceExecute is used to forcefully execute a run by its ID.
//
// Note: While useful at times, force-executing a run circumvents the typical
// workflow of applying runs using HCP Terraform. It is not intended for
// regular use. If you find yourself using it frequently, please reach out to
// HashiCorp Support for help in developing an alternative approach.
func (s *runs) ForceExecute(ctx context.Context, runID string) error {
if !validStringID(&runID) {
return ErrInvalidRunID
}
u := fmt.Sprintf("runs/%s/actions/force-execute", url.PathEscape(runID))
req, err := s.client.NewRequest("POST", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// Discard a run by its ID.
func (s *runs) Discard(ctx context.Context, runID string, options RunDiscardOptions) error {
if !validStringID(&runID) {
return ErrInvalidRunID
}
u := fmt.Sprintf("runs/%s/actions/discard", url.PathEscape(runID))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o RunCreateOptions) valid() error {
if o.Workspace == nil {
return ErrRequiredWorkspace
}
if validString(o.TerraformVersion) && (o.PlanOnly == nil || !*o.PlanOnly) {
return ErrTerraformVersionValidForPlanOnly
}
return nil
}
func (o *RunReadOptions) valid() error {
return nil
}
func (o *RunListOptions) valid() error {
return nil
}
func (o *RunListForOrganizationOptions) valid() error {
return nil
}
================================================
FILE: run_event.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ RunEvents = (*runEvents)(nil)
// RunEvents describes all the run events that the Terraform Enterprise
// API supports.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run
type RunEvents interface {
// List all the runs events of the given run.
List(ctx context.Context, runID string, options *RunEventListOptions) (*RunEventList, error)
// Read a run event by its ID.
Read(ctx context.Context, runEventID string) (*RunEvent, error)
// ReadWithOptions reads a run event by its ID using the options supplied
ReadWithOptions(ctx context.Context, runEventID string, options *RunEventReadOptions) (*RunEvent, error)
}
// runEvents implements RunEvents.
type runEvents struct {
client *Client
}
// RunEventList represents a list of run events.
type RunEventList struct {
// Pagination is not supported by the API
*Pagination
Items []*RunEvent
}
// RunEvent represents a Terraform Enterprise run event.
type RunEvent struct {
ID string `jsonapi:"primary,run-events"`
Action string `jsonapi:"attr,action"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Description string `jsonapi:"attr,description"`
// Relations - Note that `target` is not supported yet
Actor *User `jsonapi:"relation,actor"`
Comment *Comment `jsonapi:"relation,comment"`
}
// RunEventIncludeOpt represents the available options for include query params.
type RunEventIncludeOpt string
const (
RunEventComment RunEventIncludeOpt = "comment"
RunEventActor RunEventIncludeOpt = "actor"
)
// RunEventListOptions represents the options for listing run events.
type RunEventListOptions struct {
// Optional: A list of relations to include. See available resources:
Include []RunEventIncludeOpt `url:"include,omitempty"`
}
// RunEventReadOptions represents the options for reading a run event.
type RunEventReadOptions struct {
// Optional: A list of relations to include. See available resources:
Include []RunEventIncludeOpt `url:"include,omitempty"`
}
// List all the run events of the given run.
func (s *runEvents) List(ctx context.Context, runID string, options *RunEventListOptions) (*RunEventList, error) {
if !validStringID(&runID) {
return nil, ErrInvalidRunID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("runs/%s/run-events", url.PathEscape(runID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
rl := &RunEventList{}
err = req.Do(ctx, rl)
if err != nil {
return nil, err
}
return rl, nil
}
// Read a run by its ID.
func (s *runEvents) Read(ctx context.Context, runEventID string) (*RunEvent, error) {
return s.ReadWithOptions(ctx, runEventID, nil)
}
// ReadWithOptions reads a run by its ID with the given options.
func (s *runEvents) ReadWithOptions(ctx context.Context, runEventID string, options *RunEventReadOptions) (*RunEvent, error) {
if !validStringID(&runEventID) {
return nil, ErrInvalidRunEventID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("run-events/%s", url.PathEscape(runEventID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
r := &RunEvent{}
err = req.Do(ctx, r)
if err != nil {
return nil, err
}
return r, nil
}
func (o *RunEventReadOptions) valid() error {
return nil
}
func (o *RunEventListOptions) valid() error {
return nil
}
================================================
FILE: run_event_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRunEventsList_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wTest, _ := createWorkspace(t, client, orgTest)
rTest, _ := createRun(t, client, wTest)
commentText := "Test comment"
_, err := client.Comments.Create(ctx, rTest.ID, CommentCreateOptions{
Body: commentText,
})
require.NoError(t, err)
t.Run("without list options", func(t *testing.T) {
rl, err := client.RunEvents.List(ctx, rTest.ID, nil)
require.NoError(t, err)
require.NotEmpty(t, rl.Items)
// Find the comment that was added
var commentEvent *RunEvent = nil
for _, event := range rl.Items {
if event.Action == "commented" {
commentEvent = event
}
}
assert.NotNil(t, commentEvent)
// We didn't include any resources so these should be empty
assert.Empty(t, commentEvent.Actor.Username)
assert.Empty(t, commentEvent.Comment.Body)
})
t.Run("with all includes", func(t *testing.T) {
rl, err := client.RunEvents.List(ctx, rTest.ID, &RunEventListOptions{
Include: []RunEventIncludeOpt{RunEventActor, RunEventComment},
})
require.NoError(t, err)
// Find the comment that was added
var commentEvent *RunEvent = nil
for _, event := range rl.Items {
if event.Action == "commented" {
commentEvent = event
}
}
require.NotNil(t, commentEvent)
// Assert that the include resources are included
require.NotNil(t, commentEvent.Actor)
assert.NotEmpty(t, commentEvent.Actor.Username)
require.NotNil(t, commentEvent.Comment)
assert.Equal(t, commentEvent.Comment.Body, commentText)
})
t.Run("without a valid run ID", func(t *testing.T) {
rl, err := client.RunEvents.List(ctx, badIdentifier, nil)
assert.Nil(t, rl)
assert.EqualError(t, err, ErrInvalidRunID.Error())
})
}
func TestRunEventsRead_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wTest, _ := createWorkspace(t, client, orgTest)
rTest, _ := createRun(t, client, wTest)
commentText := "Test comment"
_, err := client.Comments.Create(ctx, rTest.ID, CommentCreateOptions{
Body: commentText,
})
require.NoError(t, err)
rl, err := client.RunEvents.List(ctx, rTest.ID, nil)
require.NoError(t, err)
// Find the comment that was added
var commentEvent *RunEvent = nil
for _, event := range rl.Items {
if event.Action == "commented" {
commentEvent = event
}
}
assert.NotNil(t, commentEvent)
t.Run("without read options", func(t *testing.T) {
re, err := client.RunEvents.Read(ctx, commentEvent.ID)
require.NoError(t, err)
// We didn't include any resources so these should be empty
assert.Empty(t, re.Actor.Username)
assert.Empty(t, re.Comment.Body)
})
t.Run("with all includes", func(t *testing.T) {
re, err := client.RunEvents.ReadWithOptions(ctx, commentEvent.ID, &RunEventReadOptions{
Include: []RunEventIncludeOpt{RunEventActor, RunEventComment},
})
require.NoError(t, err)
// Assert that the include resources are included
require.NotNil(t, re.Actor)
assert.NotEmpty(t, re.Actor.Username)
require.NotNil(t, re.Comment)
assert.Equal(t, re.Comment.Body, commentText)
})
t.Run("without a valid run event ID", func(t *testing.T) {
rl, err := client.RunEvents.Read(ctx, badIdentifier)
assert.Nil(t, rl)
assert.EqualError(t, err, ErrInvalidRunEventID.Error())
})
}
================================================
FILE: run_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"encoding/json"
"fmt"
"testing"
"time"
retryablehttp "github.com/hashicorp/go-retryablehttp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRunsList_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wTest, _ := createWorkspace(t, client, orgTest)
rTest1, _ := createRun(t, client, wTest)
rTest2, _ := createRun(t, client, wTest)
t.Run("without list options", func(t *testing.T) {
rl, err := client.Runs.List(ctx, wTest.ID, nil)
require.NoError(t, err)
found := []string{}
for _, r := range rl.Items {
found = append(found, r.ID)
}
assert.Contains(t, found, rTest1.ID)
assert.Contains(t, found, rTest2.ID)
assert.Equal(t, 1, rl.CurrentPage)
assert.Equal(t, 2, rl.TotalCount)
})
t.Run("without list options and include as nil", func(t *testing.T) {
rl, err := client.Runs.List(ctx, wTest.ID, &RunListOptions{
Include: []RunIncludeOpt{},
})
require.NoError(t, err)
require.NotEmpty(t, rl.Items)
found := []string{}
for _, r := range rl.Items {
found = append(found, r.ID)
}
assert.Contains(t, found, rTest1.ID)
assert.Contains(t, found, rTest2.ID)
assert.Equal(t, 1, rl.CurrentPage)
assert.Equal(t, 2, rl.TotalCount)
})
t.Run("with list options", func(t *testing.T) {
t.Skip("paging not supported yet in API")
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
rl, err := client.Runs.List(ctx, wTest.ID, &RunListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, rl.Items)
assert.Equal(t, 999, rl.CurrentPage)
assert.Equal(t, 2, rl.TotalCount)
})
t.Run("with workspace included", func(t *testing.T) {
rl, err := client.Runs.List(ctx, wTest.ID, &RunListOptions{
Include: []RunIncludeOpt{RunWorkspace},
})
require.NoError(t, err)
require.NotEmpty(t, rl.Items)
require.NotNil(t, rl.Items[0].Workspace)
assert.NotEmpty(t, rl.Items[0].Workspace.Name)
})
t.Run("without a valid workspace ID", func(t *testing.T) {
rl, err := client.Runs.List(ctx, badIdentifier, nil)
assert.Nil(t, rl)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
}
func TestRunsListQueryParams_RunDependent(t *testing.T) {
type testCase struct {
options *RunListOptions
description string
assertion func(tc testCase, rl *RunList, err error)
}
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
workspaceTest, _ := createWorkspace(t, client, orgTest)
createPlannedRun(t, client, workspaceTest)
createRun(t, client, workspaceTest)
testCases := []testCase{
{
description: "with status query parameter",
options: &RunListOptions{Status: string(RunPending), Include: []RunIncludeOpt{RunWorkspace}},
assertion: func(tc testCase, rl *RunList, err error) {
require.NoError(t, err)
assert.Equal(t, 1, len(rl.Items))
},
},
{
description: "with source query parameter",
options: &RunListOptions{Source: string(RunSourceAPI), Include: []RunIncludeOpt{RunWorkspace}},
assertion: func(tc testCase, rl *RunList, err error) {
require.NoError(t, err)
assert.Equal(t, 2, len(rl.Items))
assert.Equal(t, rl.Items[0].Source, RunSourceAPI)
},
},
{
description: "with operation of plan_only parameter",
options: &RunListOptions{Operation: string(RunOperationPlanOnly), Include: []RunIncludeOpt{RunWorkspace}},
assertion: func(tc testCase, rl *RunList, err error) {
require.NoError(t, err)
assert.Equal(t, 0, len(rl.Items))
},
},
{
description: "with mismatch user & commit parameter",
options: &RunListOptions{User: randomString(t), Commit: randomString(t), Include: []RunIncludeOpt{RunWorkspace}},
assertion: func(tc testCase, rl *RunList, err error) {
require.NoError(t, err)
assert.Equal(t, 0, len(rl.Items))
},
},
{
description: "with operation of save_plan parameter",
options: &RunListOptions{Operation: string(RunOperationSavePlan), Include: []RunIncludeOpt{RunWorkspace}},
assertion: func(tc testCase, rl *RunList, err error) {
require.NoError(t, err)
assert.Equal(t, 0, len(rl.Items))
},
},
}
betaTestCases := []testCase{}
for _, testCase := range testCases {
t.Run(testCase.description, func(t *testing.T) {
runs, err := client.Runs.List(ctx, workspaceTest.ID, testCase.options)
testCase.assertion(testCase, runs, err)
})
}
for _, testCase := range betaTestCases {
t.Run(testCase.description, func(t *testing.T) {
skipUnlessBeta(t)
runs, err := client.Runs.List(ctx, workspaceTest.ID, testCase.options)
testCase.assertion(testCase, runs, err)
})
}
}
func TestRunsCreate_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
wTest, wTestCleanup := createWorkspace(t, client, nil)
defer wTestCleanup()
cvTest, _ := createUploadedConfigurationVersion(t, client, wTest)
t.Run("without a configuration version", func(t *testing.T) {
options := RunCreateOptions{
Workspace: wTest,
}
r, err := client.Runs.Create(ctx, options)
require.NoError(t, err)
assert.NotNil(t, r.ID)
assert.NotNil(t, r.CreatedAt)
assert.NotNil(t, r.Source)
require.NotNil(t, r.StatusTimestamps)
assert.NotZero(t, r.StatusTimestamps.PlanQueueableAt)
})
t.Run("with a configuration version", func(t *testing.T) {
options := RunCreateOptions{
ConfigurationVersion: cvTest,
Workspace: wTest,
}
r, err := client.Runs.Create(ctx, options)
require.NoError(t, err)
require.NotNil(t, r.ConfigurationVersion)
assert.Equal(t, cvTest.ID, r.ConfigurationVersion.ID)
})
t.Run("with allow empty apply", func(t *testing.T) {
options := RunCreateOptions{
Workspace: wTest,
AllowEmptyApply: Bool(true),
}
r, err := client.Runs.Create(ctx, options)
require.NoError(t, err)
assert.Equal(t, true, r.AllowEmptyApply)
})
t.Run("with save-plan", func(t *testing.T) {
options := RunCreateOptions{
Workspace: wTest,
SavePlan: Bool(true),
}
r, err := client.Runs.Create(ctx, options)
require.NoError(t, err)
assert.Equal(t, true, r.SavePlan)
})
t.Run("with terraform version and plan only", func(t *testing.T) {
options := RunCreateOptions{
Workspace: wTest,
TerraformVersion: String("1.0.0"),
}
_, err := client.Runs.Create(ctx, options)
require.ErrorIs(t, err, ErrTerraformVersionValidForPlanOnly)
options.PlanOnly = Bool(true)
r, err := client.Runs.Create(ctx, options)
require.NoError(t, err)
assert.Equal(t, true, r.PlanOnly)
assert.Equal(t, "1.0.0", r.TerraformVersion)
})
t.Run("refresh defaults to true if not set as a create option", func(t *testing.T) {
options := RunCreateOptions{
Workspace: wTest,
}
r, err := client.Runs.Create(ctx, options)
require.NoError(t, err)
assert.Equal(t, true, r.Refresh)
})
t.Run("with refresh-only requested", func(t *testing.T) {
// TODO: remove this skip after the release of Terraform 0.15.4
t.Skip("Skipping this test until -refresh-only is released in the Terraform CLI")
options := RunCreateOptions{
Workspace: wTest,
RefreshOnly: Bool(true),
}
r, err := client.Runs.Create(ctx, options)
require.NoError(t, err)
assert.Equal(t, true, r.RefreshOnly)
})
t.Run("with auto-apply requested", func(t *testing.T) {
// ensure the worksapce auto-apply is false so it does not default to that.
assert.Equal(t, false, wTest.AutoApply)
options := RunCreateOptions{
Workspace: wTest,
AutoApply: Bool(true),
}
r, err := client.Runs.Create(ctx, options)
require.NoError(t, err)
assert.Equal(t, true, r.AutoApply)
})
t.Run("without auto-apply, defaulting to workspace autoapply", func(t *testing.T) {
options := RunCreateOptions{
Workspace: wTest,
}
r, err := client.Runs.Create(ctx, options)
require.NoError(t, err)
assert.Equal(t, wTest.AutoApply, r.AutoApply)
})
t.Run("without a workspace", func(t *testing.T) {
r, err := client.Runs.Create(ctx, RunCreateOptions{})
assert.Nil(t, r)
assert.Equal(t, err, ErrRequiredWorkspace)
})
t.Run("with additional attributes", func(t *testing.T) {
options := RunCreateOptions{
Message: String("yo"),
Workspace: wTest,
Refresh: Bool(false),
ReplaceAddrs: []string{"null_resource.example"},
TargetAddrs: []string{"null_resource.example"},
}
r, err := client.Runs.Create(ctx, options)
require.NoError(t, err)
assert.Equal(t, *options.Message, r.Message)
assert.Equal(t, *options.Refresh, r.Refresh)
assert.Equal(t, options.ReplaceAddrs, r.ReplaceAddrs)
assert.Equal(t, options.TargetAddrs, r.TargetAddrs)
assert.Nil(t, r.Variables)
})
t.Run("with variables", func(t *testing.T) {
vars := []*RunVariable{
{
Key: "test_variable",
Value: "Hello, World!",
},
{
Key: "test_foo",
Value: "Hello, Foo!",
},
}
options := RunCreateOptions{
Message: String("yo"),
Workspace: wTest,
Variables: vars,
}
r, err := client.Runs.Create(ctx, options)
require.NoError(t, err)
assert.NotNil(t, r.Variables)
assert.Equal(t, len(vars), len(r.Variables))
for _, v := range r.Variables {
switch v.Key {
case "test_foo":
assert.Equal(t, v.Value, "Hello, Foo!")
case "test_variable":
assert.Equal(t, v.Value, "Hello, World!")
default:
t.Fatalf("Unexpected variable key: %s", v.Key)
}
}
})
t.Run("with policy paths", func(t *testing.T) {
skipUnlessBeta(t)
opts := RunCreateOptions{
Message: String("creating with policy paths"),
Workspace: wTest,
PolicyPaths: []string{"./path/to/dir1", "./path/to/dir2"},
}
r, err := client.Runs.Create(ctx, opts)
require.NoError(t, err)
require.NotEmpty(t, r.PolicyPaths)
assert.Len(t, r.PolicyPaths, 2)
assert.Contains(t, r.PolicyPaths, "./path/to/dir1")
assert.Contains(t, r.PolicyPaths, "./path/to/dir2")
})
t.Run("with action invocations", func(t *testing.T) {
skipUnlessBeta(t)
opts := RunCreateOptions{
Message: String("creating with policy paths"),
Workspace: wTest,
InvokeActionAddrs: []string{"actions.foo.bar"},
}
r, err := client.Runs.Create(ctx, opts)
require.NoError(t, err)
require.NotEmpty(t, r.InvokeActionAddrs)
assert.Len(t, r.InvokeActionAddrs, 1)
assert.Contains(t, r.InvokeActionAddrs, "actions.foo.bar")
})
}
func TestRunsRead_CostEstimate_RunDependent(t *testing.T) {
skipIfEnterprise(t)
client := testClient(t)
ctx := context.Background()
rTest, rTestCleanup := createCostEstimatedRun(t, client, nil)
defer rTestCleanup()
t.Run("when the run exists", func(t *testing.T) {
r, err := client.Runs.Read(ctx, rTest.ID)
require.NoError(t, err)
assert.Equal(t, rTest, r)
})
t.Run("when the run does not exist", func(t *testing.T) {
r, err := client.Runs.Read(ctx, "nonexisting")
assert.Nil(t, r)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("with invalid run ID", func(t *testing.T) {
r, err := client.Runs.Read(ctx, badIdentifier)
assert.Nil(t, r)
assert.EqualError(t, err, ErrInvalidRunID.Error())
})
}
func TestRunsReadWithOptions_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
rTest, rTestCleanup := createRun(t, client, nil)
defer rTestCleanup()
t.Run("when the run exists", func(t *testing.T) {
curOpts := &RunReadOptions{
Include: []RunIncludeOpt{RunCreatedBy},
}
r, err := client.Runs.ReadWithOptions(ctx, rTest.ID, curOpts)
require.NoError(t, err)
require.NotEmpty(t, r.CreatedBy)
assert.NotEmpty(t, r.CreatedBy.Username)
})
}
func TestRunsReadWithPolicyPaths(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
wTest, wTestCleanup := createWorkspace(t, client, nil)
t.Cleanup(wTestCleanup)
_, cvCleanup := createUploadedConfigurationVersion(t, client, wTest)
t.Cleanup(cvCleanup)
r, err := client.Runs.Create(ctx, RunCreateOptions{
Workspace: wTest,
PolicyPaths: []string{"./foo"},
})
require.NoError(t, err)
r, err = client.Runs.Read(ctx, r.ID)
require.NoError(t, err)
require.NotEmpty(t, r.PolicyPaths)
assert.Contains(t, r.PolicyPaths, "./foo")
}
func TestRunsConfirmedBy_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
t.Run("with apply", func(t *testing.T) {
rTest, rTestCleanup := createRunApply(t, client, nil)
t.Cleanup(rTestCleanup)
r, err := client.Runs.Read(ctx, rTest.ID)
require.NoError(t, err)
assert.NotNil(t, r.ConfirmedBy)
assert.NotZero(t, r.ConfirmedBy.ID)
})
t.Run("without apply", func(t *testing.T) {
rTest, rTestCleanup := createPlannedRun(t, client, nil)
t.Cleanup(rTestCleanup)
r, err := client.Runs.Read(ctx, rTest.ID)
require.NoError(t, err)
assert.Equal(t, rTest, r)
assert.Nil(t, r.ConfirmedBy)
})
}
func TestRunsCanceledAt_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
wTest, wTestCleanup := createWorkspace(t, client, nil)
t.Cleanup(wTestCleanup)
// We need to create 2 runs here. The first run will automatically
// be planned so that one cannot be cancelled. The second one will
// be pending until the first one is confirmed or discarded, so we
// can cancel that one.
createRun(t, client, wTest)
rTest, _ := createRun(t, client, wTest)
t.Run("when the run is not canceled", func(t *testing.T) {
r, err := client.Runs.Read(ctx, rTest.ID)
require.NoError(t, err)
assert.Empty(t, r.CanceledAt)
})
t.Run("when the run is canceled", func(t *testing.T) {
err := client.Runs.Cancel(ctx, rTest.ID, RunCancelOptions{})
require.NoError(t, err)
for i := 1; ; i++ {
// Refresh the view of the run
rTest, err = client.Runs.Read(ctx, rTest.ID)
require.NoError(t, err)
// Check if the timestamp is present.
if !rTest.ForceCancelAvailableAt.IsZero() {
break
}
if i > 30 {
t.Fatal("Timeout waiting for run to be canceled")
}
time.Sleep(time.Second)
}
r, err := client.Runs.Read(ctx, rTest.ID)
require.NoError(t, err)
assert.NotEmpty(t, r.CanceledAt)
})
}
func TestRunsRunEvents(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
wTest, wTestCleanup := createWorkspace(t, client, nil)
defer wTestCleanup()
_, cvCleanup := createUploadedConfigurationVersion(t, client, wTest)
t.Cleanup(cvCleanup)
options := RunCreateOptions{
Workspace: wTest,
}
r, err := client.Runs.Create(ctx, options)
require.NoError(t, err)
assert.NotEmpty(t, r.RunEvents)
}
func TestRunsTriggerReason(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
wTest, wTestCleanup := createWorkspace(t, client, nil)
defer wTestCleanup()
_, cvCleanup := createUploadedConfigurationVersion(t, client, wTest)
t.Cleanup(cvCleanup)
options := RunCreateOptions{
Workspace: wTest,
}
r, err := client.Runs.Create(ctx, options)
require.NoError(t, err)
assert.NotNil(t, r.TriggerReason)
}
func TestRunsApply_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wTest, _ := createWorkspace(t, client, orgTest)
rTest, _ := createPlannedRun(t, client, wTest)
t.Run("when the run exists", func(t *testing.T) {
err := client.Runs.Apply(ctx, rTest.ID, RunApplyOptions{
Comment: String("Hello, Earl"),
})
require.NoError(t, err)
r, err := client.Runs.Read(ctx, rTest.ID)
require.NoError(t, err)
assert.Len(t, r.Comments, 1)
c, err := client.Comments.Read(ctx, r.Comments[0].ID)
require.NoError(t, err)
assert.Equal(t, "Hello, Earl", c.Body)
})
t.Run("when the run does not exist", func(t *testing.T) {
err := client.Runs.Apply(ctx, "nonexisting", RunApplyOptions{})
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("with invalid run ID", func(t *testing.T) {
err := client.Runs.Apply(ctx, badIdentifier, RunApplyOptions{})
assert.EqualError(t, err, ErrInvalidRunID.Error())
})
}
func TestRunsCancel_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
wTest, wTestCleanup := createWorkspace(t, client, nil)
t.Cleanup(wTestCleanup)
// We need to create 2 runs here. The first run will automatically
// be planned so that one cannot be cancelled. The second one will
// be pending until the first one is confirmed or discarded, so we
// can cancel that one.
createRun(t, client, wTest)
rTest, _ := createRun(t, client, wTest)
t.Run("when the run exists", func(t *testing.T) {
err := client.Runs.Cancel(ctx, rTest.ID, RunCancelOptions{})
require.NoError(t, err)
})
t.Run("when the run does not exist", func(t *testing.T) {
err := client.Runs.Cancel(ctx, "nonexisting", RunCancelOptions{})
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("with invalid run ID", func(t *testing.T) {
err := client.Runs.Cancel(ctx, badIdentifier, RunCancelOptions{})
assert.EqualError(t, err, ErrInvalidRunID.Error())
})
}
func TestRunsForceCancel_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
wTest, wTestCleanup := createWorkspace(t, client, nil)
defer wTestCleanup()
// We need to create 2 runs here. The first run will automatically
// be planned so that one cannot be cancelled. The second one will
// be pending until the first one is confirmed or discarded, so we
// can cancel that one.
createRun(t, client, wTest)
rTest, _ := createRun(t, client, wTest)
t.Run("run is not force-cancelable", func(t *testing.T) {
assert.False(t, rTest.Actions.IsForceCancelable)
})
t.Run("user is allowed to force-cancel", func(t *testing.T) {
assert.True(t, rTest.Permissions.CanForceCancel)
})
t.Run("after a normal cancel", func(t *testing.T) {
// Request the normal cancel
err := client.Runs.Cancel(ctx, rTest.ID, RunCancelOptions{})
require.NoError(t, err)
rTest, err := retryPatientlyIf(
func() (any, error) {
return client.Runs.Read(ctx, rTest.ID)
},
func(r *Run) bool {
return r.ForceCancelAvailableAt.IsZero()
},
)
require.NoError(t, err)
t.Run("force-cancel-available-at timestamp is present", func(t *testing.T) {
assert.True(t, rTest.ForceCancelAvailableAt.After(time.Now()))
})
// This test case is minimal because a force-cancel is not needed in
// any normal circumstance. Only if Terraform encounters unexpected
// errors or behaves abnormally should this functionality be required.
// Force-cancel only becomes available if a normal cancel is performed
// first, and the desired canceled state is not reached within a pre-
// determined amount of time (see
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#forcefully-cancel-a-run).
})
t.Run("when the run does not exist", func(t *testing.T) {
err := client.Runs.ForceCancel(ctx, "nonexisting", RunForceCancelOptions{})
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("with invalid run ID", func(t *testing.T) {
err := client.Runs.ForceCancel(ctx, badIdentifier, RunForceCancelOptions{})
assert.EqualError(t, err, ErrInvalidRunID.Error())
})
}
func TestRunsForceExecute_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
wTest, wTestCleanup := createWorkspace(t, client, nil)
defer wTestCleanup()
// We need to create 2 runs here:
// - The first run will automatically be planned so that the second
// run can't be executed.
// - The second run will be pending until the first run is confirmed or
// discarded, so we will force execute this run.
rToCancel, _ := createPlannedRun(t, client, wTest)
rTest, _ := createRunWaitForStatus(t, client, wTest, RunPending)
t.Run("a successful force-execute", func(t *testing.T) {
// Verify the user has permission to force-execute the run
assert.True(t, rTest.Permissions.CanForceExecute)
err := client.Runs.ForceExecute(ctx, rTest.ID)
require.NoError(t, err)
timeout := 2 * time.Minute
ctxPollRunForceExecute, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
// Verify the second run has a status that is an applyable status
rTest = pollRunStatus(t,
client,
ctxPollRunForceExecute,
rTest,
applyableStatuses(rTest))
if rTest.Status == RunErrored {
fatalDumpRunLog(t, client, ctx, rTest)
}
// Refresh the view of the first run
rToCancel, err = client.Runs.Read(ctx, rToCancel.ID)
require.NoError(t, err)
// Verify the first run was discarded
assert.Equal(t, RunDiscarded, rToCancel.Status)
})
t.Run("when the run does not exist", func(t *testing.T) {
err := client.Runs.ForceExecute(ctx, "nonexisting")
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("with invalid run ID", func(t *testing.T) {
err := client.Runs.ForceExecute(ctx, badIdentifier)
assert.EqualError(t, err, ErrInvalidRunID.Error())
})
}
func TestRunsDiscard_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
wTest, wTestCleanup := createWorkspace(t, client, nil)
defer wTestCleanup()
rTest, _ := createPlannedRun(t, client, wTest)
t.Run("when the run exists", func(t *testing.T) {
err := client.Runs.Discard(ctx, rTest.ID, RunDiscardOptions{})
require.NoError(t, err)
})
t.Run("when the run does not exist", func(t *testing.T) {
err := client.Runs.Discard(ctx, "nonexisting", RunDiscardOptions{})
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("with invalid run ID", func(t *testing.T) {
err := client.Runs.Discard(ctx, badIdentifier, RunDiscardOptions{})
assert.EqualError(t, err, ErrInvalidRunID.Error())
})
}
func TestRun_Unmarshal(t *testing.T) {
t.Parallel()
data := map[string]interface{}{
"data": map[string]interface{}{
"type": "runs",
"id": "1",
"attributes": map[string]interface{}{
"created-at": "2018-03-02T23:42:06.651Z",
"has-changes": true,
"is-destroy": false,
"message": "run message",
"actions": map[string]interface{}{
"is-cancelable": true,
"is-confirmable": true,
"is-discardable": true,
"is-force-cancelable": true,
},
"permissions": map[string]interface{}{
"can-apply": true,
"can-cancel": true,
"can-discard": true,
"can-force-cancel": true,
"can-force-execute": true,
},
"status-timestamps": map[string]string{
"plan-queued-at": "2020-03-16T23:15:59+00:00",
"errored-at": "2019-03-16T23:23:59+00:00",
},
"variables": []map[string]string{{"key": "a-key", "value": "\"a-value\""}},
},
},
}
byteData, err := json.Marshal(data)
require.NoError(t, err)
responseBody := bytes.NewReader(byteData)
run := &Run{}
err = unmarshalResponse(responseBody, run)
require.NoError(t, err)
planQueuedParsedTime, err := time.Parse(time.RFC3339, "2020-03-16T23:15:59+00:00")
require.NoError(t, err)
erroredParsedTime, err := time.Parse(time.RFC3339, "2019-03-16T23:23:59+00:00")
require.NoError(t, err)
iso8601TimeFormat := "2006-01-02T15:04:05Z"
parsedTime, err := time.Parse(iso8601TimeFormat, "2018-03-02T23:42:06.651Z")
require.NoError(t, err)
assert.Equal(t, run.ID, "1")
assert.Equal(t, run.CreatedAt, parsedTime)
assert.Equal(t, run.HasChanges, true)
assert.Equal(t, run.IsDestroy, false)
assert.Equal(t, run.Message, "run message")
assert.Equal(t, run.Actions.IsConfirmable, true)
assert.Equal(t, run.Actions.IsCancelable, true)
assert.Equal(t, run.Actions.IsDiscardable, true)
assert.Equal(t, run.Actions.IsForceCancelable, true)
assert.Equal(t, run.Permissions.CanApply, true)
assert.Equal(t, run.Permissions.CanCancel, true)
assert.Equal(t, run.Permissions.CanDiscard, true)
assert.Equal(t, run.Permissions.CanForceExecute, true)
assert.Equal(t, run.Permissions.CanForceCancel, true)
assert.Equal(t, run.StatusTimestamps.PlanQueuedAt, planQueuedParsedTime)
assert.Equal(t, run.StatusTimestamps.ErroredAt, erroredParsedTime)
require.NotEmpty(t, run.Variables)
assert.Equal(t, run.Variables[0].Key, "a-key")
assert.Equal(t, run.Variables[0].Value, "\"a-value\"")
}
func TestRunCreateOptions_Marshal(t *testing.T) {
t.Parallel()
client := testClient(t)
wTest, wTestCleanup := createWorkspace(t, client, nil)
defer wTestCleanup()
opts := RunCreateOptions{
Workspace: wTest,
Variables: []*RunVariable{
{
Key: "test_variable",
Value: "Hello, World!",
},
{
Key: "test_foo",
Value: "Hello, Foo!",
},
},
}
reqBody, err := serializeRequestBody(&opts)
require.NoError(t, err)
req, err := retryablehttp.NewRequest("POST", "url", reqBody)
require.NoError(t, err)
bodyBytes, err := req.BodyBytes()
require.NoError(t, err)
expectedBody := fmt.Sprintf(`{"data":{"type":"runs","attributes":{"variables":[{"key":"test_variable","value":"Hello, World!"},{"key":"test_foo","value":"Hello, Foo!"}]},"relationships":{"configuration-version":{"data":null},"workspace":{"data":{"type":"workspaces","id":"%s"}}}}}
`, wTest.ID)
assert.Equal(t, string(bodyBytes), expectedBody)
}
func TestRunsListForOrganization_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
apTest, _ := createAgentPool(t, client, orgTest)
wTest, _ := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{
Name: String(randomString(t)),
ExecutionMode: String("agent"),
AgentPoolID: &apTest.ID,
})
rTest1, _ := createRun(t, client, wTest)
rTest2, _ := createRun(t, client, wTest)
t.Run("without list options", func(t *testing.T) {
rl, err := client.Runs.ListForOrganization(ctx, orgTest.Name, nil)
require.NoError(t, err)
found := []string{}
for _, r := range rl.Items {
found = append(found, r.ID)
}
assert.Contains(t, found, rTest1.ID)
assert.Contains(t, found, rTest2.ID)
assert.Equal(t, 1, rl.CurrentPage)
assert.Empty(t, rl.NextPage)
})
t.Run("without list options and include as nil", func(t *testing.T) {
rl, err := client.Runs.ListForOrganization(ctx, orgTest.Name, &RunListForOrganizationOptions{
Include: []RunIncludeOpt{},
})
require.NoError(t, err)
require.NotEmpty(t, rl.Items)
found := []string{}
for _, r := range rl.Items {
found = append(found, r.ID)
}
assert.Contains(t, found, rTest1.ID)
assert.Contains(t, found, rTest2.ID)
assert.Equal(t, 1, rl.CurrentPage)
assert.Empty(t, rl.NextPage)
})
t.Run("with list options", func(t *testing.T) {
// Request a page number that is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
rl, err := client.Runs.ListForOrganization(ctx, orgTest.Name, &RunListForOrganizationOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, rl.Items)
assert.Equal(t, 999, rl.CurrentPage)
assert.Empty(t, rl.NextPage)
})
t.Run("with workspace included", func(t *testing.T) {
rl, err := client.Runs.ListForOrganization(ctx, orgTest.Name, &RunListForOrganizationOptions{
Include: []RunIncludeOpt{RunWorkspace},
})
require.NoError(t, err)
require.NotEmpty(t, rl.Items)
require.NotNil(t, rl.Items[0].Workspace)
assert.NotEmpty(t, rl.Items[0].Workspace.Name)
})
t.Run("without a valid organization name", func(t *testing.T) {
rl, err := client.Runs.ListForOrganization(ctx, badIdentifier, nil)
assert.Nil(t, rl)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("with filter by agent pool", func(t *testing.T) {
rl, err := client.Runs.ListForOrganization(ctx, orgTest.Name, &RunListForOrganizationOptions{
AgentPoolNames: apTest.Name,
})
require.NoError(t, err)
found := make([]string, len(rl.Items))
for i, r := range rl.Items {
found[i] = r.ID
}
assert.Contains(t, found, rTest1.ID)
assert.Contains(t, found, rTest2.ID)
assert.Equal(t, 1, rl.CurrentPage)
assert.Empty(t, rl.NextPage)
})
t.Run("with filter by workspace", func(t *testing.T) {
rl, err := client.Runs.ListForOrganization(ctx, orgTest.Name, &RunListForOrganizationOptions{
WorkspaceNames: wTest.Name,
Include: []RunIncludeOpt{RunWorkspace},
})
require.NoError(t, err)
found := make([]string, len(rl.Items))
for i, r := range rl.Items {
found[i] = r.ID
}
assert.Contains(t, found, rTest1.ID)
assert.Contains(t, found, rTest2.ID)
require.NotNil(t, rl.Items[0].Workspace)
assert.NotEmpty(t, rl.Items[0].Workspace.Name)
require.NotNil(t, rl.Items[1].Workspace)
assert.NotEmpty(t, rl.Items[1].Workspace.Name)
assert.Equal(t, 1, rl.CurrentPage)
assert.Empty(t, rl.NextPage)
})
}
================================================
FILE: run_task.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation
var _ RunTasks = (*runTasks)(nil)
// RunTasks represents all the run task related methods in the context of an organization
// that the HCP Terraform and Terraform Enterprise API supports.
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run-tasks/run-tasks#run-tasks-api
type RunTasks interface {
// Create a run task for an organization
Create(ctx context.Context, organization string, options RunTaskCreateOptions) (*RunTask, error)
// List all run tasks for an organization
List(ctx context.Context, organization string, options *RunTaskListOptions) (*RunTaskList, error)
// Read an organization's run task by ID
Read(ctx context.Context, runTaskID string) (*RunTask, error)
// Read an organization's run task by ID with given options
ReadWithOptions(ctx context.Context, runTaskID string, options *RunTaskReadOptions) (*RunTask, error)
// Update a run task for an organization
Update(ctx context.Context, runTaskID string, options RunTaskUpdateOptions) (*RunTask, error)
// Delete an organization's run task
Delete(ctx context.Context, runTaskID string) error
// Attach a run task to an organization's workspace
AttachToWorkspace(ctx context.Context, workspaceID string, runTaskID string, enforcementLevel TaskEnforcementLevel) (*WorkspaceRunTask, error)
}
// runTasks implements RunTasks
type runTasks struct {
client *Client
}
// RunTask represents a HCP Terraform or Terraform Enterprise run task
type RunTask struct {
ID string `jsonapi:"primary,tasks"`
Name string `jsonapi:"attr,name"`
URL string `jsonapi:"attr,url"`
Description string `jsonapi:"attr,description"`
Category string `jsonapi:"attr,category"`
HMACKey *string `jsonapi:"attr,hmac-key,omitempty"`
Enabled bool `jsonapi:"attr,enabled"`
Global *GlobalRunTask `jsonapi:"attr,global-configuration,omitempty"`
AgentPool *AgentPool `jsonapi:"relation,agent-pool"`
Organization *Organization `jsonapi:"relation,organization"`
WorkspaceRunTasks []*WorkspaceRunTask `jsonapi:"relation,workspace-tasks"`
}
// GlobalRunTask represents the global configuration of a HCP Terraform or Terraform Enterprise run task
type GlobalRunTask struct {
Enabled bool `jsonapi:"attr,enabled"`
Stages []Stage `jsonapi:"attr,stages"`
EnforcementLevel TaskEnforcementLevel `jsonapi:"attr,enforcement-level"`
}
// RunTaskList represents a list of run tasks
type RunTaskList struct {
*Pagination
Items []*RunTask
}
// RunTaskIncludeOpt represents the available options for include query params.
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run-tasks/run-tasks#list-run-tasks
type RunTaskIncludeOpt string
const (
RunTaskWorkspaceTasks RunTaskIncludeOpt = "workspace_tasks"
RunTaskWorkspace RunTaskIncludeOpt = "workspace_tasks.workspace"
)
// RunTaskListOptions represents the set of options for listing run tasks
type RunTaskListOptions struct {
ListOptions
// Optional: A list of relations to include with a run task. See available resources:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run-tasks/run-tasks#list-run-tasks
Include []RunTaskIncludeOpt `url:"include,omitempty"`
}
// RunTaskReadOptions represents the set of options for reading a run task
type RunTaskReadOptions struct {
// Optional: A list of relations to include with a run task. See available resources:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run-tasks/run-tasks#list-run-tasks
Include []RunTaskIncludeOpt `url:"include,omitempty"`
}
// GlobalRunTask represents the optional global configuration of a HCP Terraform or Terraform Enterprise run task
type GlobalRunTaskOptions struct {
Enabled *bool `jsonapi:"attr,enabled,omitempty"`
Stages *[]Stage `jsonapi:"attr,stages,omitempty"`
EnforcementLevel *TaskEnforcementLevel `jsonapi:"attr,enforcement-level,omitempty"`
}
// RunTaskCreateOptions represents the set of options for creating a run task
type RunTaskCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,tasks"`
// Required: The name of the run task
Name string `jsonapi:"attr,name"`
// Required: The URL to send a run task payload
URL string `jsonapi:"attr,url"`
// Optional: Description of the task
Description *string `jsonapi:"attr,description"`
// Required: Must be "task"
Category string `jsonapi:"attr,category"`
// Optional: An HMAC key to verify the run task
HMACKey *string `jsonapi:"attr,hmac-key,omitempty"`
// Optional: Whether the task should be enabled
Enabled *bool `jsonapi:"attr,enabled,omitempty"`
// Optional: Whether the task contains global configuration
Global *GlobalRunTaskOptions `jsonapi:"attr,global-configuration,omitempty"`
// Optional: Whether the task will be executed using an Agent Pool
// Requires the PrivateRunTasks entitlement
AgentPool *AgentPool `jsonapi:"relation,agent-pool,omitempty"`
}
// RunTaskUpdateOptions represents the set of options for updating an organization's run task
type RunTaskUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,tasks"`
// Optional: The name of the run task, defaults to previous value
Name *string `jsonapi:"attr,name,omitempty"`
// Optional: The URL to send a run task payload, defaults to previous value
URL *string `jsonapi:"attr,url,omitempty"`
// Optional: An optional description of the task
Description *string `jsonapi:"attr,description,omitempty"`
// Optional: Must be "task", defaults to "task"
Category *string `jsonapi:"attr,category,omitempty"`
// Optional: An HMAC key to verify the run task
HMACKey *string `jsonapi:"attr,hmac-key,omitempty"`
// Optional: Whether the task should be enabled
Enabled *bool `jsonapi:"attr,enabled,omitempty"`
// Optional: Whether the task contains global configuration
Global *GlobalRunTaskOptions `jsonapi:"attr,global-configuration,omitempty"`
// Optional: Whether the task will be executed using an Agent Pool
// Requires the PrivateRunTasks entitlement
AgentPool *AgentPool `jsonapi:"relation,agent-pool,omitempty"`
}
// Create is used to create a new run task for an organization
func (s *runTasks) Create(ctx context.Context, organization string, options RunTaskCreateOptions) (*RunTask, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("organizations/%s/tasks", url.PathEscape(organization))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
r := &internalRunTask{}
err = req.Do(ctx, r)
if err != nil {
return nil, err
}
return r.ToRunTask(), nil
}
// List all the run tasks for an organization
func (s *runTasks) List(ctx context.Context, organization string, options *RunTaskListOptions) (*RunTaskList, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("organizations/%s/tasks", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
rl := &internalRunTaskList{}
err = req.Do(ctx, rl)
if err != nil {
return nil, err
}
return rl.ToRunTaskList(), nil
}
// Read is used to read an organization's run task by ID
func (s *runTasks) Read(ctx context.Context, runTaskID string) (*RunTask, error) {
return s.ReadWithOptions(ctx, runTaskID, nil)
}
// Read is used to read an organization's run task by ID with options
func (s *runTasks) ReadWithOptions(ctx context.Context, runTaskID string, options *RunTaskReadOptions) (*RunTask, error) {
if !validStringID(&runTaskID) {
return nil, ErrInvalidRunTaskID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("tasks/%s", url.PathEscape(runTaskID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
r := &internalRunTask{}
err = req.Do(ctx, r)
if err != nil {
return nil, err
}
return r.ToRunTask(), nil
}
// Update an existing run task for an organization by ID
func (s *runTasks) Update(ctx context.Context, runTaskID string, options RunTaskUpdateOptions) (*RunTask, error) {
if !validStringID(&runTaskID) {
return nil, ErrInvalidRunTaskID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("tasks/%s", url.PathEscape(runTaskID))
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
r := &internalRunTask{}
err = req.Do(ctx, r)
if err != nil {
return nil, err
}
return r.ToRunTask(), nil
}
// Delete an existing run task for an organization by ID
func (s *runTasks) Delete(ctx context.Context, runTaskID string) error {
if !validStringID(&runTaskID) {
return ErrInvalidRunTaskID
}
u := fmt.Sprintf("tasks/%s", runTaskID)
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// AttachToWorkspace is a convenient method to attach a run task to a workspace. See: WorkspaceRunTasks.Create()
func (s *runTasks) AttachToWorkspace(ctx context.Context, workspaceID, runTaskID string, enforcement TaskEnforcementLevel) (*WorkspaceRunTask, error) {
return s.client.WorkspaceRunTasks.Create(ctx, workspaceID, WorkspaceRunTaskCreateOptions{
EnforcementLevel: enforcement,
RunTask: &RunTask{ID: runTaskID},
})
}
func (o *RunTaskCreateOptions) valid() error {
if !validString(&o.Name) {
return ErrRequiredName
}
if !validString(&o.URL) {
return ErrInvalidRunTaskURL
}
if o.Category != "task" {
return ErrInvalidRunTaskCategory
}
return nil
}
func (o *RunTaskUpdateOptions) valid() error {
if o.Name != nil && !validString(o.Name) {
return ErrRequiredName
}
if o.URL != nil && !validString(o.URL) {
return ErrInvalidRunTaskURL
}
if o.Category != nil && *o.Category != "task" {
return ErrInvalidRunTaskCategory
}
return nil
}
func (o *RunTaskListOptions) valid() error {
return nil
}
func (o *RunTaskReadOptions) valid() error {
return nil
}
================================================
FILE: run_task_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRunTasksCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t)
if v, err := hasGlobalRunTasks(client, orgTest.Name); err != nil {
t.Fatalf("Could not retrieve the entitlements for the test organization.: %s", err)
} else if !v {
t.Fatal("The test organization requires the global-run-tasks entitlement but is not entitled.")
return
}
runTaskServerURL := os.Getenv("TFC_RUN_TASK_URL")
if runTaskServerURL == "" {
t.Error("Cannot create a run task with an empty URL. You must set TFC_RUN_TASK_URL for run task related tests.")
}
runTaskName := "tst-runtask-" + randomString(t)
runTaskDescription := "A Run Task Description"
globalEnabled := true
globalStages := []Stage{
PostPlan,
PrePlan,
}
globalEnforce := Mandatory
t.Run("with an agent pool", func(t *testing.T) {
// We can only test if the org, supports private run tasks. For now this isn't
// a fatal error and we just skip the test.
if v, err := hasPrivateRunTasks(client, orgTest.Name); err != nil {
t.Fatalf("Could not retrieve the entitlements for the test organization.: %s", err)
} else if !v {
t.Skip("The test organization requires the private-run-tasks entitlement but is not entitled.")
return
}
// Unfortunately when we create a Run Task it automatically verifies that the URL by sending a test payload. But
// this means with an agent pool, we need an agent pool to exist, and an agent created with request forwarding enabled.
// This is too much to create for this one test suite. So instead, we really only need to assert that; when the options include an
// agent pool, then we expect HCP Terraform to process the agent pool. So, if we send it a nonsense agent pool ID, then we
// expect an error to be returned saying that the ID was nonsense.
_, err := client.RunTasks.Create(ctx, orgTest.Name, RunTaskCreateOptions{
Name: runTaskName,
URL: runTaskServerURL,
Description: &runTaskDescription,
Category: "task",
AgentPool: &AgentPool{
ID: "apool-this-pool-id-will-never-exist-so-we-expect-http-error-response",
},
})
require.ErrorContains(t, err, "The provided agent pool does not exist")
})
t.Run("add run task to organization", func(t *testing.T) {
r, err := client.RunTasks.Create(ctx, orgTest.Name, RunTaskCreateOptions{
Name: runTaskName,
URL: runTaskServerURL,
Description: &runTaskDescription,
Category: "task",
Enabled: Bool(true),
Global: &GlobalRunTaskOptions{
Enabled: &globalEnabled,
Stages: &globalStages,
EnforcementLevel: &globalEnforce,
},
})
require.NoError(t, err)
assert.NotEmpty(t, r.ID)
assert.Equal(t, r.Name, runTaskName)
assert.Equal(t, r.URL, runTaskServerURL)
assert.Equal(t, r.Category, "task")
assert.Equal(t, r.Description, runTaskDescription)
assert.NotNil(t, r.Global)
assert.Equal(t, globalEnabled, r.Global.Enabled)
assert.Equal(t, globalEnforce, r.Global.EnforcementLevel)
assert.Equal(t, globalStages, r.Global.Stages)
t.Run("ensure org is deserialized properly", func(t *testing.T) {
assert.Equal(t, r.Organization.Name, orgTest.Name)
})
})
}
func TestRunTasksCreateWithoutGlobalEntitlement(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
newSubscriptionUpdater(orgTest).WithTrialPlan().Update(t)
if v, err := hasGlobalRunTasks(client, orgTest.Name); err != nil {
t.Fatalf("Could not retrieve the entitlements for the test organization.: %s", err)
} else if v {
t.Fatal("The test organization should not have the global-run-tasks entitlement but it does.")
return
}
runTaskServerURL := os.Getenv("TFC_RUN_TASK_URL")
if runTaskServerURL == "" {
t.Error("Cannot create a run task with an empty URL. You must set TFC_RUN_TASK_URL for run task related tests.")
}
runTaskName := "tst-runtask-" + randomString(t)
runTaskDescription := "A Run Task Description"
globalStages := []Stage{
PostPlan,
PrePlan,
}
globalEnforce := Mandatory
t.Run("add run task to organization", func(t *testing.T) {
r, err := client.RunTasks.Create(ctx, orgTest.Name, RunTaskCreateOptions{
Name: runTaskName,
URL: runTaskServerURL,
Description: &runTaskDescription,
Category: "task",
// Even though we pass in these global parameters,
// they should be ignored and not throw an API error
Global: &GlobalRunTaskOptions{
Enabled: Bool(true),
Stages: &globalStages,
EnforcementLevel: &globalEnforce,
},
})
require.NoError(t, err)
assert.NotEmpty(t, r.ID)
assert.Nil(t, r.Global)
})
}
func TestRunTasksList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
_, runTaskTest1Cleanup := createRunTask(t, client, orgTest)
defer runTaskTest1Cleanup()
_, runTaskTest2Cleanup := createRunTask(t, client, orgTest)
defer runTaskTest2Cleanup()
t.Run("with no params", func(t *testing.T) {
runTaskList, err := client.RunTasks.List(ctx, orgTest.Name, nil)
require.NoError(t, err)
assert.NotNil(t, runTaskList.Items)
assert.NotEmpty(t, runTaskList.Items[0].ID)
assert.NotEmpty(t, runTaskList.Items[0].URL)
assert.NotEmpty(t, runTaskList.Items[1].ID)
assert.NotEmpty(t, runTaskList.Items[1].URL)
})
}
func TestRunTasksRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest)
defer runTaskTestCleanup()
t.Run("by ID", func(t *testing.T) {
r, err := client.RunTasks.Read(ctx, runTaskTest.ID)
require.NoError(t, err)
assert.Equal(t, runTaskTest.ID, r.ID)
assert.Equal(t, runTaskTest.URL, r.URL)
assert.Equal(t, runTaskTest.Category, r.Category)
assert.Equal(t, runTaskTest.Description, r.Description)
assert.Equal(t, runTaskTest.HMACKey, r.HMACKey)
assert.Equal(t, runTaskTest.Enabled, r.Enabled)
})
t.Run("with options", func(t *testing.T) {
wkTest1, wkTest1Cleanup := createWorkspace(t, client, orgTest)
defer wkTest1Cleanup()
wkTest2, wkTest2Cleanup := createWorkspace(t, client, orgTest)
defer wkTest2Cleanup()
_, wrTest1Cleanup := createWorkspaceRunTask(t, client, wkTest1, runTaskTest)
defer wrTest1Cleanup()
_, wrTest2Cleanup := createWorkspaceRunTask(t, client, wkTest2, runTaskTest)
defer wrTest2Cleanup()
r, err := client.RunTasks.ReadWithOptions(ctx, runTaskTest.ID, &RunTaskReadOptions{
Include: []RunTaskIncludeOpt{RunTaskWorkspaceTasks},
})
require.NoError(t, err)
require.NotEmpty(t, r.WorkspaceRunTasks)
assert.NotEmpty(t, r.WorkspaceRunTasks[0].ID)
assert.NotEmpty(t, r.WorkspaceRunTasks[0].EnforcementLevel)
assert.NotEmpty(t, r.WorkspaceRunTasks[1].ID)
assert.NotEmpty(t, r.WorkspaceRunTasks[1].EnforcementLevel)
})
}
func TestRunTasksUpdate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest)
defer runTaskTestCleanup()
t.Run("rename task", func(t *testing.T) {
rename := runTaskTest.Name + "-UPDATED"
r, err := client.RunTasks.Update(ctx, runTaskTest.ID, RunTaskUpdateOptions{
Name: &rename,
})
require.NoError(t, err)
r, err = client.RunTasks.Read(ctx, r.ID)
require.NoError(t, err)
assert.Equal(t, rename, r.Name)
})
t.Run("toggle enabled", func(t *testing.T) {
runTaskTest.Enabled = !runTaskTest.Enabled
r, err := client.RunTasks.Update(ctx, runTaskTest.ID, RunTaskUpdateOptions{
Enabled: &runTaskTest.Enabled,
})
require.NoError(t, err)
r, err = client.RunTasks.Read(ctx, r.ID)
require.NoError(t, err)
assert.Equal(t, runTaskTest.Enabled, r.Enabled)
})
t.Run("update description", func(t *testing.T) {
newDescription := "An updated task description"
r, err := client.RunTasks.Update(ctx, runTaskTest.ID, RunTaskUpdateOptions{
Description: &newDescription,
})
require.NoError(t, err)
r, err = client.RunTasks.Read(ctx, r.ID)
require.NoError(t, err)
assert.Equal(t, newDescription, r.Description)
})
t.Run("with an agent pool", func(t *testing.T) {
// We can only test if the org, supports private run tasks. For now this isn't
// a fatal error and we just skip the test.
if v, err := hasPrivateRunTasks(client, orgTest.Name); err != nil {
t.Fatalf("Could not retrieve the entitlements for the test organization.: %s", err)
} else if !v {
t.Skip("The test organization requires the private-run-tasks entitlement but is not entitled.")
return
}
// Unfortunately when we update a Run Task it automatically verifies that the URL by sending a test payload. But
// this means with an agent pool, we need an agent pool to exist, and an agent created with request forwarding enabled.
// This is too much to create for this one test suite. So instead, we really only need to assert that; when the options include an
// agent pool, then we expect HCP Terraform to process the agent pool. So, if we send it a nonsense agent pool ID, then we
// expect an error to be returned saying that the ID was nonsense.
_, err := client.RunTasks.Update(ctx, runTaskTest.ID, RunTaskUpdateOptions{
AgentPool: &AgentPool{
ID: "apool-this-pool-id-will-never-exist-so-we-expect-http-error-response",
},
})
require.ErrorContains(t, err, "The provided agent pool does not exist")
})
}
func TestRunTasksDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
runTaskTest, _ := createRunTask(t, client, orgTest)
t.Run("with valid options", func(t *testing.T) {
err := client.RunTasks.Delete(ctx, runTaskTest.ID)
require.NoError(t, err)
_, err = client.RunTasks.Read(ctx, runTaskTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the run task does not exist", func(t *testing.T) {
err := client.RunTasks.Delete(ctx, runTaskTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the run task ID is invalid", func(t *testing.T) {
err := client.RunTasks.Delete(ctx, badIdentifier)
assert.EqualError(t, err, ErrInvalidRunTaskID.Error())
})
}
func TestRunTasksAttachToWorkspace(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest)
defer runTaskTestCleanup()
wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest)
defer wkspaceTestCleanup()
t.Run("to a valid workspace", func(t *testing.T) {
wr, err := client.RunTasks.AttachToWorkspace(ctx, wkspaceTest.ID, runTaskTest.ID, Advisory)
require.NoError(t, err)
defer func() {
err = client.WorkspaceRunTasks.Delete(ctx, wkspaceTest.ID, wr.ID)
require.NoError(t, err)
}()
require.NotNil(t, wr.ID)
})
}
================================================
FILE: run_task_request.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"time"
)
// RunTaskRequest is the payload object that TFC/E sends to the Run Task's URL.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#common-properties
type RunTaskRequest struct {
AccessToken string `json:"access_token"`
Capabilitites RunTaskRequestCapabilitites `json:"capabilitites,omitempty"`
ConfigurationVersionDownloadURL string `json:"configuration_version_download_url,omitempty"`
ConfigurationVersionID string `json:"configuration_version_id,omitempty"`
IsSpeculative bool `json:"is_speculative"`
OrganizationName string `json:"organization_name"`
PayloadVersion int `json:"payload_version"`
PlanJSONAPIURL string `json:"plan_json_api_url,omitempty"` // Specific to post_plan, pre_apply or post_apply stage
RunAppURL string `json:"run_app_url"`
RunCreatedAt time.Time `json:"run_created_at"`
RunCreatedBy string `json:"run_created_by"`
RunID string `json:"run_id"`
RunMessage string `json:"run_message"`
Stage string `json:"stage"`
TaskResultCallbackURL string `json:"task_result_callback_url"`
TaskResultEnforcementLevel string `json:"task_result_enforcement_level"`
TaskResultID string `json:"task_result_id"`
VcsBranch string `json:"vcs_branch,omitempty"`
VcsCommitURL string `json:"vcs_commit_url,omitempty"`
VcsPullRequestURL string `json:"vcs_pull_request_url,omitempty"`
VcsRepoURL string `json:"vcs_repo_url,omitempty"`
WorkspaceAppURL string `json:"workspace_app_url"`
WorkspaceID string `json:"workspace_id"`
WorkspaceName string `json:"workspace_name"`
WorkspaceWorkingDirectory string `json:"workspace_working_directory,omitempty"`
}
// RunTaskRequestCapabilitites defines the capabilities that the caller supports.
type RunTaskRequestCapabilitites struct {
Outcomes bool `json:"outcomes"`
}
================================================
FILE: run_tasks_integration.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"net/http"
)
// Compile-time proof of interface implementation.
var _ RunTasksIntegration = (*runTaskIntegration)(nil)
// RunTasksIntegration describes all the Run Tasks Integration Callback API methods.
//
// TFE API docs:
// https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration
type RunTasksIntegration interface {
// Update sends updates to TFC/E Run Task Callback URL
Callback(ctx context.Context, callbackURL string, accessToken string, options TaskResultCallbackRequestOptions) error
}
// taskResultsCallback implements RunTasksIntegration.
type runTaskIntegration struct {
client *Client
}
// TaskResultCallbackRequestOptions represents the TFC/E Task result callback request
// https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#request-body-1
type TaskResultCallbackRequestOptions struct {
Type string `jsonapi:"primary,task-results"`
Status TaskResultStatus `jsonapi:"attr,status"`
Message string `jsonapi:"attr,message,omitempty"`
URL string `jsonapi:"attr,url,omitempty"`
Outcomes []*TaskResultOutcome `jsonapi:"relation,outcomes,omitempty"`
}
// TaskResultOutcome represents a detailed TFC/E run task outcome, which improves result visibility and content in the TFC/E UI.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#outcomes-payload-body
type TaskResultOutcome struct {
Type string `jsonapi:"primary,task-result-outcomes"`
OutcomeID string `jsonapi:"attr,outcome-id,omitempty"`
Description string `jsonapi:"attr,description,omitempty"`
Body string `jsonapi:"attr,body,omitempty"`
URL string `jsonapi:"attr,url,omitempty"`
Tags map[string][]*TaskResultTag `jsonapi:"attr,tags,omitempty"`
}
// TaskResultTag can be used to enrich outcomes display list in TFC/E.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#severity-and-status-tags
type TaskResultTag struct {
Label string `json:"label"`
Level string `json:"level,omitempty"`
}
// Update sends updates to TFC/E Run Task Callback URL
func (s *runTaskIntegration) Callback(ctx context.Context, callbackURL, accessToken string, options TaskResultCallbackRequestOptions) error {
if !validString(&callbackURL) {
return ErrInvalidCallbackURL
}
if !validString(&accessToken) {
return ErrInvalidAccessToken
}
if err := options.valid(); err != nil {
return err
}
req, err := s.client.NewRequest(http.MethodPatch, callbackURL, &options)
if err != nil {
return err
}
// The PATCH request must use the token supplied in the originating request (access_token) for authentication.
// https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#request-headers-1
req.Header.Set("Authorization", "Bearer "+accessToken)
return req.Do(ctx, nil)
}
func (o *TaskResultCallbackRequestOptions) valid() error {
if o.Status != TaskFailed && o.Status != TaskPassed && o.Status != TaskRunning {
return ErrInvalidTaskResultsCallbackStatus
}
return nil
}
================================================
FILE: run_tasks_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestRunTasksIntegration_Validate runs a series of tests that test whether various TaskResultCallbackRequestOptions objects can be considered valid or not
func TestRunTasksIntegration_Validate(t *testing.T) {
t.Parallel()
t.Run("with an empty status", func(t *testing.T) {
opts := TaskResultCallbackRequestOptions{Status: ""}
err := opts.valid()
assert.EqualError(t, err, ErrInvalidTaskResultsCallbackStatus.Error())
})
t.Run("without valid Status options", func(t *testing.T) {
for _, s := range []TaskResultStatus{TaskPending, TaskErrored, "foo"} {
opts := TaskResultCallbackRequestOptions{Status: s}
err := opts.valid()
assert.EqualError(t, err, ErrInvalidTaskResultsCallbackStatus.Error())
}
})
t.Run("with valid Status options", func(t *testing.T) {
for _, s := range []TaskResultStatus{TaskFailed, TaskPassed, TaskRunning} {
opts := TaskResultCallbackRequestOptions{Status: s}
err := opts.valid()
require.NoError(t, err)
}
})
}
// TestTaskResultsCallbackRequestOptions_Marshal tests whether you can properly serialise a TaskResultCallbackRequestOptions object
// You may find the expected body here: https://developer.hashicorp.com/terraform/enterprise/api-docs/run-tasks/run-tasks-integration#request-body-1
func TestTaskResultsCallbackRequestOptions_Marshal(t *testing.T) {
t.Parallel()
opts := TaskResultCallbackRequestOptions{
Status: TaskPassed,
Message: "4 passed, 0 skipped, 0 failed",
URL: "https://external.service.dev/terraform-plan-checker/run-i3Df5to9ELvibKpQ",
Outcomes: []*TaskResultOutcome{
{
OutcomeID: "PRTNR-CC-TF-127",
Description: "ST-2942:S3 Bucket will not enforce MFA login on delete requests",
Body: "# Resolution for issue ST-2942\n\n## Impact\n\nFollow instructions in the [AWS S3 docs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/MultiFactorAuthenticationDelete.html) to manually configure the MFA setting.\n—-- Payload truncated —--",
URL: "https://external.service.dev/result/PRTNR-CC-TF-127",
Tags: map[string][]*TaskResultTag{
"Status": {&TaskResultTag{Label: "Denied", Level: "error"}},
"Severity": {
&TaskResultTag{Label: "High", Level: "error"},
&TaskResultTag{Label: "Recoverable", Level: "info"},
},
"Cost Centre": {&TaskResultTag{Label: "IT-OPS"}},
},
},
},
}
require.NoError(t, opts.valid())
reqBody, err := serializeRequestBody(&opts)
require.NoError(t, err)
expectedBody := `{"data":{"type":"task-results","attributes":{"message":"4 passed, 0 skipped, 0 failed","status":"passed","url":"https://external.service.dev/terraform-plan-checker/run-i3Df5to9ELvibKpQ"},"relationships":{"outcomes":{"data":[{"type":"task-result-outcomes","attributes":{"body":"# Resolution for issue ST-2942\n\n## Impact\n\nFollow instructions in the [AWS S3 docs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/MultiFactorAuthenticationDelete.html) to manually configure the MFA setting.\n—-- Payload truncated —--","description":"ST-2942:S3 Bucket will not enforce MFA login on delete requests","outcome-id":"PRTNR-CC-TF-127","tags":{"Cost Centre":[{"label":"IT-OPS"}],"Severity":[{"label":"High","level":"error"},{"label":"Recoverable","level":"info"}],"Status":[{"label":"Denied","level":"error"}]},"url":"https://external.service.dev/result/PRTNR-CC-TF-127"}}]}}}}
`
buf, ok := reqBody.(*bytes.Buffer)
require.True(t, ok, "expected request body to be a bytes.Buffer")
assert.Equal(t, buf.String(), expectedBody)
}
func TestRunTasksIntegration_ValidateCallback(t *testing.T) {
t.Parallel()
t.Run("with invalid callbackURL", func(t *testing.T) {
trc := runTaskIntegration{client: nil}
err := trc.Callback(context.Background(), "", "", TaskResultCallbackRequestOptions{})
assert.EqualError(t, err, ErrInvalidCallbackURL.Error())
})
t.Run("with invalid accessToken", func(t *testing.T) {
trc := runTaskIntegration{client: nil}
err := trc.Callback(context.Background(), "https://app.terraform.io/foo", "", TaskResultCallbackRequestOptions{})
assert.EqualError(t, err, ErrInvalidAccessToken.Error())
})
}
func TestRunTasksIntegration_Callback(t *testing.T) {
t.Parallel()
ts := runTaskCallbackMockServer(t)
defer ts.Close()
client, err := NewClient(&Config{
RetryServerErrors: true,
Token: testInitialClientToken,
Address: ts.URL,
})
require.NoError(t, err)
trc := runTaskIntegration{
client: client,
}
req := RunTaskRequest{
AccessToken: testTaskResultCallbackToken,
TaskResultCallbackURL: ts.URL,
}
err = trc.Callback(context.Background(), req.TaskResultCallbackURL, req.AccessToken, TaskResultCallbackRequestOptions{Status: TaskPassed})
require.NoError(t, err)
}
================================================
FILE: run_trigger.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ RunTriggers = (*runTriggers)(nil)
// RunTriggers describes all the Run Trigger
// related methods that the HCP Terraform API supports.
//
// TFE API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run-triggers
type RunTriggers interface {
// List all the run triggers within a workspace.
List(ctx context.Context, workspaceID string, options *RunTriggerListOptions) (*RunTriggerList, error)
// Create a new run trigger with the given options.
Create(ctx context.Context, workspaceID string, options RunTriggerCreateOptions) (*RunTrigger, error)
// Read a run trigger by its ID.
Read(ctx context.Context, RunTriggerID string) (*RunTrigger, error)
// ReadWithOptions reads a run trigger by its ID using the options supplied
ReadWithOptions(ctx context.Context, runID string, options *RunTriggerReadOptions) (*RunTrigger, error)
// Delete a run trigger by its ID.
Delete(ctx context.Context, RunTriggerID string) error
}
// runTriggers implements RunTriggers.
type runTriggers struct {
client *Client
}
// RunTriggerList represents a list of Run Triggers
type RunTriggerList struct {
*Pagination
Items []*RunTrigger
}
// SourceableChoice is a choice type struct that represents the possible values
// within a polymorphic relation. If a value is available, exactly one field
// will be non-nil.
type SourceableChoice struct {
Workspace *Workspace
}
// RunTrigger represents a run trigger.
type RunTrigger struct {
ID string `jsonapi:"primary,run-triggers"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
SourceableName string `jsonapi:"attr,sourceable-name"`
WorkspaceName string `jsonapi:"attr,workspace-name"`
// DEPRECATED. The sourceable field is polymorphic. Use SourceableChoice instead.
Sourceable *Workspace `jsonapi:"relation,sourceable"`
SourceableChoice *SourceableChoice `jsonapi:"polyrelation,sourceable"`
Workspace *Workspace `jsonapi:"relation,workspace"`
}
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run-triggers#query-parameters
type RunTriggerFilterOp string
const (
RunTriggerOutbound RunTriggerFilterOp = "outbound" // create runs in other workspaces.
RunTriggerInbound RunTriggerFilterOp = "inbound" // create runs in the specified workspace
)
// A list of relations to include
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run-triggers#available-related-resources
type RunTriggerIncludeOpt string
const (
RunTriggerWorkspace RunTriggerIncludeOpt = "workspace"
RunTriggerSourceable RunTriggerIncludeOpt = "sourceable"
)
// RunTriggerListOptions represents the options for listing
// run triggers.
type RunTriggerListOptions struct {
ListOptions
RunTriggerType RunTriggerFilterOp `url:"filter[run-trigger][type]"` // Required
Include []RunTriggerIncludeOpt `url:"include,omitempty"` // optional
}
// RunTriggerCreateOptions represents the options for
// creating a new run trigger.
type RunTriggerCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,run-triggers"`
// The source workspace
Sourceable *Workspace `jsonapi:"relation,sourceable"`
}
// RunTriggerCreateOptions represents the options for reading a run.
type RunTriggerReadOptions struct {
Include []RunTriggerIncludeOpt `url:"include,omitempty"` // optional`
}
// List all the run triggers associated with a workspace.
func (s *runTriggers) List(ctx context.Context, workspaceID string, options *RunTriggerListOptions) (*RunTriggerList, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("workspaces/%s/run-triggers", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
rtl := &RunTriggerList{}
err = req.Do(ctx, rtl)
if err != nil {
return nil, err
}
for i := range rtl.Items {
backfillDeprecatedSourceable(rtl.Items[i])
}
return rtl, nil
}
// Create a run trigger with the given options.
func (s *runTriggers) Create(ctx context.Context, workspaceID string, options RunTriggerCreateOptions) (*RunTrigger, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("workspaces/%s/run-triggers", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
rt := &RunTrigger{}
err = req.Do(ctx, rt)
if err != nil {
return nil, err
}
backfillDeprecatedSourceable(rt)
return rt, nil
}
// Read a run trigger by its ID.
func (s *runTriggers) Read(ctx context.Context, runTriggerID string) (*RunTrigger, error) {
return s.ReadWithOptions(ctx, runTriggerID, nil)
}
// Read a run trigger by its ID.
func (s *runTriggers) ReadWithOptions(ctx context.Context, runTriggerID string, options *RunTriggerReadOptions) (*RunTrigger, error) {
if !validStringID(&runTriggerID) {
return nil, ErrInvalidRunTriggerID
}
u := fmt.Sprintf("run-triggers/%s", url.PathEscape(runTriggerID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
rt := &RunTrigger{}
err = req.Do(ctx, rt)
if err != nil {
return nil, err
}
backfillDeprecatedSourceable(rt)
return rt, nil
}
// Delete a run trigger by its ID.
func (s *runTriggers) Delete(ctx context.Context, runTriggerID string) error {
if !validStringID(&runTriggerID) {
return ErrInvalidRunTriggerID
}
u := fmt.Sprintf("run-triggers/%s", url.PathEscape(runTriggerID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o RunTriggerCreateOptions) valid() error {
if o.Sourceable == nil {
return ErrRequiredSourceable
}
return nil
}
func (o *RunTriggerListOptions) valid() error {
if o == nil {
return ErrRequiredRunTriggerListOps
}
if err := validateRunTriggerFilterParam(o.RunTriggerType, o.Include); err != nil {
return err
}
return nil
}
func backfillDeprecatedSourceable(runTrigger *RunTrigger) {
if runTrigger.Sourceable != nil || runTrigger.SourceableChoice == nil {
return
}
runTrigger.Sourceable = runTrigger.SourceableChoice.Workspace
}
func validateRunTriggerFilterParam(filterParam RunTriggerFilterOp, includeParams []RunTriggerIncludeOpt) error {
switch filterParam {
case RunTriggerOutbound, RunTriggerInbound:
// Do nothing
default:
return ErrInvalidRunTriggerType // return an error even if string is empty because this a required field
}
if len(includeParams) > 0 {
if filterParam != RunTriggerInbound {
return ErrUnsupportedRunTriggerType // if user passes RunTriggerOutbound the platform will not return any "include" data
}
}
return nil
}
================================================
FILE: run_trigger_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRunTriggerList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
sourceable1Test, sourceable1TestCleanup := createWorkspace(t, client, orgTest)
defer sourceable1TestCleanup()
sourceable2Test, sourceable2TestCleanup := createWorkspace(t, client, orgTest)
defer sourceable2TestCleanup()
rtTest1, rtTestCleanup1 := createRunTrigger(t, client, wTest, sourceable1Test)
defer rtTestCleanup1()
rtTest2, rtTestCleanup2 := createRunTrigger(t, client, wTest, sourceable2Test)
defer rtTestCleanup2()
t.Run("without ListOptions and with RunTriggerType", func(t *testing.T) {
rtl, err := client.RunTriggers.List(
ctx,
wTest.ID,
&RunTriggerListOptions{
RunTriggerType: RunTriggerInbound,
},
)
require.NoError(t, err)
assert.Contains(t, rtl.Items, rtTest1)
assert.Contains(t, rtl.Items, rtTest2)
assert.Equal(t, 1, rtl.CurrentPage)
assert.Equal(t, 2, rtl.TotalCount)
})
t.Run("with ListOptions and a RunTriggerType", func(t *testing.T) {
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
rtl, err := client.RunTriggers.List(
ctx,
wTest.ID,
&RunTriggerListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
RunTriggerType: RunTriggerInbound,
},
)
require.NoError(t, err)
assert.Empty(t, rtl.Items)
assert.Equal(t, 999, rtl.CurrentPage)
assert.Equal(t, 2, rtl.TotalCount)
})
t.Run("without a valid workspace", func(t *testing.T) {
rtl, err := client.RunTriggers.List(
ctx,
badIdentifier,
&RunTriggerListOptions{
RunTriggerType: RunTriggerInbound,
},
)
assert.Nil(t, rtl)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
t.Run("without defining RunTriggerListOptions", func(t *testing.T) {
rtl, err := client.RunTriggers.List(
ctx,
wTest.ID,
nil,
)
assert.Nil(t, rtl)
assert.Equal(t, err, ErrRequiredRunTriggerListOps)
})
t.Run("without defining RunTriggerFilterOp as a filter param", func(t *testing.T) {
rtl, err := client.RunTriggers.List(
ctx,
wTest.ID,
&RunTriggerListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
},
)
assert.Nil(t, rtl)
assert.Equal(t, err, ErrInvalidRunTriggerType)
})
t.Run("with invalid option for runTriggerType", func(t *testing.T) {
rtl, err := client.RunTriggers.List(
ctx,
wTest.ID,
&RunTriggerListOptions{
RunTriggerType: "oubound",
},
)
assert.Nil(t, rtl)
assert.Equal(t, err, ErrInvalidRunTriggerType)
})
t.Run("with sourceable include option", func(t *testing.T) {
rtl, err := client.RunTriggers.List(
ctx,
wTest.ID,
&RunTriggerListOptions{
RunTriggerType: RunTriggerInbound,
Include: []RunTriggerIncludeOpt{RunTriggerSourceable},
},
)
require.NoError(t, err)
require.NotEmpty(t, rtl.Items)
require.NotNil(t, rtl.Items[0].Sourceable)
assert.NotEmpty(t, rtl.Items[0].Sourceable)
assert.NotNil(t, rtl.Items[0].SourceableChoice.Workspace)
assert.NotEmpty(t, rtl.Items[0].SourceableChoice.Workspace)
})
t.Run("with a RunTriggerType that does not return included data", func(t *testing.T) {
_, err := client.RunTriggers.List(
ctx,
wTest.ID,
&RunTriggerListOptions{
RunTriggerType: RunTriggerOutbound,
Include: []RunTriggerIncludeOpt{RunTriggerSourceable},
},
)
assert.Equal(t, err, ErrUnsupportedRunTriggerType)
})
}
func TestRunTriggerCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
sourceableTest, sourceableTestCleanup := createWorkspace(t, client, orgTest)
defer sourceableTestCleanup()
t.Run("with all required values", func(t *testing.T) {
options := RunTriggerCreateOptions{
Sourceable: sourceableTest,
}
_, err := client.RunTriggers.Create(ctx, wTest.ID, options)
require.NoError(t, err)
})
t.Run("without a required value", func(t *testing.T) {
options := RunTriggerCreateOptions{}
rt, err := client.RunTriggers.Create(ctx, wTest.ID, options)
assert.Nil(t, rt)
assert.Equal(t, err, ErrRequiredSourceable)
})
t.Run("without a valid workspace", func(t *testing.T) {
rt, err := client.RunTriggers.Create(ctx, badIdentifier, RunTriggerCreateOptions{})
assert.Nil(t, rt)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
t.Run("when an error is returned from the api", func(t *testing.T) {
// There are many cases that would cause the server to return an error
// on run trigger creation. This tests one of them: setting workspace
// and sourceable to the same workspace
options := RunTriggerCreateOptions{
Sourceable: sourceableTest,
}
rt, err := client.RunTriggers.Create(ctx, sourceableTest.ID, options)
assert.Nil(t, rt)
assert.Error(t, err)
})
}
func TestRunTriggerRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
sourceableTest, sourceableTestCleanup := createWorkspace(t, client, orgTest)
defer sourceableTestCleanup()
rtTest, rtTestCleanup := createRunTrigger(t, client, wTest, sourceableTest)
defer rtTestCleanup()
t.Run("with a valid ID", func(t *testing.T) {
rt, err := client.RunTriggers.Read(ctx, rtTest.ID)
require.NoError(t, err)
assert.Equal(t, rtTest.ID, rt.ID)
})
t.Run("when the run trigger does not exist", func(t *testing.T) {
_, err := client.RunTriggers.Read(ctx, "nonexisting")
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the run trigger ID is invalid", func(t *testing.T) {
_, err := client.RunTriggers.Read(ctx, badIdentifier)
assert.Equal(t, err, ErrInvalidRunTriggerID)
})
}
func TestRunTriggerReadWithOptions(t *testing.T) {
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
sourceableTest, sourceableTestCleanup := createWorkspace(t, client, orgTest)
defer sourceableTestCleanup()
rtTest, rtTestCleanup := createRunTrigger(t, client, wTest, sourceableTest)
defer rtTestCleanup()
t.Run("with include options", func(t *testing.T) {
rt, err := client.RunTriggers.ReadWithOptions(ctx, rtTest.ID, &RunTriggerReadOptions{
Include: []RunTriggerIncludeOpt{RunTriggerSourceable, RunTriggerWorkspace},
})
require.NoError(t, err)
assert.Equal(t, rtTest.ID, rt.ID)
require.NotNil(t, rt.Sourceable)
assert.Equal(t, sourceableTest.ID, rt.Sourceable.ID)
require.NotNil(t, rt.Workspace)
assert.Equal(t, wTest.ID, rt.Workspace.ID)
})
}
func TestRunTriggerDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
sourceableTest, sourceableTestCleanup := createWorkspace(t, client, orgTest)
defer sourceableTestCleanup()
// No need to cleanup here, as this test will delete this run trigger
rtTest, _ := createRunTrigger(t, client, wTest, sourceableTest)
t.Run("with a valid ID", func(t *testing.T) {
err := client.RunTriggers.Delete(ctx, rtTest.ID)
require.NoError(t, err)
_, err = client.RunTriggers.Read(ctx, rtTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the run trigger does not exist", func(t *testing.T) {
err := client.RunTriggers.Delete(ctx, "nonexisting")
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the run trigger ID is invalid", func(t *testing.T) {
err := client.RunTriggers.Delete(ctx, badIdentifier)
assert.Equal(t, err, ErrInvalidRunTriggerID)
})
}
================================================
FILE: scripts/generate_resource/go.mod
================================================
module generate_resource
go 1.17
require github.com/iancoleman/strcase v0.2.0
require github.com/gertd/go-pluralize v0.2.1
================================================
FILE: scripts/generate_resource/go.sum
================================================
github.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA=
github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk=
github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
================================================
FILE: scripts/generate_resource/main.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package main
import (
"fmt"
"log"
"os"
"regexp"
"strings"
"text/template"
"github.com/gertd/go-pluralize"
"github.com/iancoleman/strcase"
)
var validResourceName = regexp.MustCompile(`^[a-zA-Z_]+$`).MatchString
func generateResourceTemplate(name string) ResourceTemplate {
var pluralName string
pluralize := pluralize.NewClient()
if pluralize.IsPlural(name) {
pluralName = name
name = pluralize.Singular(name)
} else {
pluralName = pluralize.Plural(name)
}
camelName := strcase.ToCamel(name)
return ResourceTemplate{
PrimaryTag: strings.ReplaceAll(name, "_", "-"),
Name: strings.ReplaceAll(name, "_", " "),
PluralName: strings.ReplaceAll(pluralName, "_", " "),
Resource: camelName,
ResourceInterface: strcase.ToCamel(pluralName),
ResourceStruct: strcase.ToLowerCamel(pluralName),
ResourceID: fmt.Sprintf("%sID", camelName),
ListOptions: fmt.Sprintf("%sListOptions", camelName),
ReadOptions: fmt.Sprintf("%sReadOptions", camelName),
CreateOptions: fmt.Sprintf("%sCreateOptions", camelName),
UpdateOptions: fmt.Sprintf("%sUpdateOptions", camelName),
}
}
func main() {
var resourceName string
if len(os.Args) < 2 {
log.Fatal("usage: ")
}
if os.Args[1] == "-h" {
fmt.Println(helpTemplate)
return
} else {
resourceName = strings.ToLower(os.Args[1])
}
if !validResourceName(resourceName) {
log.Fatal("resource name can only contain letters or underscores.")
}
resourceTmpl := generateResourceTemplate(resourceName)
tmp, err := template.New("source").Parse(sourceTemplate)
if err != nil {
log.Fatal(err)
}
sourceFile, err := os.Create("../../" + resourceName + ".go")
if err != nil {
log.Fatal(err)
}
defer sourceFile.Close()
fmt.Printf("Generating %s.go\n", resourceName)
err = tmp.Execute(sourceFile, resourceTmpl)
if err != nil {
log.Fatal(err)
}
tmp, err = template.New("source").Parse(testTemplate)
if err != nil {
log.Fatal(err)
}
testFile, err := os.Create("../../" + resourceName + "_integration_test.go")
if err != nil {
log.Fatal(err)
}
defer testFile.Close()
fmt.Printf("Generating %s_integration_test.go\n", resourceName)
err = tmp.Execute(testFile, resourceTmpl)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Done generating files for new resource: %s\n", resourceTmpl.Resource)
}
================================================
FILE: scripts/generate_resource/templates.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package main
type ResourceTemplate struct {
// Lower cased name of a resource, not plural
Name string
// Lower cased name of a resource, plural
PluralName string
// Name of resource model
Resource string
// Name of resource interface
ResourceInterface string
// Name of resource struct that implements resource interface
ResourceStruct string
// The resource ID
ResourceID string
// Struct tag name for (un)marshalling the JSON+API resource.
PrimaryTag string
ListOptions string
ReadOptions string
CreateOptions string
UpdateOptions string
}
const helpTemplate = `
This script is used to quickly scaffold a resource in go-tfe. Simply provide a
resource name as the first argument and it will generate standard boilerplate.
Note: A resource name can only contain letters and underscores.
Allowed: policy_set, Run_task, orGanizatIon
Not Allowed: policy123, #user, my cool resource
If your resource contains multiple terms, e.g policy set, you must use an
underscore delimiter for each term in order to generate proper casing in your
code. For example, if you wanted to generate the policy set resource as
PolicySet you would pass policy_set as your argument.
Example usage: go run ./scripts/generate_resource/main.go policy_set`
const sourceTemplate = `
package tfe
import (
"context"
)
var _ {{ .ResourceInterface }} = (*{{ .ResourceStruct }})(nil)
// {{ .ResourceInterface }} describes all the {{ .Name }} related methods that the Terraform
// Enterprise API supports
//
// TFE API docs: (TODO: ADD DOCS URL)
type {{ .ResourceInterface }} interface {
// List all {{ .PluralName }}.
List(ctx context.Context, options *{{ .ListOptions }}) (*{{ .Resource }}List, error)
// Create a {{ .Name }}.
Create(ctx context.Context, options {{ .CreateOptions }}) (*{{ .Resource }}, error)
// Read a {{ .Name }} by its ID.
Read(ctx context.Context, {{ .ResourceID }} string) (*{{ .Resource }}, error)
// Read a {{ .Name }} by its ID with options.
ReadWithOptions(ctx context.Context, {{ .ResourceID }} string, options *{{ .ReadOptions }}) (*{{ .Resource }}, error)
// Update a {{ .Name }}.
Update(ctx context.Context, {{ .ResourceID }} string, options {{ .UpdateOptions }}) (*{{ .Resource }}, error)
// Delete a {{ .Name }}.
Delete(ctx context.Context, {{ .ResourceID }} string) error
}
// {{ .ResourceStruct }} implements {{ .ResourceInterface }}
type {{ .ResourceStruct }} struct {
client *Client
}
// {{ .Resource }}List represents a list of {{ .PluralName }}
type {{ .Resource }}List struct {
*Pagination
Items []*{{ .Resource }}
}
// {{ .Resource }} represents a Terraform Enterprise $resource
type {{ .Resource }} struct {
ID string ` + "`jsonapi:\"primary," + `{{ .PrimaryTag }}` + "\"`" + `
// Add more fields here
}
// {{ .ListOptions }} represents the options for listing {{ .PluralName }}
type {{ .ListOptions }} struct {
ListOptions
// Add more list options here
}
// {{ .CreateOptions }} represents the options for creating a {{ .Name }}
type {{ .CreateOptions }} struct {
Type string ` + "`jsonapi:\"primary," + `{{ .PrimaryTag }}` + "\"`" + `
// Add more create options here
}
// {{ .ReadOptions }} represents the options for reading a {{ .Name }}
type {{ .ReadOptions }} struct {
// Add more read options here
}
// {{ .UpdateOptions }} represents the options for updating a {{ .Name }}
type {{ .UpdateOptions }} struct {
ID string ` + "`jsonapi:\"primary," + `{{ .PrimaryTag }}` + "\"`" + `
// Add more update options here
}
// List all {{ .PluralName }}.
func List(ctx context.Context, options *{{ .ListOptions }}) (*{{ .Resource }}List, error) {
panic("not yet implemented")
}
// Create a {{ .Name }}.
func Create(ctx context.Context, options {{ .CreateOptions }}) (*{{ .Resource }}, error) {
panic("not yet implemented")
}
// Read a {{ .Name }} by its ID.
func Read(ctx context.Context, {{ .ResourceID }} string) (*{{ .Resource }}, error) {
panic("not yet implemented")
}
// Read a {{ .Name }} by its ID with options.
func ReadWithOptions(ctx context.Context, {{ .ResourceID }} string, options *{{ .ReadOptions }}) (*{{ .Resource }}, error) {
panic("not yet implemented")
}
// Update a {{ .Name }}.
func Update(ctx context.Context, {{ .ResourceID }} string, options {{ .UpdateOptions }}) (*{{ .Resource }}, error) {
panic("not yet implemented")
}
// Delete a {{ .Name }}.
func Delete(ctx context.Context, {{ .ResourceID }} string) error {
panic("not yet implemented")
}`
const testTemplate = `package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test{{ .ResourceInterface }}List(t *testing.T) {
client := testClient(t)
ctx := context.Background()
// Create your test helper resources here
t.Run("test not yet implemented", func(t *testing.T) {
require.NotNil(t, nil)
})
}
func Test{{ .ResourceInterface }}Read(t *testing.T) {
client := testClient(t)
ctx := context.Background()
// Create your test helper resources here
t.Run("test not yet implemented", func(t *testing.T) {
require.NotNil(t, nil)
})
}
func Test{{ .ResourceInterface }}Create(t *testing.T) {
client := testClient(t)
ctx := context.Background()
// Create your test helper resources here
t.Run("test not yet implemented", func(t *testing.T) {
require.NotNil(t, nil)
})
}
func Test{{ .ResourceInterface }}Update(t *testing.T) {
client := testClient(t)
ctx := context.Background()
// Create your test helper resources here
t.Run("test not yet implemented", func(t *testing.T) {
require.NotNil(t, nil)
})
}`
================================================
FILE: scripts/gofmtcheck.sh
================================================
#!/usr/bin/env bash
# Copyright IBM Corp. 2018, 2025
# SPDX-License-Identifier: MPL-2.0
if ! gofmt -l -s .; then
echo "gofmt found some files that need to be formatted. You can use the command: \`make fmt\` to reformat code."
exit 1
fi
exit 0
================================================
FILE: scripts/hyok-testing.sh
================================================
#!/bin/bash
env="STAGING_ENVCHAIN"
pairs=(
# HYOK Attributes testing
# -- Agent Pools
"TestAgentPoolsRead:read_hyok_configurations_of_an_agent_pool"
# -- Plans
"TestPlansRead:read_hyok_encrypted_data_key_of_a_plan"
"TestPlansRead:read_sanitized_plan_of_a_plan"
# -- Workspaces
"TestWorkspacesCreate:create_workspace_with_hyok_enabled_set_to_false"
"TestWorkspacesCreate:create_workspace_with_hyok_enabled_set_to_true"
"TestWorkspacesRead:read_hyok_enabled_of_a_workspace"
"TestWorkspacesRead:read_hyok_encrypted_data_key_of_a_workspace"
"TestWorkspacesUpdate:update_hyok_enabled_of_a_workspace_from_false_to_false"
"TestWorkspacesUpdate:update_hyok_enabled_of_a_workspace_from_false_to_true"
"TestWorkspacesUpdate:update_hyok_enabled_of_a_workspace_from_true_to_true"
"TestWorkspacesUpdate:update_hyok_enabled_of_a_workspace_from_true_to_false"
# -- Organizations
"TestOrganizationsRead:read_primary_hyok_configuration_of_an_organization"
"TestOrganizationsRead:read_enforce_hyok_of_an_organization"
"TestOrganizationsUpdate:update_enforce_hyok_of_an_organization_to_true"
"TestOrganizationsUpdate:update_enforce_hyok_of_an_organization_to_false"
# -- State Versions
"TestStateVersionsRead:read_encrypted_state_download_url_of_a_state_version"
"TestStateVersionsRead:read_sanitized_state_download_url_of_a_state_version"
"TestStateVersionsRead:read_hyok_encrypted_data_key_of_a_state_version"
"TestStateVersionsUpload:uploading_state_using_SanitizedStateUploadURL_and_verifying_SanitizedStateDownloadURL_exists"
"TestStateVersionsUpload:SanitizedStateUploadURL_is_required_when_uploading_sanitized_state"
# AWS OIDC Configuration testing
"TestAWSOIDCConfigurationCreateDelete:with_valid_options"
"TestAWSOIDCConfigurationCreateDelete:missing_role_ARN"
"TestAWSOIDCConfigurationRead:fetch_existing_configuration"
"TestAWSOIDCConfigurationRead:fetching_non-existing_configuration"
"TestAWSOIDCConfigurationsUpdate:with_valid_options"
"TestAWSOIDCConfigurationsUpdate:missing_role_ARN"
# Azure OIDC Configuration testing
"TestAzureOIDCConfigurationCreateDelete:with_valid_options"
"TestAzureOIDCConfigurationCreateDelete:missing_client_ID"
"TestAzureOIDCConfigurationCreateDelete:missing_subscription_ID"
"TestAzureOIDCConfigurationCreateDelete:missing_tenant_ID"
"TestAzureOIDCConfigurationRead:fetch_existing_configuration"
"TestAzureOIDCConfigurationRead:fetching_non-existing_configuration"
"TestAzureOIDCConfigurationUpdate:update_all_fields"
"TestAzureOIDCConfigurationUpdate:client_ID_not_provided"
"TestAzureOIDCConfigurationUpdate:subscription_ID_not_provided"
"TestAzureOIDCConfigurationUpdate:tenant_ID_not_provided"
# GCP OIDC Configuration testing
"TestGCPOIDCConfigurationCreateDelete:with_valid_options"
"TestGCPOIDCConfigurationCreateDelete:missing_workload_provider_name"
"TestGCPOIDCConfigurationCreateDelete:missing_service_account_email"
"TestGCPOIDCConfigurationCreateDelete:missing_project_number"
"TestGCPOIDCConfigurationRead:fetch_existing_configuration"
"TestGCPOIDCConfigurationRead:fetching_non-existing_configuration"
"TestGCPOIDCConfigurationUpdate:update_all_fields"
"TestGCPOIDCConfigurationUpdate:workload_provider_name_not_provided"
"TestGCPOIDCConfigurationUpdate:service_account_email_not_provided"
"TestGCPOIDCConfigurationUpdate:project_number_not_provided"
# Vault OIDC Configuration testing
"TestVaultOIDCConfigurationCreateDelete:with_valid_options"
"TestVaultOIDCConfigurationCreateDelete:missing_address"
"TestVaultOIDCConfigurationCreateDelete:missing_role_name"
"TestVaultOIDCConfigurationRead:fetch_existing_configuration"
"TestVaultOIDCConfigurationRead:fetching_non-existing_configuration"
"TestVaultOIDCConfigurationUpdate:update_all_fields"
"TestVaultOIDCConfigurationUpdate:address_not_provided"
"TestVaultOIDCConfigurationUpdate:role_name_not_provided"
"TestVaultOIDCConfigurationUpdate:namespace_not_provided"
"TestVaultOIDCConfigurationUpdate:JWTAuthPath_not_provided"
"TestVaultOIDCConfigurationUpdate:TLSCACertificate_not_provided"
# HYOK Customer Key Version testing
"TestHYOKCustomerKeyVersionsList:with_no_list_options"
"TestHYOKCustomerKeyVersionsRead:read_an_existing_key_version"
# HYOK Encrypted Data Key testing
"TestHYOKEncryptedDataKeyRead:read_an_existing_encrypted_data_key"
# HYOK Configurations testing
"TestHYOKConfigurationCreateRevokeDelete:AWS_with_valid_options"
"TestHYOKConfigurationCreateRevokeDelete:AWS_with_missing_key_region"
"TestHYOKConfigurationCreateRevokeDelete:GCP_with_valid_options"
"TestHYOKConfigurationCreateRevokeDelete:GCP_with_missing_key_location"
"TestHYOKConfigurationCreateRevokeDelete:GCP_with_missing_key_ring_ID"
"TestHYOKConfigurationCreateRevokeDelete:Vault_with_valid_options"
"TestHYOKConfigurationCreateRevokeDelete:Azure_with_valid_options"
"TestHYOKConfigurationCreateRevokeDelete:with_missing_KEK_ID"
"TestHYOKConfigurationCreateRevokeDelete:with_missing_agent_pool"
"TestHYOKConfigurationCreateRevokeDelete:with_missing_OIDC_config"
"TestHyokConfigurationList:without_list_options"
"TestHyokConfigurationRead:AWS"
"TestHyokConfigurationRead:Azure"
"TestHyokConfigurationRead:GCP"
"TestHyokConfigurationRead:Vault"
"TestHyokConfigurationRead:fetching_non-existing_configuration"
"TestHYOKConfigurationUpdate:AWS_with_valid_options"
"TestHYOKConfigurationUpdate:GCP_with_valid_options"
"TestHYOKConfigurationUpdate:Vault_with_valid_options"
"TestHYOKConfigurationUpdate:Azure_with_valid_options"
)
for pair in "${pairs[@]}"; do
IFS=':' read -r parent child <<< "$pair"
result=$(envchain ${env} go test -run "^${parent}$/^${child}$" -v ./...)
status="\033[33mUNKNOWN\033[0m" # yellow by default
if echo "$result" | grep -q "^ --- SKIP: ${parent}/${child}"; then
status="\033[33mSKIP\033[0m" # yellow
elif echo "$result" | grep -q "^--- PASS: ${parent}"; then
status="\033[32mPASS\033[0m" # green
elif echo "$result" | grep -q "^--- FAIL: ${parent}"; then
status="\033[31mFAIL\033[0m" # red
fi
echo -e "\033[34m${parent}/${child}\033[0m: ${status}"
done
================================================
FILE: scripts/rebase-fork.sh
================================================
#!/usr/bin/env bash
# Copyright IBM Corp. 2018, 2025
# SPDX-License-Identifier: MPL-2.0
if [[ -z $1 ]]; then
echo "Please specify the pull request number you want to rebase to a local branch."
echo "Usage: ./scripts/rebase-fork.sh "
echo ""
echo "Example: ./scripts/rebase-fork.sh 557"
exit 1
fi
PR_NUMBER=$1
declare -a req_tools=("gh" "git" "jq")
for tool in "${req_tools[@]}"; do
if ! command -v "${tool}" > /dev/null; then
echo "It looks like '${tool}' is not installed; please install it and run this script again."
exit 1
fi
done
# Check if the PR specified is a valid number
re='^[0-9]+$'
if ! [[ ${PR_NUMBER} =~ ${re} ]] ; then
echo "The PR you specify must be a valid integer number." >&2; exit 1
fi
# Check if the specified PR exists
# We only capture stderr here and redirect stdout to /dev/null
errormsg=$(gh pr view ${PR_NUMBER} 2>&1 1>/dev/null)
if [[ ! -z ${errormsg} ]]; then
# strip GraphQL log prefix to keep the error message clean
errormsg=${errormsg#"GraphQL: "}
echo "Failed to fetch pull request #${PR_NUMBER}: ${errormsg}"
exit 1
fi
# Check if the pull request we want to rebase is already closed. If so
# exit.
closed=$(gh pr view ${PR_NUMBER} --json closed | jq '.closed')
if [[ $closed = "true" ]]; then
echo "The pull request #${PR_NUMBER} to rebase is already closed."
exit 1
fi
# Save the name of the current branch we're in so we can go back to it after
# we are done
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
# Checkout the fork PR locally
gh pr checkout ${PR_NUMBER}
# Grab the PR title and branch name
FORK_PR_TITLE=$(gh pr view ${PR_NUMBER} --json title | jq '.title' | tr -d '"')
FORK_PR_BRANCH=$(gh pr view ${PR_NUMBER} --json headRefName | jq '.headRefName')
# Fetch the username of the user currently authenticated with gh cli
USER=$(gh api user | jq -r '.login')
# Name of the local branch that will be pushed upstream
LOCAL_BRANCH="${USER}/$(echo ${FORK_PR_BRANCH} | tr -d '"')"
# Fetch the PR body and write to local markdown file
gh pr view ${PR_NUMBER} --json body | jq -r '.body' > ${PR_NUMBER}.md
git checkout -b ${LOCAL_BRANCH}
git commit --allow-empty -m "Rebased ${FORK_PR_BRANCH} onto a local branch"
git push -u origin ${LOCAL_BRANCH}
# Finally we can automagically open a new PR using the fork PR's original title
# and description
gh pr create --title="${FORK_PR_TITLE}" --body-file=${PR_NUMBER}.md
# Cleanup
rm ${PR_NUMBER}.md
git checkout ${CURRENT_BRANCH}
================================================
FILE: scripts/setup-test-envvars.sh
================================================
#!/usr/bin/env bash -e
# Copyright IBM Corp. 2018, 2025
# SPDX-License-Identifier: MPL-2.0
# setup-test-envvars.sh
#
# A helper script that uses envchain (https://github.com/sorah/envchain) to set environment variables for tests.
# It's required to have TFE_ADDRESS and TFE_TOKEN set, the others are optional.
#
type envchain >/dev/null 2>&1 || { echo >&2 "Required executable 'envchain' not found - install it via 'brew install envchain'. Exiting."; exit 1; }
echo "Set environment variables (envvars) for running tests locally"
echo " envchain will prompt you for values for 4 envvars"
echo " TFE_ADDRESS and TFE_TOKEN are required, the others are optional,"
echo " press 'return' to skip them"
echo ""
read -p "Enter the namespace you want to use in envchain [go-tfe]: " namespace
namespace=${namespace:-go-tfe}
envchain --set ${namespace} TFE_ADDRESS TFE_TOKEN OAUTH_CLIENT_GITHUB_TOKEN GITHUB_POLICY_SET_IDENTIFIER
echo "Done! To see the values: envchain ${namespace} env"
================================================
FILE: ssh_key.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ SSHKeys = (*sshKeys)(nil)
// SSHKeys describes all the SSH key related methods that the Terraform
// Enterprise API supports.
//
// TFE API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/ssh-keys
type SSHKeys interface {
// List all the SSH keys for a given organization
List(ctx context.Context, organization string, options *SSHKeyListOptions) (*SSHKeyList, error)
// Create an SSH key and associate it with an organization.
Create(ctx context.Context, organization string, options SSHKeyCreateOptions) (*SSHKey, error)
// Read an SSH key by its ID.
Read(ctx context.Context, sshKeyID string) (*SSHKey, error)
// Update an SSH key by its ID.
Update(ctx context.Context, sshKeyID string, options SSHKeyUpdateOptions) (*SSHKey, error)
// Delete an SSH key by its ID.
Delete(ctx context.Context, sshKeyID string) error
}
// sshKeys implements SSHKeys.
type sshKeys struct {
client *Client
}
// SSHKeyList represents a list of SSH keys.
type SSHKeyList struct {
*Pagination
Items []*SSHKey
}
// SSHKey represents a SSH key.
type SSHKey struct {
ID string `jsonapi:"primary,ssh-keys"`
Name string `jsonapi:"attr,name"`
}
// SSHKeyListOptions represents the options for listing SSH keys.
type SSHKeyListOptions struct {
ListOptions
}
// SSHKeyCreateOptions represents the options for creating an SSH key.
type SSHKeyCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,ssh-keys"`
// A name to identify the SSH key.
Name *string `jsonapi:"attr,name"`
// The content of the SSH private key.
Value *string `jsonapi:"attr,value"`
}
// SSHKeyUpdateOptions represents the options for updating an SSH key.
type SSHKeyUpdateOptions struct {
// For internal use only!
ID string `jsonapi:"primary,ssh-keys"`
// Optional: A new name to identify the SSH key.
Name *string `jsonapi:"attr,name,omitempty"`
}
// List all the SSH keys for a given organization
func (s *sshKeys) List(ctx context.Context, organization string, options *SSHKeyListOptions) (*SSHKeyList, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf("organizations/%s/ssh-keys", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
kl := &SSHKeyList{}
err = req.Do(ctx, kl)
if err != nil {
return nil, err
}
return kl, nil
}
// Create an SSH key and associate it with an organization.
func (s *sshKeys) Create(ctx context.Context, organization string, options SSHKeyCreateOptions) (*SSHKey, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("organizations/%s/ssh-keys", url.PathEscape(organization))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
k := &SSHKey{}
err = req.Do(ctx, k)
if err != nil {
return nil, err
}
return k, nil
}
// Read an SSH key by its ID.
func (s *sshKeys) Read(ctx context.Context, sshKeyID string) (*SSHKey, error) {
if !validStringID(&sshKeyID) {
return nil, ErrInvalidSHHKeyID
}
u := fmt.Sprintf("ssh-keys/%s", url.PathEscape(sshKeyID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
k := &SSHKey{}
err = req.Do(ctx, k)
if err != nil {
return nil, err
}
return k, nil
}
// Update an SSH key by its ID.
func (s *sshKeys) Update(ctx context.Context, sshKeyID string, options SSHKeyUpdateOptions) (*SSHKey, error) {
if !validStringID(&sshKeyID) {
return nil, ErrInvalidSHHKeyID
}
u := fmt.Sprintf("ssh-keys/%s", url.PathEscape(sshKeyID))
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
k := &SSHKey{}
err = req.Do(ctx, k)
if err != nil {
return nil, err
}
return k, nil
}
// Delete an SSH key by its ID.
func (s *sshKeys) Delete(ctx context.Context, sshKeyID string) error {
if !validStringID(&sshKeyID) {
return ErrInvalidSHHKeyID
}
u := fmt.Sprintf("ssh-keys/%s", url.PathEscape(sshKeyID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o SSHKeyCreateOptions) valid() error {
if !validString(o.Name) {
return ErrRequiredName
}
if !validString(o.Value) {
return ErrRequiredValue
}
return nil
}
================================================
FILE: ssh_key_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSSHKeysList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
kTest1, kTestCleanup1 := createSSHKey(t, client, orgTest)
defer kTestCleanup1()
kTest2, kTestCleanup2 := createSSHKey(t, client, orgTest)
defer kTestCleanup2()
t.Run("without list options", func(t *testing.T) {
kl, err := client.SSHKeys.List(ctx, orgTest.Name, nil)
require.NoError(t, err)
assert.Contains(t, kl.Items, kTest1)
assert.Contains(t, kl.Items, kTest2)
t.Skip("paging not supported yet in API")
assert.Equal(t, 1, kl.CurrentPage)
assert.Equal(t, 2, kl.TotalCount)
})
t.Run("with list options", func(t *testing.T) {
t.Skip("paging not supported yet in API")
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
kl, err := client.SSHKeys.List(ctx, orgTest.Name, &SSHKeyListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, kl.Items)
assert.Equal(t, 999, kl.CurrentPage)
assert.Equal(t, 2, kl.TotalCount)
})
t.Run("without a valid organization", func(t *testing.T) {
kl, err := client.SSHKeys.List(ctx, badIdentifier, nil)
assert.Nil(t, kl)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
func TestSSHKeysCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
t.Run("with valid options", func(t *testing.T) {
options := SSHKeyCreateOptions{
Name: String(randomString(t)),
Value: String(randomString(t)),
}
k, err := client.SSHKeys.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
// Get a refreshed view from the API.
refreshed, err := client.SSHKeys.Read(ctx, k.ID)
require.NoError(t, err)
for _, item := range []*SSHKey{
k,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, *options.Name, item.Name)
}
})
t.Run("when options is missing name", func(t *testing.T) {
k, err := client.SSHKeys.Create(ctx, "foo", SSHKeyCreateOptions{
Value: String(randomString(t)),
})
assert.Nil(t, k)
assert.EqualError(t, err, ErrRequiredName.Error())
})
t.Run("when options is missing value", func(t *testing.T) {
k, err := client.SSHKeys.Create(ctx, "foo", SSHKeyCreateOptions{
Name: String(randomString(t)),
})
assert.Nil(t, k)
assert.Equal(t, err, ErrRequiredValue)
})
t.Run("when options has an invalid organization", func(t *testing.T) {
k, err := client.SSHKeys.Create(ctx, badIdentifier, SSHKeyCreateOptions{
Name: String("foo"),
})
assert.Nil(t, k)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
func TestSSHKeysRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
kTest, kTestCleanup := createSSHKey(t, client, orgTest)
defer kTestCleanup()
t.Run("when the SSH key exists", func(t *testing.T) {
k, err := client.SSHKeys.Read(ctx, kTest.ID)
require.NoError(t, err)
assert.Equal(t, kTest, k)
})
t.Run("when the SSH key does not exist", func(t *testing.T) {
k, err := client.SSHKeys.Read(ctx, "nonexisting")
assert.Nil(t, k)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("without a valid SSH key ID", func(t *testing.T) {
k, err := client.SSHKeys.Read(ctx, badIdentifier)
assert.Nil(t, k)
assert.Equal(t, err, ErrInvalidSHHKeyID)
})
}
func TestSSHKeysUpdate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
t.Run("with valid options", func(t *testing.T) {
kBefore, kTestCleanup := createSSHKey(t, client, orgTest)
defer kTestCleanup()
kAfter, err := client.SSHKeys.Update(ctx, kBefore.ID, SSHKeyUpdateOptions{
Name: String(randomString(t)),
})
require.NoError(t, err)
assert.Equal(t, kBefore.ID, kAfter.ID)
assert.NotEqual(t, kBefore.Name, kAfter.Name)
})
t.Run("when updating the name", func(t *testing.T) {
kBefore, kTestCleanup := createSSHKey(t, client, orgTest)
defer kTestCleanup()
kAfter, err := client.SSHKeys.Update(ctx, kBefore.ID, SSHKeyUpdateOptions{
Name: String("updated-key-name"),
})
require.NoError(t, err)
assert.Equal(t, kBefore.ID, kAfter.ID)
assert.Equal(t, "updated-key-name", kAfter.Name)
})
t.Run("without a valid SSH key ID", func(t *testing.T) {
w, err := client.SSHKeys.Update(ctx, badIdentifier, SSHKeyUpdateOptions{})
assert.Nil(t, w)
assert.Equal(t, err, ErrInvalidSHHKeyID)
})
}
func TestSSHKeysDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
kTest, _ := createSSHKey(t, client, orgTest)
t.Run("with valid options", func(t *testing.T) {
err := client.SSHKeys.Delete(ctx, kTest.ID)
require.NoError(t, err)
// Try loading the SSH key - it should fail.
_, err = client.SSHKeys.Read(ctx, kTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the SSH key does not exist", func(t *testing.T) {
err := client.SSHKeys.Delete(ctx, kTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the SSH key ID is invalid", func(t *testing.T) {
err := client.SSHKeys.Delete(ctx, badIdentifier)
assert.Equal(t, err, ErrInvalidSHHKeyID)
})
}
================================================
FILE: stack.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"time"
)
// Stacks describes all the stacks-related methods that the HCP Terraform API supports.
type Stacks interface {
// List returns a list of stacks, optionally filtered by project.
List(ctx context.Context, organization string, options *StackListOptions) (*StackList, error)
// Read returns a stack by its ID.
Read(ctx context.Context, stackID string) (*Stack, error)
// Create creates a new stack.
Create(ctx context.Context, options StackCreateOptions) (*Stack, error)
// Update updates a stack.
Update(ctx context.Context, stackID string, options StackUpdateOptions) (*Stack, error)
// Delete deletes a stack.
Delete(ctx context.Context, stackID string) error
// ForceDelete deletes a stack.
ForceDelete(ctx context.Context, stackID string) error
// FetchLatestFromVcs updates the configuration of a stack, triggering stack preparation.
FetchLatestFromVcs(ctx context.Context, stackID string) (*Stack, error)
}
// stacks implements Stacks.
type stacks struct {
client *Client
}
var _ Stacks = &stacks{}
// StackSortColumn represents a string that can be used to sort items when using
// the List method.
type StackSortColumn string
const (
// StackSortByName sorts by the name attribute.
StackSortByName StackSortColumn = "name"
// StackSortByUpdatedAt sorts by the updated-at attribute.
StackSortByUpdatedAt StackSortColumn = "updated-at"
// StackSortByNameDesc sorts by the name attribute in descending order.
StackSortByNameDesc StackSortColumn = "-name"
// StackSortByUpdatedAtDesc sorts by the updated-at attribute in descending order.
StackSortByUpdatedAtDesc StackSortColumn = "-updated-at"
)
// StackList represents a list of stacks.
type StackList struct {
*Pagination
Items []*Stack
}
// StackVCSRepo represents the version control system repository for a stack.
type StackVCSRepo struct {
Identifier string `jsonapi:"attr,identifier"`
Branch string `jsonapi:"attr,branch,omitempty"`
GHAInstallationID string `jsonapi:"attr,github-app-installation-id,omitempty"`
OAuthTokenID string `jsonapi:"attr,oauth-token-id,omitempty"`
}
// StackVCSRepoOptions
type StackVCSRepoOptions struct {
Identifier string `json:"identifier"`
Branch string `json:"branch,omitempty"`
GHAInstallationID string `json:"github-app-installation-id,omitempty"`
OAuthTokenID string `json:"oauth-token-id,omitempty"`
}
// Stack represents a stack.
type Stack struct {
ID string `jsonapi:"primary,stacks"`
Name string `jsonapi:"attr,name"`
Description string `jsonapi:"attr,description"`
VCSRepo *StackVCSRepo `jsonapi:"attr,vcs-repo"`
SpeculativeEnabled bool `jsonapi:"attr,speculative-enabled"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
UpstreamCount int `jsonapi:"attr,upstream-count"`
DownstreamCount int `jsonapi:"attr,downstream-count"`
InputsCount int `jsonapi:"attr,inputs-count"`
OutputsCount int `jsonapi:"attr,outputs-count"`
CreationSource string `jsonapi:"attr,creation-source"`
WorkingDirectory string `jsonapi:"attr,working-directory,omitempty"`
TriggerPatterns []string `jsonapi:"attr,trigger-patterns,omitempty"`
// Relationships
Project *Project `jsonapi:"relation,project"`
AgentPool *AgentPool `jsonapi:"relation,agent-pool"`
LatestStackConfiguration *StackConfiguration `jsonapi:"relation,latest-stack-configuration"`
}
// StackConfigurationStatusTimestamps represents the timestamps for a stack configuration
type StackConfigurationStatusTimestamps struct {
QueuedAt *time.Time `jsonapi:"attr,queued-at,omitempty,rfc3339"`
CompletedAt *time.Time `jsonapi:"attr,completed-at,omitempty,rfc3339"`
PreparingAt *time.Time `jsonapi:"attr,preparing-at,omitempty,rfc3339"`
EnqueueingAt *time.Time `jsonapi:"attr,enqueueing-at,omitempty,rfc3339"`
CanceledAt *time.Time `jsonapi:"attr,canceled-at,omitempty,rfc3339"`
ErroredAt *time.Time `jsonapi:"attr,errored-at,omitempty,rfc3339"`
}
// StackComponent represents a stack component, specified by configuration
type StackComponent struct {
Name string `json:"name"`
Correlator string `json:"correlator"`
Expanded bool `json:"expanded"`
Removed bool `json:"removed"`
}
// StackConfiguration represents a stack configuration snapshot
type StackConfiguration struct {
// Attributes
ID string `jsonapi:"primary,stack-configurations"`
Status StackConfigurationStatus `jsonapi:"attr,status"`
SequenceNumber int `jsonapi:"attr,sequence-number"`
Components []*StackComponent `jsonapi:"attr,components"`
PreparingEventStreamURL string `jsonapi:"attr,preparing-event-stream-url"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
Speculative bool `jsonapi:"attr,speculative"`
// Relationships
Stack *Stack `jsonapi:"relation,stack"`
IngressAttributes *IngressAttributes `jsonapi:"relation,ingress-attributes"`
}
// StackIncludeOpt represents the include options for a stack.
type StackIncludeOpt string
const (
StackIncludeOrganization StackIncludeOpt = "organization"
StackIncludeProject StackIncludeOpt = "project"
StackIncludeLatestStackConfiguration StackIncludeOpt = "latest_stack_configuration"
StackIncludeStackDiagnostics StackIncludeOpt = "latest_stack_configuration.stack_diagnostics"
)
// StackListOptions represents the options for listing stacks.
type StackListOptions struct {
ListOptions
ProjectID string `url:"filter[project][id],omitempty"`
Sort StackSortColumn `url:"sort,omitempty"`
SearchByName string `url:"search[name],omitempty"`
}
// StackCreateOptions represents the options for creating a stack. The project
// relation is required.
type StackCreateOptions struct {
Type string `jsonapi:"primary,stacks"`
Name string `jsonapi:"attr,name"`
Migration *bool `jsonapi:"attr,migration,omitempty"`
SpeculativeEnabled *bool `jsonapi:"attr,speculative-enabled,omitempty"`
Description *string `jsonapi:"attr,description,omitempty"`
VCSRepo *StackVCSRepoOptions `jsonapi:"attr,vcs-repo"`
Project *Project `jsonapi:"relation,project"`
AgentPool *AgentPool `jsonapi:"relation,agent-pool"`
WorkingDirectory *string `jsonapi:"attr,working-directory,omitempty"`
TriggerPatterns []string `jsonapi:"attr,trigger-patterns"`
}
// StackUpdateOptions represents the options for updating a stack.
type StackUpdateOptions struct {
Name *string `jsonapi:"attr,name,omitempty"`
Description *string `jsonapi:"attr,description,omitempty"`
SpeculativeEnabled *bool `jsonapi:"attr,speculative-enabled,omitempty"`
VCSRepo *StackVCSRepoOptions `jsonapi:"attr,vcs-repo"`
AgentPool *AgentPool `jsonapi:"relation,agent-pool"`
WorkingDirectory *string `jsonapi:"attr,working-directory,omitempty"`
TriggerPatterns []string `jsonapi:"attr,trigger-patterns"`
}
// WaitForStatusResult is the data structure that is sent over the channel
// returned by various status polling functions. For each result, either the
// Error or the Status will be set, but not both. If the Quit field is set,
// the channel will be closed. If the Quit field is set and the Error is
// nil, the Status field will be set to a specified quit status.
type WaitForStatusResult struct {
ID string
Status string
ReadAttempts int
Error error
Quit bool
}
const minimumPollingIntervalMs = 3000
const maximumPollingIntervalMs = 5000
// FetchLatestFromVcs fetches the latest configuration of a stack from VCS, triggering stack operations
func (s *stacks) FetchLatestFromVcs(ctx context.Context, stackID string) (*Stack, error) {
req, err := s.client.NewRequest("POST", fmt.Sprintf("stacks/%s/fetch-latest-from-vcs", url.PathEscape(stackID)), nil)
if err != nil {
return nil, err
}
stack := &Stack{}
err = req.Do(ctx, stack)
if err != nil {
return nil, err
}
return stack, nil
}
// List returns a list of stacks, optionally filtered by additional paameters.
func (s stacks) List(ctx context.Context, organization string, options *StackListOptions) (*StackList, error) {
if err := options.valid(); err != nil {
return nil, err
}
req, err := s.client.NewRequest("GET", fmt.Sprintf("organizations/%s/stacks", organization), options)
if err != nil {
return nil, err
}
sl := &StackList{}
err = req.Do(ctx, sl)
if err != nil {
return nil, err
}
return sl, nil
}
// Read returns a stack by its ID.
func (s stacks) Read(ctx context.Context, stackID string) (*Stack, error) {
req, err := s.client.NewRequest("GET", fmt.Sprintf("stacks/%s", url.PathEscape(stackID)), nil)
if err != nil {
return nil, err
}
stack := &Stack{}
err = req.Do(ctx, stack)
if err != nil {
return nil, err
}
return stack, nil
}
// Create creates a new stack.
func (s stacks) Create(ctx context.Context, options StackCreateOptions) (*Stack, error) {
if err := options.valid(); err != nil {
return nil, err
}
req, err := s.client.NewRequest("POST", "stacks", &options)
if err != nil {
return nil, err
}
stack := &Stack{}
err = req.Do(ctx, stack)
if err != nil {
return nil, err
}
return stack, nil
}
// Update updates a stack.
func (s stacks) Update(ctx context.Context, stackID string, options StackUpdateOptions) (*Stack, error) {
req, err := s.client.NewRequest("PATCH", fmt.Sprintf("stacks/%s", url.PathEscape(stackID)), &options)
if err != nil {
return nil, err
}
stack := &Stack{}
err = req.Do(ctx, stack)
if err != nil {
return nil, err
}
return stack, nil
}
// Delete deletes a stack.
func (s stacks) Delete(ctx context.Context, stackID string) error {
req, err := s.client.NewRequest("DELETE", fmt.Sprintf("stacks/%s", url.PathEscape(stackID)), nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// ForceDelete deletes a stack that still has deployments.
func (s stacks) ForceDelete(ctx context.Context, stackID string) error {
req, err := s.client.NewRequest("DELETE", fmt.Sprintf("stacks/%s?force=true", url.PathEscape(stackID)), nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (s *StackListOptions) valid() error {
return nil
}
func (s StackCreateOptions) valid() error {
if s.Name == "" {
return ErrRequiredName
}
if s.Project.ID == "" {
return ErrRequiredProject
}
return nil
}
// awaitPoll is a helper function that uses a callback to read a status, then
// waits for a terminal status or an error. The callback should return the
// current status, or an error. For each time the status changes, the channel
// emits a new result. The id parameter should be the ID of the resource being
// polled, which is used in the result to help identify the resource being polled.
func awaitPoll(ctx context.Context, id string, reader func(ctx context.Context) (string, error), quitStatus []string) <-chan WaitForStatusResult {
resultCh := make(chan WaitForStatusResult)
mapStatus := make(map[string]struct{}, len(quitStatus))
for _, status := range quitStatus {
mapStatus[status] = struct{}{}
}
go func() {
defer close(resultCh)
reads := 0
lastStatus := ""
for {
select {
case <-ctx.Done():
resultCh <- WaitForStatusResult{ID: id, Error: fmt.Errorf("context canceled: %w", ctx.Err())}
return
case <-time.After(backoff(minimumPollingIntervalMs, maximumPollingIntervalMs, reads)):
status, err := reader(ctx)
if err != nil {
resultCh <- WaitForStatusResult{ID: id, Error: err, Quit: true}
return
}
_, terminal := mapStatus[status]
if status != lastStatus {
resultCh <- WaitForStatusResult{
ID: id,
Status: status,
ReadAttempts: reads + 1,
Quit: terminal,
}
}
lastStatus = status
if terminal {
return
}
reads += 1
}
}
}()
return resultCh
}
================================================
FILE: stack_configuration.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"fmt"
"io"
"net/url"
"time"
)
// StackConfigurations describes all the stacks configurations-related methods that the
// HCP Terraform API supports.
type StackConfigurations interface {
// CreateAndUpload packages and uploads the specified Terraform Stacks
// configuration files in association with a Stack.
CreateAndUpload(ctx context.Context, stackID string, path string, opts *CreateStackConfigurationOptions) (*StackConfiguration, error)
// Upload a tar gzip archive to the specified stack configuration upload URL.
UploadTarGzip(ctx context.Context, url string, archive io.Reader) error
// ReadConfiguration returns a stack configuration by its ID.
Read(ctx context.Context, id string) (*StackConfiguration, error)
// ListStackConfigurations returns a list of stack configurations for a stack.
List(ctx context.Context, stackID string, opts *StackConfigurationListOptions) (*StackConfigurationList, error)
// JSONSchemas returns a byte slice of the JSON schema for the stack configuration.
JSONSchemas(ctx context.Context, stackConfigurationID string) ([]byte, error)
// AwaitCompleted generates a channel that will receive the status of the
// stack configuration as it progresses, until that status is "converged",
// "converging", "errored", "canceled".
AwaitCompleted(ctx context.Context, stackConfigurationID string) <-chan WaitForStatusResult
// AwaitPrepared generates a channel that will receive the status of the
// stack configuration as it progresses, until that status is "",
// "errored", "canceled".
AwaitStatus(ctx context.Context, stackConfigurationID string, status StackConfigurationStatus) <-chan WaitForStatusResult
// Diagnostics returns the diagnostics for this stack configuration.
Diagnostics(ctx context.Context, stackConfigurationID string) (*StackDiagnosticsList, error)
}
type StackConfigurationStatus string
const (
StackConfigurationStatusPending StackConfigurationStatus = "pending"
StackConfigurationStatusQueued StackConfigurationStatus = "queued"
StackConfigurationStatusPreparing StackConfigurationStatus = "preparing"
StackConfigurationStatusCompleted StackConfigurationStatus = "completed"
StackConfigurationStatusFailed StackConfigurationStatus = "failed"
)
func (s StackConfigurationStatus) String() string {
return string(s)
}
type stackConfigurations struct {
client *Client
}
var _ StackConfigurations = &stackConfigurations{}
func (s stackConfigurations) Read(ctx context.Context, id string) (*StackConfiguration, error) {
req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-configurations/%s", url.PathEscape(id)), nil)
if err != nil {
return nil, err
}
stackConfiguration := &StackConfiguration{}
err = req.Do(ctx, stackConfiguration)
if err != nil {
return nil, err
}
return stackConfiguration, nil
}
/**
* Returns the JSON schema for the stack configuration as a byte slice.
* The return value needs to be unmarshalled into a struct to be useful.
* It is meant to be unmarshalled with terraform/internal/command/jsonproivder.Providers.
*/
func (s stackConfigurations) JSONSchemas(ctx context.Context, stackConfigurationID string) ([]byte, error) {
req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-configurations/%s/json-schemas", url.PathEscape(stackConfigurationID)), nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
var raw bytes.Buffer
err = req.Do(ctx, &raw)
if err != nil {
return nil, err
}
return raw.Bytes(), nil
}
// AwaitCompleted generates a channel that will receive the status of the stack configuration as it progresses.
// The channel will be closed when the stack configuration reaches a status indicating that or an error occurs. The
// read will be retried dependending on the configuration of the client. When the channel is closed,
// the last value will either be a completed status or an error.
func (s stackConfigurations) AwaitCompleted(ctx context.Context, stackConfigurationID string) <-chan WaitForStatusResult {
return awaitPoll(ctx, stackConfigurationID, func(ctx context.Context) (string, error) {
stackConfiguration, err := s.Read(ctx, stackConfigurationID)
if err != nil {
return "", err
}
return stackConfiguration.Status.String(), nil
}, []string{StackConfigurationStatusCompleted.String(), StackConfigurationStatusFailed.String()})
}
// AwaitStatus generates a channel that will receive the status of the stack configuration as it progresses.
// The channel will be closed when the stack configuration reaches a status indicating that or an error occurs. The
// read will be retried dependending on the configuration of the client. When the channel is closed,
// the last value will either be the specified status, "errored" status, or "canceled" status, or an error.
func (s stackConfigurations) AwaitStatus(ctx context.Context, stackConfigurationID string, status StackConfigurationStatus) <-chan WaitForStatusResult {
return awaitPoll(ctx, stackConfigurationID, func(ctx context.Context) (string, error) {
stackConfiguration, err := s.Read(ctx, stackConfigurationID)
if err != nil {
return "", err
}
return stackConfiguration.Status.String(), nil
}, []string{status.String(), StackConfigurationStatusFailed.String()})
}
// StackConfigurationList represents a paginated list of stack configurations.
type StackConfigurationList struct {
Pagination *Pagination
Items []*StackConfiguration
}
// StackConfigurationListOptions represents the options for listing stack configurations.
type StackConfigurationListOptions struct {
ListOptions
}
func (s stackConfigurations) List(ctx context.Context, stackID string, options *StackConfigurationListOptions) (*StackConfigurationList, error) {
if options == nil {
options = &StackConfigurationListOptions{}
}
req, err := s.client.NewRequest("GET", fmt.Sprintf("stacks/%s/stack-configurations", url.PathEscape(stackID)), options)
if err != nil {
return nil, err
}
result := &StackConfigurationList{}
err = req.Do(ctx, result)
if err != nil {
return nil, err
}
return result, nil
}
type CreateStackConfigurationOptions struct {
SelectedDeployments []string `jsonapi:"attr,selected-deployments,omitempty"`
SpeculativeEnabled *bool `jsonapi:"attr,speculative,omitempty"`
}
// CreateAndUpload packages and uploads the specified Terraform Stacks
// configuration files in association with a Stack.
func (s stackConfigurations) CreateAndUpload(ctx context.Context, stackID, path string, opts *CreateStackConfigurationOptions) (*StackConfiguration, error) {
if opts == nil {
opts = &CreateStackConfigurationOptions{}
}
u := fmt.Sprintf("stacks/%s/stack-configurations", url.PathEscape(stackID))
req, err := s.client.NewRequest("POST", u, opts)
if err != nil {
return nil, fmt.Errorf("error creating stack configuration request for stack %q: %w", stackID, err)
}
sc := &StackConfiguration{}
err = req.Do(ctx, sc)
if err != nil {
return nil, fmt.Errorf("error creating stack configuration for stack %q: %w", stackID, err)
}
uploadURL, err := s.pollForUploadURL(ctx, sc.ID)
if err != nil {
return nil, fmt.Errorf("error retrieving upload URL for stack configuration %q: %w", sc.ID, err)
}
body, err := packContents(path)
if err != nil {
return nil, err
}
err = s.UploadTarGzip(ctx, uploadURL, body)
if err != nil {
return nil, err
}
return sc, nil
}
// PollForUploadURL polls for the upload URL of a stack configuration until it becomes available.
// It makes a request every 2 seconds until the upload URL is present in the response.
// It will timeout after 10 seconds.
func (s stackConfigurations) pollForUploadURL(ctx context.Context, stackConfigurationID string) (string, error) {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
timeout := time.NewTimer(15 * time.Second)
defer timeout.Stop()
for {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-timeout.C:
return "", fmt.Errorf("timeout waiting for upload URL for stack configuration %q", stackConfigurationID)
case <-ticker.C:
urlReq, err := s.client.NewRequest("GET", fmt.Sprintf("stack-configurations/%s/upload-url", stackConfigurationID), nil)
if err != nil {
return "", fmt.Errorf("error creating upload URL request for stack configuration %q: %w", stackConfigurationID, err)
}
type UploadURLResponse struct {
Data struct {
SourceUploadURL *string `json:"source-upload-url"`
} `json:"data"`
}
uploadResp := &UploadURLResponse{}
err = urlReq.DoJSON(ctx, uploadResp)
if err != nil {
return "", fmt.Errorf("error getting upload URL for stack configuration %q: %w", stackConfigurationID, err)
}
if uploadResp.Data.SourceUploadURL != nil {
return *uploadResp.Data.SourceUploadURL, nil
}
}
}
}
// UploadTarGzip is used to upload Terraform configuration files contained a tar gzip archive.
// Any stream implementing io.Reader can be passed into this method. This method is also
// particularly useful for tar streams created by non-default go-slug configurations.
//
// **Note**: This method does not validate the content being uploaded and is therefore the caller's
// responsibility to ensure the raw content is a valid Terraform configuration.
func (s stackConfigurations) UploadTarGzip(ctx context.Context, uploadURL string, archive io.Reader) error {
return s.client.doForeignPUTRequest(ctx, uploadURL, archive)
}
// Diagnostics returns the diagnostics for this stack configuration.
func (s stackConfigurations) Diagnostics(ctx context.Context, stackConfigurationID string) (*StackDiagnosticsList, error) {
req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-configurations/%s/stack-diagnostics", url.PathEscape(stackConfigurationID)), nil)
if err != nil {
return nil, err
}
diagnostics := &StackDiagnosticsList{}
err = req.Do(ctx, diagnostics)
if err != nil {
return nil, err
}
return diagnostics, nil
}
================================================
FILE: stack_configuration_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStackConfigurationList(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "test-stack-list",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
},
Project: &Project{
ID: orgTest.DefaultProject.ID,
},
})
require.NoError(t, err)
// Trigger first stack configuration by updating configuration
_, err = client.Stacks.FetchLatestFromVcs(ctx, stack.ID)
require.NoError(t, err)
// Wait a bit and trigger second stack configuration
time.Sleep(2 * time.Second)
_, err = client.Stacks.FetchLatestFromVcs(ctx, stack.ID)
require.NoError(t, err)
list, err := client.StackConfigurations.List(ctx, stack.ID, nil)
require.NoError(t, err)
require.NotNil(t, list)
assert.Equal(t, len(list.Items), 2)
// Assert attributes for each configuration
for _, cfg := range list.Items {
require.NotEmpty(t, cfg.ID)
require.NotEmpty(t, cfg.Status)
require.GreaterOrEqual(t, cfg.SequenceNumber, 1)
require.NotNil(t, cfg.Stack)
require.NotEmpty(t, cfg.Stack.ID)
}
// Test with pagination options
t.Run("with pagination options", func(t *testing.T) {
options := &StackConfigurationListOptions{
ListOptions: ListOptions{
PageNumber: 1,
PageSize: 10,
},
}
listWithOptions, err := client.StackConfigurations.List(ctx, stack.ID, options)
require.NoError(t, err)
require.NotNil(t, listWithOptions)
assert.GreaterOrEqual(t, len(listWithOptions.Items), 2)
require.NotNil(t, listWithOptions.Pagination)
assert.GreaterOrEqual(t, listWithOptions.Pagination.TotalCount, 2)
})
}
func TestStackConfigurationCreateUploadAndRead(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
Project: orgTest.DefaultProject,
Name: "test-stack",
})
require.NoError(t, err)
ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
done := make(chan struct{})
go func() {
for {
sc, err := client.StackConfigurations.CreateAndUpload(ctx, stack.ID, "test-fixtures/stack-source", &CreateStackConfigurationOptions{
SelectedDeployments: []string{"simple"},
})
require.NoError(t, err)
if sc != nil {
done <- struct{}{}
return
}
time.Sleep(2 * time.Second)
}
}()
select {
case <-done:
t.Logf("Created and uploaded config to stack configuration")
return
case <-ctx.Done():
require.Fail(t, "timed out waiting for stack configuration to be processed")
}
}
func TestStackConfigurationDiagnostics(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
Project: orgTest.DefaultProject,
Name: "test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "ctrombley/linked-stacks-demo-network",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
Branch: "diagnostics", // This branch will produce diagnostics
},
})
require.NoError(t, err)
require.NotNil(t, stack)
stackUpdated, err := client.Stacks.FetchLatestFromVcs(ctx, stack.ID)
require.NoError(t, err)
require.NotNil(t, stackUpdated)
stackUpdated, err = client.Stacks.Read(ctx, stackUpdated.ID)
require.NoError(t, err)
require.NotNil(t, stackUpdated.LatestStackConfiguration)
pollStackConfigurationStatus(t, ctx, client, stackUpdated.LatestStackConfiguration.ID, "failed")
t.Run("Diagnostics with valid ID", func(t *testing.T) {
diags, err := client.StackConfigurations.Diagnostics(ctx, stackUpdated.LatestStackConfiguration.ID)
assert.NoError(t, err)
require.NotEmpty(t, diags.Items)
diag := diags.Items[0]
assert.NotEmpty(t, diag.ID)
assert.NotEmpty(t, diag.Severity)
assert.NotEmpty(t, diag.Summary)
assert.NotEmpty(t, diag.Detail)
assert.NotEmpty(t, diag.Diags)
assert.False(t, diag.Acknowledged)
assert.Nil(t, diag.AcknowledgedAt)
assert.NotZero(t, diag.CreatedAt)
assert.Nil(t, diag.StackDeploymentStep)
assert.NotNil(t, diag.StackConfiguration)
assert.Nil(t, diag.AcknowledgedBy)
})
t.Run("Diagnostics with invalid ID", func(t *testing.T) {
_, err := client.StackConfigurations.Diagnostics(ctx, "invalid-id")
require.Error(t, err)
})
}
================================================
FILE: stack_configuration_summary.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
type StackConfigurationSummaries interface {
// List lists all the stack configuration summaries for a stack.
List(ctx context.Context, stackID string, options *StackConfigurationSummaryListOptions) (*StackConfigurationSummaryList, error)
}
type stackConfigurationSummaries struct {
client *Client
}
var _ StackConfigurationSummaries = &stackConfigurationSummaries{}
type StackConfigurationSummaryList struct {
*Pagination
Items []*StackConfigurationSummary
}
type StackConfigurationSummaryListOptions struct {
ListOptions
}
type StackConfigurationSummary struct {
ID string `jsonapi:"primary,stack-configuration-summaries"`
Status string `jsonapi:"attr,status"`
SequenceNumber int `jsonapi:"attr,sequence-number"`
}
func (s stackConfigurationSummaries) List(ctx context.Context, stackID string, options *StackConfigurationSummaryListOptions) (*StackConfigurationSummaryList, error) {
if !validStringID(&stackID) {
return nil, fmt.Errorf("invalid stack ID: %s", stackID)
}
if options == nil {
options = &StackConfigurationSummaryListOptions{}
}
req, err := s.client.NewRequest("GET", fmt.Sprintf("stacks/%s/stack-configuration-summaries", url.PathEscape(stackID)), options)
if err != nil {
return nil, err
}
scl := &StackConfigurationSummaryList{}
err = req.Do(ctx, scl)
if err != nil {
return nil, err
}
return scl, nil
}
================================================
FILE: stack_configuration_summary_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStackConfigurationSummaryList(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "aa-test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
},
Project: &Project{
ID: orgTest.DefaultProject.ID,
},
})
require.NoError(t, err)
require.NotNil(t, stack)
stack2, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "bb-test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
},
Project: &Project{
ID: orgTest.DefaultProject.ID,
},
})
require.NoError(t, err)
require.NotNil(t, stack2)
// Trigger first stack configuration by updating configuration
_, err = client.Stacks.FetchLatestFromVcs(ctx, stack2.ID)
require.NoError(t, err)
// Wait a bit and trigger second stack configuration
time.Sleep(2 * time.Second)
_, err = client.Stacks.FetchLatestFromVcs(ctx, stack2.ID)
require.NoError(t, err)
t.Run("Successful empty list", func(t *testing.T) {
stackConfigSummaryList, err := client.StackConfigurationSummaries.List(ctx, stack.ID, nil)
require.NoError(t, err)
assert.Len(t, stackConfigSummaryList.Items, 0)
})
t.Run("Successful multiple config summary list", func(t *testing.T) {
stackConfigSummaryList, err := client.StackConfigurationSummaries.List(ctx, stack2.ID, nil)
require.NoError(t, err)
assert.Len(t, stackConfigSummaryList.Items, 2)
})
t.Run("Unsuccessful list", func(t *testing.T) {
_, err := client.StackConfigurationSummaries.List(ctx, "", nil)
require.Error(t, err)
})
}
================================================
FILE: stack_deployment.go
================================================
package tfe
import (
"context"
"fmt"
"net/url"
)
type StackDeployments interface {
// List returns a list of stack deployments for a given stack.
List(ctx context.Context, stackID string, opts *StackDeploymentListOptions) (*StackDeploymentList, error)
}
type StackDeployment struct {
// Attributes
ID string `jsonapi:"primary,stack-deployments"`
Name string `jsonapi:"attr,name"`
// Relationships
Stack *Stack `jsonapi:"relation,stack"`
LatestDeploymentRun *StackDeploymentRun `jsonapi:"relation,latest-deployment-run"`
}
type stackDeployments struct {
client *Client
}
type StackDeploymentListOptions struct {
ListOptions
}
type StackDeploymentList struct {
*Pagination
Items []*StackDeployment
}
func (s stackDeployments) List(ctx context.Context, stackID string, opts *StackDeploymentListOptions) (*StackDeploymentList, error) {
if !validStringID(&stackID) {
return nil, ErrInvalidStackID
}
req, err := s.client.NewRequest("GET", fmt.Sprintf("stacks/%s/stack-deployments", url.PathEscape(stackID)), opts)
if err != nil {
return nil, err
}
var deployments StackDeploymentList
if err := req.Do(ctx, &deployments); err != nil {
return nil, err
}
return &deployments, nil
}
================================================
FILE: stack_deployment_groups.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"strings"
"time"
)
// StackDeploymentGroups describes all the stack-deployment-groups related methods that the HCP Terraform API supports.
type StackDeploymentGroups interface {
// List returns a list of Deployment Groups in a stack.
List(ctx context.Context, stackConfigID string, options *StackDeploymentGroupListOptions) (*StackDeploymentGroupList, error)
// Read retrieves a stack deployment group by its ID.
Read(ctx context.Context, stackDeploymentGroupID string) (*StackDeploymentGroup, error)
// ReadByName retrieves a stack deployment group by its Name.
ReadByName(ctx context.Context, stackConfigurationID, stackDeploymentName string) (*StackDeploymentGroup, error)
// ApproveAllPlans approves all pending plans in a stack deployment group.
ApproveAllPlans(ctx context.Context, stackDeploymentGroupID string) error
// Rerun re-runs all the stack deployment runs in a deployment group.
Rerun(ctx context.Context, stackDeploymentGroupID string, options *StackDeploymentGroupRerunOptions) error
}
type DeploymentGroupStatus string
const (
DeploymentGroupStatusPending DeploymentGroupStatus = "pending"
DeploymentGroupStatusDeploying DeploymentGroupStatus = "deploying"
DeploymentGroupStatusSucceeded DeploymentGroupStatus = "succeeded"
DeploymentGroupStatusFailed DeploymentGroupStatus = "failed"
DeploymentGroupStatusAbandoned DeploymentGroupStatus = "abandoned"
)
func (s DeploymentGroupStatus) String() string {
return string(s)
}
// stackDeploymentGroups implements StackDeploymentGroups.
type stackDeploymentGroups struct {
client *Client
}
var _ StackDeploymentGroups = &stackDeploymentGroups{}
// StackDeploymentGroup represents a stack deployment group.
type StackDeploymentGroup struct {
// Attributes
ID string `jsonapi:"primary,stack-deployment-groups"`
Name string `jsonapi:"attr,name"`
Status DeploymentGroupStatus `jsonapi:"attr,status"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
// Relationships
StackConfiguration *StackConfiguration `jsonapi:"relation,stack-configuration"`
}
// StackDeploymentGroupList represents a list of stack deployment groups.
type StackDeploymentGroupList struct {
*Pagination
Items []*StackDeploymentGroup
}
// StackDeploymentGroupListOptions represents additional options when listing stack deployment groups.
type StackDeploymentGroupListOptions struct {
ListOptions
}
// StackDeploymentGroupRerunOptions represents options for rerunning deployments in a stack deployment group.
type StackDeploymentGroupRerunOptions struct {
// Required query parameter: A list of deployment run IDs to rerun.
Deployments []string
}
// List returns a list of Deployment Groups in a stack, optionally filtered by additional parameters.
func (s stackDeploymentGroups) List(ctx context.Context, stackConfigID string, options *StackDeploymentGroupListOptions) (*StackDeploymentGroupList, error) {
if !validStringID(&stackConfigID) {
return nil, fmt.Errorf("invalid stack configuration ID: %s", stackConfigID)
}
if options == nil {
options = &StackDeploymentGroupListOptions{}
}
req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-configurations/%s/stack-deployment-groups", url.PathEscape(stackConfigID)), options)
if err != nil {
return nil, err
}
sdgl := &StackDeploymentGroupList{}
err = req.Do(ctx, sdgl)
if err != nil {
return nil, err
}
return sdgl, nil
}
// ReadByName retrieves a stack deployment group by its Name.
func (s stackDeploymentGroups) ReadByName(ctx context.Context, stackConfigurationID, stackDeploymentName string) (*StackDeploymentGroup, error) {
if !validStringID(&stackConfigurationID) {
return nil, fmt.Errorf("invalid stack configuration id: %s", stackConfigurationID)
}
if !validStringID(&stackDeploymentName) {
return nil, fmt.Errorf("invalid stack deployment group name: %s", stackDeploymentName)
}
req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-configurations/%s/stack-deployment-groups/%s", url.PathEscape(stackConfigurationID), url.PathEscape(stackDeploymentName)), nil)
if err != nil {
return nil, err
}
sdg := &StackDeploymentGroup{}
err = req.Do(ctx, sdg)
if err != nil {
return nil, err
}
return sdg, nil
}
// Read retrieves a stack deployment group by its ID.
func (s stackDeploymentGroups) Read(ctx context.Context, stackDeploymentGroupID string) (*StackDeploymentGroup, error) {
if !validStringID(&stackDeploymentGroupID) {
return nil, fmt.Errorf("invalid stack deployment group ID: %s", stackDeploymentGroupID)
}
req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-deployment-groups/%s", url.PathEscape(stackDeploymentGroupID)), nil)
if err != nil {
return nil, err
}
sdg := &StackDeploymentGroup{}
err = req.Do(ctx, sdg)
if err != nil {
return nil, err
}
return sdg, nil
}
// ApproveAllPlans approves all pending plans in a stack deployment group.
func (s stackDeploymentGroups) ApproveAllPlans(ctx context.Context, stackDeploymentGroupID string) error {
if !validStringID(&stackDeploymentGroupID) {
return fmt.Errorf("invalid stack deployment group ID: %s", stackDeploymentGroupID)
}
req, err := s.client.NewRequest("POST", fmt.Sprintf("stack-deployment-groups/%s/approve-all-plans", url.PathEscape(stackDeploymentGroupID)), nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// Rerun re-runs all the stack deployment runs in a deployment group.
func (s stackDeploymentGroups) Rerun(ctx context.Context, stackDeploymentGroupID string, options *StackDeploymentGroupRerunOptions) error {
if !validStringID(&stackDeploymentGroupID) {
return fmt.Errorf("invalid stack deployment group ID: %s", stackDeploymentGroupID)
}
if options == nil || len(options.Deployments) == 0 {
return fmt.Errorf("no deployments specified for rerun")
}
u := fmt.Sprintf("stack-deployment-groups/%s/rerun", url.PathEscape(stackDeploymentGroupID))
type DeploymentQueryParams struct {
Deployments string `url:"deployments"`
}
qp, err := decodeQueryParams(&DeploymentQueryParams{
Deployments: strings.Join(options.Deployments, ","),
})
if err != nil {
return err
}
req, err := s.client.NewRequestWithAdditionalQueryParams("POST", u, nil, qp)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
================================================
FILE: stack_deployment_groups_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStackDeploymentGroupsList(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
},
Project: &Project{
ID: orgTest.DefaultProject.ID,
},
})
require.NoError(t, err)
require.NotNil(t, stack)
stackUpdated, err := client.Stacks.FetchLatestFromVcs(ctx, stack.ID)
require.NoError(t, err)
require.NotNil(t, stackUpdated)
stackUpdated = pollStackDeploymentGroups(t, ctx, client, stackUpdated.ID)
require.NotEmpty(t, stackUpdated.LatestStackConfiguration.ID)
t.Run("List with valid stack configuration ID", func(t *testing.T) {
sdgl, err := client.StackDeploymentGroups.List(ctx, stackUpdated.LatestStackConfiguration.ID, nil)
require.NoError(t, err)
require.NotNil(t, sdgl)
for _, item := range sdgl.Items {
assert.NotNil(t, item.ID)
assert.NotEmpty(t, item.Name)
assert.NotEmpty(t, item.Status)
assert.NotNil(t, item.CreatedAt)
assert.NotNil(t, item.UpdatedAt)
}
require.Len(t, sdgl.Items, 2)
})
t.Run("List with invalid stack configuration ID", func(t *testing.T) {
_, err := client.StackDeploymentGroups.List(ctx, "", nil)
require.Error(t, err)
})
t.Run("List with pagination", func(t *testing.T) {
options := &StackDeploymentGroupListOptions{
ListOptions: ListOptions{
PageNumber: 1,
PageSize: 1,
},
}
sdgl, err := client.StackDeploymentGroups.List(ctx, stackUpdated.LatestStackConfiguration.ID, options)
require.NoError(t, err)
require.NotNil(t, sdgl)
require.Len(t, sdgl.Items, 1)
})
}
func TestStackDeploymentGroupsRead(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
},
Project: &Project{
ID: orgTest.DefaultProject.ID,
},
})
require.NoError(t, err)
require.NotNil(t, stack)
stackUpdated, err := client.Stacks.FetchLatestFromVcs(ctx, stack.ID)
require.NoError(t, err)
require.NotNil(t, stackUpdated)
stackUpdated = pollStackDeploymentGroups(t, ctx, client, stackUpdated.ID)
require.NotNil(t, stackUpdated.LatestStackConfiguration)
sdgl, err := client.StackDeploymentGroups.List(ctx, stackUpdated.LatestStackConfiguration.ID, nil)
require.NoError(t, err)
require.NotNil(t, sdgl)
require.Len(t, sdgl.Items, 2)
t.Run("Read with valid ID", func(t *testing.T) {
sdgRead, err := client.StackDeploymentGroups.Read(ctx, sdgl.Items[0].ID)
require.NoError(t, err)
assert.Equal(t, sdgl.Items[0].ID, sdgRead.ID)
assert.Equal(t, sdgl.Items[0].Name, sdgRead.Name)
assert.Equal(t, sdgl.Items[0].Status, sdgRead.Status)
})
t.Run("Read with invalid ID", func(t *testing.T) {
_, err := client.StackDeploymentGroups.Read(ctx, "")
require.Error(t, err)
})
}
func TestStackDeploymentGroupsApproveAllPlans(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
Branch: "main",
},
Project: &Project{
ID: orgTest.DefaultProject.ID,
},
})
require.NoError(t, err)
require.NotNil(t, stack)
stackUpdated, err := client.Stacks.FetchLatestFromVcs(ctx, stack.ID)
require.NoError(t, err)
require.NotNil(t, stackUpdated)
stackUpdated = pollStackDeploymentGroups(t, ctx, client, stackUpdated.ID)
require.NotNil(t, stackUpdated.LatestStackConfiguration)
// Get the deployment group ID from the stack configuration
deploymentGroups, err := client.StackDeploymentGroups.List(ctx, stackUpdated.LatestStackConfiguration.ID, nil)
require.NoError(t, err)
require.NotNil(t, deploymentGroups)
require.NotEmpty(t, deploymentGroups.Items)
deploymentGroupID := deploymentGroups.Items[0].ID
t.Run("Approving all plans", func(t *testing.T) {
err := client.StackDeploymentGroups.ApproveAllPlans(ctx, deploymentGroupID)
require.NoError(t, err)
})
}
func TestStackDeploymentGroupsRerun(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
Branch: "main",
},
Project: &Project{
ID: orgTest.DefaultProject.ID,
},
})
require.NoError(t, err)
require.NotNil(t, stack)
stackUpdated, err := client.Stacks.FetchLatestFromVcs(ctx, stack.ID)
require.NoError(t, err)
require.NotNil(t, stackUpdated)
stackUpdated = pollStackDeploymentGroups(t, ctx, client, stackUpdated.ID)
require.NotNil(t, stackUpdated.LatestStackConfiguration)
deploymentGroups, err := client.StackDeploymentGroups.List(ctx, stackUpdated.LatestStackConfiguration.ID, nil)
require.NoError(t, err)
require.NotNil(t, deploymentGroups)
require.NotEmpty(t, deploymentGroups.Items)
deploymentGroupID := deploymentGroups.Items[0].ID
deploymentRuns, err := client.StackDeploymentRuns.List(ctx, deploymentGroupID, nil)
require.NoError(t, err)
require.NotNil(t, deploymentRuns)
require.NotEmpty(t, deploymentRuns.Items)
err = client.StackDeploymentGroups.ApproveAllPlans(ctx, deploymentGroupID)
require.NoError(t, err)
pollStackDeploymentRunStatus(t, ctx, client, deploymentRuns.Items[0].ID, "deploying")
deploymentRunIds := []string{deploymentRuns.Items[0].ID}
for _, dr := range deploymentRuns.Items {
deploymentRunIds = append(deploymentRunIds, dr.ID)
}
t.Run("No deployments specified for rerun", func(t *testing.T) {
err := client.StackDeploymentGroups.Rerun(ctx, deploymentGroupID, nil)
require.Error(t, err)
assert.Contains(t, err.Error(), "no deployments specified for rerun")
})
t.Run("Rerun with invalid ID", func(t *testing.T) {
err := client.StackDeploymentGroups.Rerun(ctx, "", &StackDeploymentGroupRerunOptions{
Deployments: deploymentRunIds,
})
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid stack deployment group ID")
})
}
================================================
FILE: stack_deployment_groups_summary.go
================================================
package tfe
import (
"context"
"fmt"
"net/url"
)
type StackDeploymentGroupSummaries interface {
// List lists all the stack deployment group summaries for a stack.
List(ctx context.Context, configurationID string, options *StackDeploymentGroupSummaryListOptions) (*StackDeploymentGroupSummaryList, error)
}
type stackDeploymentGroupSummaries struct {
client *Client
}
var _ StackDeploymentGroupSummaries = &stackDeploymentGroupSummaries{}
type StackDeploymentGroupSummaryList struct {
*Pagination
Items []*StackDeploymentGroupSummary
}
type StackDeploymentGroupSummaryListOptions struct {
ListOptions
}
type StackDeploymentGroupStatusCounts struct {
Pending int `jsonapi:"attr,pending"`
PreDeploying int `jsonapi:"attr,pre-deploying"`
PreDeployingPendingOperator int `jsonapi:"attr,pending-operator"`
AcquiringLock int `jsonapi:"attr,acquiring-lock"`
Deploying int `jsonapi:"attr,deploying"`
Succeeded int `jsonapi:"attr,succeeded"`
Failed int `jsonapi:"attr,failed"`
Abandoned int `jsonapi:"attr,abandoned"`
}
type StackDeploymentGroupSummary struct {
ID string `jsonapi:"primary,stack-deployment-group-summaries"`
// Attributes
Name string `jsonapi:"attr,name"`
Status string `jsonapi:"attr,status"`
StatusCounts *StackDeploymentGroupStatusCounts `jsonapi:"attr,status-counts"`
// Relationships
StackDeploymentGroup *StackDeploymentGroup `jsonapi:"relation,stack-deployment-group"`
}
func (s stackDeploymentGroupSummaries) List(ctx context.Context, stackID string, options *StackDeploymentGroupSummaryListOptions) (*StackDeploymentGroupSummaryList, error) {
if !validStringID(&stackID) {
return nil, fmt.Errorf("invalid stack ID: %s", stackID)
}
if options == nil {
options = &StackDeploymentGroupSummaryListOptions{}
}
req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-configurations/%s/stack-deployment-group-summaries", url.PathEscape(stackID)), options)
if err != nil {
return nil, err
}
scl := &StackDeploymentGroupSummaryList{}
err = req.Do(ctx, scl)
if err != nil {
return nil, err
}
return scl, nil
}
================================================
FILE: stack_deployment_groups_summary_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStackDeploymentGroupSummaryList(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "aa-test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
},
Project: &Project{
ID: orgTest.DefaultProject.ID,
},
})
require.NoError(t, err)
require.NotNil(t, stack)
stack2, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "bb-test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
},
Project: &Project{
ID: orgTest.DefaultProject.ID,
},
})
require.NoError(t, err)
require.NotNil(t, stack2)
// Trigger first stack configuration with a fetch
_, err = client.Stacks.FetchLatestFromVcs(ctx, stack.ID)
require.NoError(t, err)
updatedStack := pollStackDeploymentGroups(t, ctx, client, stack.ID)
require.NotNil(t, updatedStack.LatestStackConfiguration.ID)
// Trigger second stack configuration with a fetch
_, err = client.Stacks.FetchLatestFromVcs(ctx, stack2.ID)
require.NoError(t, err)
updatedStack2 := pollStackDeploymentGroups(t, ctx, client, stack2.ID)
require.NotNil(t, updatedStack2.LatestStackConfiguration.ID)
t.Run("Successful multiple deployment group summary list", func(t *testing.T) {
stackConfigSummaryList, err := client.StackDeploymentGroupSummaries.List(ctx, updatedStack2.LatestStackConfiguration.ID, nil)
require.NoError(t, err)
assert.Len(t, stackConfigSummaryList.Items, 2)
})
t.Run("Unsuccessful list", func(t *testing.T) {
_, err := client.StackDeploymentGroupSummaries.List(ctx, "", nil)
require.Error(t, err)
})
}
================================================
FILE: stack_deployment_integration_test.go
================================================
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
func TestStackDeploymentsList(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "aa-test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
},
Project: &Project{
ID: orgTest.DefaultProject.ID,
},
})
require.NoError(t, err)
require.NotNil(t, stack)
stackUpdated, err := client.Stacks.FetchLatestFromVcs(ctx, stack.ID)
require.NoError(t, err)
require.NotNil(t, stackUpdated)
stackUpdated = pollStackDeploymentGroups(t, ctx, client, stackUpdated.ID)
t.Run("List with valid options", func(t *testing.T) {
opts := &StackDeploymentListOptions{
ListOptions: ListOptions{
PageNumber: 1,
PageSize: 1,
},
}
sdl, err := client.StackDeployments.List(ctx, stackUpdated.ID, opts)
require.NoError(t, err)
require.NotNil(t, sdl)
require.Len(t, sdl.Items, 1)
})
t.Run("List with invalid options", func(t *testing.T) {
opts := &StackDeploymentListOptions{
ListOptions: ListOptions{
PageNumber: -1,
PageSize: -1,
},
}
_, err := client.StackDeployments.List(ctx, stackUpdated.ID, opts)
require.Error(t, err)
})
}
================================================
FILE: stack_deployment_runs.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"time"
)
// StackDeploymentRuns describes all the stack deployment runs-related methods that the HCP Terraform API supports.
type StackDeploymentRuns interface {
// List returns a list of stack deployment runs for a given deployment group.
List(ctx context.Context, deploymentGroupID string, options *StackDeploymentRunListOptions) (*StackDeploymentRunList, error)
Read(ctx context.Context, stackDeploymentRunID string) (*StackDeploymentRun, error)
ReadWithOptions(ctx context.Context, stackDeploymentRunID string, options *StackDeploymentRunReadOptions) (*StackDeploymentRun, error)
ApproveAllPlans(ctx context.Context, deploymentRunID string) error
Cancel(ctx context.Context, stackDeploymentRunID string) error
}
type DeploymentRunStatus string
const (
DeploymentRunStatusPending DeploymentRunStatus = "pending"
DeploymentRunStatusPreDeploying DeploymentRunStatus = "pre-deploying"
DeploymentRunStatusPreDeployingPendingOperator DeploymentRunStatus = "pre-deploying-pending-operator"
DeploymentRunStatusAcquiringLock DeploymentRunStatus = "acquiring-lock"
DeploymentRunStatusDeploying DeploymentRunStatus = "deploying"
DeploymentRunStatusDeployingPendingOperator DeploymentRunStatus = "deploying-pending-operator"
DeploymentRunStatusSucceeded DeploymentRunStatus = "succeeded"
DeploymentRunStatusFailed DeploymentRunStatus = "failed"
DeploymentRunStatusAbandoned DeploymentRunStatus = "abandoned"
)
func (s DeploymentRunStatus) String() string {
return string(s)
}
// stackDeploymentRuns implements StackDeploymentRuns.
type stackDeploymentRuns struct {
client *Client
}
var _ StackDeploymentRuns = &stackDeploymentRuns{}
// StackDeploymentRun represents a stack deployment run.
type StackDeploymentRun struct {
ID string `jsonapi:"primary,stack-deployment-runs"`
Status DeploymentRunStatus `jsonapi:"attr,status"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
// Relationships
StackDeploymentGroup *StackDeploymentGroup `jsonapi:"relation,stack-deployment-group"`
}
type SDRIncludeOpt string
const (
SDRDeploymentGroup SDRIncludeOpt = "stack-deployment-group"
)
// StackDeploymentRunList represents a list of stack deployment runs.
type StackDeploymentRunList struct {
*Pagination
Items []*StackDeploymentRun
}
type StackDeploymentRunReadOptions struct {
// Optional: A list of relations to include.
Include []SDRIncludeOpt `url:"include,omitempty"`
}
// StackDeploymentRunListOptions represents the options for listing stack deployment runs.
type StackDeploymentRunListOptions struct {
ListOptions
// Optional: A list of relations to include.
Include []SDRIncludeOpt `url:"include,omitempty"`
}
// List returns a list of stack deployment runs for a given deployment group.
func (s *stackDeploymentRuns) List(ctx context.Context, deploymentGroupID string, options *StackDeploymentRunListOptions) (*StackDeploymentRunList, error) {
req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-deployment-groups/%s/stack-deployment-runs", url.PathEscape(deploymentGroupID)), options)
if err != nil {
return nil, err
}
sdrl := &StackDeploymentRunList{}
err = req.Do(ctx, sdrl)
if err != nil {
return nil, err
}
return sdrl, nil
}
func (s stackDeploymentRuns) Read(ctx context.Context, stackDeploymentRunID string) (*StackDeploymentRun, error) {
req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-deployment-runs/%s", url.PathEscape(stackDeploymentRunID)), nil)
if err != nil {
return nil, err
}
run := StackDeploymentRun{}
err = req.Do(ctx, &run)
if err != nil {
return nil, err
}
return &run, nil
}
func (s stackDeploymentRuns) ReadWithOptions(ctx context.Context, stackDeploymentRunID string, options *StackDeploymentRunReadOptions) (*StackDeploymentRun, error) {
if err := options.valid(); err != nil {
return nil, err
}
req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-deployment-runs/%s", url.PathEscape(stackDeploymentRunID)), options)
if err != nil {
return nil, err
}
run := StackDeploymentRun{}
err = req.Do(ctx, &run)
if err != nil {
return nil, err
}
return &run, nil
}
func (s stackDeploymentRuns) ApproveAllPlans(ctx context.Context, stackDeploymentRunID string) error {
req, err := s.client.NewRequest("POST", fmt.Sprintf("stack-deployment-runs/%s/approve-all-plans", url.PathEscape(stackDeploymentRunID)), nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (s stackDeploymentRuns) Cancel(ctx context.Context, stackDeploymentRunID string) error {
req, err := s.client.NewRequest("POST", fmt.Sprintf("stack-deployment-runs/%s/cancel", url.PathEscape(stackDeploymentRunID)), nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o *StackDeploymentRunReadOptions) valid() error {
for _, include := range o.Include {
switch include {
case SDRDeploymentGroup:
// Valid option, do nothing.
default:
return fmt.Errorf("invalid include option: %s", include)
}
}
return nil
}
================================================
FILE: stack_deployment_runs_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStackDeploymentRunsList(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
Branch: "main",
},
Project: &Project{
ID: orgTest.DefaultProject.ID,
},
})
require.NoError(t, err)
require.NotNil(t, stack)
stackUpdated, err := client.Stacks.FetchLatestFromVcs(ctx, stack.ID)
require.NoError(t, err)
require.NotNil(t, stackUpdated)
stackUpdated = pollStackDeploymentGroups(t, ctx, client, stackUpdated.ID)
require.NotNil(t, stackUpdated.LatestStackConfiguration)
// Get the deployment group ID from the stack configuration
deploymentGroups, err := client.StackDeploymentGroups.List(ctx, stackUpdated.LatestStackConfiguration.ID, nil)
require.NoError(t, err)
require.NotNil(t, deploymentGroups)
require.NotEmpty(t, deploymentGroups.Items)
deploymentGroupID := deploymentGroups.Items[0].ID
t.Run("List without options", func(t *testing.T) {
t.Parallel()
runList, err := client.StackDeploymentRuns.List(ctx, deploymentGroupID, nil)
require.NoError(t, err)
assert.NotNil(t, runList)
})
t.Run("List with pagination", func(t *testing.T) {
t.Parallel()
runList, err := client.StackDeploymentRuns.List(ctx, deploymentGroupID, &StackDeploymentRunListOptions{
ListOptions: ListOptions{
PageNumber: 1,
PageSize: 10,
},
})
require.NoError(t, err)
assert.NotNil(t, runList)
})
t.Run("With include option", func(t *testing.T) {
t.Parallel()
runList, err := client.StackDeploymentRuns.List(ctx, deploymentGroupID, &StackDeploymentRunListOptions{
Include: []SDRIncludeOpt{"stack-deployment-group"},
})
assert.NoError(t, err)
assert.NotNil(t, runList)
for _, run := range runList.Items {
assert.NotNil(t, run.StackDeploymentGroup.ID)
}
})
t.Run("With invalid include option", func(t *testing.T) {
t.Parallel()
_, err := client.StackDeploymentRuns.List(ctx, deploymentGroupID, &StackDeploymentRunListOptions{
Include: []SDRIncludeOpt{"invalid-option"},
})
assert.Error(t, err)
})
}
func TestStackDeploymentRunsRead(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
Project: orgTest.DefaultProject,
Name: "test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
Branch: "main",
},
})
require.NoError(t, err)
require.NotNil(t, stack)
stackUpdated, err := client.Stacks.FetchLatestFromVcs(ctx, stack.ID)
require.NoError(t, err)
require.NotNil(t, stackUpdated)
stackUpdated = pollStackDeploymentGroups(t, ctx, client, stackUpdated.ID)
require.NotNil(t, stackUpdated.LatestStackConfiguration)
stackDeploymentGroups, err := client.StackDeploymentGroups.List(ctx, stackUpdated.LatestStackConfiguration.ID, nil)
require.NoError(t, err)
require.NotEmpty(t, stackDeploymentGroups)
sdg := stackDeploymentGroups.Items[0]
stackDeploymentRuns, err := client.StackDeploymentRuns.List(ctx, sdg.ID, nil)
require.NoError(t, err)
require.NotEmpty(t, stackDeploymentRuns)
sdr := stackDeploymentRuns.Items[0]
t.Run("Read with valid ID", func(t *testing.T) {
run, err := client.StackDeploymentRuns.Read(ctx, sdr.ID)
assert.NoError(t, err)
assert.NotNil(t, run)
})
t.Run("Read with invalid ID", func(t *testing.T) {
_, err := client.StackDeploymentRuns.Read(ctx, "")
assert.Error(t, err)
})
t.Run("Read with options", func(t *testing.T) {
run, err := client.StackDeploymentRuns.ReadWithOptions(ctx, sdr.ID, &StackDeploymentRunReadOptions{
Include: []SDRIncludeOpt{"stack-deployment-group"},
})
assert.NoError(t, err)
assert.NotNil(t, run)
assert.NotNil(t, run.StackDeploymentGroup.ID)
})
t.Run("Read with invalid options", func(t *testing.T) {
_, err := client.StackDeploymentRuns.ReadWithOptions(ctx, sdr.ID, &StackDeploymentRunReadOptions{
Include: []SDRIncludeOpt{"invalid-option"},
})
assert.Error(t, err)
})
}
func TestStackDeploymentRunsApproveAllPlans(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
Project: orgTest.DefaultProject,
Name: "test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
Branch: "main",
},
})
require.NoError(t, err)
require.NotNil(t, stack)
stackUpdated, err := client.Stacks.FetchLatestFromVcs(ctx, stack.ID)
require.NoError(t, err)
require.NotNil(t, stackUpdated)
stackUpdated = pollStackDeploymentGroups(t, ctx, client, stackUpdated.ID)
require.NotNil(t, stackUpdated.LatestStackConfiguration)
// Get the deployment group ID from the stack configuration
deploymentGroups, err := client.StackDeploymentGroups.List(ctx, stackUpdated.LatestStackConfiguration.ID, nil)
require.NoError(t, err)
require.NotNil(t, deploymentGroups)
require.NotEmpty(t, deploymentGroups.Items)
deploymentGroupID := deploymentGroups.Items[0].ID
runList, err := client.StackDeploymentRuns.List(ctx, deploymentGroupID, nil)
require.NoError(t, err)
assert.NotNil(t, runList)
deploymentRunID := runList.Items[0].ID
t.Run("Approve all plans", func(t *testing.T) {
t.Parallel()
err := client.StackDeploymentRuns.ApproveAllPlans(ctx, deploymentRunID)
require.NoError(t, err)
})
}
func TestStackDeploymentRunsCancel(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
Project: orgTest.DefaultProject,
Name: "test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
Branch: "main",
},
})
require.NoError(t, err)
require.NotNil(t, stack)
stackUpdated, err := client.Stacks.FetchLatestFromVcs(ctx, stack.ID)
require.NoError(t, err)
require.NotNil(t, stackUpdated)
stackUpdated = pollStackDeploymentGroups(t, ctx, client, stackUpdated.ID)
require.NotNil(t, stackUpdated.LatestStackConfiguration)
// Get the deployment group ID from the stack configuration
configurationID := stackUpdated.LatestStackConfiguration.ID
deploymentGroups, err := client.StackDeploymentGroups.List(ctx, configurationID, nil)
require.NoError(t, err)
require.NotNil(t, deploymentGroups)
require.NotEmpty(t, deploymentGroups.Items)
deploymentGroupID := deploymentGroups.Items[0].ID
runList, err := client.StackDeploymentRuns.List(ctx, deploymentGroupID, nil)
require.NoError(t, err)
assert.NotNil(t, runList)
run := runList.Items[0]
steps, err := client.StackDeploymentSteps.List(ctx, run.ID, nil)
require.NoError(t, err)
require.NotNil(t, steps)
require.NotEmpty(t, steps.Items)
step := steps.Items[0]
t.Run("cancel deployment run", func(t *testing.T) {
t.Parallel()
pollStackDeploymentStepStatus(t, ctx, client, step.ID, "pending_operator")
err = client.StackDeploymentRuns.Cancel(ctx, run.ID)
require.NoError(t, err)
pollStackDeploymentStepStatus(t, ctx, client, step.ID, "failed")
pollStackDeploymentRunStatus(t, ctx, client, run.ID, "abandoned")
})
}
================================================
FILE: stack_deployment_steps.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"io"
"net/url"
"time"
)
// StackDeploymentSteps describes all the stacks deployment step-related methods that the
// HCP Terraform API supports.
type StackDeploymentSteps interface {
// List returns the stack deployment steps for a stack deployment run.
List(ctx context.Context, stackDeploymentRunID string, opts *StackDeploymentStepsListOptions) (*StackDeploymentStepList, error)
// Read returns a stack deployment step by its ID.
Read(ctx context.Context, stackDeploymentStepID string) (*StackDeploymentStep, error)
// Advance advances the stack deployment step when in the "pending_operator" state.
Advance(ctx context.Context, stackDeploymentStepID string) error
// Diagnostics returns the diagnostics for this stack deployment step.
Diagnostics(ctx context.Context, stackConfigurationID string) (*StackDiagnosticsList, error)
// Artifacts returns the artifacts for this stack deployment step.
// Valid artifact names are "plan-description" and "apply-description".
Artifacts(ctx context.Context, stackDeploymentStepID string, artifactType StackDeploymentStepArtifactType) (io.ReadCloser, error)
}
type StackDeploymentStepArtifactType string
const (
// StackDeploymentStepArtifactPlanDescription represents the plan description artifact type.
StackDeploymentStepArtifactPlanDescription StackDeploymentStepArtifactType = "plan-description"
// StackDeploymentStepArtifactApplyDescription represents the apply description artifact type.
StackDeploymentStepArtifactApplyDescription StackDeploymentStepArtifactType = "apply-description"
// StackDeploymentStepArtifactPlanDescription represents the plan debug log artifact type.
StackDeploymentStepArtifactPlanDebugLog StackDeploymentStepArtifactType = "plan-debug-log"
// StackDeploymentStepArtifactApplyDescription represents the apply debug log artifact type.
StackDeploymentStepArtifactApplyDebugLog StackDeploymentStepArtifactType = "apply-debug-log"
)
type DeploymentStepStatus string
const (
DeploymentStepStatusBlocked DeploymentStepStatus = "blocked"
DeploymentStepStatusAbandoned DeploymentStepStatus = "abandoned"
DeploymentStepStatusQueued DeploymentStepStatus = "queued"
DeploymentStepStatusRunning DeploymentStepStatus = "running"
DeploymentStepStatusPendingOperator DeploymentStepStatus = "pending-operator"
DeploymentStepStatusCompleted DeploymentStepStatus = "completed"
DeploymentStepStatusFailed DeploymentStepStatus = "failed"
)
func (s DeploymentStepStatus) String() string {
return string(s)
}
// StackDeploymentStep represents a step from a stack deployment
type StackDeploymentStep struct {
// Attributes
ID string `jsonapi:"primary,stack-deployment-steps"`
Status DeploymentStepStatus `jsonapi:"attr,status"`
OperationType string `jsonapi:"attr,operation-type"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
// Links
Links map[string]interface{} `jsonapi:"links,omitempty"`
// Relationships
StackDeploymentRun *StackDeploymentRun `jsonapi:"relation,stack-deployment-run"`
}
// StackDeploymentStepList represents a list of stack deployment steps
type StackDeploymentStepList struct {
*Pagination
Items []*StackDeploymentStep
}
type stackDeploymentSteps struct {
client *Client
}
// StackDeploymentStepsListOptions represents the options for listing stack
// deployment steps.
type StackDeploymentStepsListOptions struct {
ListOptions
}
func (s stackDeploymentSteps) List(ctx context.Context, stackDeploymentRunID string, opts *StackDeploymentStepsListOptions) (*StackDeploymentStepList, error) {
req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-deployment-runs/%s/stack-deployment-steps", url.PathEscape(stackDeploymentRunID)), opts)
if err != nil {
return nil, err
}
steps := StackDeploymentStepList{}
err = req.Do(ctx, &steps)
if err != nil {
return nil, err
}
return &steps, nil
}
// Read returns a stack deployment step by its ID.
func (s stackDeploymentSteps) Read(ctx context.Context, stackDeploymentStepID string) (*StackDeploymentStep, error) {
req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-deployment-steps/%s", url.PathEscape(stackDeploymentStepID)), nil)
if err != nil {
return nil, err
}
step := StackDeploymentStep{}
err = req.Do(ctx, &step)
if err != nil {
return nil, err
}
return &step, nil
}
// Advance advances the stack deployment step when in the "pending_operator" state.
func (s stackDeploymentSteps) Advance(ctx context.Context, stackDeploymentStepID string) error {
req, err := s.client.NewRequest("POST", fmt.Sprintf("stack-deployment-steps/%s/advance", url.PathEscape(stackDeploymentStepID)), nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// Diagnostics returns the diagnostics for this stack deployment step.
func (s stackDeploymentSteps) Diagnostics(ctx context.Context, stackDeploymentStepID string) (*StackDiagnosticsList, error) {
req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-deployment-steps/%s/stack-diagnostics", url.PathEscape(stackDeploymentStepID)), nil)
if err != nil {
return nil, err
}
diagnostics := &StackDiagnosticsList{}
err = req.Do(ctx, diagnostics)
if err != nil {
return nil, err
}
return diagnostics, nil
}
// Artifacts returns the artifacts for this stack deployment step.
// Valid artifact names are "plan-description" and "apply-description".
func (s stackDeploymentSteps) Artifacts(ctx context.Context, stackDeploymentStepID string, artifactType StackDeploymentStepArtifactType) (io.ReadCloser, error) {
req, err := s.client.NewRequestWithAdditionalQueryParams("GET",
fmt.Sprintf("stack-deployment-steps/%s/artifacts", url.PathEscape(stackDeploymentStepID)),
nil,
map[string][]string{"name": {url.PathEscape(string(artifactType))}},
)
if err != nil {
return nil, err
}
return req.DoRaw(ctx)
}
================================================
FILE: stack_deployment_steps_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"io"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStackDeploymentStepsList(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
Project: orgTest.DefaultProject,
Name: "test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
Branch: "main",
},
})
require.NoError(t, err)
require.NotNil(t, stack)
stackUpdated, err := client.Stacks.FetchLatestFromVcs(ctx, stack.ID)
require.NoError(t, err)
require.NotNil(t, stackUpdated)
stackUpdated = pollStackDeploymentGroups(t, ctx, client, stackUpdated.ID)
require.NotNil(t, stackUpdated.LatestStackConfiguration)
stackDeploymentGroups, err := client.StackDeploymentGroups.List(ctx, stackUpdated.LatestStackConfiguration.ID, nil)
require.NoError(t, err)
require.NotEmpty(t, stackDeploymentGroups)
sdg := stackDeploymentGroups.Items[0]
stackDeploymentRuns, err := client.StackDeploymentRuns.List(ctx, sdg.ID, nil)
require.NoError(t, err)
require.NotEmpty(t, stackDeploymentRuns)
sdr := stackDeploymentRuns.Items[0]
t.Run("List with invalid stack deployment run ID", func(t *testing.T) {
t.Parallel()
_, err := client.StackDeploymentSteps.List(ctx, "", nil)
assert.Error(t, err)
})
t.Run("List without options", func(t *testing.T) {
t.Parallel()
steps, err := client.StackDeploymentSteps.List(ctx, sdr.ID, nil)
assert.NoError(t, err)
assert.NotEmpty(t, steps)
step := steps.Items[0]
assert.NotNil(t, step)
assert.NotNil(t, step.ID)
assert.NotNil(t, step.Status)
require.NotNil(t, step.StackDeploymentRun)
assert.Equal(t, sdr.ID, step.StackDeploymentRun.ID)
})
t.Run("List with pagination", func(t *testing.T) {
t.Parallel()
steps, err := client.StackDeploymentSteps.List(ctx, sdr.ID, &StackDeploymentStepsListOptions{
ListOptions: ListOptions{
PageNumber: 1,
PageSize: 10,
},
})
assert.NoError(t, err)
assert.NotEmpty(t, steps)
step := steps.Items[0]
assert.NotNil(t, step)
assert.NotNil(t, step.ID)
assert.NotNil(t, step.Status)
require.NotNil(t, step.StackDeploymentRun)
assert.Equal(t, sdr.ID, step.StackDeploymentRun.ID)
})
}
func TestStackDeploymentStepsRead(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
Project: orgTest.DefaultProject,
Name: "test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
Branch: "main",
},
})
require.NoError(t, err)
require.NotNil(t, stack)
stackUpdated, err := client.Stacks.FetchLatestFromVcs(ctx, stack.ID)
require.NoError(t, err)
require.NotNil(t, stackUpdated)
stackUpdated = pollStackDeploymentGroups(t, ctx, client, stackUpdated.ID)
require.NotNil(t, stackUpdated.LatestStackConfiguration)
stackDeploymentGroups, err := client.StackDeploymentGroups.List(ctx, stackUpdated.LatestStackConfiguration.ID, nil)
require.NoError(t, err)
require.NotEmpty(t, stackDeploymentGroups)
sdg := stackDeploymentGroups.Items[0]
stackDeploymentRuns, err := client.StackDeploymentRuns.List(ctx, sdg.ID, nil)
require.NoError(t, err)
require.NotEmpty(t, stackDeploymentRuns)
sdr := stackDeploymentRuns.Items[0]
steps, err := client.StackDeploymentSteps.List(ctx, sdr.ID, nil)
assert.NoError(t, err)
assert.NotEmpty(t, steps)
step := steps.Items[0]
t.Run("Read with valid ID", func(t *testing.T) {
sds, err := client.StackDeploymentSteps.Read(ctx, step.ID)
assert.NoError(t, err)
assert.NotEmpty(t, sds.ID)
assert.NotEmpty(t, sds.Status)
assert.NotEmpty(t, sds.OperationType)
})
t.Run("Read with invalid ID", func(t *testing.T) {
_, err := client.StackDeploymentSteps.Read(ctx, "")
require.Error(t, err)
})
}
func TestStackDeploymentStepsAdvance(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
Project: orgTest.DefaultProject,
Name: "testing-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
Branch: "main",
},
})
require.NoError(t, err)
require.NotNil(t, stack)
stackUpdated, err := client.Stacks.FetchLatestFromVcs(ctx, stack.ID)
require.NoError(t, err)
require.NotNil(t, stackUpdated)
stackUpdated = pollStackDeploymentGroups(t, ctx, client, stackUpdated.ID)
require.NotNil(t, stackUpdated.LatestStackConfiguration)
stackDeploymentGroups, err := client.StackDeploymentGroups.List(ctx, stackUpdated.LatestStackConfiguration.ID, nil)
require.NoError(t, err)
require.NotEmpty(t, stackDeploymentGroups)
sdg := stackDeploymentGroups.Items[0]
stackDeploymentRuns, err := client.StackDeploymentRuns.List(ctx, sdg.ID, nil)
require.NoError(t, err)
require.NotEmpty(t, stackDeploymentRuns)
sdr := stackDeploymentRuns.Items[0]
steps, err := client.StackDeploymentSteps.List(ctx, sdr.ID, nil)
assert.NoError(t, err)
assert.NotEmpty(t, steps)
step := steps.Items[0]
step = pollStackDeploymentStepStatus(t, ctx, client, step.ID, "pending_operator")
require.NotNil(t, step)
t.Run("Advance with valid ID", func(t *testing.T) {
err := client.StackDeploymentSteps.Advance(ctx, step.ID)
assert.NoError(t, err)
// Verify that the step status has changed to "completed"
sds, err := client.StackDeploymentSteps.Read(ctx, step.ID)
assert.NoError(t, err)
assert.Equal(t, "completed", sds.Status)
})
t.Run("Advance with invalid ID", func(t *testing.T) {
err := client.StackDeploymentSteps.Advance(ctx, "")
require.Error(t, err)
})
}
func pollStackDeploymentStepStatus(t *testing.T, ctx context.Context, client *Client, stackDeploymentStepID, status string) (deploymentStep *StackDeploymentStep) {
// pollStackDeploymentStepStatus will poll the given stack deployment step until its status changes or the deadline is reached.
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(5*time.Minute))
defer cancel()
deadline, _ := ctx.Deadline()
t.Logf("Polling stack deployment step %q for change in status to %s with deadline of %s", stackDeploymentStepID, status, deadline)
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
var err error
for finished := false; !finished; {
t.Log("...")
select {
case <-ctx.Done():
t.Fatalf("Stack deployment step %s did not have status %q at deadline", stackDeploymentStepID, status)
case <-ticker.C:
deploymentStep, err = client.StackDeploymentSteps.Read(ctx, stackDeploymentStepID)
if err != nil {
t.Fatalf("Failed to read stack deployment step %s: %s", stackDeploymentStepID, err)
}
t.Logf("Stack deployment step %s had status %q", deploymentStep.ID, deploymentStep.Status)
if deploymentStep.Status.String() == status {
finished = true
}
}
}
return
}
func TestStackDeploymentStepsDiagnosticsArtifacts(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
Project: orgTest.DefaultProject,
Name: "test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
Branch: "main",
},
})
require.NoError(t, err)
require.NotNil(t, stack)
stackUpdated, err := client.Stacks.FetchLatestFromVcs(ctx, stack.ID)
require.NoError(t, err)
require.NotNil(t, stackUpdated)
stackUpdated = pollStackDeploymentGroups(t, ctx, client, stackUpdated.ID)
require.NotNil(t, stackUpdated.LatestStackConfiguration)
stackDeploymentGroups, err := client.StackDeploymentGroups.List(ctx, stackUpdated.LatestStackConfiguration.ID, nil)
require.NoError(t, err)
require.NotEmpty(t, stackDeploymentGroups)
sdg := stackDeploymentGroups.Items[0]
stackDeploymentRuns, err := client.StackDeploymentRuns.List(ctx, sdg.ID, nil)
require.NoError(t, err)
require.NotEmpty(t, stackDeploymentRuns)
sdr := stackDeploymentRuns.Items[0]
steps, err := client.StackDeploymentSteps.List(ctx, sdr.ID, nil)
assert.NoError(t, err)
assert.NotEmpty(t, steps)
step := steps.Items[0]
step = pollStackDeploymentStepStatus(t, ctx, client, step.ID, "pending_operator")
require.NotNil(t, step)
t.Run("Diagnostics with valid ID", func(t *testing.T) {
sds, err := client.StackDeploymentSteps.Diagnostics(ctx, step.ID)
assert.NoError(t, err)
assert.NotEmpty(t, sds)
})
t.Run("Diagnostics with invalid ID", func(t *testing.T) {
_, err := client.StackDeploymentSteps.Diagnostics(ctx, "invalid-id")
require.Error(t, err)
})
t.Run("Artifacts with valid artifact name (plan-description)", func(t *testing.T) {
rawBytes, err := client.StackDeploymentSteps.Artifacts(ctx, step.ID, StackDeploymentStepArtifactPlanDescription)
assert.NoError(t, err)
b, err := io.ReadAll(rawBytes)
assert.NoError(t, err)
assert.NotEmpty(t, string(b))
})
t.Run("Artifacts with invalid artifact name", func(t *testing.T) {
_, err := client.StackDeploymentSteps.Artifacts(ctx, step.ID, "invalid-artifact-name")
assert.Error(t, err)
})
t.Run("Artifacts with invalid step ID", func(t *testing.T) {
_, err := client.StackDeploymentSteps.Artifacts(ctx, "invalid-id", StackDeploymentStepArtifactPlanDescription)
require.Error(t, err)
})
}
================================================
FILE: stack_diagnostic.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"time"
)
type StackDiagnostics interface {
// Read retrieves a stack diagnostic by its ID.
Read(ctx context.Context, stackConfigurationID string) (*StackDiagnostic, error)
// Acknowledge marks a diagnostic as acknowledged.
Acknowledge(ctx context.Context, stackDiagnosticID string) error
}
// StackDiagnostic represents any sourcebundle.Diagnostic value. The simplest form has
// just a severity, single line summary, and optional detail. If there is more
// information about the source of the diagnostic, this is represented in the
// range field.
type StackDiagnostic struct {
ID string `jsonapi:"primary,stack-diagnostics"`
Severity string `jsonapi:"attr,severity"`
Summary string `jsonapi:"attr,summary"`
Detail string `jsonapi:"attr,detail"`
Diags []*StackDiagnosticSummary `jsonapi:"attr,diags"`
Acknowledged bool `jsonapi:"attr,acknowledged"`
AcknowledgedAt *time.Time `jsonapi:"attr,acknowledged-at,iso8601"`
CreatedAt *time.Time `jsonapi:"attr,created-at,iso8601"`
// Relationships
StackDeploymentStep *StackDeploymentStep `jsonapi:"relation,stack-deployment-step"`
StackConfiguration *StackConfiguration `jsonapi:"relation,stack-configuration"`
AcknowledgedBy *User `jsonapi:"relation,acknowledged-by"`
}
type StackDiagnosticSummary struct {
Severity string `jsonapi:"attr,severity"`
Summary string `jsonapi:"attr,summary"`
Detail string `jsonapi:"attr,detail"`
Range *DiagnosticRange `jsonapi:"attr,range"`
Origin string `jsonapi:"attr,origin"`
Snippet *DiagnosticSnippet `jsonapi:"attr,snippet"`
}
type DiagnosticSnippet struct {
Code string `jsonapi:"attr,code"`
Values []string `jsonapi:"attr,values"`
Context *string `jsonapi:"attr,context"`
StartLine int `jsonapi:"attr,start_line"`
HighlightEndOffset int `jsonapi:"attr,highlight_end_offset"`
HighlightStartOffset int `jsonapi:"attr,highlight_start_offset"`
}
type stackDiagnostics struct {
client *Client
}
type StackDiagnosticsList struct {
Items []*StackDiagnostic
}
// DiagnosticPos represents a position in the source code.
type DiagnosticPos struct {
// Line is a one-based count for the line in the indicated file.
Line int `jsonapi:"attr,line"`
// Column is a one-based count of Unicode characters from the start of the line.
Column int `jsonapi:"attr,column"`
// Byte is a zero-based offset into the indicated file.
Byte int `jsonapi:"attr,byte"`
}
// DiagnosticRange represents the filename and position of the diagnostic
// subject. This defines the range of the source to be highlighted in the
// output. Note that the snippet may include additional surrounding source code
// if the diagnostic has a context range.
//
// The stacks-specific source field represents the full source bundle address
// of the file, while the filename field is the sub path relative to its
// enclosing package. This represents an attempt to be somewhat backwards
// compatible with the existing Terraform JSON diagnostic format, where
// filename is root module relative.
//
// The Start position is inclusive, and the End position is exclusive. Exact
// positions are intended for highlighting for human interpretation only and
// are subject to change.
type DiagnosticRange struct {
Filename string `jsonapi:"attr,filename"`
Source string `jsonapi:"attr,source"`
Start DiagnosticPos `jsonapi:"attr,start"`
End DiagnosticPos `jsonapi:"attr,end"`
}
// Read retrieves a stack diagnostic by its ID.
func (s stackDiagnostics) Read(ctx context.Context, stackDiagnosticID string) (*StackDiagnostic, error) {
req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-diagnostics/%s", url.PathEscape(stackDiagnosticID)), nil)
if err != nil {
return nil, err
}
var diagnostics StackDiagnostic
if err := req.Do(ctx, &diagnostics); err != nil {
return nil, err
}
return &diagnostics, nil
}
// Acknowledge marks a diagnostic as acknowledged.
func (s stackDiagnostics) Acknowledge(ctx context.Context, stackDiagnosticID string) error {
req, err := s.client.NewRequest("POST", fmt.Sprintf("stack-diagnostics/%s/acknowledge", url.PathEscape(stackDiagnosticID)), nil)
if err != nil {
return err
}
diagnostic := StackDiagnostic{}
if err := req.Do(ctx, &diagnostic); err != nil {
return err
}
return nil
}
================================================
FILE: stack_diagnostic_integration_test.go
================================================
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStackDiagnosticsReadAcknowledge(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "cc-test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "ctrombley/linked-stacks-demo-network",
Branch: "diagnostics",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
},
Project: &Project{
ID: orgTest.DefaultProject.ID,
},
})
require.NoError(t, err)
require.NotNil(t, stack)
stackUpdated, err := client.Stacks.FetchLatestFromVcs(ctx, stack.ID)
require.NoError(t, err)
require.NotNil(t, stackUpdated)
stackUpdated, err = client.Stacks.Read(ctx, stackUpdated.ID)
require.NoError(t, err)
require.NotNil(t, stackUpdated.LatestStackConfiguration)
pollStackConfigurationStatus(t, ctx, client, stackUpdated.LatestStackConfiguration.ID, "failed")
diags, err := client.StackConfigurations.Diagnostics(ctx, stackUpdated.LatestStackConfiguration.ID)
assert.NoError(t, err)
require.NotEmpty(t, diags.Items)
diag := diags.Items[0]
t.Run("Read with valid ID", func(t *testing.T) {
diag, err := client.StackDiagnostics.Read(ctx, diag.ID)
require.NoError(t, err)
assert.NotNil(t, diag)
assert.NotEmpty(t, diag.ID)
assert.NotEmpty(t, diag.Severity)
assert.NotEmpty(t, diag.Summary)
assert.NotEmpty(t, diag.Detail)
assert.NotEmpty(t, diag.Diags)
for _, d := range diag.Diags {
assert.NotEmpty(t, d.Detail)
assert.NotEmpty(t, d.Severity)
assert.NotEmpty(t, d.Summary)
assert.Empty(t, d.Origin)
require.NotNil(t, d.Range)
assert.NotEmpty(t, d.Range.Filename)
assert.NotEmpty(t, d.Range.Source)
require.NotNil(t, d.Range.Start)
assert.NotZero(t, d.Range.Start.Line)
assert.NotZero(t, d.Range.Start.Column)
assert.NotZero(t, d.Range.Start.Byte)
require.NotNil(t, d.Range.End)
assert.NotZero(t, d.Range.End.Line)
assert.NotZero(t, d.Range.End.Column)
assert.NotZero(t, d.Range.End.Byte)
require.NotNil(t, d.Snippet)
assert.NotEmpty(t, d.Snippet.Code)
assert.Empty(t, d.Snippet.Values)
assert.Nil(t, d.Snippet.Context)
assert.Zero(t, d.Snippet.HighlightStartOffset)
assert.NotZero(t, d.Snippet.HighlightEndOffset)
}
assert.False(t, diag.Acknowledged)
assert.Nil(t, diag.AcknowledgedAt)
assert.NotZero(t, diag.CreatedAt)
assert.Nil(t, diag.StackDeploymentStep)
assert.NotNil(t, diag.StackConfiguration)
assert.Nil(t, diag.AcknowledgedBy)
})
t.Run("Read with invalid ID", func(t *testing.T) {
_, err := client.StackDiagnostics.Read(ctx, "")
require.Error(t, err)
})
t.Run("Acknowledge with valid ID", func(t *testing.T) {
err := client.StackDiagnostics.Acknowledge(ctx, diag.ID)
require.NoError(t, err)
diag, err := client.StackDiagnostics.Read(ctx, diag.ID)
require.NoError(t, err)
assert.NotNil(t, diag)
assert.True(t, diag.Acknowledged)
assert.NotNil(t, diag.AcknowledgedAt)
assert.NotNil(t, diag.AcknowledgedBy)
})
t.Run("Acknowledge with invalid ID", func(t *testing.T) {
err := client.StackDiagnostics.Acknowledge(ctx, "")
require.Error(t, err)
})
}
================================================
FILE: stack_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStackCreateAndList(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
project2, err := client.Projects.Create(ctx, orgTest.Name, ProjectCreateOptions{
Name: "test-project-2",
})
require.NoError(t, err)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
stack1, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "aa-test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
},
Project: &Project{
ID: orgTest.DefaultProject.ID,
},
Migration: Bool(true),
SpeculativeEnabled: Bool(true),
})
require.NoError(t, err)
require.NotNil(t, stack1)
require.True(t, stack1.SpeculativeEnabled)
stack2, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "zz-test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
},
Project: &Project{
ID: project2.ID,
},
})
require.NoError(t, err)
require.NotNil(t, stack2)
require.False(t, stack2.SpeculativeEnabled)
t.Run("List without options", func(t *testing.T) {
t.Parallel()
stackList, err := client.Stacks.List(ctx, orgTest.Name, nil)
require.NoError(t, err)
assert.Len(t, stackList.Items, 2)
assert.Equal(t, stack1.CreationSource, "migration-api")
assert.Equal(t, stack2.CreationSource, "api")
})
t.Run("List with project filter", func(t *testing.T) {
t.Parallel()
stackList, err := client.Stacks.List(ctx, orgTest.Name, &StackListOptions{
ProjectID: project2.ID,
})
require.NoError(t, err)
assert.Len(t, stackList.Items, 1)
assert.Equal(t, stack2.ID, stackList.Items[0].ID)
})
t.Run("List with name filter", func(t *testing.T) {
t.Parallel()
stackList, err := client.Stacks.List(ctx, orgTest.Name, &StackListOptions{
SearchByName: "zz",
})
require.NoError(t, err)
assert.Len(t, stackList.Items, 1)
assert.Equal(t, stack2.ID, stackList.Items[0].ID)
})
t.Run("List with sort options", func(t *testing.T) {
t.Parallel()
// By name ASC
stackList, err := client.Stacks.List(ctx, orgTest.Name, &StackListOptions{
Sort: StackSortByName,
})
require.NoError(t, err)
assert.Len(t, stackList.Items, 2)
assert.Equal(t, stack1.ID, stackList.Items[0].ID)
// By name DESC
stackList, err = client.Stacks.List(ctx, orgTest.Name, &StackListOptions{
Sort: StackSortByNameDesc,
})
require.NoError(t, err)
assert.Len(t, stackList.Items, 2)
assert.Equal(t, stack2.ID, stackList.Items[0].ID)
})
t.Run("List with pagination", func(t *testing.T) {
t.Parallel()
stackList, err := client.Stacks.List(ctx, orgTest.Name, &StackListOptions{
ListOptions: ListOptions{
PageNumber: 1,
PageSize: 1,
},
})
require.NoError(t, err)
assert.Len(t, stackList.Items, 1)
assert.Equal(t, 2, stackList.TotalPages)
assert.Equal(t, 2, stackList.TotalCount)
})
}
func TestStackReadUpdateDelete(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
initialPool, err := client.AgentPools.Create(ctx, orgTest.Name, AgentPoolCreateOptions{
Name: String("initial-test-pool"),
})
require.NoError(t, err)
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
Branch: "main",
},
Project: &Project{
ID: orgTest.DefaultProject.ID,
},
AgentPool: initialPool,
WorkingDirectory: String("envs"),
TriggerPatterns: []string{"/**/*"},
})
require.NoError(t, err)
require.NotNil(t, stack)
require.NotEmpty(t, stack.VCSRepo.Identifier)
require.NotEmpty(t, stack.VCSRepo.OAuthTokenID)
require.NotEmpty(t, stack.VCSRepo.Branch)
require.False(t, stack.SpeculativeEnabled)
stackRead, err := client.Stacks.Read(ctx, stack.ID)
require.NoError(t, err)
require.Equal(t, stack.VCSRepo.Identifier, stackRead.VCSRepo.Identifier)
require.Equal(t, stack.VCSRepo.OAuthTokenID, stackRead.VCSRepo.OAuthTokenID)
require.Equal(t, stack.VCSRepo.Branch, stackRead.VCSRepo.Branch)
require.Equal(t, stack.AgentPool.ID, stackRead.AgentPool.ID)
assert.Equal(t, stack, stackRead)
assert.Equal(t, stackRead.WorkingDirectory, "envs")
assert.Equal(t, stackRead.TriggerPatterns, []string{"/**/*"})
assert.False(t, stackRead.SpeculativeEnabled)
updatedPool, err := client.AgentPools.Create(ctx, orgTest.Name, AgentPoolCreateOptions{
Name: String("updated-test-pool"),
})
require.NoError(t, err)
stackUpdated, err := client.Stacks.Update(ctx, stack.ID, StackUpdateOptions{
Description: String("updated description"),
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
Branch: "main",
},
AgentPool: updatedPool,
SpeculativeEnabled: Bool(true),
WorkingDirectory: String(""),
TriggerPatterns: []string{""},
})
require.NoError(t, err)
require.Equal(t, "updated description", stackUpdated.Description)
require.Equal(t, updatedPool.ID, stackUpdated.AgentPool.ID)
require.True(t, stackUpdated.SpeculativeEnabled)
stackUpdatedConfig, err := client.Stacks.FetchLatestFromVcs(ctx, stack.ID)
require.NoError(t, err)
require.Equal(t, stack.Name, stackUpdatedConfig.Name)
require.Equal(t, stackUpdated.WorkingDirectory, "")
require.Equal(t, stackUpdated.TriggerPatterns, []string{""})
err = client.Stacks.Delete(ctx, stack.ID)
require.NoError(t, err)
stackReadAfterDelete, err := client.Stacks.Read(ctx, stack.ID)
require.ErrorIs(t, err, ErrResourceNotFound)
require.Nil(t, stackReadAfterDelete)
}
func TestStackRemoveVCSBacking(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
Branch: "main",
},
Project: &Project{
ID: orgTest.DefaultProject.ID,
},
})
require.NoError(t, err)
require.NotNil(t, stack)
require.NotEmpty(t, stack.VCSRepo.Identifier)
require.NotEmpty(t, stack.VCSRepo.OAuthTokenID)
require.NotEmpty(t, stack.VCSRepo.Branch)
stackRead, err := client.Stacks.Read(ctx, stack.ID)
require.NoError(t, err)
require.Equal(t, stack.VCSRepo.Identifier, stackRead.VCSRepo.Identifier)
require.Equal(t, stack.VCSRepo.OAuthTokenID, stackRead.VCSRepo.OAuthTokenID)
require.Equal(t, stack.VCSRepo.Branch, stackRead.VCSRepo.Branch)
assert.Equal(t, stack, stackRead)
stackUpdated, err := client.Stacks.Update(ctx, stack.ID, StackUpdateOptions{
VCSRepo: nil,
})
require.NoError(t, err)
require.Nil(t, stackUpdated.VCSRepo)
}
func TestStackReadUpdateForceDelete(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
Branch: "main",
},
Project: &Project{
ID: orgTest.DefaultProject.ID,
},
})
require.NoError(t, err)
require.NotNil(t, stack)
require.NotEmpty(t, stack.VCSRepo.Identifier)
require.NotEmpty(t, stack.VCSRepo.OAuthTokenID)
require.NotEmpty(t, stack.VCSRepo.Branch)
stackRead, err := client.Stacks.Read(ctx, stack.ID)
require.NoError(t, err)
require.Equal(t, stack.VCSRepo.Identifier, stackRead.VCSRepo.Identifier)
require.Equal(t, stack.VCSRepo.OAuthTokenID, stackRead.VCSRepo.OAuthTokenID)
require.Equal(t, stack.VCSRepo.Branch, stackRead.VCSRepo.Branch)
assert.Equal(t, stack, stackRead)
stackUpdated, err := client.Stacks.Update(ctx, stack.ID, StackUpdateOptions{
Description: String("updated description"),
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
Branch: "main",
},
})
require.NoError(t, err)
require.Equal(t, "updated description", stackUpdated.Description)
stackUpdatedConfig, err := client.Stacks.FetchLatestFromVcs(ctx, stack.ID)
require.NoError(t, err)
require.Equal(t, stack.Name, stackUpdatedConfig.Name)
err = client.Stacks.ForceDelete(ctx, stack.ID)
require.NoError(t, err)
stackReadAfterDelete, err := client.Stacks.Read(ctx, stack.ID)
require.ErrorIs(t, err, ErrResourceNotFound)
require.Nil(t, stackReadAfterDelete)
}
func pollStackDeploymentGroups(t *testing.T, ctx context.Context, client *Client, stackID string) (stack *Stack) {
t.Helper()
// pollStackDeployments will poll the given stack until it has deployments or the deadline is reached.
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(5*time.Minute))
defer cancel()
deadline, _ := ctx.Deadline()
t.Logf("Polling stack %q for deployment groups with deadline of %s", stackID, deadline)
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for finished := false; !finished; {
t.Log("...")
select {
case <-ctx.Done():
t.Fatalf("Stack %q had no deployment groups at deadline", stackID)
case <-ticker.C:
var err error
stack, err = client.Stacks.Read(ctx, stackID)
if err != nil {
t.Fatalf("Failed to read stack %q: %s", stackID, err)
}
groups, err := client.StackDeploymentGroups.List(ctx, stack.LatestStackConfiguration.ID, nil)
if err != nil {
t.Fatalf("Failed to read deployment groups %q: %s", stackID, err)
}
t.Logf("Stack %q had %d deployment groups", stack.ID, groups.TotalCount)
if groups.TotalCount > 0 {
finished = true
}
}
}
return stack
}
func pollStackDeploymentGroupStatus(t *testing.T, ctx context.Context, client *Client, configurationID, status string) {
// pollStackDeploymentGroupStatus will poll the given stack until its deployment groups
// all match the given status, or the deadline is reached.
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(5*time.Minute))
defer cancel()
deadline, _ := ctx.Deadline()
t.Logf("Polling configuration %q for deployments with deadline of %s", configurationID, deadline)
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for finished := false; !finished; {
t.Log("...")
select {
case <-ctx.Done():
t.Fatalf("Stack deployment groups for config %s did not have status %q at deadline", configurationID, status)
case <-ticker.C:
var err error
summaries, err := client.StackDeploymentGroupSummaries.List(ctx, configurationID, nil)
if err != nil {
t.Fatalf("Failed to read stack deployment groups for config %s: %s", configurationID, err)
}
for _, group := range summaries.Items {
t.Logf("Stack deployment group %s for config %s had status %q", group.ID, configurationID, group.Status)
if group.Status == status {
finished = true
}
}
}
}
}
func pollStackDeploymentRunStatus(t *testing.T, ctx context.Context, client *Client, deploymentRunID, status string) {
// pollStackDeploymentRunStatus will poll the given stack until the targeted
// deployment run matches the given status, or the deadline is reached.
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(5*time.Minute))
defer cancel()
deadline, _ := ctx.Deadline()
t.Logf("Polling deployment run %q for status %s with deadline of %s", deploymentRunID, status, deadline)
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for finished := false; !finished; {
t.Log("...")
select {
case <-ctx.Done():
t.Fatalf("Stack deployment run %s did not have status %q at deadline", deploymentRunID, status)
case <-ticker.C:
var err error
deploymentRun, err := client.StackDeploymentRuns.Read(ctx, deploymentRunID)
if err != nil {
t.Fatalf("Failed to read stack deployment run %s: %s", deploymentRunID, err)
}
t.Logf("Stack deployment run %s had status %q", deploymentRunID, deploymentRun.Status)
if deploymentRun.Status.String() == status {
finished = true
}
}
}
}
func pollStackConfigurationStatus(t *testing.T, ctx context.Context, client *Client, stackConfigID, status string) (stackConfig *StackConfiguration) {
// pollStackDeployments will poll the given stack until it has deployments or the deadline is reached.
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(5*time.Minute))
defer cancel()
deadline, _ := ctx.Deadline()
t.Logf("Polling stack configuration %q for status %q with deadline of %s", stackConfigID, status, deadline)
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
var err error
for finished := false; !finished; {
t.Log("...")
select {
case <-ctx.Done():
t.Fatalf("Stack configuration %q did not have status %q at deadline", stackConfigID, status)
case <-ticker.C:
stackConfig, err = client.StackConfigurations.Read(ctx, stackConfigID)
if err != nil {
t.Fatalf("Failed to read stack configuration %q: %s", stackConfigID, err)
}
t.Logf("Stack configuration %q had status %q", stackConfigID, stackConfig.Status)
if stackConfig.Status.String() == status {
finished = true
}
}
}
return
}
func pollNewStackConfiguration(t *testing.T, ctx context.Context, client *Client, stackID string) (stackConfig *StackConfiguration) {
// pollNewStackConfiguration can be used after a new configuration is fetched
// to ensure a non-nil configuration is returned.
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(5*time.Minute))
defer cancel()
deadline, _ := ctx.Deadline()
t.Logf("Polling stack %s for new stack configuration with deadline of %s", stackID, deadline)
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for finished := false; !finished; {
t.Log("...")
select {
case <-ctx.Done():
t.Fatalf("Stack %q had no new configuration at deadline", stackID)
case <-ticker.C:
stackConfigs, err := client.StackConfigurations.List(ctx, stackID, nil)
if err != nil {
t.Fatalf("Failed to list stack configurations for stack %q: %s", stackID, err)
}
if len(stackConfigs.Items) > 0 {
stackConfig = stackConfigs.Items[0]
finished = true
}
}
}
return
}
================================================
FILE: stack_state.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"io"
"net/url"
)
// StackState describes all the stack state-related methods that the
// HCP Terraform API supports.
type StackStates interface {
// List returns the stack states for a stack.
List(ctx context.Context, stackID string, opts *StackStateListOptions) (*StackStateList, error)
// Read returns a stack state by its ID.
Read(ctx context.Context, stackStateID string) (*StackState, error)
// Description returns the state description for the given stack state.
// The description is returned as an io.ReadCloser and should be closed and
// unmarshaled by the caller.
Description(ctx context.Context, stackStateID string) (io.ReadCloser, error)
}
// StackState represents a stack state.
type StackState struct {
// Attributes
ID string `jsonapi:"primary,stack-states"`
Generation int `jsonapi:"attr,generation"`
Status string `jsonapi:"attr,status"`
Deployment string `jsonapi:"attr,deployment"`
Components []*StackComponent `jsonapi:"attr,components"`
IsCurrent bool `jsonapi:"attr,is-current"`
ResourceInstanceCount int `jsonapi:"attr,resource-instance-count"`
// Relationships
Stack *Stack `jsonapi:"relation,stack"`
StackDeploymentRun *StackDeploymentRun `jsonapi:"relation,stack-deployment-run"`
// Links
Links map[string]interface{} `jsonapi:"links,omitempty"`
}
// StackStateList represents a list of stack states.
type StackStateList struct {
*Pagination
Items []*StackState
}
type stackStates struct {
client *Client
}
// StackStateListOptions represents the options for listing stack states.
type StackStateListOptions struct {
ListOptions
}
// List returns the stack states for a stack.
func (s stackStates) List(ctx context.Context, stackID string, opts *StackStateListOptions) (*StackStateList, error) {
req, err := s.client.NewRequest("GET", fmt.Sprintf("stacks/%s/stack-states", url.PathEscape(stackID)), opts)
if err != nil {
return nil, err
}
states := StackStateList{}
if err := req.Do(ctx, &states); err != nil {
return nil, err
}
return &states, nil
}
// Read returns a stack state by its ID.
func (s stackStates) Read(ctx context.Context, stackStateID string) (*StackState, error) {
req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-states/%s", url.PathEscape(stackStateID)), nil)
if err != nil {
return nil, err
}
state := StackState{}
if err := req.Do(ctx, &state); err != nil {
return nil, err
}
return &state, nil
}
// Description returns the state description for the given stack state.
func (s stackStates) Description(ctx context.Context, stackStateID string) (io.ReadCloser, error) {
req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-states/%s/description", url.PathEscape(stackStateID)), nil)
if err != nil {
return nil, err
}
return req.DoRaw(ctx)
}
================================================
FILE: stack_state_integration_test.go
================================================
package tfe
import (
"context"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStackStateListReadDescription(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "aa-test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
},
Project: &Project{
ID: orgTest.DefaultProject.ID,
},
})
require.NoError(t, err)
require.NotNil(t, stack)
stack2, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "bb-test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
},
Project: &Project{
ID: orgTest.DefaultProject.ID,
},
})
require.NoError(t, err)
require.NotNil(t, stack2)
stackUpdated, err := client.Stacks.FetchLatestFromVcs(ctx, stack.ID)
require.NoError(t, err)
require.NotNil(t, stackUpdated)
stackUpdated = pollStackDeploymentGroups(t, ctx, client, stackUpdated.ID)
require.NotNil(t, stackUpdated.LatestStackConfiguration)
// Get the deployment group ID from the stack configuration
deploymentGroups, err := client.StackDeploymentGroups.List(ctx, stackUpdated.LatestStackConfiguration.ID, nil)
require.NoError(t, err)
require.NotNil(t, deploymentGroups)
require.NotEmpty(t, deploymentGroups.Items)
for _, dg := range deploymentGroups.Items {
err = client.StackDeploymentGroups.ApproveAllPlans(ctx, dg.ID)
require.NoError(t, err)
}
pollStackDeploymentGroupStatus(t, ctx, client, stackUpdated.LatestStackConfiguration.ID, "succeeded")
t.Run("List with valid ID", func(t *testing.T) {
states, err := client.StackStates.List(ctx, stackUpdated.ID, nil)
require.NoError(t, err)
require.NotNil(t, states)
require.NotEmpty(t, states.Items)
})
t.Run("List with invalid ID", func(t *testing.T) {
_, err := client.StackStates.List(ctx, "invalid-id", nil)
require.Error(t, err)
})
states, err := client.StackStates.List(ctx, stackUpdated.ID, nil)
require.NoError(t, err)
require.NotNil(t, states)
require.NotEmpty(t, states.Items)
state := states.Items[0]
t.Run("Read with valid ID", func(t *testing.T) {
state, err := client.StackStates.Read(ctx, state.ID)
require.NoError(t, err)
require.NotNil(t, state)
assert.NotEmpty(t, state.ID)
// Assert attribute presence
assert.NotZero(t, state.Generation)
assert.NotEmpty(t, state.Status)
assert.NotEmpty(t, state.Deployment)
assert.NotNil(t, state.Components)
assert.True(t, state.IsCurrent)
assert.NotZero(t, state.ResourceInstanceCount)
// Assert relationship presence
assert.NotNil(t, state.Stack)
assert.NotEmpty(t, state.Stack.ID)
assert.NotNil(t, state.StackDeploymentRun)
assert.NotEmpty(t, state.StackDeploymentRun)
// Assert link presence
assert.NotEmpty(t, state.Links)
// Description link
description, ok := state.Links["description"].(string)
require.True(t, ok)
assert.NotEmpty(t, description)
})
t.Run("Read with invalid ID", func(t *testing.T) {
_, err := client.StackStates.Read(ctx, "invalid-id")
require.Error(t, err)
})
t.Run("Description with valid ID", func(t *testing.T) {
rawBytes, err := client.StackStates.Description(ctx, state.ID)
require.NoError(t, err)
defer rawBytes.Close()
b, err := io.ReadAll(rawBytes)
require.NoError(t, err)
require.NotEmpty(t, string(b))
})
t.Run("Description with invalid ID", func(t *testing.T) {
_, err := client.StackStates.Description(ctx, "invalid-id")
require.Error(t, err)
})
}
================================================
FILE: state_version.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"fmt"
"net/url"
"strings"
"time"
"golang.org/x/sync/errgroup"
)
// Compile-time proof of interface implementation.
var _ StateVersions = (*stateVersions)(nil)
// StateVersionStatus are available state version status values
type StateVersionStatus string
// Available state version statuses.
const (
StateVersionPending StateVersionStatus = "pending"
StateVersionFinalized StateVersionStatus = "finalized"
StateVersionDiscarded StateVersionStatus = "discarded"
)
// StateVersions describes all the state version related methods that
// the Terraform Enterprise API supports.
//
// TFE API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/state-versions
type StateVersions interface {
// List all the state versions for a given workspace.
List(ctx context.Context, options *StateVersionListOptions) (*StateVersionList, error)
// Create a new state version for the given workspace.
Create(ctx context.Context, workspaceID string, options StateVersionCreateOptions) (*StateVersion, error)
// Upload creates a new state version but uploads the state content directly to the object store.
// This is a more resilient form of Create and is the recommended approach to creating state versions.
Upload(ctx context.Context, workspaceID string, options StateVersionUploadOptions) (*StateVersion, error)
// UploadSanitizedState uploads a sanitized version of the state to the provided sanitized state upload url.
// The SanitizedStateUploadURL cannot be empty.
UploadSanitizedState(ctx context.Context, sanitizedStateUploadURL *string, sanitizedState []byte) error
// Read a state version by its ID.
Read(ctx context.Context, svID string) (*StateVersion, error)
// ReadWithOptions reads a state version by its ID using the options supplied
ReadWithOptions(ctx context.Context, svID string, options *StateVersionReadOptions) (*StateVersion, error)
// ReadCurrent reads the latest available state from the given workspace.
ReadCurrent(ctx context.Context, workspaceID string) (*StateVersion, error)
// ReadCurrentWithOptions reads the latest available state from the given workspace using the options supplied
ReadCurrentWithOptions(ctx context.Context, workspaceID string, options *StateVersionCurrentOptions) (*StateVersion, error)
// Download retrieves the actual stored state of a state version
Download(ctx context.Context, url string) ([]byte, error)
// ListOutputs retrieves all the outputs of a state version by its ID. IMPORTANT: HCP Terraform might
// process outputs asynchronously. When consuming outputs or other async StateVersion fields, be sure to
// wait for ResourcesProcessed to become `true` before assuming they are empty.
ListOutputs(ctx context.Context, svID string, options *StateVersionOutputsListOptions) (*StateVersionOutputsList, error)
// SoftDeleteBackingData soft deletes the state version's backing data
// **Note: This functionality is only available in Terraform Enterprise.**
SoftDeleteBackingData(ctx context.Context, svID string) error
// RestoreBackingData restores a soft deleted state version's backing data
// **Note: This functionality is only available in Terraform Enterprise.**
RestoreBackingData(ctx context.Context, svID string) error
// PermanentlyDeleteBackingData permanently deletes a soft deleted state version's backing data
// **Note: This functionality is only available in Terraform Enterprise.**
PermanentlyDeleteBackingData(ctx context.Context, svID string) error
}
// stateVersions implements StateVersions.
type stateVersions struct {
client *Client
}
// StateVersionList represents a list of state versions.
type StateVersionList struct {
*Pagination
Items []*StateVersion
}
// StateVersion represents a Terraform Enterprise state version.
type StateVersion struct {
ID string `jsonapi:"primary,state-versions"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
DownloadURL string `jsonapi:"attr,hosted-state-download-url"`
UploadURL string `jsonapi:"attr,hosted-state-upload-url"`
Status StateVersionStatus `jsonapi:"attr,status"`
JSONUploadURL string `jsonapi:"attr,hosted-json-state-upload-url"`
JSONDownloadURL string `jsonapi:"attr,hosted-json-state-download-url"`
Serial int64 `jsonapi:"attr,serial"`
Size int64 `jsonapi:"attr,size"`
VCSCommitSHA string `jsonapi:"attr,vcs-commit-sha"`
VCSCommitURL string `jsonapi:"attr,vcs-commit-url"`
BillableRUMCount *uint32 `jsonapi:"attr,billable-rum-count"`
EncryptedStateDownloadURL *string `jsonapi:"attr,encrypted-state-download-url,omitempty"`
SanitizedStateUploadURL *string `jsonapi:"attr,sanitized-state-upload-url,omitempty"`
SanitizedStateDownloadURL *string `jsonapi:"attr,sanitized-state-download-url,omitempty"`
// Whether HCP Terraform has finished populating any StateVersion fields that required async processing.
// If `false`, some fields may appear empty even if they should actually contain data; see comments on
// individual fields for details.
ResourcesProcessed bool `jsonapi:"attr,resources-processed"`
StateVersion int `jsonapi:"attr,state-version"`
// Populated asynchronously.
TerraformVersion string `jsonapi:"attr,terraform-version"`
// Populated asynchronously.
Modules *StateVersionModules `jsonapi:"attr,modules"`
// Populated asynchronously.
Providers *StateVersionProviders `jsonapi:"attr,providers"`
// Populated asynchronously.
Resources []*StateVersionResources `jsonapi:"attr,resources"`
// Relations
Run *Run `jsonapi:"relation,run"`
Outputs []*StateVersionOutput `jsonapi:"relation,outputs"`
HYOKEncryptedDataKey *HYOKEncryptedDataKey `jsonapi:"relation,hyok-encrypted-data-key,omitempty"`
}
// StateVersionOutputsList represents a list of StateVersionOutput items.
type StateVersionOutputsList struct {
*Pagination
Items []*StateVersionOutput
}
// StateVersionListOptions represents the options for listing state versions.
type StateVersionListOptions struct {
ListOptions
Organization string `url:"filter[organization][name]"`
Workspace string `url:"filter[workspace][name]"`
}
// StateVersionIncludeOpt represents the available options for include query params.
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/state-versions#available-related-resources
type StateVersionIncludeOpt string
const (
SVcreatedby StateVersionIncludeOpt = "created_by"
SVrun StateVersionIncludeOpt = "run"
SVrunCreatedBy StateVersionIncludeOpt = "run.created_by"
SVrunConfigurationVersion StateVersionIncludeOpt = "run.configuration_version"
SVoutputs StateVersionIncludeOpt = "outputs"
)
// StateVersionReadOptions represents the options for reading state version.
type StateVersionReadOptions struct {
// Optional: A list of relations to include. See available resources:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/state-versions#available-related-resources
Include []StateVersionIncludeOpt `url:"include,omitempty"`
}
// StateVersionOutputsListOptions represents the options for listing state
// version outputs.
type StateVersionOutputsListOptions struct {
ListOptions
}
// StateVersionCurrentOptions represents the options for reading the current state version.
type StateVersionCurrentOptions struct {
// Optional: A list of relations to include. See available resources:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/state-versions#available-related-resources
Include []StateVersionIncludeOpt `url:"include,omitempty"`
}
// StateVersionCreateOptions represents the options for creating a state version.
type StateVersionCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,state-versions"`
// Optional: The lineage of the state.
Lineage *string `jsonapi:"attr,lineage,omitempty"`
// Required: The MD5 hash of the state version.
MD5 *string `jsonapi:"attr,md5"`
// Required: The serial of the state.
Serial *int64 `jsonapi:"attr,serial"`
// Optional: The base64 encoded state.
State *string `jsonapi:"attr,state,omitempty"`
// Optional: Force can be set to skip certain validations. Wrong use
// of this flag can cause data loss, so USE WITH CAUTION!
Force *bool `jsonapi:"attr,force,omitempty"`
// Optional: Specifies the run to associate the state with.
Run *Run `jsonapi:"relation,run,omitempty"`
// Optional: The external, json representation of state data, base64 encoded.
// https://developer.hashicorp.com/terraform/internals/json-format#state-representation
// Supplying this state representation can provide more details to the platform
// about the current terraform state.
JSONState *string `jsonapi:"attr,json-state,omitempty"`
// Optional: The external, json representation of state outputs, base64 encoded. Supplying this field
// will provide more detailed output type information to TFE.
// For more information on the contents of this field: https://developer.hashicorp.com/terraform/internals/json-format#values-representation
// about the current terraform state.
JSONStateOutputs *string `jsonapi:"attr,json-state-outputs,omitempty"`
}
type StateVersionUploadOptions struct {
StateVersionCreateOptions
RawState []byte
RawJSONState []byte
}
type StateVersionModules struct {
Root StateVersionModuleRoot `jsonapi:"attr,root"`
}
type StateVersionModuleRoot struct {
NullResource int `jsonapi:"attr,null-resource"`
TerraformRemoteState int `jsonapi:"attr,data.terraform-remote-state"`
}
type StateVersionProviders struct {
Data ProviderData `jsonapi:"attr,provider[map]string"`
}
type ProviderData struct {
NullResource int `json:"null-resource"`
TerraformRemoteState int `json:"data.terraform-remote-state"`
}
type StateVersionResources struct {
Name string `jsonapi:"attr,name"`
Count int `jsonapi:"attr,count"`
Type string `jsonapi:"attr,type"`
Module string `jsonapi:"attr,module"`
Provider string `jsonapi:"attr,provider"`
}
// List all the state versions for a given workspace.
func (s *stateVersions) List(ctx context.Context, options *StateVersionListOptions) (*StateVersionList, error) {
if err := options.valid(); err != nil {
return nil, err
}
req, err := s.client.NewRequest("GET", "state-versions", options)
if err != nil {
return nil, err
}
svl := &StateVersionList{}
err = req.Do(ctx, svl)
if err != nil {
return nil, err
}
return svl, nil
}
// Create a new state version for the given workspace.
func (s *stateVersions) Create(ctx context.Context, workspaceID string, options StateVersionCreateOptions) (*StateVersion, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("workspaces/%s/state-versions", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
sv := &StateVersion{}
err = req.Do(ctx, sv)
if err != nil {
return nil, err
}
return sv, nil
}
// Upload creates a new state version but uploads the state content directly to the object store.
// This is a more resilient form of Create and is the recommended approach to creating state versions.
func (s *stateVersions) Upload(ctx context.Context, workspaceID string, options StateVersionUploadOptions) (*StateVersion, error) {
if err := options.valid(); err != nil {
return nil, err
}
sv, err := s.Create(ctx, workspaceID, options.StateVersionCreateOptions)
if err != nil {
if strings.Contains(err.Error(), "param is missing or the value is empty: state") {
return nil, ErrStateVersionUploadNotSupported
}
return nil, err
}
g, _ := errgroup.WithContext(ctx)
g.Go(func() error {
return s.client.doForeignPUTRequest(ctx, sv.UploadURL, bytes.NewReader(options.RawState))
})
if options.RawJSONState != nil {
g.Go(func() error {
return s.client.doForeignPUTRequest(ctx, sv.JSONUploadURL, bytes.NewReader(options.RawJSONState))
})
}
if err := g.Wait(); err != nil {
return nil, err
}
// Re-read the state version to get the updated status, if available
return s.Read(ctx, sv.ID)
}
// UploadSanitizedState uploads a sanitized version of the state to the provided sanitized state upload url.
// The SanitizedStateUploadURL cannot be empty.
func (s *stateVersions) UploadSanitizedState(ctx context.Context, sanitizedStateUploadURL *string, sanitizedState []byte) error {
if sanitizedStateUploadURL == nil {
return ErrSanitizedStateUploadURLMissing
}
return s.client.doForeignPUTRequest(ctx, *sanitizedStateUploadURL, bytes.NewReader(sanitizedState))
}
// Read a state version by its ID.
func (s *stateVersions) ReadWithOptions(ctx context.Context, svID string, options *StateVersionReadOptions) (*StateVersion, error) {
if !validStringID(&svID) {
return nil, ErrInvalidStateVerID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("state-versions/%s", url.PathEscape(svID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
sv := &StateVersion{}
err = req.Do(ctx, sv)
if err != nil {
return nil, err
}
return sv, nil
}
// Read a state version by its ID.
func (s *stateVersions) Read(ctx context.Context, svID string) (*StateVersion, error) {
return s.ReadWithOptions(ctx, svID, nil)
}
// ReadCurrentWithOptions reads the latest available state from the given workspace using the options supplied.
func (s *stateVersions) ReadCurrentWithOptions(ctx context.Context, workspaceID string, options *StateVersionCurrentOptions) (*StateVersion, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("workspaces/%s/current-state-version", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
sv := &StateVersion{}
err = req.Do(ctx, sv)
if err != nil {
return nil, err
}
return sv, nil
}
// ReadCurrent reads the latest available state from the given workspace.
func (s *stateVersions) ReadCurrent(ctx context.Context, workspaceID string) (*StateVersion, error) {
return s.ReadCurrentWithOptions(ctx, workspaceID, nil)
}
// Download retrieves the actual stored state of a state version
func (s *stateVersions) Download(ctx context.Context, u string) ([]byte, error) {
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
var buf bytes.Buffer
err = req.Do(ctx, &buf)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// ListOutputs retrieves all the outputs of a state version by its ID. IMPORTANT: HCP Terraform might
// process outputs asynchronously. When consuming outputs or other async StateVersion fields, be sure to
// wait for ResourcesProcessed to become `true` before assuming they are empty.
func (s *stateVersions) ListOutputs(ctx context.Context, svID string, options *StateVersionOutputsListOptions) (*StateVersionOutputsList, error) {
if !validStringID(&svID) {
return nil, ErrInvalidStateVerID
}
u := fmt.Sprintf("state-versions/%s/outputs", url.PathEscape(svID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
sv := &StateVersionOutputsList{}
err = req.Do(ctx, sv)
if err != nil {
return nil, err
}
return sv, nil
}
func (s *stateVersions) SoftDeleteBackingData(ctx context.Context, svID string) error {
return s.manageBackingData(ctx, svID, "soft_delete_backing_data")
}
func (s *stateVersions) RestoreBackingData(ctx context.Context, svID string) error {
return s.manageBackingData(ctx, svID, "restore_backing_data")
}
func (s *stateVersions) PermanentlyDeleteBackingData(ctx context.Context, svID string) error {
return s.manageBackingData(ctx, svID, "permanently_delete_backing_data")
}
func (s *stateVersions) manageBackingData(ctx context.Context, svID, action string) error {
if !validStringID(&svID) {
return ErrInvalidStateVerID
}
u := fmt.Sprintf("state-versions/%s/actions/%s", svID, action)
req, err := s.client.NewRequest("POST", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// check that StateVersionListOptions fields had valid values
func (o *StateVersionListOptions) valid() error {
if o == nil {
return ErrRequiredStateVerListOps
}
if !validString(&o.Organization) {
return ErrRequiredOrg
}
if !validString(&o.Workspace) {
return ErrRequiredWorkspace
}
return nil
}
func (o StateVersionCreateOptions) valid() error {
if !validString(o.MD5) {
return ErrRequiredM5
}
if o.Serial == nil {
return ErrRequiredSerial
}
return nil
}
func (o StateVersionUploadOptions) valid() error {
if err := o.StateVersionCreateOptions.valid(); err != nil {
return err
}
if o.State != nil || o.JSONState != nil {
return ErrStateMustBeOmitted
}
if o.RawState == nil {
return ErrRequiredRawState
}
return nil
}
func (o *StateVersionReadOptions) valid() error {
return nil
}
func (o *StateVersionCurrentOptions) valid() error {
return nil
}
================================================
FILE: state_version_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"crypto/md5"
"encoding/base64"
"fmt"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func containsStateVersion(versions []*StateVersion, item *StateVersion) bool {
for _, sv := range versions {
if sv.ID == item.ID {
return true
}
}
return false
}
func TestStateVersionsList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup)
svTest1, svTestCleanup1 := createStateVersion(t, client, 0, wTest)
t.Cleanup(svTestCleanup1)
svTest2, svTestCleanup2 := createStateVersion(t, client, 1, wTest)
t.Cleanup(svTestCleanup2)
t.Run("without StateVersionListOptions", func(t *testing.T) {
svl, err := client.StateVersions.List(ctx, nil)
assert.Nil(t, svl)
assert.Equal(t, err, ErrRequiredStateVerListOps)
})
t.Run("without list options", func(t *testing.T) {
options := &StateVersionListOptions{
Organization: orgTest.Name,
Workspace: wTest.Name,
}
svl, err := client.StateVersions.List(ctx, options)
require.NoError(t, err)
require.NotEmpty(t, svl.Items)
assert.True(t, containsStateVersion(svl.Items, svTest1), fmt.Sprintf("State Versions did not contain %s", svTest1.ID))
assert.True(t, containsStateVersion(svl.Items, svTest2), fmt.Sprintf("State Versions did not contain %s", svTest2.ID))
assert.Equal(t, 1, svl.CurrentPage)
assert.Equal(t, 2, svl.TotalCount)
})
t.Run("with list options", func(t *testing.T) {
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
options := &StateVersionListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
Organization: orgTest.Name,
Workspace: wTest.Name,
}
svl, err := client.StateVersions.List(ctx, options)
require.NoError(t, err)
assert.Empty(t, svl.Items)
assert.Equal(t, 999, svl.CurrentPage)
assert.Equal(t, 2, svl.TotalCount)
})
t.Run("without an organization", func(t *testing.T) {
options := &StateVersionListOptions{
Workspace: wTest.Name,
}
svl, err := client.StateVersions.List(ctx, options)
assert.Nil(t, svl)
assert.Equal(t, err, ErrRequiredOrg)
})
t.Run("without a workspace", func(t *testing.T) {
options := &StateVersionListOptions{
Organization: orgTest.Name,
}
svl, err := client.StateVersions.List(ctx, options)
assert.Nil(t, svl)
assert.Equal(t, err, ErrRequiredWorkspace)
})
}
func TestStateVersionsUpload(t *testing.T) {
t.Parallel()
client := testClient(t)
wTest, wTestCleanup := createWorkspace(t, client, nil)
t.Cleanup(wTestCleanup)
state, err := os.ReadFile("test-fixtures/state-version/terraform.tfstate")
if err != nil {
t.Fatal(err)
}
jsonState, err := os.ReadFile("test-fixtures/json-state/state.json")
if err != nil {
t.Fatal(err)
}
jsonStateOutputs, err := os.ReadFile("test-fixtures/json-state-outputs/everything.json")
if err != nil {
t.Fatal(err)
}
t.Run("can create finalized state versions", func(t *testing.T) {
ctx := context.Background()
_, err := client.Workspaces.Lock(ctx, wTest.ID, WorkspaceLockOptions{})
require.NoError(t, err)
sv, err := client.StateVersions.Upload(ctx, wTest.ID, StateVersionUploadOptions{
StateVersionCreateOptions: StateVersionCreateOptions{
Lineage: String("741c4949-60b9-5bb1-5bf8-b14f4bb14af3"),
MD5: String(fmt.Sprintf("%x", md5.Sum(state))),
Serial: Int64(1),
JSONStateOutputs: String(base64.StdEncoding.EncodeToString(jsonStateOutputs)),
},
RawState: state,
RawJSONState: jsonState,
})
require.NoError(t, err)
_, err = client.Workspaces.Unlock(ctx, wTest.ID)
require.NoError(t, err)
// HCP Terraform does some async processing on state versions, so we must await it
// lest we flake. Should take well less than a minute tho.
timeout := time.Minute / 2
ctxPollSVReady, cancelPollSVReady := context.WithTimeout(ctx, timeout)
defer cancelPollSVReady()
sv = pollStateVersionStatus(t, client, ctxPollSVReady, sv, []StateVersionStatus{StateVersionFinalized})
assert.NotEmpty(t, sv.DownloadURL)
assert.Equal(t, StateVersionFinalized, sv.Status)
})
t.Run("cannot provide base64 state parameter when uploading", func(t *testing.T) {
ctx := context.Background()
_, err = client.StateVersions.Upload(ctx, wTest.ID, StateVersionUploadOptions{
StateVersionCreateOptions: StateVersionCreateOptions{
Lineage: String("741c4949-60b9-5bb1-5bf8-b14f4bb14af3"),
MD5: String(fmt.Sprintf("%x", md5.Sum(state))),
Serial: Int64(1),
State: String(base64.StdEncoding.EncodeToString(state)),
JSONStateOutputs: String(base64.StdEncoding.EncodeToString(jsonStateOutputs)),
},
RawState: state,
RawJSONState: jsonState,
})
require.ErrorIs(t, err, ErrStateMustBeOmitted)
})
t.Run("RawState parameter is required when uploading", func(t *testing.T) {
ctx := context.Background()
_, err = client.StateVersions.Upload(ctx, wTest.ID, StateVersionUploadOptions{
StateVersionCreateOptions: StateVersionCreateOptions{
Lineage: String("741c4949-60b9-5bb1-5bf8-b14f4bb14af3"),
MD5: String(fmt.Sprintf("%x", md5.Sum(state))),
Serial: Int64(1),
JSONStateOutputs: String(base64.StdEncoding.EncodeToString(jsonStateOutputs)),
},
RawJSONState: jsonState,
})
require.ErrorIs(t, err, ErrRequiredRawState)
})
t.Run("uploading state using SanitizedStateUploadURL and verifying SanitizedStateDownloadURL exists", func(t *testing.T) {
skipHYOKIntegrationTests(t)
hyokOrganizationName := os.Getenv("HYOK_ORGANIZATION_NAME")
if hyokOrganizationName == "" {
t.Fatal("Export a valid HYOK_ORGANIZATION_NAME before running this test!")
}
hyokWorkspaceName := os.Getenv("HYOK_WORKSPACE_NAME")
if hyokWorkspaceName == "" {
t.Fatal("Export a valid HYOK_WORKSPACE_NAME before running this test!")
}
w, err := client.Workspaces.Read(context.Background(), hyokOrganizationName, hyokWorkspaceName)
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
_, err = client.Workspaces.Lock(ctx, w.ID, WorkspaceLockOptions{})
if err != nil {
t.Fatal(err)
}
sv, err := client.StateVersions.Create(ctx, w.ID, StateVersionCreateOptions{
Lineage: String("741c4949-60b9-5bb1-5bf8-b14f4bb14af3"),
MD5: String(fmt.Sprintf("%x", md5.Sum(state))),
Serial: Int64(1),
})
require.NoError(t, err)
err = client.StateVersions.UploadSanitizedState(ctx, sv.SanitizedStateUploadURL, jsonState)
require.NoError(t, err)
// Get a refreshed view of the configuration version.
sv, err = retryPatientlyIf(
func() (any, error) { return client.StateVersions.Read(ctx, sv.ID) },
func(sv *StateVersion) bool { return sv.DownloadURL == "" },
)
require.NoError(t, err)
assert.NotEmpty(t, sv.SanitizedStateDownloadURL)
assert.Empty(t, sv.SanitizedStateUploadURL)
_, err = client.Workspaces.ForceUnlock(ctx, w.ID)
if err != nil {
t.Fatal(err)
}
})
t.Run("SanitizedStateUploadURL is required when uploading sanitized state", func(t *testing.T) {
skipHYOKIntegrationTests(t)
ctx := context.Background()
_, err := client.Workspaces.Lock(ctx, wTest.ID, WorkspaceLockOptions{})
if err != nil {
t.Fatal(err)
}
sv, err := client.StateVersions.Create(ctx, wTest.ID, StateVersionCreateOptions{
Lineage: String("741c4949-60b9-5bb1-5bf8-b14f4bb14af3"),
MD5: String(fmt.Sprintf("%x", md5.Sum(state))),
Serial: Int64(1),
})
require.NoError(t, err)
err = client.StateVersions.UploadSanitizedState(ctx, sv.SanitizedStateUploadURL, state)
require.Error(t, err, ErrSanitizedStateUploadURLMissing)
// Workspaces must be force-unlocked when there is a pending state version
_, err = client.Workspaces.ForceUnlock(ctx, wTest.ID)
if err != nil {
t.Fatal(err)
}
})
}
func TestStateVersionsCreate_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
wTest, wTestCleanup := createWorkspace(t, client, nil)
t.Cleanup(wTestCleanup)
state, err := os.ReadFile("test-fixtures/state-version/terraform.tfstate")
if err != nil {
t.Fatal(err)
}
jsonState, err := os.ReadFile("test-fixtures/json-state/state.json")
if err != nil {
t.Fatal(err)
}
jsonStateOutputs, err := os.ReadFile("test-fixtures/json-state-outputs/everything.json")
if err != nil {
t.Fatal(err)
}
t.Run("can create pending state versions", func(t *testing.T) {
ctx := context.Background()
_, err := client.Workspaces.Lock(ctx, wTest.ID, WorkspaceLockOptions{})
if err != nil {
t.Fatal(err)
}
_, err = client.StateVersions.Create(ctx, wTest.ID, StateVersionCreateOptions{
Lineage: String("741c4949-60b9-5bb1-5bf8-b14f4bb14af3"),
MD5: String(fmt.Sprintf("%x", md5.Sum(state))),
Serial: Int64(1),
})
require.NoError(t, err)
// Workspaces must be force-unlocked when there is a pending state version
_, err = client.Workspaces.ForceUnlock(ctx, wTest.ID)
if err != nil {
t.Fatal(err)
}
})
t.Run("with valid options", func(t *testing.T) {
ctx := context.Background()
_, err := client.Workspaces.Lock(ctx, wTest.ID, WorkspaceLockOptions{})
if err != nil {
t.Fatal(err)
}
sv, err := client.StateVersions.Create(ctx, wTest.ID, StateVersionCreateOptions{
Lineage: String("741c4949-60b9-5bb1-5bf8-b14f4bb14af3"),
MD5: String(fmt.Sprintf("%x", md5.Sum(state))),
Serial: Int64(1),
State: String(base64.StdEncoding.EncodeToString(state)),
})
require.NoError(t, err)
// Get a refreshed view of the configuration version.
refreshed, err := retryPatientlyIf(
func() (any, error) { return client.StateVersions.Read(ctx, sv.ID) },
func(sv *StateVersion) bool { return sv.DownloadURL == "" },
)
require.NoError(t, err)
_, err = client.Workspaces.Unlock(ctx, wTest.ID)
if err != nil {
t.Fatal(err)
}
for _, item := range []*StateVersion{
sv,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, int64(1), item.Serial)
assert.NotEmpty(t, item.CreatedAt)
}
assert.NotEmpty(t, refreshed.DownloadURL)
})
t.Run("with external state representation", func(t *testing.T) {
ctx := context.Background()
_, err := client.Workspaces.Lock(ctx, wTest.ID, WorkspaceLockOptions{})
if err != nil {
t.Fatal(err)
}
sv, err := client.StateVersions.Create(ctx, wTest.ID, StateVersionCreateOptions{
Lineage: String("741c4949-60b9-5bb1-5bf8-b14f4bb14af3"),
MD5: String(fmt.Sprintf("%x", md5.Sum(state))),
Serial: Int64(1),
State: String(base64.StdEncoding.EncodeToString(state)),
JSONState: String(base64.StdEncoding.EncodeToString(jsonState)),
JSONStateOutputs: String(base64.StdEncoding.EncodeToString(jsonStateOutputs)),
})
require.NoError(t, err)
// Get a refreshed view of the configuration version.
refreshed, err := retryPatientlyIf(
func() (any, error) { return client.StateVersions.Read(ctx, sv.ID) },
func(sv *StateVersion) bool { return sv.DownloadURL == "" },
)
require.NoError(t, err)
_, err = client.Workspaces.Unlock(ctx, wTest.ID)
if err != nil {
t.Fatal(err)
}
// TODO: check state outputs for the ones we sent in JSONStateOutputs
for _, item := range []*StateVersion{
sv,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, int64(1), item.Serial)
assert.NotEmpty(t, item.CreatedAt)
}
assert.NotEmpty(t, refreshed.DownloadURL)
})
t.Run("with the force flag set", func(t *testing.T) {
ctx := context.Background()
_, err := client.Workspaces.Lock(ctx, wTest.ID, WorkspaceLockOptions{})
if err != nil {
t.Fatal(err)
}
_, err = client.StateVersions.Create(ctx, wTest.ID, StateVersionCreateOptions{
Lineage: String("741c4949-60b9-5bb1-5bf8-b14f4bb14af3"),
MD5: String(fmt.Sprintf("%x", md5.Sum(state))),
Serial: Int64(1),
State: String(base64.StdEncoding.EncodeToString(state)),
})
require.NoError(t, err)
sv, err := client.StateVersions.Create(ctx, wTest.ID, StateVersionCreateOptions{
Lineage: String("821c4747-a0b9-3bd1-8bf3-c14f4bb14be7"),
MD5: String(fmt.Sprintf("%x", md5.Sum(state))),
Serial: Int64(2),
State: String(base64.StdEncoding.EncodeToString(state)),
Force: Bool(true),
})
require.NoError(t, err)
// Get a refreshed view of the configuration version.
refreshed, err := retryPatientlyIf(
func() (any, error) { return client.StateVersions.Read(ctx, sv.ID) },
func(sv *StateVersion) bool { return sv.DownloadURL == "" },
)
require.NoError(t, err)
_, err = client.Workspaces.Unlock(ctx, wTest.ID)
if err != nil {
t.Fatal(err)
}
for _, item := range []*StateVersion{
sv,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, int64(2), item.Serial)
assert.NotEmpty(t, item.CreatedAt)
}
assert.NotEmpty(t, refreshed.DownloadURL)
})
t.Run("with a run to associate with", func(t *testing.T) {
t.Skip("This can only be tested with the run specific token")
rTest, rTestCleanup := createRun(t, client, wTest)
t.Cleanup(rTestCleanup)
ctx := context.Background()
sv, err := client.StateVersions.Create(ctx, wTest.ID, StateVersionCreateOptions{
MD5: String(fmt.Sprintf("%x", md5.Sum(state))),
Serial: Int64(0),
State: String(base64.StdEncoding.EncodeToString(state)),
Run: rTest,
})
require.NoError(t, err)
require.NotEmpty(t, sv.Run)
// Get a refreshed view of the configuration version.
refreshed, err := retryPatientlyIf(
func() (any, error) { return client.StateVersions.Read(ctx, sv.ID) },
func(sv *StateVersion) bool { return sv.DownloadURL == "" },
)
require.NoError(t, err)
require.NotEmpty(t, refreshed.Run)
for _, item := range []*StateVersion{
sv,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, int64(0), item.Serial)
assert.NotEmpty(t, item.CreatedAt)
assert.NotEmpty(t, item.DownloadURL)
assert.Equal(t, rTest.ID, item.Run.ID)
}
})
t.Run("without md5 hash", func(t *testing.T) {
sv, err := client.StateVersions.Create(ctx, wTest.ID, StateVersionCreateOptions{
Serial: Int64(0),
State: String(base64.StdEncoding.EncodeToString(state)),
})
assert.Nil(t, sv)
assert.Equal(t, err, ErrRequiredM5)
})
t.Run("without serial", func(t *testing.T) {
sv, err := client.StateVersions.Create(ctx, wTest.ID, StateVersionCreateOptions{
MD5: String(fmt.Sprintf("%x", md5.Sum(state))),
State: String(base64.StdEncoding.EncodeToString(state)),
})
assert.Nil(t, sv)
assert.Equal(t, err, ErrRequiredSerial)
})
t.Run("with invalid workspace id", func(t *testing.T) {
sv, err := client.StateVersions.Create(ctx, badIdentifier, StateVersionCreateOptions{})
assert.Nil(t, sv)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
}
func TestStateVersionsRead(t *testing.T) {
t.Parallel()
t.Skip("Skipping due to persistent failures - see TF-31172")
client := testClient(t)
ctx := context.Background()
svTest, svTestCleanup := createStateVersion(t, client, 0, nil)
t.Cleanup(svTestCleanup)
t.Run("when the state version exists", func(t *testing.T) {
var sv *StateVersion
var ok bool
sv, err := client.StateVersions.Read(ctx, svTest.ID)
require.NoError(t, err)
if !sv.ResourcesProcessed {
svRetry, err := retryPatiently(func() (interface{}, error) {
svTest, err := client.StateVersions.Read(ctx, svTest.ID)
require.NoError(t, err)
if !svTest.ResourcesProcessed || svTest.BillableRUMCount == nil || *svTest.BillableRUMCount == 0 {
return nil, fmt.Errorf("resources not processed %v / %d", svTest.ResourcesProcessed, svTest.BillableRUMCount)
}
return svTest, nil
})
if err != nil {
t.Fatalf("error retrying state version read, err=%s", err)
}
require.NotNil(t, svRetry, "timed out waiting for resources to finish processing")
sv, ok = svRetry.(*StateVersion)
if !ok {
t.Fatalf("Expected sv to be type *StateVersion, got %T", sv)
}
}
assert.NotEmpty(t, sv.DownloadURL)
assert.NotEmpty(t, sv.StateVersion)
assert.NotEmpty(t, sv.TerraformVersion)
assert.NotEmpty(t, sv.Outputs)
require.NotNil(t, sv.BillableRUMCount)
assert.Greater(t, *sv.BillableRUMCount, uint32(0))
assert.Greater(t, sv.Size, int64(0))
})
t.Run("when the state version does not exist", func(t *testing.T) {
sv, err := client.StateVersions.Read(ctx, "nonexisting")
assert.Nil(t, sv)
assert.Equal(t, ErrResourceNotFound, err)
})
t.Run("with invalid state version id", func(t *testing.T) {
sv, err := client.StateVersions.Read(ctx, badIdentifier)
assert.Nil(t, sv)
assert.Equal(t, err, ErrInvalidStateVerID)
})
t.Run("read encrypted state download url of a state version", func(t *testing.T) {
skipHYOKIntegrationTests(t)
hyokStateVersionID := os.Getenv("HYOK_STATE_VERSION_ID")
if hyokStateVersionID == "" {
t.Fatal("Export a valid HYOK_STATE_VERSION_ID before running this test!")
}
sv, err := client.StateVersions.Read(ctx, hyokStateVersionID)
require.NoError(t, err)
assert.NotEmpty(t, sv.EncryptedStateDownloadURL)
})
t.Run("read sanitized state download url of a state version", func(t *testing.T) {
skipHYOKIntegrationTests(t)
hyokStateVersionID := os.Getenv("HYOK_STATE_VERSION_ID")
if hyokStateVersionID == "" {
t.Fatal("Export a valid HYOK_STATE_VERSION_ID before running this test!")
}
sv, err := client.StateVersions.Read(ctx, hyokStateVersionID)
require.NoError(t, err)
assert.NotEmpty(t, sv.SanitizedStateDownloadURL)
})
t.Run("read hyok encrypted data key of a state version", func(t *testing.T) {
skipHYOKIntegrationTests(t)
hyokStateVersionID := os.Getenv("HYOK_STATE_VERSION_ID")
if hyokStateVersionID == "" {
t.Fatal("Export a valid HYOK_STATE_VERSION_ID before running this test!")
}
sv, err := client.StateVersions.Read(ctx, hyokStateVersionID)
require.NoError(t, err)
assert.NotEmpty(t, sv.HYOKEncryptedDataKey)
})
}
func TestStateVersionsReadWithOptions(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
svTest, svTestCleanup := createStateVersion(t, client, 0, nil)
t.Cleanup(svTestCleanup)
// give HCP Terraform some time to process the statefile and extract the outputs.
waitForSVOutputs(t, client, svTest.ID)
t.Run("when the state version exists", func(t *testing.T) {
curOpts := &StateVersionReadOptions{
Include: []StateVersionIncludeOpt{SVoutputs},
}
sv, err := client.StateVersions.ReadWithOptions(ctx, svTest.ID, curOpts)
require.NoError(t, err)
assert.NotEmpty(t, sv.Outputs)
})
}
func TestStateVersionsCurrent(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
wTest1, wTest1Cleanup := createWorkspace(t, client, nil)
t.Cleanup(wTest1Cleanup)
wTest2, wTest2Cleanup := createWorkspace(t, client, nil)
t.Cleanup(wTest2Cleanup)
svTest, svTestCleanup := createStateVersion(t, client, 0, wTest1)
t.Cleanup(svTestCleanup)
t.Run("when a state version exists", func(t *testing.T) {
sv, err := client.StateVersions.ReadCurrent(ctx, wTest1.ID)
require.NoError(t, err)
for _, stateVersion := range []*StateVersion{svTest, sv} {
// Don't compare the DownloadURL because it will be generated twice
// in this test - once at creation of the configuration version, and
// again during the GET.
stateVersion.DownloadURL = ""
// outputs, providers are populated only once the state has been parsed by HCP Terraform
// which can cause the tests to fail if it doesn't happen fast enough.
stateVersion.Outputs = nil
stateVersion.Providers = nil
}
assert.Equal(t, svTest.ID, sv.ID)
})
t.Run("when a state version does not exist", func(t *testing.T) {
sv, err := client.StateVersions.ReadCurrent(ctx, wTest2.ID)
assert.Nil(t, sv)
assert.Equal(t, ErrResourceNotFound, err)
})
t.Run("with invalid workspace id", func(t *testing.T) {
sv, err := client.StateVersions.ReadCurrent(ctx, badIdentifier)
assert.Nil(t, sv)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
}
func TestStateVersionsCurrentWithOptions(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
wTest1, wTest1Cleanup := createWorkspace(t, client, nil)
t.Cleanup(wTest1Cleanup)
svTest, svTestCleanup := createStateVersion(t, client, 0, wTest1)
t.Cleanup(svTestCleanup)
// give HCP Terraform some time to process the statefile and extract the outputs.
waitForSVOutputs(t, client, svTest.ID)
t.Run("when the state version exists", func(t *testing.T) {
curOpts := &StateVersionCurrentOptions{
Include: []StateVersionIncludeOpt{SVoutputs},
}
sv, err := client.StateVersions.ReadCurrentWithOptions(ctx, wTest1.ID, curOpts)
require.NoError(t, err)
assert.NotEmpty(t, sv.Outputs)
})
}
func TestStateVersionsDownload(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
svTest, svTestCleanup := createStateVersion(t, client, 0, nil)
t.Cleanup(svTestCleanup)
stateTest, err := os.ReadFile("test-fixtures/state-version/terraform.tfstate")
require.NoError(t, err)
t.Run("when the state version exists", func(t *testing.T) {
state, err := client.StateVersions.Download(ctx, svTest.DownloadURL)
require.NoError(t, err)
assert.Equal(t, stateTest, state)
})
t.Run("with an invalid url", func(t *testing.T) {
state, err := client.StateVersions.Download(ctx, badIdentifier)
assert.Nil(t, state)
assert.Equal(t, ErrResourceNotFound, err)
})
}
func TestStateVersionOutputs(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
wTest1, wTest1Cleanup := createWorkspace(t, client, nil)
t.Cleanup(wTest1Cleanup)
sv, svTestCleanup := createStateVersion(t, client, 0, wTest1)
t.Cleanup(svTestCleanup)
// give HCP Terraform some time to process the statefile and extract the outputs.
waitForSVOutputs(t, client, sv.ID)
t.Run("when the state version exists", func(t *testing.T) {
outputs, err := client.StateVersions.ListOutputs(ctx, sv.ID, nil)
require.NoError(t, err)
assert.NotEmpty(t, outputs.Items)
values := map[string]interface{}{}
for _, op := range outputs.Items {
values[op.Name] = op.Value
}
testOutputString, ok := values["test_output_string"].(string)
require.True(t, ok)
testOutputNumber, ok := values["test_output_number"].(float64)
require.True(t, ok)
testOutputBool, ok := values["test_output_bool"].(bool)
require.True(t, ok)
testOutputListString, ok := values["test_output_list_string"].([]interface{})
require.True(t, ok)
testOutputTupleNumber, ok := values["test_output_tuple_number"].([]interface{})
require.True(t, ok)
testOutputTupleString, ok := values["test_output_tuple_string"].([]interface{})
require.True(t, ok)
testOutputObject, ok := values["test_output_object"].(map[string]interface{})
require.True(t, ok)
// These asserts are based off of the values in
// test-fixtures/state-version/terraform.tfstate
assert.Equal(t, "9023256633839603543", testOutputString)
assert.Equal(t, float64(5), testOutputNumber)
assert.Equal(t, true, testOutputBool)
assert.Equal(t, []interface{}{"us-west-1a"}, testOutputListString)
assert.Equal(t, []interface{}{float64(1), float64(2)}, testOutputTupleNumber)
assert.Equal(t, []interface{}{"one", "two"}, testOutputTupleString)
assert.Equal(t, map[string]interface{}{"foo": "bar"}, testOutputObject)
})
t.Run("with list options", func(t *testing.T) {
options := &StateVersionOutputsListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
}
outputs, err := client.StateVersions.ListOutputs(ctx, sv.ID, options)
require.NoError(t, err)
assert.Empty(t, outputs.Items)
assert.Equal(t, 999, outputs.CurrentPage)
// Based on fixture test-fixtures/state-version/terraform.tfstate
assert.Equal(t, 7, outputs.TotalCount)
})
t.Run("when the state version does not exist", func(t *testing.T) {
outputs, err := client.StateVersions.ListOutputs(ctx, "sv-999999999", nil)
assert.Nil(t, outputs)
assert.Error(t, err)
})
}
func TestStateVersions_ManageBackingData(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
workspace, workspaceCleanup := createWorkspace(t, client, nil)
t.Cleanup(workspaceCleanup)
nonCurrentStateVersion, svTestCleanup := createStateVersion(t, client, 0, workspace)
t.Cleanup(svTestCleanup)
_, svTestCleanup = createStateVersion(t, client, 0, workspace)
t.Cleanup(svTestCleanup)
t.Run("soft delete backing data", func(t *testing.T) {
err := client.StateVersions.SoftDeleteBackingData(ctx, nonCurrentStateVersion.ID)
require.NoError(t, err)
_, err = client.StateVersions.Download(ctx, nonCurrentStateVersion.DownloadURL)
assert.Equal(t, ErrResourceNotFound, err)
})
t.Run("restore backing data", func(t *testing.T) {
err := client.StateVersions.RestoreBackingData(ctx, nonCurrentStateVersion.ID)
require.NoError(t, err)
_, err = client.StateVersions.Download(ctx, nonCurrentStateVersion.DownloadURL)
require.NoError(t, err)
})
t.Run("permanently delete backing data", func(t *testing.T) {
err := client.StateVersions.SoftDeleteBackingData(ctx, nonCurrentStateVersion.ID)
require.NoError(t, err)
err = client.StateVersions.PermanentlyDeleteBackingData(ctx, nonCurrentStateVersion.ID)
require.NoError(t, err)
err = client.StateVersions.RestoreBackingData(ctx, nonCurrentStateVersion.ID)
require.ErrorContainsf(t, err, "transition not allowed", "Restore backing data should fail")
_, err = client.StateVersions.Download(ctx, nonCurrentStateVersion.DownloadURL)
assert.Equal(t, ErrResourceNotFound, err)
})
}
================================================
FILE: state_version_output.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ StateVersionOutputs = (*stateVersionOutputs)(nil)
// State version outputs are the output values from a Terraform state file.
// They include the name and value of the output, as well as a sensitive boolean
// if the value should be hidden by default in UIs.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/state-version-outputs
type StateVersionOutputs interface {
Read(ctx context.Context, outputID string) (*StateVersionOutput, error)
ReadCurrent(ctx context.Context, workspaceID string) (*StateVersionOutputsList, error)
}
// stateVersionOutputs implements StateVersionOutputs.
type stateVersionOutputs struct {
client *Client
}
// StateVersionOutput represents a State Version Outputs
type StateVersionOutput struct {
ID string `jsonapi:"primary,state-version-outputs"`
Name string `jsonapi:"attr,name"`
Sensitive bool `jsonapi:"attr,sensitive"`
Type string `jsonapi:"attr,type"`
Value interface{} `jsonapi:"attr,value"`
// BETA: This field is experimental and not universally present in all versions of TFE/Terraform
DetailedType interface{} `jsonapi:"attr,detailed-type"`
}
// ReadCurrent reads the current state version outputs for the specified workspace
func (s *stateVersionOutputs) ReadCurrent(ctx context.Context, workspaceID string) (*StateVersionOutputsList, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
u := fmt.Sprintf("workspaces/%s/current-state-version-outputs", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
so := &StateVersionOutputsList{}
err = req.Do(ctx, so)
if err != nil {
return nil, err
}
return so, nil
}
// Read a State Version Output
func (s *stateVersionOutputs) Read(ctx context.Context, outputID string) (*StateVersionOutput, error) {
if !validStringID(&outputID) {
return nil, ErrInvalidOutputID
}
u := fmt.Sprintf("state-version-outputs/%s", url.PathEscape(outputID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
so := &StateVersionOutput{}
err = req.Do(ctx, so)
if err != nil {
return nil, err
}
return so, nil
}
================================================
FILE: state_version_output_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStateVersionOutputsRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
wTest1, wTest1Cleanup := createWorkspace(t, client, nil)
defer wTest1Cleanup()
svTest, svTestCleanup := createStateVersion(t, client, 0, wTest1)
defer svTestCleanup()
// give HCP Terraform some time to process the statefile and extract the outputs.
waitForSVOutputs(t, client, svTest.ID)
curOpts := &StateVersionCurrentOptions{
Include: []StateVersionIncludeOpt{SVoutputs},
}
sv, err := client.StateVersions.ReadCurrentWithOptions(ctx, wTest1.ID, curOpts)
if err != nil {
t.Fatal(err)
}
require.NotEmpty(t, sv.Outputs)
require.NotNil(t, sv.Outputs[0])
output := sv.Outputs[0]
t.Run("Read by ID", func(t *testing.T) {
t.Run("when a state output exists", func(t *testing.T) {
so, err := client.StateVersionOutputs.Read(ctx, output.ID)
require.NoError(t, err)
assert.Equal(t, so.ID, output.ID)
assert.Equal(t, so.Name, output.Name)
assert.Equal(t, so.Value, output.Value)
})
t.Run("when a state output does not exist", func(t *testing.T) {
so, err := client.StateVersionOutputs.Read(ctx, "wsout-J2zM24JPAAAAAAAA")
assert.Nil(t, so)
assert.Equal(t, ErrResourceNotFound, err)
})
})
t.Run("Read current workspace outputs", func(t *testing.T) {
so, err := client.StateVersionOutputs.ReadCurrent(ctx, wTest1.ID)
require.NoError(t, err)
assert.NotEmpty(t, so.Items)
})
t.Run("Sensitive secrets are null", func(t *testing.T) {
so, err := client.StateVersionOutputs.ReadCurrent(ctx, wTest1.ID)
require.NoError(t, err)
require.NotEmpty(t, so.Items)
var found *StateVersionOutput = nil
for _, s := range so.Items {
if s.Name == "test_output_string" {
found = s
break
}
}
assert.NotNil(t, found)
assert.True(t, found.Sensitive)
assert.Nil(t, found.Value)
})
}
================================================
FILE: subscription_updater_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"testing"
"time"
)
type featureSet struct {
ID string `jsonapi:"primary,feature-sets"`
}
type featureSetList struct {
Items []*featureSet
*Pagination
}
type featureSetListOptions struct {
Q string `url:"q,omitempty"`
}
type retryableFn func() (any, error)
type updateFeatureSetOptions struct {
Type string `jsonapi:"primary,subscription"`
RunsCeiling *int `jsonapi:"attr,runs-ceiling,omitempty"`
AgentsCeiling *int `jsonapi:"attr,agents-ceiling,omitempty"`
ContractStartAt *time.Time `jsonapi:"attr,contract-start-at,iso8601,omitempty"`
EndAt *time.Time `jsonapi:"attr,end-at,iso8601,omitempty"`
ContractUserLimit *int `jsonapi:"attr,contract-user-limit,omitempty"`
ContractApplyLimit *int `jsonapi:"attr,contract-apply-limit,omitempty"`
ContractManagedResourcesLimit *int `jsonapi:"attr,contract-managed-resources-limit,omitempty"`
FeatureSet *featureSet `jsonapi:"relation,feature-set"`
}
type organizationSubscriptionUpdater struct {
organization *Organization
planName string
updateOpts updateFeatureSetOptions
}
func newSubscriptionUpdater(organization *Organization) *organizationSubscriptionUpdater {
return &organizationSubscriptionUpdater{
organization: organization,
updateOpts: updateFeatureSetOptions{},
}
}
func (b *organizationSubscriptionUpdater) WithBusinessPlan() *organizationSubscriptionUpdater {
b.planName = "Business"
ceiling := 10
agentsCeiling := 10
start := time.Now()
end := time.Now().AddDate(1, 0, 0) // 1 year from now
userLimit := 1000
applyLimit := 5000
b.updateOpts.RunsCeiling = &ceiling
b.updateOpts.AgentsCeiling = &agentsCeiling
b.updateOpts.ContractStartAt = &start
b.updateOpts.EndAt = &end
b.updateOpts.ContractUserLimit = &userLimit
b.updateOpts.ContractApplyLimit = &applyLimit
return b
}
func (b *organizationSubscriptionUpdater) WithTrialPlan() *organizationSubscriptionUpdater {
b.planName = "Trial"
ceiling := 1
b.updateOpts.RunsCeiling = &ceiling
return b
}
func (b *organizationSubscriptionUpdater) WithStandardEntitlementPlan() *organizationSubscriptionUpdater {
b.planName = "Standard (entitlement)"
start := time.Now()
end := time.Now().AddDate(1, 0, 0) // 1 year from now
ceiling := 1
managedResourcesLimit := 1000
b.updateOpts.ContractStartAt = &start
b.updateOpts.EndAt = &end
b.updateOpts.RunsCeiling = &ceiling
b.updateOpts.ContractManagedResourcesLimit = &managedResourcesLimit
return b
}
// Attempts to change an organization's subscription to a different plan. Requires a user token with admin access.
func (b *organizationSubscriptionUpdater) Update(t *testing.T) {
if enterpriseEnabled() {
t.Skip("Cannot upgrade an organization's subscription when enterprise is enabled. Set ENABLE_TFE=0 to run.")
}
if b.planName == "" {
t.Fatal("organizationSubscriptionUpdater requires a plan")
return
}
adminClient := testAdminClient(t, provisionLicensesAdmin)
req, err := adminClient.NewRequest("GET", "admin/feature-sets", featureSetListOptions{
Q: b.planName,
})
if err != nil {
t.Fatal(err)
return
}
fsl := &featureSetList{}
err = req.Do(context.Background(), fsl)
if err != nil {
t.Fatalf("failed to enumerate feature sets: %v", err)
return
} else if len(fsl.Items) == 0 {
t.Fatalf("feature set response was empty")
return
}
b.updateOpts.FeatureSet = fsl.Items[0]
u := fmt.Sprintf("admin/organizations/%s/subscription", url.PathEscape(b.organization.Name))
req, err = adminClient.NewRequest("POST", u, &b.updateOpts)
if err != nil {
t.Fatalf("Failed to create request: %v", err)
return
}
err = req.Do(context.Background(), nil)
if err != nil {
t.Fatalf("Failed to upgrade subscription: %v", err)
}
}
================================================
FILE: tag.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"fmt"
)
type TagList struct {
*Pagination
Items []*Tag
}
// Tag is owned by an organization and applied to workspaces. Used for grouping and search.
type Tag struct {
ID string `jsonapi:"primary,tags"`
Name string `jsonapi:"attr,name,omitempty"`
}
type TagBinding struct {
ID string `jsonapi:"primary,tag-bindings"`
Key string `jsonapi:"attr,key"`
Value string `jsonapi:"attr,value,omitempty"`
}
type EffectiveTagBinding struct {
ID string `jsonapi:"primary,effective-tag-bindings"`
Key string `jsonapi:"attr,key"`
Value string `jsonapi:"attr,value,omitempty"`
Links map[string]interface{} `jsonapi:"links,omitempty"`
}
func encodeTagFiltersAsParams(filters []*TagBinding) map[string][]string {
if len(filters) == 0 {
return nil
}
var tagFilter = make(map[string][]string, len(filters))
for index, tag := range filters {
tagFilter[fmt.Sprintf("filter[tagged][%d][key]", index)] = []string{tag.Key}
tagFilter[fmt.Sprintf("filter[tagged][%d][value]", index)] = []string{tag.Value}
}
return tagFilter
}
================================================
FILE: task_result.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"time"
)
// Compile-time proof of interface implementation
var _ TaskResults = (*taskResults)(nil)
// TaskResults describes all the task result related methods that the HCP Terraform or Terraform Enterprise API supports.
type TaskResults interface {
// Read a task result by ID
Read(ctx context.Context, taskResultID string) (*TaskResult, error)
}
// taskResults implements TaskResults
type taskResults struct {
client *Client
}
// TaskResultStatus is an enum that represents all possible statuses for a task result
type TaskResultStatus string
const (
TaskPassed TaskResultStatus = "passed"
TaskFailed TaskResultStatus = "failed"
TaskPending TaskResultStatus = "pending"
TaskRunning TaskResultStatus = "running"
TaskUnreachable TaskResultStatus = "unreachable"
TaskErrored TaskResultStatus = "errored"
)
// TaskEnforcementLevel is an enum that describes the enforcement levels for a run task
type TaskEnforcementLevel string
const (
Advisory TaskEnforcementLevel = "advisory"
Mandatory TaskEnforcementLevel = "mandatory"
)
// TaskResultStatusTimestamps represents the set of timestamps recorded for a task result
type TaskResultStatusTimestamps struct {
ErroredAt time.Time `jsonapi:"attr,errored-at,rfc3339"`
RunningAt time.Time `jsonapi:"attr,running-at,rfc3339"`
CanceledAt time.Time `jsonapi:"attr,canceled-at,rfc3339"`
FailedAt time.Time `jsonapi:"attr,failed-at,rfc3339"`
PassedAt time.Time `jsonapi:"attr,passed-at,rfc3339"`
}
// TaskResult represents the result of a HCP Terraform or Terraform Enterprise run task
type TaskResult struct {
ID string `jsonapi:"primary,task-results"`
Status TaskResultStatus `jsonapi:"attr,status"`
Message string `jsonapi:"attr,message"`
StatusTimestamps TaskResultStatusTimestamps `jsonapi:"attr,status-timestamps"`
URL string `jsonapi:"attr,url"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
TaskID string `jsonapi:"attr,task-id"`
TaskName string `jsonapi:"attr,task-name"`
TaskURL string `jsonapi:"attr,task-url"`
WorkspaceTaskID string `jsonapi:"attr,workspace-task-id"`
WorkspaceTaskEnforcementLevel TaskEnforcementLevel `jsonapi:"attr,workspace-task-enforcement-level"`
AgentPoolID *string `jsonapi:"attr,agent-pool-id,omitempty"`
// The task stage this result belongs to
TaskStage *TaskStage `jsonapi:"relation,task_stage"`
}
// Read a task result by ID
func (t *taskResults) Read(ctx context.Context, taskResultID string) (*TaskResult, error) {
if !validStringID(&taskResultID) {
return nil, ErrInvalidTaskResultID
}
u := fmt.Sprintf("task-results/%s", taskResultID)
req, err := t.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
r := &TaskResult{}
err = req.Do(ctx, r)
if err != nil {
return nil, err
}
return r, nil
}
================================================
FILE: task_stages.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"time"
)
// Compile-time proof of interface implementation
var _ TaskStages = (*taskStages)(nil)
// TaskStages describes all the task stage related methods that the HCP Terraform and Terraform Enterprise API
// supports.
type TaskStages interface {
// Read a task stage by ID
Read(ctx context.Context, taskStageID string, options *TaskStageReadOptions) (*TaskStage, error)
// List all task stages for a given run
List(ctx context.Context, runID string, options *TaskStageListOptions) (*TaskStageList, error)
// **Note: This function is still in BETA and subject to change.**
// Override a task stage for a given run
Override(ctx context.Context, taskStageID string, options TaskStageOverrideOptions) (*TaskStage, error)
}
// taskStages implements TaskStages
type taskStages struct {
client *Client
}
// Stage is an enum that represents the possible run stages for run tasks
type Stage string
const (
PrePlan Stage = "pre_plan"
PostPlan Stage = "post_plan"
PreApply Stage = "pre_apply"
PostApply Stage = "post_apply"
)
// TaskStageStatus is an enum that represents all possible statuses for a task stage
type TaskStageStatus string
const (
TaskStagePending TaskStageStatus = "pending"
TaskStageRunning TaskStageStatus = "running"
TaskStagePassed TaskStageStatus = "passed"
TaskStageFailed TaskStageStatus = "failed"
TaskStageAwaitingOverride TaskStageStatus = "awaiting_override"
TaskStageCanceled TaskStageStatus = "canceled"
TaskStageErrored TaskStageStatus = "errored"
TaskStageUnreachable TaskStageStatus = "unreachable"
)
// Permissions represents the permission types for overridding a task stage
type Permissions struct {
CanOverridePolicy *bool `jsonapi:"attr,can-override-policy"`
CanOverrideTasks *bool `jsonapi:"attr,can-override-tasks"`
CanOverride *bool `jsonapi:"attr,can-override"`
}
// Actions represents a task stage actions
type Actions struct {
IsOverridable *bool `jsonapi:"attr,is-overridable"`
}
// TaskStage represents a HCP Terraform or Terraform Enterprise run's stage where run tasks can occur
type TaskStage struct {
ID string `jsonapi:"primary,task-stages"`
Stage Stage `jsonapi:"attr,stage"`
Status TaskStageStatus `jsonapi:"attr,status"`
StatusTimestamps TaskStageStatusTimestamps `jsonapi:"attr,status-timestamps"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
Permissions *Permissions `jsonapi:"attr,permissions"`
Actions *Actions `jsonapi:"attr,actions"`
Run *Run `jsonapi:"relation,run"`
TaskResults []*TaskResult `jsonapi:"relation,task-results"`
PolicyEvaluations []*PolicyEvaluation `jsonapi:"relation,policy-evaluations"`
}
// TaskStageOverrideOptions represents the options for overriding a TaskStage.
type TaskStageOverrideOptions struct {
// An optional explanation for why the stage was overridden
Comment *string `json:"comment,omitempty"`
}
// TaskStageList represents a list of task stages
type TaskStageList struct {
*Pagination
Items []*TaskStage
}
// TaskStageStatusTimestamps represents the set of timestamps recorded for a task stage
type TaskStageStatusTimestamps struct {
ErroredAt time.Time `jsonapi:"attr,errored-at,rfc3339"`
RunningAt time.Time `jsonapi:"attr,running-at,rfc3339"`
CanceledAt time.Time `jsonapi:"attr,canceled-at,rfc3339"`
FailedAt time.Time `jsonapi:"attr,failed-at,rfc3339"`
PassedAt time.Time `jsonapi:"attr,passed-at,rfc3339"`
}
// TaskStageIncludeOpt represents the available options for include query params.
type TaskStageIncludeOpt string
const TaskStageTaskResults TaskStageIncludeOpt = "task_results"
// **Note: This field is still in BETA and subject to change.**
const PolicyEvaluationsTaskResults TaskStageIncludeOpt = "policy_evaluations"
// TaskStageReadOptions represents the set of options when reading a task stage
type TaskStageReadOptions struct {
// Optional: A list of relations to include.
Include []TaskStageIncludeOpt `url:"include,omitempty"`
}
// TaskStageListOptions represents the options for listing task stages for a run
type TaskStageListOptions struct {
ListOptions
}
// Read a task stage by ID
func (s *taskStages) Read(ctx context.Context, taskStageID string, options *TaskStageReadOptions) (*TaskStage, error) {
if !validStringID(&taskStageID) {
return nil, ErrInvalidTaskStageID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("task-stages/%s", taskStageID)
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
t := &TaskStage{}
err = req.Do(ctx, t)
if err != nil {
return nil, err
}
return t, nil
}
// List task stages for a run
func (s *taskStages) List(ctx context.Context, runID string, options *TaskStageListOptions) (*TaskStageList, error) {
if !validStringID(&runID) {
return nil, ErrInvalidRunID
}
u := fmt.Sprintf("runs/%s/task-stages", runID)
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
tlist := &TaskStageList{}
err = req.Do(ctx, tlist)
if err != nil {
return nil, err
}
return tlist, nil
}
// **Note: This function is still in BETA and subject to change.**
// Override a task stages for a run
func (s *taskStages) Override(ctx context.Context, taskStageID string, options TaskStageOverrideOptions) (*TaskStage, error) {
if !validStringID(&taskStageID) {
return nil, ErrInvalidTaskStageID
}
u := fmt.Sprintf("task-stages/%s/actions/override", taskStageID)
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
t := &TaskStage{}
err = req.Do(ctx, t)
if err != nil {
return nil, err
}
return t, nil
}
func (o *TaskStageReadOptions) valid() error {
return nil
}
================================================
FILE: task_stages_integration_beta_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTaskStagesRead_Beta_RunDependent(t *testing.T) {
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest)
defer runTaskTestCleanup()
wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest)
defer wkspaceTestCleanup()
options := PolicyCreateOptions{
Description: String("A sample policy"),
Kind: OPA,
Query: String("data.example.rule"),
Enforce: []*EnforcementOptions{
{
Path: String(".rego"),
Mode: EnforcementMode(EnforcementAdvisory),
},
},
}
policyTest, policyTestCleanup := createUploadedPolicyWithOptions(t, client, true, orgTest, options)
defer policyTestCleanup()
policySet := []*Policy{policyTest}
_, psTestCleanup1 := createPolicySet(t, client, orgTest, policySet, []*Workspace{wkspaceTest}, nil, nil, OPA)
defer psTestCleanup1()
wrTaskTest, wrTaskTestCleanup := createWorkspaceRunTask(t, client, wkspaceTest, runTaskTest)
defer wrTaskTestCleanup()
rTest, rTestCleanup := createRun(t, client, wkspaceTest)
defer rTestCleanup()
r, err := client.Runs.ReadWithOptions(ctx, rTest.ID, &RunReadOptions{
Include: []RunIncludeOpt{RunTaskStages},
})
require.NoError(t, err)
require.NotEmpty(t, r.TaskStages)
require.NotNil(t, r.TaskStages[0])
t.Run("without read options", func(t *testing.T) {
taskStage, err := client.TaskStages.Read(ctx, r.TaskStages[0].ID, nil)
require.NoError(t, err)
assert.NotEmpty(t, taskStage.ID)
assert.NotEmpty(t, taskStage.Stage)
assert.NotNil(t, taskStage.StatusTimestamps.ErroredAt)
assert.NotNil(t, taskStage.StatusTimestamps.RunningAt)
assert.NotNil(t, taskStage.CreatedAt)
assert.NotNil(t, taskStage.UpdatedAt)
assert.NotNil(t, taskStage.Run)
assert.NotNil(t, taskStage.TaskResults)
// so this bit is interesting, if the relation is not specified in the include
// param, the fields of the struct will be zeroed out, minus the ID
assert.NotEmpty(t, taskStage.TaskResults[0].ID)
assert.Empty(t, taskStage.TaskResults[0].Status)
assert.Empty(t, taskStage.TaskResults[0].Message)
assert.NotEmpty(t, taskStage.PolicyEvaluations[0].ID)
})
t.Run("with include param task_results", func(t *testing.T) {
taskStage, err := client.TaskStages.Read(ctx, r.TaskStages[0].ID, &TaskStageReadOptions{
Include: []TaskStageIncludeOpt{TaskStageTaskResults, PolicyEvaluationsTaskResults},
})
require.NoError(t, err)
require.NotEmpty(t, taskStage.TaskResults)
require.NotNil(t, taskStage.TaskResults[0])
require.NotEmpty(t, taskStage.PolicyEvaluations)
require.NotNil(t, taskStage.PolicyEvaluations[0])
t.Run("task results are properly decoded", func(t *testing.T) {
assert.NotEmpty(t, taskStage.TaskResults[0].ID)
assert.NotEmpty(t, taskStage.TaskResults[0].Status)
assert.NotEmpty(t, taskStage.TaskResults[0].CreatedAt)
assert.Equal(t, wrTaskTest.ID, taskStage.TaskResults[0].WorkspaceTaskID)
assert.Equal(t, runTaskTest.Name, taskStage.TaskResults[0].TaskName)
})
t.Run("policy evaluations are properly decoded", func(t *testing.T) {
assert.NotEmpty(t, taskStage.PolicyEvaluations[0].ID)
assert.NotEmpty(t, taskStage.PolicyEvaluations[0].Status)
assert.NotEmpty(t, taskStage.PolicyEvaluations[0].CreatedAt)
assert.Equal(t, OPA, taskStage.PolicyEvaluations[0].PolicyKind)
assert.NotEmpty(t, taskStage.PolicyEvaluations[0].UpdatedAt)
assert.NotNil(t, taskStage.PolicyEvaluations[0].ResultCount)
})
})
}
func TestTaskStagesList_Beta_RunDependent(t *testing.T) {
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest)
defer runTaskTestCleanup()
runTaskTest2, runTaskTest2Cleanup := createRunTask(t, client, orgTest)
defer runTaskTest2Cleanup()
wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest)
defer wkspaceTestCleanup()
options := PolicyCreateOptions{
Description: String("A sample policy"),
Kind: OPA,
Query: String("data.example.rule"),
Enforce: []*EnforcementOptions{
{
Path: String(".rego"),
Mode: EnforcementMode(EnforcementAdvisory),
},
},
}
policyTest, policyTestCleanup := createUploadedPolicyWithOptions(t, client, true, orgTest, options)
defer policyTestCleanup()
policyTest2, policyTestCleanup2 := createUploadedPolicyWithOptions(t, client, true, orgTest, options)
defer policyTestCleanup2()
policySet := []*Policy{policyTest, policyTest2}
_, psTestCleanup1 := createPolicySet(t, client, orgTest, policySet, []*Workspace{wkspaceTest}, nil, nil, OPA)
defer psTestCleanup1()
policySet2 := []*Policy{policyTest2}
_, psTestCleanup2 := createPolicySet(t, client, orgTest, policySet2, []*Workspace{wkspaceTest}, nil, nil, OPA)
defer psTestCleanup2()
_, wrTaskTestCleanup := createWorkspaceRunTask(t, client, wkspaceTest, runTaskTest)
defer wrTaskTestCleanup()
_, wrTaskTest2Cleanup := createWorkspaceRunTask(t, client, wkspaceTest, runTaskTest2)
defer wrTaskTest2Cleanup()
rTest, rTestCleanup := createRun(t, client, wkspaceTest)
defer rTestCleanup()
t.Run("with no params", func(t *testing.T) {
taskStageList, err := client.TaskStages.List(ctx, rTest.ID, nil)
require.NoError(t, err)
require.NotEmpty(t, taskStageList.Items)
assert.NotEmpty(t, taskStageList.Items[0].ID)
assert.Equal(t, 2, len(taskStageList.Items[0].TaskResults))
assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluations))
})
}
func TestTaskStageOverride_Beta_RunDependent(t *testing.T) {
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
t.Run("when the policy failed", func(t *testing.T) {
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
options := PolicyCreateOptions{
Description: String("A sample policy"),
Kind: OPA,
Query: String("data.example.rule"),
Enforce: []*EnforcementOptions{
{
Mode: EnforcementMode(EnforcementMandatory),
},
},
}
pTest, pTestCleanup := createUploadedPolicyWithOptions(t, client, false, orgTest, options)
defer pTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
opts := PolicySetCreateOptions{
Kind: OPA,
Overridable: Bool(true),
}
createPolicySetWithOptions(t, client, orgTest, []*Policy{pTest}, []*Workspace{wTest}, nil, nil, opts)
rTest, tTestCleanup := createRunWaitForStatus(t, client, wTest, RunPostPlanAwaitingDecision)
defer tTestCleanup()
taskStageList, err := client.TaskStages.List(ctx, rTest.ID, nil)
require.NoError(t, err)
require.NotEmpty(t, taskStageList.Items)
assert.NotEmpty(t, taskStageList.Items[0].ID)
assert.Equal(t, TaskStageAwaitingOverride, taskStageList.Items[0].Status)
assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluations))
_, err = client.TaskStages.Override(ctx, taskStageList.Items[0].ID, TaskStageOverrideOptions{})
require.NoError(t, err)
})
t.Run("when the policy failed with options", func(t *testing.T) {
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
options := PolicyCreateOptions{
Description: String("A sample policy"),
Kind: OPA,
Query: String("data.example.rule"),
Enforce: []*EnforcementOptions{
{
Mode: EnforcementMode(EnforcementMandatory),
},
},
}
pTest, pTestCleanup := createUploadedPolicyWithOptions(t, client, false, orgTest, options)
defer pTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
opts := PolicySetCreateOptions{
Kind: OPA,
Overridable: Bool(true),
}
createPolicySetWithOptions(t, client, orgTest, []*Policy{pTest}, []*Workspace{wTest}, nil, nil, opts)
rTest, tTestCleanup := createRunWaitForStatus(t, client, wTest, RunPostPlanAwaitingDecision)
defer tTestCleanup()
taskStageList, err := client.TaskStages.List(ctx, rTest.ID, nil)
require.NoError(t, err)
require.NotEmpty(t, taskStageList.Items)
assert.NotEmpty(t, taskStageList.Items[0].ID)
assert.Equal(t, TaskStageAwaitingOverride, taskStageList.Items[0].Status)
assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluations))
taskStageOverrideOptions := TaskStageOverrideOptions{
Comment: String("test comment"),
}
_, err = client.TaskStages.Override(ctx, taskStageList.Items[0].ID, taskStageOverrideOptions)
require.NoError(t, err)
})
t.Run("when the policy passed", func(t *testing.T) {
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
options := PolicyCreateOptions{
Description: String("A sample policy"),
Kind: OPA,
Query: String("data.example.rule"),
Enforce: []*EnforcementOptions{
{
Mode: EnforcementMode(EnforcementMandatory),
},
},
}
pTest, pTestCleanup := createUploadedPolicyWithOptions(t, client, true, orgTest, options)
defer pTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
opts := PolicySetCreateOptions{
Kind: OPA,
Overridable: Bool(true),
}
createPolicySetWithOptions(t, client, orgTest, []*Policy{pTest}, []*Workspace{wTest}, nil, nil, opts)
rTest, tTestCleanup := createPlannedRun(t, client, wTest)
defer tTestCleanup()
taskStageList, err := client.TaskStages.List(ctx, rTest.ID, nil)
require.NoError(t, err)
require.NotEmpty(t, taskStageList.Items)
assert.NotEmpty(t, taskStageList.Items[0].ID)
assert.Equal(t, TaskStagePassed, taskStageList.Items[0].Status)
assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluations))
_, err = client.TaskStages.Override(ctx, taskStageList.Items[0].ID, TaskStageOverrideOptions{})
assert.Errorf(t, err, "transition not allowed")
})
}
================================================
FILE: task_stages_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTaskStagesRead_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest)
defer runTaskTestCleanup()
wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest)
defer wkspaceTestCleanup()
wrTaskTest, wrTaskTestCleanup := createWorkspaceRunTask(t, client, wkspaceTest, runTaskTest)
defer wrTaskTestCleanup()
rTest, rTestCleanup := createRun(t, client, wkspaceTest)
defer rTestCleanup()
r, err := client.Runs.ReadWithOptions(ctx, rTest.ID, &RunReadOptions{
Include: []RunIncludeOpt{RunTaskStages},
})
require.NoError(t, err)
require.NotEmpty(t, r.TaskStages)
require.NotNil(t, r.TaskStages[0])
t.Run("without read options", func(t *testing.T) {
taskStage, err := client.TaskStages.Read(ctx, r.TaskStages[0].ID, nil)
require.NoError(t, err)
assert.NotEmpty(t, taskStage.ID)
assert.NotEmpty(t, taskStage.Stage)
assert.NotNil(t, taskStage.StatusTimestamps.ErroredAt)
assert.NotNil(t, taskStage.StatusTimestamps.RunningAt)
assert.NotNil(t, taskStage.CreatedAt)
assert.NotNil(t, taskStage.UpdatedAt)
assert.NotNil(t, taskStage.Run)
assert.NotNil(t, taskStage.TaskResults)
// so this bit is interesting, if the relation is not specified in the include
// param, the fields of the struct will be zeroed out, minus the ID
assert.NotEmpty(t, taskStage.TaskResults[0].ID)
assert.Empty(t, taskStage.TaskResults[0].Status)
assert.Empty(t, taskStage.TaskResults[0].Message)
})
t.Run("with include param task_results", func(t *testing.T) {
taskStage, err := client.TaskStages.Read(ctx, r.TaskStages[0].ID, &TaskStageReadOptions{
Include: []TaskStageIncludeOpt{TaskStageTaskResults},
})
require.NoError(t, err)
require.NotEmpty(t, taskStage.TaskResults)
require.NotNil(t, taskStage.TaskResults[0])
t.Run("task results are properly decoded", func(t *testing.T) {
assert.NotEmpty(t, taskStage.TaskResults[0].ID)
assert.NotEmpty(t, taskStage.TaskResults[0].Status)
assert.NotEmpty(t, taskStage.TaskResults[0].CreatedAt)
assert.Equal(t, wrTaskTest.ID, taskStage.TaskResults[0].WorkspaceTaskID)
assert.Equal(t, runTaskTest.Name, taskStage.TaskResults[0].TaskName)
})
})
}
func TestTaskStagesList_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest)
defer runTaskTestCleanup()
runTaskTest2, runTaskTest2Cleanup := createRunTask(t, client, orgTest)
defer runTaskTest2Cleanup()
wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest)
defer wkspaceTestCleanup()
_, wrTaskTestCleanup := createWorkspaceRunTask(t, client, wkspaceTest, runTaskTest)
defer wrTaskTestCleanup()
_, wrTaskTest2Cleanup := createWorkspaceRunTask(t, client, wkspaceTest, runTaskTest2)
defer wrTaskTest2Cleanup()
rTest, rTestCleanup := createRun(t, client, wkspaceTest)
defer rTestCleanup()
t.Run("with no params", func(t *testing.T) {
taskStageList, err := client.TaskStages.List(ctx, rTest.ID, nil)
require.NoError(t, err)
require.NotEmpty(t, taskStageList.Items)
assert.NotEmpty(t, taskStageList.Items[0].ID)
assert.Equal(t, 2, len(taskStageList.Items[0].TaskResults))
})
}
================================================
FILE: team.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ Teams = (*teams)(nil)
// Teams describes all the team related methods that the Terraform
// Enterprise API supports.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/teams
type Teams interface {
// List all the teams of the given organization.
List(ctx context.Context, organization string, options *TeamListOptions) (*TeamList, error)
// Create a new team with the given options.
Create(ctx context.Context, organization string, options TeamCreateOptions) (*Team, error)
// Read a team by its ID.
Read(ctx context.Context, teamID string) (*Team, error)
// Update a team by its ID.
Update(ctx context.Context, teamID string, options TeamUpdateOptions) (*Team, error)
// Delete a team by its ID.
Delete(ctx context.Context, teamID string) error
}
// teams implements Teams.
type teams struct {
client *Client
}
// TeamList represents a list of teams.
type TeamList struct {
*Pagination
Items []*Team
}
// Team represents a Terraform Enterprise team.
type Team struct {
ID string `jsonapi:"primary,teams"`
IsUnified bool `jsonapi:"attr,is-unified"`
Name string `jsonapi:"attr,name"`
OrganizationAccess *OrganizationAccess `jsonapi:"attr,organization-access"`
Visibility string `jsonapi:"attr,visibility"`
Permissions *TeamPermissions `jsonapi:"attr,permissions"`
UserCount int `jsonapi:"attr,users-count"`
SSOTeamID string `jsonapi:"attr,sso-team-id"`
// AllowMemberTokenManagement is false for TFE versions older than v202408
AllowMemberTokenManagement bool `jsonapi:"attr,allow-member-token-management"`
// Relations
Users []*User `jsonapi:"relation,users"`
OrganizationMemberships []*OrganizationMembership `jsonapi:"relation,organization-memberships"`
}
// OrganizationAccess represents the team's permissions on its organization
type OrganizationAccess struct {
ManagePolicies bool `jsonapi:"attr,manage-policies"`
ManagePolicyOverrides bool `jsonapi:"attr,manage-policy-overrides"`
// **Note: This API is still in BETA and subject to change.**
DelegatePolicyOverrides bool `jsonapi:"attr,delegate-policy-overrides"`
ManageWorkspaces bool `jsonapi:"attr,manage-workspaces"`
ManageVCSSettings bool `jsonapi:"attr,manage-vcs-settings"`
ManageProviders bool `jsonapi:"attr,manage-providers"`
ManageModules bool `jsonapi:"attr,manage-modules"`
ManageRunTasks bool `jsonapi:"attr,manage-run-tasks"`
ManageProjects bool `jsonapi:"attr,manage-projects"`
ReadWorkspaces bool `jsonapi:"attr,read-workspaces"`
ReadProjects bool `jsonapi:"attr,read-projects"`
ManageMembership bool `jsonapi:"attr,manage-membership"`
ManageTeams bool `jsonapi:"attr,manage-teams"`
ManageOrganizationAccess bool `jsonapi:"attr,manage-organization-access"`
AccessSecretTeams bool `jsonapi:"attr,access-secret-teams"`
ManageAgentPools bool `jsonapi:"attr,manage-agent-pools"`
}
// TeamPermissions represents the current user's permissions on the team.
type TeamPermissions struct {
CanDestroy bool `jsonapi:"attr,can-destroy"`
CanUpdateMembership bool `jsonapi:"attr,can-update-membership"`
}
// TeamIncludeOpt represents the available options for include query params.
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/teams#available-related-resources
type TeamIncludeOpt string
const (
TeamUsers TeamIncludeOpt = "users"
TeamOrganizationMemberships TeamIncludeOpt = "organization-memberships"
)
// TeamListOptions represents the options for listing teams.
type TeamListOptions struct {
ListOptions
// Optional: A list of relations to include.
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/teams#available-related-resources
Include []TeamIncludeOpt `url:"include,omitempty"`
// Optional: A list of team names to filter by.
Names []string `url:"filter[names],omitempty"`
// Optional: A query string to search teams by names.
Query string `url:"q,omitempty"`
}
// TeamCreateOptions represents the options for creating a team.
type TeamCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,teams"`
// Name of the team.
Name *string `jsonapi:"attr,name"`
// Optional: Unique Identifier to control team membership via SAML
SSOTeamID *string `jsonapi:"attr,sso-team-id,omitempty"`
// The team's organization access
OrganizationAccess *OrganizationAccessOptions `jsonapi:"attr,organization-access,omitempty"`
// The team's visibility ("secret", "organization")
Visibility *string `jsonapi:"attr,visibility,omitempty"`
// Optional: Used by Owners and users with "Manage Teams" permissions to control whether team members can manage team tokens
AllowMemberTokenManagement *bool `jsonapi:"attr,allow-member-token-management,omitempty"`
}
// TeamUpdateOptions represents the options for updating a team.
type TeamUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,teams"`
// Optional: New name for the team
Name *string `jsonapi:"attr,name,omitempty"`
// Optional: Unique Identifier to control team membership via SAML
SSOTeamID *string `jsonapi:"attr,sso-team-id,omitempty"`
// Optional: The team's organization access
OrganizationAccess *OrganizationAccessOptions `jsonapi:"attr,organization-access,omitempty"`
// Optional: The team's visibility ("secret", "organization")
Visibility *string `jsonapi:"attr,visibility,omitempty"`
// Optional: Used by Owners and users with "Manage Teams" permissions to control whether team members can manage team tokens
AllowMemberTokenManagement *bool `jsonapi:"attr,allow-member-token-management,omitempty"`
}
// OrganizationAccessOptions represents the organization access options of a team.
type OrganizationAccessOptions struct {
ManagePolicies *bool `json:"manage-policies,omitempty"`
ManagePolicyOverrides *bool `json:"manage-policy-overrides,omitempty"`
// **Note: This API is still in BETA and subject to change.**
DelegatePolicyOverrides *bool `json:"delegate-policy-overrides,omitempty"`
ManageWorkspaces *bool `json:"manage-workspaces,omitempty"`
ManageVCSSettings *bool `json:"manage-vcs-settings,omitempty"`
ManageProviders *bool `json:"manage-providers,omitempty"`
ManageModules *bool `json:"manage-modules,omitempty"`
ManageRunTasks *bool `json:"manage-run-tasks,omitempty"`
ManageProjects *bool `json:"manage-projects,omitempty"`
ReadWorkspaces *bool `json:"read-workspaces,omitempty"`
ReadProjects *bool `json:"read-projects,omitempty"`
ManageMembership *bool `json:"manage-membership,omitempty"`
ManageTeams *bool `json:"manage-teams,omitempty"`
ManageOrganizationAccess *bool `json:"manage-organization-access,omitempty"`
AccessSecretTeams *bool `json:"access-secret-teams,omitempty"`
ManageAgentPools *bool `json:"manage-agent-pools,omitempty"`
}
// List all the teams of the given organization.
func (s *teams) List(ctx context.Context, organization string, options *TeamListOptions) (*TeamList, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("organizations/%s/teams", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
tl := &TeamList{}
err = req.Do(ctx, tl)
if err != nil {
return nil, err
}
return tl, nil
}
// Create a new team with the given options.
func (s *teams) Create(ctx context.Context, organization string, options TeamCreateOptions) (*Team, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("organizations/%s/teams", url.PathEscape(organization))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
t := &Team{}
err = req.Do(ctx, t)
if err != nil {
return nil, err
}
return t, nil
}
// Read a single team by its ID.
func (s *teams) Read(ctx context.Context, teamID string) (*Team, error) {
if !validStringID(&teamID) {
return nil, ErrInvalidTeamID
}
u := fmt.Sprintf("teams/%s", url.PathEscape(teamID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
t := &Team{}
err = req.Do(ctx, t)
if err != nil {
return nil, err
}
return t, nil
}
// Update a team by its ID.
func (s *teams) Update(ctx context.Context, teamID string, options TeamUpdateOptions) (*Team, error) {
if !validStringID(&teamID) {
return nil, ErrInvalidTeamID
}
u := fmt.Sprintf("teams/%s", url.PathEscape(teamID))
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
t := &Team{}
err = req.Do(ctx, t)
if err != nil {
return nil, err
}
return t, nil
}
// Delete a team by its ID.
func (s *teams) Delete(ctx context.Context, teamID string) error {
if !validStringID(&teamID) {
return ErrInvalidTeamID
}
u := fmt.Sprintf("teams/%s", url.PathEscape(teamID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o TeamCreateOptions) valid() error {
if !validString(o.Name) {
return ErrRequiredName
}
return nil
}
func (o *TeamListOptions) valid() error {
if o == nil {
return nil // nothing to validate
}
if err := validateTeamNames(o.Names); err != nil {
return err
}
return nil
}
func validateTeamNames(names []string) error {
for _, name := range names {
if name == "" {
return ErrEmptyTeamName
}
}
return nil
}
================================================
FILE: team_access.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ TeamAccesses = (*teamAccesses)(nil)
// TeamAccesses describes all the team access related methods that the
// Terraform Enterprise API supports.
//
// TFE API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/team-access
type TeamAccesses interface {
// List all the team accesses for a given workspace.
List(ctx context.Context, options *TeamAccessListOptions) (*TeamAccessList, error)
// Add team access for a workspace.
Add(ctx context.Context, options TeamAccessAddOptions) (*TeamAccess, error)
// Read a team access by its ID.
Read(ctx context.Context, teamAccessID string) (*TeamAccess, error)
// Update a team access by its ID.
Update(ctx context.Context, teamAccessID string, options TeamAccessUpdateOptions) (*TeamAccess, error)
// Remove team access from a workspace.
Remove(ctx context.Context, teamAccessID string) error
}
// teamAccesses implements TeamAccesses.
type teamAccesses struct {
client *Client
}
// AccessType represents a team access type.
type AccessType string
const (
AccessAdmin AccessType = "admin"
AccessPlan AccessType = "plan"
AccessRead AccessType = "read"
AccessWrite AccessType = "write"
AccessCustom AccessType = "custom"
)
// RunsPermissionType represents the permissiontype to a workspace's runs.
type RunsPermissionType string
const (
RunsPermissionRead RunsPermissionType = "read"
RunsPermissionPlan RunsPermissionType = "plan"
RunsPermissionApply RunsPermissionType = "apply"
)
// VariablesPermissionType represents the permissiontype to a workspace's variables.
type VariablesPermissionType string
const (
VariablesPermissionNone VariablesPermissionType = "none"
VariablesPermissionRead VariablesPermissionType = "read"
VariablesPermissionWrite VariablesPermissionType = "write"
)
// StateVersionsPermissionType represents the permissiontype to a workspace's state versions.
type StateVersionsPermissionType string
const (
StateVersionsPermissionNone StateVersionsPermissionType = "none"
StateVersionsPermissionReadOutputs StateVersionsPermissionType = "read-outputs"
StateVersionsPermissionRead StateVersionsPermissionType = "read"
StateVersionsPermissionWrite StateVersionsPermissionType = "write"
)
// SentinelMocksPermissionType represents the permissiontype to a workspace's Sentinel mocks.
type SentinelMocksPermissionType string
const (
SentinelMocksPermissionNone SentinelMocksPermissionType = "none"
SentinelMocksPermissionRead SentinelMocksPermissionType = "read"
)
// TeamAccessList represents a list of team accesses.
type TeamAccessList struct {
*Pagination
Items []*TeamAccess
}
// TeamAccess represents the workspace access for a team.
type TeamAccess struct {
ID string `jsonapi:"primary,team-workspaces"`
Access AccessType `jsonapi:"attr,access"`
Runs RunsPermissionType `jsonapi:"attr,runs"`
Variables VariablesPermissionType `jsonapi:"attr,variables"`
StateVersions StateVersionsPermissionType `jsonapi:"attr,state-versions"`
SentinelMocks SentinelMocksPermissionType `jsonapi:"attr,sentinel-mocks"`
WorkspaceLocking bool `jsonapi:"attr,workspace-locking"`
RunTasks bool `jsonapi:"attr,run-tasks"`
// **Note: This API is still in BETA and subject to change.**
PolicyOverrides bool `jsonapi:"attr,policy-overrides"`
// Relations
Team *Team `jsonapi:"relation,team"`
Workspace *Workspace `jsonapi:"relation,workspace"`
}
// TeamAccessListOptions represents the options for listing team accesses.
type TeamAccessListOptions struct {
ListOptions
WorkspaceID string `url:"filter[workspace][id]"`
}
// TeamAccessAddOptions represents the options for adding team access.
type TeamAccessAddOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,team-workspaces"`
// The type of access to grant.
Access *AccessType `jsonapi:"attr,access"`
// Custom workspace access permissions. These can only be edited when Access is 'custom'; otherwise, they are
// read-only and reflect the Access level's implicit permissions.
Runs *RunsPermissionType `jsonapi:"attr,runs,omitempty"`
Variables *VariablesPermissionType `jsonapi:"attr,variables,omitempty"`
StateVersions *StateVersionsPermissionType `jsonapi:"attr,state-versions,omitempty"`
SentinelMocks *SentinelMocksPermissionType `jsonapi:"attr,sentinel-mocks,omitempty"`
WorkspaceLocking *bool `jsonapi:"attr,workspace-locking,omitempty"`
RunTasks *bool `jsonapi:"attr,run-tasks,omitempty"`
// **Note: This API is still in BETA and subject to change.**
PolicyOverrides *bool `jsonapi:"attr,policy-overrides,omitempty"`
// The team to add to the workspace
Team *Team `jsonapi:"relation,team"`
// The workspace to which the team is to be added.
Workspace *Workspace `jsonapi:"relation,workspace"`
}
// TeamAccessUpdateOptions represents the options for updating team access.
type TeamAccessUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,team-workspaces"`
// The type of access to grant.
Access *AccessType `jsonapi:"attr,access,omitempty"`
// Custom workspace access permissions. These can only be edited when Access is 'custom'; otherwise, they are
// read-only and reflect the Access level's implicit permissions.
Runs *RunsPermissionType `jsonapi:"attr,runs,omitempty"`
Variables *VariablesPermissionType `jsonapi:"attr,variables,omitempty"`
StateVersions *StateVersionsPermissionType `jsonapi:"attr,state-versions,omitempty"`
SentinelMocks *SentinelMocksPermissionType `jsonapi:"attr,sentinel-mocks,omitempty"`
WorkspaceLocking *bool `jsonapi:"attr,workspace-locking,omitempty"`
RunTasks *bool `jsonapi:"attr,run-tasks,omitempty"`
// **Note: This API is still in BETA and subject to change.**
PolicyOverrides *bool `jsonapi:"attr,policy-overrides,omitempty"`
}
// List all the team accesses for a given workspace.
func (s *teamAccesses) List(ctx context.Context, options *TeamAccessListOptions) (*TeamAccessList, error) {
if err := options.valid(); err != nil {
return nil, err
}
req, err := s.client.NewRequest("GET", "team-workspaces", options)
if err != nil {
return nil, err
}
tal := &TeamAccessList{}
err = req.Do(ctx, tal)
if err != nil {
return nil, err
}
return tal, nil
}
// Add team access for a workspace.
func (s *teamAccesses) Add(ctx context.Context, options TeamAccessAddOptions) (*TeamAccess, error) {
if err := options.valid(); err != nil {
return nil, err
}
req, err := s.client.NewRequest("POST", "team-workspaces", &options)
if err != nil {
return nil, err
}
ta := &TeamAccess{}
err = req.Do(ctx, ta)
if err != nil {
return nil, err
}
return ta, nil
}
// Read a team access by its ID.
func (s *teamAccesses) Read(ctx context.Context, teamAccessID string) (*TeamAccess, error) {
if !validStringID(&teamAccessID) {
return nil, ErrInvalidAccessTeamID
}
u := fmt.Sprintf("team-workspaces/%s", url.PathEscape(teamAccessID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
ta := &TeamAccess{}
err = req.Do(ctx, ta)
if err != nil {
return nil, err
}
return ta, nil
}
// Update team access for a workspace
func (s *teamAccesses) Update(ctx context.Context, teamAccessID string, options TeamAccessUpdateOptions) (*TeamAccess, error) {
if !validStringID(&teamAccessID) {
return nil, ErrInvalidAccessTeamID
}
u := fmt.Sprintf("team-workspaces/%s", url.PathEscape(teamAccessID))
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
ta := &TeamAccess{}
err = req.Do(ctx, ta)
if err != nil {
return nil, err
}
return ta, err
}
// Remove team access from a workspace.
func (s *teamAccesses) Remove(ctx context.Context, teamAccessID string) error {
if !validStringID(&teamAccessID) {
return ErrInvalidAccessTeamID
}
u := fmt.Sprintf("team-workspaces/%s", url.PathEscape(teamAccessID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o *TeamAccessListOptions) valid() error {
if o == nil {
return ErrRequiredTeamAccessListOps
}
if !validString(&o.WorkspaceID) {
return ErrRequiredWorkspaceID
}
if !validStringID(&o.WorkspaceID) {
return ErrInvalidWorkspaceID
}
return nil
}
func (o TeamAccessAddOptions) valid() error {
if o.Access == nil {
return ErrRequiredAccess
}
if o.Team == nil {
return ErrRequiredTeam
}
if o.Workspace == nil {
return ErrRequiredWorkspace
}
return nil
}
================================================
FILE: team_access_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTeamAccessesList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
tmTest1, tmTest1Cleanup := createTeam(t, client, orgTest)
defer tmTest1Cleanup()
tmTest2, tmTest2Cleanup := createTeam(t, client, orgTest)
defer tmTest2Cleanup()
taTest1, taTest1Cleanup := createTeamAccess(t, client, tmTest1, wTest, orgTest)
defer taTest1Cleanup()
taTest2, taTest2Cleanup := createTeamAccess(t, client, tmTest2, wTest, orgTest)
defer taTest2Cleanup()
t.Run("with valid options", func(t *testing.T) {
tal, err := client.TeamAccess.List(ctx, &TeamAccessListOptions{
WorkspaceID: wTest.ID,
})
require.NoError(t, err)
assert.Contains(t, tal.Items, taTest1)
assert.Contains(t, tal.Items, taTest2)
t.Skip("paging not supported yet in API")
assert.Equal(t, 1, tal.CurrentPage)
assert.Equal(t, 2, tal.TotalCount)
})
t.Run("with list options", func(t *testing.T) {
t.Skip("paging not supported yet in API")
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
tal, err := client.TeamAccess.List(ctx, &TeamAccessListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, tal.Items)
assert.Equal(t, 999, tal.CurrentPage)
assert.Equal(t, 2, tal.TotalCount)
})
t.Run("without TeamAccessListOptions", func(t *testing.T) {
tal, err := client.TeamAccess.List(ctx, nil)
assert.Nil(t, tal)
assert.Equal(t, err, ErrRequiredTeamAccessListOps)
})
t.Run("without WorkspaceID options", func(t *testing.T) {
tal, err := client.TeamAccess.List(ctx, &TeamAccessListOptions{
ListOptions: ListOptions{
PageNumber: 2,
PageSize: 25,
},
})
assert.Nil(t, tal)
assert.Equal(t, err, ErrRequiredWorkspaceID)
})
t.Run("without a valid workspaceID", func(t *testing.T) {
tal, err := client.TeamAccess.List(ctx, &TeamAccessListOptions{
WorkspaceID: badIdentifier,
})
assert.Nil(t, tal)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
}
func TestTeamAccessesAdd(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
defer tmTestCleanup()
t.Run("with valid options", func(t *testing.T) {
options := TeamAccessAddOptions{
Access: Access(AccessAdmin),
Team: tmTest,
Workspace: wTest,
}
ta, err := client.TeamAccess.Add(ctx, options)
require.NoError(t, err)
defer func() {
err := client.TeamAccess.Remove(ctx, ta.ID)
if err != nil {
t.Logf("error removing team access (%s): %s", ta.ID, err)
}
}()
// Get a refreshed view from the API.
refreshed, err := client.TeamAccess.Read(ctx, ta.ID)
require.NoError(t, err)
for _, item := range []*TeamAccess{
ta,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, *options.Access, item.Access)
}
})
t.Run("with valid custom options", func(t *testing.T) {
options := TeamAccessAddOptions{
Access: Access(AccessCustom),
Runs: RunsPermission(RunsPermissionRead),
StateVersions: StateVersionsPermission(StateVersionsPermissionNone),
Team: tmTest,
Workspace: wTest,
}
ta, err := client.TeamAccess.Add(ctx, options)
require.NoError(t, err)
defer func() {
err := client.TeamAccess.Remove(ctx, ta.ID)
if err != nil {
t.Logf("error removing team access (%s): %s", ta.ID, err)
}
}()
// Get a refreshed view from the API.
refreshed, err := client.TeamAccess.Read(ctx, ta.ID)
require.NoError(t, err)
for _, item := range []*TeamAccess{
ta,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, *options.Access, item.Access)
}
})
t.Run("with invalid custom options", func(t *testing.T) {
options := TeamAccessAddOptions{
Access: Access(AccessRead),
Runs: RunsPermission(RunsPermissionApply),
Team: tmTest,
Workspace: wTest,
}
_, err := client.TeamAccess.Add(ctx, options)
assert.EqualError(t, err, "invalid attribute\n\nRuns is read-only when access level is 'read'; use the 'custom' access level to set this attribute.")
})
t.Run("when the team already has access", func(t *testing.T) {
_, taTestCleanup := createTeamAccess(t, client, tmTest, wTest, nil)
defer taTestCleanup()
options := TeamAccessAddOptions{
Access: Access(AccessAdmin),
Team: tmTest,
Workspace: wTest,
}
_, err := client.TeamAccess.Add(ctx, options)
assert.Error(t, err)
})
t.Run("when options is missing access", func(t *testing.T) {
ta, err := client.TeamAccess.Add(ctx, TeamAccessAddOptions{
Team: tmTest,
Workspace: wTest,
})
assert.Nil(t, ta)
assert.Equal(t, err, ErrRequiredAccess)
})
t.Run("when options is missing team", func(t *testing.T) {
ta, err := client.TeamAccess.Add(ctx, TeamAccessAddOptions{
Access: Access(AccessAdmin),
Workspace: wTest,
})
assert.Nil(t, ta)
assert.Equal(t, err, ErrRequiredTeam)
})
t.Run("when options is missing workspace", func(t *testing.T) {
ta, err := client.TeamAccess.Add(ctx, TeamAccessAddOptions{
Access: Access(AccessAdmin),
Team: tmTest,
})
assert.Nil(t, ta)
assert.Equal(t, err, ErrRequiredWorkspace)
})
}
func TestTeamAccessesRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
defer tmTestCleanup()
taTest, taTestCleanup := createTeamAccess(t, client, tmTest, wTest, orgTest)
defer taTestCleanup()
t.Run("when the team access exists", func(t *testing.T) {
ta, err := client.TeamAccess.Read(ctx, taTest.ID)
require.NoError(t, err)
assert.Equal(t, AccessAdmin, ta.Access)
t.Run("permission attributes are decoded", func(t *testing.T) {
assert.Equal(t, RunsPermissionApply, ta.Runs)
assert.Equal(t, VariablesPermissionWrite, ta.Variables)
assert.Equal(t, StateVersionsPermissionWrite, ta.StateVersions)
assert.Equal(t, SentinelMocksPermissionRead, ta.SentinelMocks)
assert.Equal(t, true, ta.WorkspaceLocking)
})
t.Run("team relationship is decoded", func(t *testing.T) {
assert.NotEmpty(t, ta.Team)
})
t.Run("workspace relationship is decoded", func(t *testing.T) {
assert.NotEmpty(t, ta.Workspace)
})
})
t.Run("when the team access does not exist", func(t *testing.T) {
ta, err := client.TeamAccess.Read(ctx, "nonexisting")
assert.Nil(t, ta)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("without a valid team access ID", func(t *testing.T) {
ta, err := client.TeamAccess.Read(ctx, badIdentifier)
assert.Nil(t, ta)
assert.Equal(t, err, ErrInvalidAccessTeamID)
})
}
func TestTeamAccessesUpdate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
defer tmTestCleanup()
taTest, taTestCleanup := createTeamAccess(t, client, tmTest, wTest, orgTest)
defer taTestCleanup()
t.Run("with valid attributes", func(t *testing.T) {
options := TeamAccessUpdateOptions{
Access: Access(AccessCustom),
Runs: RunsPermission(RunsPermissionPlan),
}
ta, err := client.TeamAccess.Update(ctx, taTest.ID, options)
require.NoError(t, err)
assert.Equal(t, ta.Access, AccessCustom)
assert.Equal(t, ta.Runs, RunsPermissionPlan)
})
}
func TestTeamAccessesRemove(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
defer tmTestCleanup()
taTest, _ := createTeamAccess(t, client, tmTest, nil, orgTest)
t.Run("with valid options", func(t *testing.T) {
err := client.TeamAccess.Remove(ctx, taTest.ID)
require.NoError(t, err)
// Try loading the workspace - it should fail.
_, err = client.TeamAccess.Read(ctx, taTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the team access does not exist", func(t *testing.T) {
err := client.TeamAccess.Remove(ctx, taTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the team access ID is invalid", func(t *testing.T) {
err := client.TeamAccess.Remove(ctx, badIdentifier)
assert.Equal(t, err, ErrInvalidAccessTeamID)
})
}
func TestTeamAccessesReadRunTasks(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
skipIfEnterprise(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
defer tmTestCleanup()
taTest, taTestCleanup := createTeamAccess(t, client, tmTest, wTest, orgTest)
defer taTestCleanup()
t.Run("when the team access exists", func(t *testing.T) {
ta, err := client.TeamAccess.Read(ctx, taTest.ID)
require.NoError(t, err)
assert.Equal(t, AccessAdmin, ta.Access)
t.Run("permission attributes are decoded", func(t *testing.T) {
assert.Equal(t, true, ta.RunTasks)
})
})
}
func TestTeamAccessesUpdateRunTasks(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
defer tmTestCleanup()
taTest, taTestCleanup := createTeamAccess(t, client, tmTest, wTest, orgTest)
defer taTestCleanup()
t.Run("with valid attributes", func(t *testing.T) {
newAccess := !taTest.RunTasks
options := TeamAccessUpdateOptions{
Access: Access(AccessCustom),
RunTasks: &newAccess,
}
ta, err := client.TeamAccess.Update(ctx, taTest.ID, options)
require.NoError(t, err)
assert.Equal(t, AccessCustom, ta.Access)
assert.Equal(t, newAccess, ta.RunTasks)
})
}
func TestTeamAccessesAddPolicyOverrides(t *testing.T) {
skipUnlessBeta(t)
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
defer tmTestCleanup()
t.Run("with valid custom options", func(t *testing.T) {
options := TeamAccessAddOptions{
Access: Access(AccessCustom),
PolicyOverrides: Bool(true),
Team: tmTest,
Workspace: wTest,
}
ta, err := client.TeamAccess.Add(ctx, options)
defer func() {
err := client.TeamAccess.Remove(ctx, ta.ID)
if err != nil {
t.Logf("error removing team access (%s): %s", ta.ID, err)
}
}()
require.NoError(t, err)
refreshed, err := client.TeamAccess.Read(ctx, ta.ID)
require.NoError(t, err)
for _, item := range []*TeamAccess{
ta,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, *options.Access, item.Access)
assert.Equal(t, true, item.PolicyOverrides)
}
})
}
================================================
FILE: team_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"encoding/json"
"testing"
retryablehttp "github.com/hashicorp/go-retryablehttp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTeamsList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
tmTest1, tmTest1Cleanup := createTeam(t, client, orgTest)
defer tmTest1Cleanup()
tmTest2, tmTest2Cleanup := createTeam(t, client, orgTest)
defer tmTest2Cleanup()
tmTest3, tmTest3Cleanup := createTeam(t, client, orgTest)
defer tmTest3Cleanup()
t.Run("without list options", func(t *testing.T) {
tl, err := client.Teams.List(ctx, orgTest.Name, nil)
require.NoError(t, err)
assert.Contains(t, tl.Items, tmTest1)
assert.Contains(t, tl.Items, tmTest2)
assert.Contains(t, tl.Items, tmTest3)
assert.Equal(t, 1, tl.CurrentPage)
assert.Equal(t, 4, tl.TotalCount)
})
t.Run("with list options", func(t *testing.T) {
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
tl, err := client.Teams.List(ctx, orgTest.Name, &TeamListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, tl.Items)
assert.Equal(t, 999, tl.CurrentPage)
assert.Equal(t, 4, tl.TotalCount)
tl, err = client.Teams.List(ctx, orgTest.Name, &TeamListOptions{
Names: []string{tmTest2.Name, tmTest3.Name},
})
require.NoError(t, err)
assert.Equal(t, len(tl.Items), 2)
assert.Contains(t, tl.Items, tmTest2)
assert.Contains(t, tl.Items, tmTest3)
tl, err = client.Teams.List(ctx, orgTest.Name, &TeamListOptions{
Query: tmTest1.Name[:len(tmTest1.Name)-2],
})
require.NoError(t, err)
assert.Equal(t, len(tl.Items), 1)
assert.Equal(t, 1, tl.TotalCount)
assert.Contains(t, tl.Items, tmTest1)
t.Run("with invalid names query param", func(t *testing.T) {
// should return an error because we've included an empty string
tl, err = client.Teams.List(ctx, orgTest.Name, &TeamListOptions{
Names: []string{tmTest2.Name, ""},
})
assert.Equal(t, err, ErrEmptyTeamName)
})
})
t.Run("without a valid organization", func(t *testing.T) {
tl, err := client.Teams.List(ctx, badIdentifier, nil)
assert.Nil(t, tl)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
func TestTeamsCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
t.Run("with valid options", func(t *testing.T) {
options := TeamCreateOptions{
Name: String("foo"),
}
tm, err := client.Teams.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
// Get a refreshed view from the API.
refreshed, err := client.Teams.Read(ctx, tm.ID)
require.NoError(t, err)
for _, item := range []*Team{
tm,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, *options.Name, item.Name)
}
})
t.Run("with beta delegate-policy-overrides", func(t *testing.T) {
skipUnlessBeta(t)
options := TeamCreateOptions{
Name: String("delegate-policy-overrides"),
OrganizationAccess: &OrganizationAccessOptions{
DelegatePolicyOverrides: Bool(true),
},
}
team, err := client.Teams.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
defer func() {
err := client.Teams.Delete(ctx, team.ID)
require.NoError(t, err)
}()
refreshed, err := client.Teams.Read(ctx, team.ID)
require.NoError(t, err)
for _, item := range []*Team{
team,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, *options.Name, item.Name)
assert.Equal(t, *options.OrganizationAccess.DelegatePolicyOverrides, item.OrganizationAccess.DelegatePolicyOverrides)
}
})
t.Run("with sso-team-id", func(t *testing.T) {
options := TeamCreateOptions{
Name: String("rockettes"),
SSOTeamID: String("7dddb675-73e0-4858-a8ad-0e597064301b"),
}
team, err := client.Teams.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, *options.Name, team.Name)
assert.NotNil(t, team.SSOTeamID)
assert.Equal(t, *options.SSOTeamID, team.SSOTeamID)
})
t.Run("when options is missing name", func(t *testing.T) {
tm, err := client.Teams.Create(ctx, "foo", TeamCreateOptions{})
assert.Nil(t, tm)
assert.EqualError(t, err, ErrRequiredName.Error())
})
t.Run("when options has an invalid organization", func(t *testing.T) {
tm, err := client.Teams.Create(ctx, badIdentifier, TeamCreateOptions{
Name: String("foo"),
})
assert.Nil(t, tm)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
func TestTeamsRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
defer tmTestCleanup()
opts := TeamCreateOptions{
Name: String(randomString(t)),
SSOTeamID: String(randomString(t)),
OrganizationAccess: &OrganizationAccessOptions{
ManagePolicies: Bool(true),
},
AllowMemberTokenManagement: Bool(true),
}
ssoTeam, err := client.Teams.Create(ctx, orgTest.Name, opts)
require.NoError(t, err)
defer func() {
err := client.Teams.Delete(ctx, ssoTeam.ID)
require.NoError(t, err)
}()
t.Run("when the team exists", func(t *testing.T) {
tm, err := client.Teams.Read(ctx, tmTest.ID)
require.NoError(t, err)
assert.Equal(t, tmTest.ID, tm.ID)
assert.Equal(t, tmTest.Name, tm.Name)
t.Run("visibility is returned", func(t *testing.T) {
assert.Equal(t, "secret", tm.Visibility)
})
t.Run("permissions are properly decoded", func(t *testing.T) {
assert.True(t, tm.Permissions.CanDestroy)
})
t.Run("organization access is properly decoded", func(t *testing.T) {
assert.Equal(t, tm.OrganizationAccess.ManagePolicies, *opts.OrganizationAccess.ManagePolicies)
})
t.Run("SSO team id is returned", func(t *testing.T) {
assert.NotNil(t, ssoTeam.SSOTeamID)
assert.Equal(t, *opts.SSOTeamID, ssoTeam.SSOTeamID)
})
t.Run("allow member token management is returned", func(t *testing.T) {
assert.Equal(t, *opts.AllowMemberTokenManagement, tm.AllowMemberTokenManagement)
})
})
t.Run("when the team does not exist", func(t *testing.T) {
tm, err := client.Teams.Read(ctx, "nonexisting")
assert.Nil(t, tm)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("without a valid team ID", func(t *testing.T) {
tm, err := client.Teams.Read(ctx, badIdentifier)
assert.Nil(t, tm)
assert.Equal(t, err, ErrInvalidTeamID)
})
}
func TestTeamsUpdate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
defer tmTestCleanup()
t.Run("with valid options", func(t *testing.T) {
options := TeamUpdateOptions{
Name: String("foo bar"),
OrganizationAccess: &OrganizationAccessOptions{
ManagePolicies: Bool(false),
ManageVCSSettings: Bool(true),
ManagePolicyOverrides: Bool(true),
ManageProviders: Bool(true),
ManageModules: Bool(false),
},
Visibility: String("organization"),
AllowMemberTokenManagement: Bool(true),
}
tm, err := client.Teams.Update(ctx, tmTest.ID, options)
require.NoError(t, err)
refreshed, err := client.Teams.Read(ctx, tmTest.ID)
require.NoError(t, err)
for _, item := range []*Team{
tm,
refreshed,
} {
assert.Equal(t, *options.Name, item.Name)
assert.Equal(t,
*options.Visibility,
item.Visibility,
)
assert.Equal(t,
*options.AllowMemberTokenManagement,
item.AllowMemberTokenManagement,
)
assert.Equal(t,
*options.OrganizationAccess.ManagePolicies,
item.OrganizationAccess.ManagePolicies,
)
assert.Equal(t,
*options.OrganizationAccess.ManageVCSSettings,
item.OrganizationAccess.ManageVCSSettings,
)
assert.Equal(t,
*options.OrganizationAccess.ManagePolicyOverrides,
item.OrganizationAccess.ManagePolicyOverrides,
)
assert.Equal(t,
*options.OrganizationAccess.ManageProviders,
item.OrganizationAccess.ManageProviders,
)
assert.Equal(t,
*options.OrganizationAccess.ManageModules,
item.OrganizationAccess.ManageModules,
)
}
})
t.Run("with beta delegate policy overrides", func(t *testing.T) {
skipUnlessBeta(t)
team, err := client.Teams.Create(ctx, orgTest.Name, TeamCreateOptions{
Name: String(randomString(t)),
})
require.NoError(t, err)
defer func() {
err := client.Teams.Delete(ctx, team.ID)
require.NoError(t, err)
}()
updated, err := client.Teams.Update(ctx, team.ID, TeamUpdateOptions{
OrganizationAccess: &OrganizationAccessOptions{
DelegatePolicyOverrides: Bool(true),
},
})
require.NoError(t, err)
refreshed, err := client.Teams.Read(ctx, team.ID)
require.NoError(t, err)
assert.True(t, updated.OrganizationAccess.DelegatePolicyOverrides)
assert.True(t, refreshed.OrganizationAccess.DelegatePolicyOverrides)
})
t.Run("when the team does not exist", func(t *testing.T) {
tm, err := client.Teams.Update(ctx, "nonexisting", TeamUpdateOptions{
Name: String("foo bar"),
})
assert.Nil(t, tm)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("without a valid team ID", func(t *testing.T) {
tm, err := client.Teams.Update(ctx, badIdentifier, TeamUpdateOptions{})
assert.Nil(t, tm)
assert.Equal(t, err, ErrInvalidTeamID)
})
}
func TestTeamsDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
tmTest, _ := createTeam(t, client, orgTest)
t.Run("with valid options", func(t *testing.T) {
err := client.Teams.Delete(ctx, tmTest.ID)
require.NoError(t, err)
// Try loading the workspace - it should fail.
_, err = client.Teams.Read(ctx, tmTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("without valid team ID", func(t *testing.T) {
err := client.Teams.Delete(ctx, badIdentifier)
assert.Equal(t, err, ErrInvalidTeamID)
})
}
func TestTeam_Unmarshal(t *testing.T) {
t.Parallel()
data := map[string]interface{}{
"data": map[string]interface{}{
"type": "teams",
"id": "1",
"attributes": map[string]interface{}{
"name": "team hashi",
"organization-access": map[string]interface{}{
"manage-policies": true,
"manage-workspaces": true,
"manage-vcs-settings": true,
"manage-projects": true,
"read-workspaces": true,
"read-projects": true,
},
"permissions": map[string]interface{}{
"can-destroy": true,
"can-update-membership": true,
},
},
},
}
byteData, err := json.Marshal(data)
require.NoError(t, err)
responseBody := bytes.NewReader(byteData)
team := &Team{}
err = unmarshalResponse(responseBody, team)
require.NoError(t, err)
assert.Equal(t, team.ID, "1")
assert.Equal(t, team.Name, "team hashi")
assert.Empty(t, team.SSOTeamID)
assert.Equal(t, team.OrganizationAccess.ManageWorkspaces, true)
assert.Equal(t, team.OrganizationAccess.ManageVCSSettings, true)
assert.Equal(t, team.OrganizationAccess.ManagePolicies, true)
assert.Equal(t, team.OrganizationAccess.ManageProjects, true)
assert.Equal(t, team.OrganizationAccess.ReadWorkspaces, true)
assert.Equal(t, team.OrganizationAccess.ReadProjects, true)
assert.Equal(t, team.Permissions.CanDestroy, true)
assert.Equal(t, team.Permissions.CanUpdateMembership, true)
}
func TestTeamCreateOptions_Marshal(t *testing.T) {
t.Parallel()
opts := TeamCreateOptions{
Name: String("team name"),
Visibility: String("organization"),
AllowMemberTokenManagement: Bool(true),
OrganizationAccess: &OrganizationAccessOptions{
ManagePolicies: Bool(true),
},
}
reqBody, err := serializeRequestBody(&opts)
require.NoError(t, err)
req, err := retryablehttp.NewRequest("POST", "url", reqBody)
require.NoError(t, err)
bodyBytes, err := req.BodyBytes()
require.NoError(t, err)
expectedBody := `{"data":{"type":"teams","attributes":{"allow-member-token-management":true,"name":"team name","organization-access":{"manage-policies":true},"visibility":"organization"}}}
`
assert.Equal(t, expectedBody, string(bodyBytes))
}
func TestTeamsUpdateRunTasks(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
skipIfEnterprise(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
defer tmTestCleanup()
t.Run("with valid options", func(t *testing.T) {
options := TeamUpdateOptions{
Name: String("foo bar"),
OrganizationAccess: &OrganizationAccessOptions{
ManageRunTasks: Bool(true),
},
Visibility: String("organization"),
}
tm, err := client.Teams.Update(ctx, tmTest.ID, options)
require.NoError(t, err)
refreshed, err := client.Teams.Read(ctx, tmTest.ID)
require.NoError(t, err)
for _, item := range []*Team{
tm,
refreshed,
} {
assert.Equal(t, *options.Name, item.Name)
assert.Equal(t,
*options.OrganizationAccess.ManageRunTasks,
item.OrganizationAccess.ManageRunTasks,
)
}
})
}
func TestTeamsUpdateManageProjects(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
defer tmTestCleanup()
t.Run("with valid options", func(t *testing.T) {
options := TeamUpdateOptions{
Name: String("foo bar"),
OrganizationAccess: &OrganizationAccessOptions{
// **Note: ManageProjects requires ManageWorkspaces field to be set and subject to change later.**
ManageWorkspaces: Bool(true),
ManageProjects: Bool(true),
},
Visibility: String("organization"),
}
tm, err := client.Teams.Update(ctx, tmTest.ID, options)
require.NoError(t, err)
refreshed, err := client.Teams.Read(ctx, tmTest.ID)
require.NoError(t, err)
for _, item := range []*Team{
tm,
refreshed,
} {
assert.Equal(t, *options.Name, item.Name)
assert.Equal(t,
*options.OrganizationAccess.ManageProjects,
item.OrganizationAccess.ManageProjects,
)
}
})
}
func TestTeamsUpdateManageManageMembership(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
defer tmTestCleanup()
teamRead, err := client.Teams.Read(ctx, tmTest.ID)
require.NoError(t, err)
assert.False(t, teamRead.OrganizationAccess.ManageMembership, "manage membership is false by default")
originalTeamAccess := teamRead.OrganizationAccess
options := TeamUpdateOptions{
OrganizationAccess: &OrganizationAccessOptions{
ManageMembership: Bool(true),
},
}
tm, err := client.Teams.Update(ctx, tmTest.ID, options)
require.NoError(t, err)
assert.True(t, tm.OrganizationAccess.ManageMembership)
refreshed, err := client.Teams.Read(ctx, tmTest.ID)
require.NoError(t, err)
assert.True(t, refreshed.OrganizationAccess.ManageMembership)
// Check that other org access fields are not updated
originalTeamAccess.ManageMembership = true
assert.Equal(t, originalTeamAccess, refreshed.OrganizationAccess)
}
func TestTeamsUpdateManageTeams(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
defer tmTestCleanup()
teamRead, err := client.Teams.Read(ctx, tmTest.ID)
require.NoError(t, err)
assert.False(t, teamRead.OrganizationAccess.ManageTeams, "manage teams is false by default")
originalTeamAccess := teamRead.OrganizationAccess
options := TeamUpdateOptions{
OrganizationAccess: &OrganizationAccessOptions{
// **Note: ManageTeams requires ManageMembership.**
ManageMembership: Bool(true),
ManageTeams: Bool(true),
},
}
tm, err := client.Teams.Update(ctx, tmTest.ID, options)
require.NoError(t, err)
assert.True(t, tm.OrganizationAccess.ManageMembership)
assert.True(t, tm.OrganizationAccess.ManageTeams)
refreshed, err := client.Teams.Read(ctx, tmTest.ID)
require.NoError(t, err)
assert.True(t, refreshed.OrganizationAccess.ManageMembership)
assert.True(t, refreshed.OrganizationAccess.ManageTeams)
// Check that other org access fields are not updated
originalTeamAccess.ManageMembership = true
originalTeamAccess.ManageTeams = true
assert.Equal(t, originalTeamAccess, refreshed.OrganizationAccess)
}
func TestTeamsUpdateManageOrganizationAccess(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
defer tmTestCleanup()
teamRead, err := client.Teams.Read(ctx, tmTest.ID)
require.NoError(t, err)
assert.False(t, teamRead.OrganizationAccess.ManageOrganizationAccess, "manage organization access is false by default")
originalTeamAccess := teamRead.OrganizationAccess
options := TeamUpdateOptions{
OrganizationAccess: &OrganizationAccessOptions{
// **Note: ManageOrganizationAccess requires ManageMembership and ManageTeams.**
ManageMembership: Bool(true),
ManageTeams: Bool(true),
ManageOrganizationAccess: Bool(true),
},
}
tm, err := client.Teams.Update(ctx, tmTest.ID, options)
require.NoError(t, err)
assert.True(t, tm.OrganizationAccess.ManageMembership)
assert.True(t, tm.OrganizationAccess.ManageTeams)
assert.True(t, tm.OrganizationAccess.ManageOrganizationAccess)
refreshed, err := client.Teams.Read(ctx, tmTest.ID)
require.NoError(t, err)
assert.True(t, refreshed.OrganizationAccess.ManageMembership)
assert.True(t, refreshed.OrganizationAccess.ManageTeams)
assert.True(t, refreshed.OrganizationAccess.ManageOrganizationAccess)
// Check that other org access fields are not updated
originalTeamAccess.ManageMembership = true
originalTeamAccess.ManageTeams = true
originalTeamAccess.ManageOrganizationAccess = true
assert.Equal(t, originalTeamAccess, refreshed.OrganizationAccess)
}
func TestTeamsUpdateAccessSecretTeams(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
defer tmTestCleanup()
teamRead, err := client.Teams.Read(ctx, tmTest.ID)
require.NoError(t, err)
assert.False(t, teamRead.OrganizationAccess.AccessSecretTeams, "access secret teams is false by default")
originalTeamAccess := teamRead.OrganizationAccess
options := TeamUpdateOptions{
OrganizationAccess: &OrganizationAccessOptions{
// **Note: AccessSecretTeams requires at least one granular permission to be set
// for it to be set, and ManageTeams requires ManageMembership.**
ManageMembership: Bool(true),
ManageTeams: Bool(true),
AccessSecretTeams: Bool(true),
},
}
tm, err := client.Teams.Update(ctx, tmTest.ID, options)
require.NoError(t, err)
assert.True(t, tm.OrganizationAccess.ManageMembership)
assert.True(t, tm.OrganizationAccess.ManageTeams)
assert.True(t, tm.OrganizationAccess.AccessSecretTeams)
refreshed, err := client.Teams.Read(ctx, tmTest.ID)
require.NoError(t, err)
assert.True(t, refreshed.OrganizationAccess.ManageMembership)
assert.True(t, refreshed.OrganizationAccess.ManageTeams)
assert.True(t, refreshed.OrganizationAccess.AccessSecretTeams)
// Check that other org access fields are not updated
originalTeamAccess.ManageMembership = true
originalTeamAccess.ManageTeams = true
originalTeamAccess.AccessSecretTeams = true
assert.Equal(t, originalTeamAccess, refreshed.OrganizationAccess)
}
func TestTeamsUpdateManageAgentPools(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
defer tmTestCleanup()
teamRead, err := client.Teams.Read(ctx, tmTest.ID)
require.NoError(t, err)
assert.False(t, teamRead.OrganizationAccess.ManageAgentPools, "manage agent pools is false by default")
originalTeamAccess := teamRead.OrganizationAccess
options := TeamUpdateOptions{
OrganizationAccess: &OrganizationAccessOptions{
ManageAgentPools: Bool(true),
},
}
tm, err := client.Teams.Update(ctx, tmTest.ID, options)
require.NoError(t, err)
assert.True(t, tm.OrganizationAccess.ManageAgentPools)
refreshed, err := client.Teams.Read(ctx, tmTest.ID)
require.NoError(t, err)
assert.True(t, refreshed.OrganizationAccess.ManageAgentPools)
// Check that other org access fields are not updated
originalTeamAccess.ManageAgentPools = true
assert.Equal(t, originalTeamAccess, refreshed.OrganizationAccess)
}
================================================
FILE: team_member.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ TeamMembers = (*teamMembers)(nil)
// TeamMembers describes all the team member related methods that the
// Terraform Enterprise API supports.
//
// TFE API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/team-members
type TeamMembers interface {
// List returns all Users of a team calling ListUsers
// See ListOrganizationMemberships for fetching memberships
List(ctx context.Context, teamID string) ([]*User, error)
// ListUsers returns the Users of this team.
ListUsers(ctx context.Context, teamID string) ([]*User, error)
// ListOrganizationMemberships returns the OrganizationMemberships of this team.
ListOrganizationMemberships(ctx context.Context, teamID string) ([]*OrganizationMembership, error)
// Add multiple users to a team.
Add(ctx context.Context, teamID string, options TeamMemberAddOptions) error
// Remove multiple users from a team.
Remove(ctx context.Context, teamID string, options TeamMemberRemoveOptions) error
}
// teamMembers implements TeamMembers.
type teamMembers struct {
client *Client
}
type teamMemberUser struct {
Username string `jsonapi:"primary,users"`
}
type teamMemberOrgMembership struct {
ID string `jsonapi:"primary,organization-memberships"`
}
// TeamMemberAddOptions represents the options for
// adding or removing team members.
type TeamMemberAddOptions struct {
Usernames []string
OrganizationMembershipIDs []string
}
// TeamMemberRemoveOptions represents the options for
// adding or removing team members.
type TeamMemberRemoveOptions struct {
Usernames []string
OrganizationMembershipIDs []string
}
// List returns all Users of a team calling ListUsers
// See ListOrganizationMemberships for fetching memberships
func (s *teamMembers) List(ctx context.Context, teamID string) ([]*User, error) {
return s.ListUsers(ctx, teamID)
}
// ListUsers returns the Users of this team.
func (s *teamMembers) ListUsers(ctx context.Context, teamID string) ([]*User, error) {
if !validStringID(&teamID) {
return nil, ErrInvalidTeamID
}
options := struct {
Include []TeamIncludeOpt `url:"include,omitempty"`
}{
Include: []TeamIncludeOpt{TeamUsers},
}
u := fmt.Sprintf("teams/%s", url.PathEscape(teamID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
t := &Team{}
err = req.Do(ctx, t)
if err != nil {
return nil, err
}
return t.Users, nil
}
// ListOrganizationMemberships returns the OrganizationMemberships of this team.
func (s *teamMembers) ListOrganizationMemberships(ctx context.Context, teamID string) ([]*OrganizationMembership, error) {
if !validStringID(&teamID) {
return nil, ErrInvalidTeamID
}
options := struct {
Include []TeamIncludeOpt `url:"include,omitempty"`
}{
Include: []TeamIncludeOpt{TeamOrganizationMemberships},
}
u := fmt.Sprintf("teams/%s", url.PathEscape(teamID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
t := &Team{}
err = req.Do(ctx, t)
if err != nil {
return nil, err
}
return t.OrganizationMemberships, nil
}
// Add multiple users to a team.
func (s *teamMembers) Add(ctx context.Context, teamID string, options TeamMemberAddOptions) error {
if !validStringID(&teamID) {
return ErrInvalidTeamID
}
if err := options.valid(); err != nil {
return err
}
usersOrMemberships := options.kind()
u := fmt.Sprintf("teams/%s/relationships/%s", url.PathEscape(teamID), usersOrMemberships)
var req *ClientRequest
if usersOrMemberships == "users" {
var err error
var members []*teamMemberUser
for _, name := range options.Usernames {
members = append(members, &teamMemberUser{Username: name})
}
req, err = s.client.NewRequest("POST", u, members)
if err != nil {
return err
}
} else {
var err error
var members []*teamMemberOrgMembership
for _, ID := range options.OrganizationMembershipIDs {
members = append(members, &teamMemberOrgMembership{ID: ID})
}
req, err = s.client.NewRequest("POST", u, members)
if err != nil {
return err
}
}
return req.Do(ctx, nil)
}
// Remove multiple users from a team.
func (s *teamMembers) Remove(ctx context.Context, teamID string, options TeamMemberRemoveOptions) error {
if !validStringID(&teamID) {
return ErrInvalidTeamID
}
if err := options.valid(); err != nil {
return err
}
usersOrMemberships := options.kind()
u := fmt.Sprintf("teams/%s/relationships/%s", url.PathEscape(teamID), usersOrMemberships)
var req *ClientRequest
if usersOrMemberships == "users" {
var err error
var members []*teamMemberUser
for _, name := range options.Usernames {
members = append(members, &teamMemberUser{Username: name})
}
req, err = s.client.NewRequest("DELETE", u, members)
if err != nil {
return err
}
} else {
var err error
var members []*teamMemberOrgMembership
for _, ID := range options.OrganizationMembershipIDs {
members = append(members, &teamMemberOrgMembership{ID: ID})
}
req, err = s.client.NewRequest("DELETE", u, members)
if err != nil {
return err
}
}
return req.Do(ctx, nil)
}
// kind returns "users" or "organization-memberships"
// depending on which is defined
func (o *TeamMemberAddOptions) kind() string {
if len(o.Usernames) != 0 {
return "users"
}
return "organization-memberships"
}
// kind returns "users" or "organization-memberships"
// depending on which is defined
func (o *TeamMemberRemoveOptions) kind() string {
if len(o.Usernames) != 0 {
return "users"
}
return "organization-memberships"
}
func (o *TeamMemberAddOptions) valid() error {
if o.Usernames == nil && o.OrganizationMembershipIDs == nil {
return ErrRequiredUsernameOrMembershipIds
}
if o.Usernames != nil && o.OrganizationMembershipIDs != nil {
return ErrRequiredOnlyOneField
}
if o.Usernames != nil && len(o.Usernames) == 0 {
return ErrInvalidUsernames
}
if o.OrganizationMembershipIDs != nil && len(o.OrganizationMembershipIDs) == 0 {
return ErrInvalidMembershipIDs
}
return nil
}
func (o *TeamMemberRemoveOptions) valid() error {
if o.Usernames == nil && o.OrganizationMembershipIDs == nil {
return ErrRequiredUsernameOrMembershipIds
}
if o.Usernames != nil && o.OrganizationMembershipIDs != nil {
return ErrRequiredOnlyOneField
}
if o.Usernames != nil && len(o.Usernames) == 0 {
return ErrInvalidUsernames
}
if o.OrganizationMembershipIDs != nil && len(o.OrganizationMembershipIDs) == 0 {
return ErrInvalidMembershipIDs
}
return nil
}
================================================
FILE: team_member_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTeamMembersList(t *testing.T) {
t.Parallel()
// The TeamMembers.List() endpoint is available for everyone,
// but this test uses extra functionality that is only available
// to paid accounts. Organizations under a free account can
// create team tokens, but they only have access to one team: the
// owners team. This test creates new teams, and that feature is
// unavaiable to paid accounts.
client := testClient(t)
ctx := context.Background()
tmTest, tmTestCleanup := createTeam(t, client, nil)
defer tmTestCleanup()
testAcct := fetchTestAccountDetails(t, client)
options := TeamMemberAddOptions{
Usernames: []string{testAcct.Username},
}
err := client.TeamMembers.Add(ctx, tmTest.ID, options)
require.NoError(t, err)
t.Run("with valid options", func(t *testing.T) {
users, err := client.TeamMembers.List(ctx, tmTest.ID)
require.NoError(t, err)
require.Equal(t, 1, len(users))
found := false
for _, user := range users {
if user.Username == testAcct.Username {
found = true
break
}
}
assert.True(t, found)
})
t.Run("when the team ID is invalid", func(t *testing.T) {
users, err := client.TeamMembers.List(ctx, badIdentifier)
assert.Equal(t, err, ErrInvalidTeamID)
assert.Nil(t, users)
})
}
func TestTeamMembersAddWithInvalidOptions(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
tmTest, tmTestCleanup := createTeam(t, client, nil)
defer tmTestCleanup()
t.Run("when options is missing usernames and organization membership ids", func(t *testing.T) {
err := client.TeamMembers.Add(ctx, tmTest.ID, TeamMemberAddOptions{})
assert.Equal(t, err, ErrRequiredUsernameOrMembershipIds)
})
t.Run("when options has both usernames and organization membership ids", func(t *testing.T) {
err := client.TeamMembers.Add(ctx, tmTest.ID, TeamMemberAddOptions{
Usernames: []string{},
OrganizationMembershipIDs: []string{},
})
assert.Equal(t, err, ErrRequiredOnlyOneField)
})
t.Run("when usernames is empty", func(t *testing.T) {
err := client.TeamMembers.Add(ctx, tmTest.ID, TeamMemberAddOptions{
Usernames: []string{},
})
assert.Equal(t, err, ErrInvalidUsernames)
})
t.Run("when organization membership ids is empty", func(t *testing.T) {
err := client.TeamMembers.Add(ctx, tmTest.ID, TeamMemberAddOptions{
OrganizationMembershipIDs: []string{},
})
assert.Equal(t, err, ErrInvalidMembershipIDs)
})
t.Run("when the team ID is invalid", func(t *testing.T) {
err := client.TeamMembers.Add(ctx, badIdentifier, TeamMemberAddOptions{
Usernames: []string{"user1"},
})
assert.Equal(t, err, ErrInvalidTeamID)
})
}
func TestTeamMembersAddByUsername(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
tmTest, tmTestCleanup := createTeam(t, client, nil)
defer tmTestCleanup()
testAcct := fetchTestAccountDetails(t, client)
t.Run("with valid username option", func(t *testing.T) {
options := TeamMemberAddOptions{
Usernames: []string{testAcct.Username},
}
err := client.TeamMembers.Add(ctx, tmTest.ID, options)
require.NoError(t, err)
users, err := client.TeamMembers.List(ctx, tmTest.ID)
require.NoError(t, err)
found := false
for _, user := range users {
if user.Username == testAcct.Username {
found = true
break
}
}
assert.True(t, found)
})
}
func TestTeamMembersAddByOrganizationMembers(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
defer tmTestCleanup()
memTest, memTestCleanup := createOrganizationMembership(t, client, orgTest)
defer memTestCleanup()
t.Run("with valid membership IDs option", func(t *testing.T) {
options := TeamMemberAddOptions{
OrganizationMembershipIDs: []string{memTest.ID},
}
err := client.TeamMembers.Add(ctx, tmTest.ID, options)
require.NoError(t, err)
orgMemberships, err := client.TeamMembers.ListOrganizationMemberships(ctx, tmTest.ID)
require.NoError(t, err)
found := false
for _, orgMembership := range orgMemberships {
if orgMembership.ID == memTest.ID {
found = true
break
}
}
assert.True(t, found)
})
}
func TestTeamMembersRemoveWithInvalidOptions(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
tmTest, tmTestCleanup := createTeam(t, client, nil)
defer tmTestCleanup()
t.Run("when options is missing usernames and organization membership ids", func(t *testing.T) {
err := client.TeamMembers.Remove(ctx, tmTest.ID, TeamMemberRemoveOptions{})
assert.Equal(t, err, ErrRequiredUsernameOrMembershipIds)
})
t.Run("when options has both usernames and organization membership ids", func(t *testing.T) {
err := client.TeamMembers.Remove(ctx, tmTest.ID, TeamMemberRemoveOptions{
Usernames: []string{},
OrganizationMembershipIDs: []string{},
})
assert.Equal(t, err, ErrRequiredOnlyOneField)
})
t.Run("when usernames is empty", func(t *testing.T) {
err := client.TeamMembers.Remove(ctx, tmTest.ID, TeamMemberRemoveOptions{
Usernames: []string{},
})
assert.Equal(t, err, ErrInvalidUsernames)
})
t.Run("when organization membership ids is empty", func(t *testing.T) {
err := client.TeamMembers.Remove(ctx, tmTest.ID, TeamMemberRemoveOptions{
OrganizationMembershipIDs: []string{},
})
assert.Equal(t, err, ErrInvalidMembershipIDs)
})
t.Run("when the team ID is invalid", func(t *testing.T) {
err := client.TeamMembers.Remove(ctx, badIdentifier, TeamMemberRemoveOptions{
Usernames: []string{"user1"},
})
assert.Equal(t, err, ErrInvalidTeamID)
})
}
func TestTeamMembersRemoveByUsernames(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
tmTest, tmTestCleanup := createTeam(t, client, nil)
defer tmTestCleanup()
testAcct := fetchTestAccountDetails(t, client)
options := TeamMemberAddOptions{
Usernames: []string{testAcct.Username},
}
err := client.TeamMembers.Add(ctx, tmTest.ID, options)
require.NoError(t, err)
t.Run("with valid usernames", func(t *testing.T) {
options := TeamMemberRemoveOptions{
Usernames: []string{testAcct.Username},
}
err := client.TeamMembers.Remove(ctx, tmTest.ID, options)
require.NoError(t, err)
})
}
func TestTeamMembersRemoveByOrganizationMemberships(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
defer tmTestCleanup()
memTest, memTestCleanup := createOrganizationMembership(t, client, orgTest)
defer memTestCleanup()
options := TeamMemberAddOptions{
OrganizationMembershipIDs: []string{memTest.ID},
}
err := client.TeamMembers.Add(ctx, tmTest.ID, options)
require.NoError(t, err)
t.Run("with valid org membership ids", func(t *testing.T) {
options := TeamMemberRemoveOptions{
OrganizationMembershipIDs: []string{memTest.ID},
}
err := client.TeamMembers.Remove(ctx, tmTest.ID, options)
require.NoError(t, err)
})
}
================================================
FILE: team_project_access.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ TeamProjectAccesses = (*teamProjectAccesses)(nil)
// TeamProjectAccesses describes all the team project access related methods that the Terraform
// Enterprise API supports
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/project-team-access
type TeamProjectAccesses interface {
// List all project accesses for a given project.
List(ctx context.Context, options TeamProjectAccessListOptions) (*TeamProjectAccessList, error)
// Add team access for a project.
Add(ctx context.Context, options TeamProjectAccessAddOptions) (*TeamProjectAccess, error)
// Read team access by project ID.
Read(ctx context.Context, teamProjectAccessID string) (*TeamProjectAccess, error)
// Update team access on a project.
Update(ctx context.Context, teamProjectAccessID string, options TeamProjectAccessUpdateOptions) (*TeamProjectAccess, error)
// Remove team access from a project.
Remove(ctx context.Context, teamProjectAccessID string) error
}
// teamProjectAccesses implements TeamProjectAccesses
type teamProjectAccesses struct {
client *Client
}
// TeamProjectAccessType represents a team project access type.
type TeamProjectAccessType string
const (
TeamProjectAccessAdmin TeamProjectAccessType = "admin"
TeamProjectAccessMaintain TeamProjectAccessType = "maintain"
TeamProjectAccessWrite TeamProjectAccessType = "write"
TeamProjectAccessRead TeamProjectAccessType = "read"
TeamProjectAccessCustom TeamProjectAccessType = "custom"
)
// TeamProjectAccessList represents a list of team project accesses
type TeamProjectAccessList struct {
*Pagination
Items []*TeamProjectAccess
}
// TeamProjectAccess represents a project access for a team
type TeamProjectAccess struct {
ID string `jsonapi:"primary,team-projects"`
Access TeamProjectAccessType `jsonapi:"attr,access"`
ProjectAccess *TeamProjectAccessProjectPermissions `jsonapi:"attr,project-access"`
WorkspaceAccess *TeamProjectAccessWorkspacePermissions `jsonapi:"attr,workspace-access"`
// Relations
Team *Team `jsonapi:"relation,team"`
Project *Project `jsonapi:"relation,project"`
}
// ProjectPermissions represents the team's permissions on its project
type TeamProjectAccessProjectPermissions struct {
ProjectSettingsPermission ProjectSettingsPermissionType `jsonapi:"attr,settings"`
ProjectTeamsPermission ProjectTeamsPermissionType `jsonapi:"attr,teams"`
// ProjectVariableSetsPermission represents read, manage, and no access custom permission for project-level variable sets
ProjectVariableSetsPermission ProjectVariableSetsPermissionType `jsonapi:"attr,variable-sets"`
}
// WorkspacePermissions represents the team's permission on all workspaces in its project
type TeamProjectAccessWorkspacePermissions struct {
WorkspaceRunsPermission WorkspaceRunsPermissionType `jsonapi:"attr,runs"`
WorkspaceSentinelMocksPermission WorkspaceSentinelMocksPermissionType `jsonapi:"attr,sentinel-mocks"`
WorkspaceStateVersionsPermission WorkspaceStateVersionsPermissionType `jsonapi:"attr,state-versions"`
WorkspaceVariablesPermission WorkspaceVariablesPermissionType `jsonapi:"attr,variables"`
WorkspaceCreatePermission bool `jsonapi:"attr,create"`
WorkspaceLockingPermission bool `jsonapi:"attr,locking"`
WorkspaceMovePermission bool `jsonapi:"attr,move"`
WorkspaceDeletePermission bool `jsonapi:"attr,delete"`
WorkspaceRunTasksPermission bool `jsonapi:"attr,run-tasks"`
// **Note: This API is still in BETA and subject to change.**
WorkspacePolicyOverridesPermission bool `jsonapi:"attr,policy-overrides"`
}
// ProjectSettingsPermissionType represents the permissiontype to a project's settings
type ProjectSettingsPermissionType string
const (
ProjectSettingsPermissionRead ProjectSettingsPermissionType = "read"
ProjectSettingsPermissionUpdate ProjectSettingsPermissionType = "update"
ProjectSettingsPermissionDelete ProjectSettingsPermissionType = "delete"
)
// ProjectTeamsPermissionType represents the permissiontype to a project's teams
type ProjectTeamsPermissionType string
const (
ProjectTeamsPermissionNone ProjectTeamsPermissionType = "none"
ProjectTeamsPermissionRead ProjectTeamsPermissionType = "read"
ProjectTeamsPermissionManage ProjectTeamsPermissionType = "manage"
)
// ProjectVariableSetsPermissionType represents the permission type to a project's variable sets
type ProjectVariableSetsPermissionType string
const (
ProjectVariableSetsPermissionNone ProjectVariableSetsPermissionType = "none"
ProjectVariableSetsPermissionRead ProjectVariableSetsPermissionType = "read"
ProjectVariableSetsPermissionWrite ProjectVariableSetsPermissionType = "write"
)
// WorkspaceRunsPermissionType represents the permissiontype to project workspaces' runs
type WorkspaceRunsPermissionType string
const (
WorkspaceRunsPermissionRead WorkspaceRunsPermissionType = "read"
WorkspaceRunsPermissionPlan WorkspaceRunsPermissionType = "plan"
WorkspaceRunsPermissionApply WorkspaceRunsPermissionType = "apply"
)
// WorkspaceSentinelMocksPermissionType represents the permissiontype to project workspaces' sentinel-mocks
type WorkspaceSentinelMocksPermissionType string
const (
WorkspaceSentinelMocksPermissionNone WorkspaceSentinelMocksPermissionType = "none"
WorkspaceSentinelMocksPermissionRead WorkspaceSentinelMocksPermissionType = "read"
)
// WorkspaceStateVersionsPermissionType represents the permissiontype to project workspaces' state-versions
type WorkspaceStateVersionsPermissionType string
const (
WorkspaceStateVersionsPermissionNone WorkspaceStateVersionsPermissionType = "none"
WorkspaceStateVersionsPermissionReadOutputs WorkspaceStateVersionsPermissionType = "read-outputs"
WorkspaceStateVersionsPermissionRead WorkspaceStateVersionsPermissionType = "read"
WorkspaceStateVersionsPermissionWrite WorkspaceStateVersionsPermissionType = "write"
)
// WorkspaceVariablesPermissionType represents the permissiontype to project workspaces' variables
type WorkspaceVariablesPermissionType string
const (
WorkspaceVariablesPermissionNone WorkspaceVariablesPermissionType = "none"
WorkspaceVariablesPermissionRead WorkspaceVariablesPermissionType = "read"
WorkspaceVariablesPermissionWrite WorkspaceVariablesPermissionType = "write"
)
type TeamProjectAccessProjectPermissionsOptions struct {
Settings *ProjectSettingsPermissionType `json:"settings,omitempty"`
Teams *ProjectTeamsPermissionType `json:"teams,omitempty"`
VariableSets *ProjectVariableSetsPermissionType `json:"variable-sets,omitempty"`
}
type TeamProjectAccessWorkspacePermissionsOptions struct {
Runs *WorkspaceRunsPermissionType `json:"runs,omitempty"`
SentinelMocks *WorkspaceSentinelMocksPermissionType `json:"sentinel-mocks,omitempty"`
StateVersions *WorkspaceStateVersionsPermissionType `json:"state-versions,omitempty"`
Variables *WorkspaceVariablesPermissionType `json:"variables,omitempty"`
Create *bool `json:"create,omitempty"`
Locking *bool `json:"locking,omitempty"`
Move *bool `json:"move,omitempty"`
Delete *bool `json:"delete,omitempty"`
RunTasks *bool `json:"run-tasks,omitempty"`
// **Note: This API is still in BETA and subject to change.**
PolicyOverrides *bool `json:"policy-overrides,omitempty"`
}
// TeamProjectAccessListOptions represents the options for listing team project accesses
type TeamProjectAccessListOptions struct {
ListOptions
ProjectID string `url:"filter[project][id]"`
}
// TeamProjectAccessAddOptions represents the options for adding team access for a project
type TeamProjectAccessAddOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,team-projects"`
// The type of access to grant.
Access TeamProjectAccessType `jsonapi:"attr,access"`
// The levels that project and workspace permissions grant
ProjectAccess *TeamProjectAccessProjectPermissionsOptions `jsonapi:"attr,project-access,omitempty"`
WorkspaceAccess *TeamProjectAccessWorkspacePermissionsOptions `jsonapi:"attr,workspace-access,omitempty"`
// The team to add to the project
Team *Team `jsonapi:"relation,team"`
// The project to which the team is to be added.
Project *Project `jsonapi:"relation,project"`
}
// TeamProjectAccessUpdateOptions represents the options for updating a team project access
type TeamProjectAccessUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,team-projects"`
// The type of access to grant.
Access *TeamProjectAccessType `jsonapi:"attr,access,omitempty"`
ProjectAccess *TeamProjectAccessProjectPermissionsOptions `jsonapi:"attr,project-access,omitempty"`
WorkspaceAccess *TeamProjectAccessWorkspacePermissionsOptions `jsonapi:"attr,workspace-access,omitempty"`
}
// List all team accesses for a given project.
func (s *teamProjectAccesses) List(ctx context.Context, options TeamProjectAccessListOptions) (*TeamProjectAccessList, error) {
if err := options.valid(); err != nil {
return nil, err
}
req, err := s.client.NewRequest("GET", "team-projects", &options)
if err != nil {
return nil, err
}
tpal := &TeamProjectAccessList{}
err = req.Do(ctx, tpal)
if err != nil {
return nil, err
}
return tpal, nil
}
// Add team access for a project.
func (s *teamProjectAccesses) Add(ctx context.Context, options TeamProjectAccessAddOptions) (*TeamProjectAccess, error) {
if err := options.valid(); err != nil {
return nil, err
}
if err := validateTeamProjectAccessType(options.Access); err != nil {
return nil, err
}
req, err := s.client.NewRequest("POST", "team-projects", &options)
if err != nil {
return nil, err
}
tpa := &TeamProjectAccess{}
err = req.Do(ctx, tpa)
if err != nil {
return nil, err
}
return tpa, nil
}
// Read a team project access by its ID.
func (s *teamProjectAccesses) Read(ctx context.Context, teamProjectAccessID string) (*TeamProjectAccess, error) {
if !validStringID(&teamProjectAccessID) {
return nil, ErrInvalidTeamProjectAccessID
}
u := fmt.Sprintf("team-projects/%s", url.PathEscape(teamProjectAccessID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
tpa := &TeamProjectAccess{}
err = req.Do(ctx, tpa)
if err != nil {
return nil, err
}
return tpa, nil
}
// Update team access for a project.
func (s *teamProjectAccesses) Update(ctx context.Context, teamProjectAccessID string, options TeamProjectAccessUpdateOptions) (*TeamProjectAccess, error) {
if !validStringID(&teamProjectAccessID) {
return nil, ErrInvalidTeamProjectAccessID
}
if err := validateTeamProjectAccessType(*options.Access); err != nil {
return nil, err
}
u := fmt.Sprintf("team-projects/%s", url.PathEscape(teamProjectAccessID))
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
ta := &TeamProjectAccess{}
err = req.Do(ctx, ta)
if err != nil {
return nil, err
}
return ta, err
}
// Remove team access from a project.
func (s *teamProjectAccesses) Remove(ctx context.Context, teamProjectAccessID string) error {
if !validStringID(&teamProjectAccessID) {
return ErrInvalidTeamProjectAccessID
}
u := fmt.Sprintf("team-projects/%s", url.PathEscape(teamProjectAccessID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o TeamProjectAccessListOptions) valid() error {
if !validStringID(&o.ProjectID) {
return ErrInvalidProjectID
}
return nil
}
func (o TeamProjectAccessAddOptions) valid() error {
if err := validateTeamProjectAccessType(o.Access); err != nil {
return err
}
if o.Team == nil {
return ErrRequiredTeam
}
if o.Project == nil {
return ErrRequiredProject
}
return nil
}
func validateTeamProjectAccessType(t TeamProjectAccessType) error {
switch t {
case TeamProjectAccessAdmin,
TeamProjectAccessMaintain,
TeamProjectAccessWrite,
TeamProjectAccessRead,
TeamProjectAccessCustom:
// do nothing
default:
return ErrInvalidTeamProjectAccessType
}
return nil
}
================================================
FILE: team_project_access_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTeamProjectAccessesList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
pTest, pTestCleanup := createProject(t, client, orgTest)
defer pTestCleanup()
tmTest1, tmTest1Cleanup := createTeam(t, client, orgTest)
defer tmTest1Cleanup()
tmTest2, tmTest2Cleanup := createTeam(t, client, orgTest)
defer tmTest2Cleanup()
tpaTest1, tpaTest1Cleanup := createTeamProjectAccess(t, client, tmTest1, pTest, orgTest)
defer tpaTest1Cleanup()
tpaTest2, tpaTest2Cleanup := createTeamProjectAccess(t, client, tmTest2, pTest, orgTest)
defer tpaTest2Cleanup()
t.Run("with valid options", func(t *testing.T) {
tpal, err := client.TeamProjectAccess.List(ctx, TeamProjectAccessListOptions{
ProjectID: pTest.ID,
})
require.NoError(t, err)
assert.Contains(t, tpal.Items, tpaTest1)
assert.Contains(t, tpal.Items, tpaTest2)
})
t.Run("with list options", func(t *testing.T) {
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
tpal, err := client.TeamProjectAccess.List(ctx, TeamProjectAccessListOptions{
ProjectID: pTest.ID,
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, tpal.Items)
assert.Equal(t, 999, tpal.CurrentPage)
assert.Equal(t, 2, tpal.TotalCount)
})
t.Run("without projectID options", func(t *testing.T) {
tpal, err := client.TeamProjectAccess.List(ctx, TeamProjectAccessListOptions{
ListOptions: ListOptions{
PageNumber: 2,
PageSize: 25,
},
})
assert.Nil(t, tpal)
assert.Equal(t, err, ErrInvalidProjectID)
})
t.Run("without a valid projectID", func(t *testing.T) {
tpal, err := client.TeamProjectAccess.List(ctx, TeamProjectAccessListOptions{
ProjectID: badIdentifier,
})
assert.Nil(t, tpal)
assert.EqualError(t, err, ErrInvalidProjectID.Error())
})
}
func TestTeamProjectAccessesRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
pTest, pTestCleanup := createProject(t, client, orgTest)
defer pTestCleanup()
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
defer tmTestCleanup()
tpaTest, tpaTestCleanup := createTeamProjectAccess(t, client, tmTest, pTest, orgTest)
defer tpaTestCleanup()
t.Run("when the team access exists", func(t *testing.T) {
tpa, err := client.TeamProjectAccess.Read(ctx, tpaTest.ID)
require.NoError(t, err)
assert.Equal(t, TeamProjectAccessAdmin, tpa.Access)
t.Run("team relationship is decoded", func(t *testing.T) {
assert.NotEmpty(t, tpa.Team)
})
t.Run("project relationship is decoded", func(t *testing.T) {
assert.NotEmpty(t, tpa.Project)
})
})
t.Run("when the team access does not exist", func(t *testing.T) {
tpa, err := client.TeamProjectAccess.Read(ctx, "nonexisting")
assert.Nil(t, tpa)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("without a valid team access ID", func(t *testing.T) {
tpa, err := client.TeamProjectAccess.Read(ctx, badIdentifier)
assert.Nil(t, tpa)
assert.Equal(t, err, ErrInvalidTeamProjectAccessID)
})
}
func TestTeamProjectAccessesAdd(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
pTest, pTestCleanup := createProject(t, client, orgTest)
defer pTestCleanup()
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
defer tmTestCleanup()
t.Run("with valid options", func(t *testing.T) {
options := TeamProjectAccessAddOptions{
Access: *ProjectAccess(TeamProjectAccessAdmin),
Team: tmTest,
Project: pTest,
}
tpa, err := client.TeamProjectAccess.Add(ctx, options)
require.NoError(t, err)
defer func() {
err := client.TeamProjectAccess.Remove(ctx, tpa.ID)
if err != nil {
t.Logf("error removing team access (%s): %s", tpa.ID, err)
}
}()
// Get a refreshed view from the API.
refreshed, err := client.TeamProjectAccess.Read(ctx, tpa.ID)
require.NoError(t, err)
for _, item := range []*TeamProjectAccess{
tpa,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, options.Access, item.Access)
}
})
t.Run("with no project access options for custom TeamProject permissions", func(t *testing.T) {
options := TeamProjectAccessAddOptions{
Access: *ProjectAccess(TeamProjectAccessCustom),
Team: tmTest,
Project: pTest,
ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{},
WorkspaceAccess: &TeamProjectAccessWorkspacePermissionsOptions{
Runs: WorkspaceRunsPermission(WorkspaceRunsPermissionApply),
SentinelMocks: WorkspaceSentinelMocksPermission(WorkspaceSentinelMocksPermissionRead),
StateVersions: WorkspaceStateVersionsPermission(WorkspaceStateVersionsPermissionWrite),
Variables: WorkspaceVariablesPermission(WorkspaceVariablesPermissionWrite),
Create: Bool(true),
Locking: Bool(true),
Move: Bool(true),
Delete: Bool(false),
RunTasks: Bool(false),
},
}
tpa, err := client.TeamProjectAccess.Add(ctx, options)
require.NoError(t, err)
defer func() {
err := client.TeamProjectAccess.Remove(ctx, tpa.ID)
if err != nil {
t.Logf("error removing team access (%s): %s", tpa.ID, err)
}
}()
// Get a refreshed view from the API.
refreshed, err := client.TeamProjectAccess.Read(ctx, tpa.ID)
require.NoError(t, err)
for _, item := range []*TeamProjectAccess{
tpa,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, options.Access, item.Access)
assert.Equal(t, *options.WorkspaceAccess.Runs, item.WorkspaceAccess.WorkspaceRunsPermission)
assert.Equal(t, *options.WorkspaceAccess.SentinelMocks, item.WorkspaceAccess.WorkspaceSentinelMocksPermission)
assert.Equal(t, *options.WorkspaceAccess.StateVersions, item.WorkspaceAccess.WorkspaceStateVersionsPermission)
assert.Equal(t, *options.WorkspaceAccess.Variables, item.WorkspaceAccess.WorkspaceVariablesPermission)
assert.Equal(t, item.WorkspaceAccess.WorkspaceCreatePermission, true)
assert.Equal(t, item.WorkspaceAccess.WorkspaceLockingPermission, true)
assert.Equal(t, item.WorkspaceAccess.WorkspaceMovePermission, true)
assert.Equal(t, item.WorkspaceAccess.WorkspaceDeletePermission, false)
assert.Equal(t, item.WorkspaceAccess.WorkspaceRunTasksPermission, false)
}
})
t.Run("with valid options for all custom TeamProject permissions", func(t *testing.T) {
options := TeamProjectAccessAddOptions{
Access: *ProjectAccess(TeamProjectAccessCustom),
Team: tmTest,
Project: pTest,
ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{
Settings: ProjectSettingsPermission(ProjectSettingsPermissionUpdate),
Teams: ProjectTeamsPermission(ProjectTeamsPermissionManage),
},
WorkspaceAccess: &TeamProjectAccessWorkspacePermissionsOptions{
Runs: WorkspaceRunsPermission(WorkspaceRunsPermissionApply),
SentinelMocks: WorkspaceSentinelMocksPermission(WorkspaceSentinelMocksPermissionRead),
StateVersions: WorkspaceStateVersionsPermission(WorkspaceStateVersionsPermissionWrite),
Variables: WorkspaceVariablesPermission(WorkspaceVariablesPermissionWrite),
Create: Bool(true),
Locking: Bool(true),
Move: Bool(true),
Delete: Bool(false),
RunTasks: Bool(false),
},
}
tpa, err := client.TeamProjectAccess.Add(ctx, options)
require.NoError(t, err)
defer func() {
err := client.TeamProjectAccess.Remove(ctx, tpa.ID)
if err != nil {
t.Logf("error removing team access (%s): %s", tpa.ID, err)
}
}()
// Get a refreshed view from the API.
refreshed, err := client.TeamProjectAccess.Read(ctx, tpa.ID)
require.NoError(t, err)
for _, item := range []*TeamProjectAccess{
tpa,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, options.Access, item.Access)
assert.Equal(t, *options.ProjectAccess.Settings, item.ProjectAccess.ProjectSettingsPermission)
assert.Equal(t, *options.ProjectAccess.Teams, item.ProjectAccess.ProjectTeamsPermission)
assert.Equal(t, *options.WorkspaceAccess.Runs, item.WorkspaceAccess.WorkspaceRunsPermission)
assert.Equal(t, *options.WorkspaceAccess.SentinelMocks, item.WorkspaceAccess.WorkspaceSentinelMocksPermission)
assert.Equal(t, *options.WorkspaceAccess.StateVersions, item.WorkspaceAccess.WorkspaceStateVersionsPermission)
assert.Equal(t, *options.WorkspaceAccess.Variables, item.WorkspaceAccess.WorkspaceVariablesPermission)
assert.Equal(t, item.WorkspaceAccess.WorkspaceCreatePermission, true)
assert.Equal(t, item.WorkspaceAccess.WorkspaceLockingPermission, true)
assert.Equal(t, item.WorkspaceAccess.WorkspaceMovePermission, true)
assert.Equal(t, item.WorkspaceAccess.WorkspaceDeletePermission, false)
assert.Equal(t, item.WorkspaceAccess.WorkspaceRunTasksPermission, false)
}
})
t.Run("with valid options for custom variable sets permissions", func(t *testing.T) {
options := TeamProjectAccessAddOptions{
Access: *ProjectAccess(TeamProjectAccessCustom),
Team: tmTest,
Project: pTest,
ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{
VariableSets: ProjectVariableSetsPermission(ProjectVariableSetsPermissionWrite),
},
WorkspaceAccess: &TeamProjectAccessWorkspacePermissionsOptions{
Runs: WorkspaceRunsPermission(WorkspaceRunsPermissionApply),
},
}
tpa, err := client.TeamProjectAccess.Add(ctx, options)
t.Cleanup(func() {
err := client.TeamProjectAccess.Remove(ctx, tpa.ID)
if err != nil {
t.Logf("error removing team access (%s): %s", tpa.ID, err)
}
})
require.NoError(t, err)
// Get a refreshed view from the API.
refreshed, err := client.TeamProjectAccess.Read(ctx, tpa.ID)
require.NoError(t, err)
for _, item := range []*TeamProjectAccess{
tpa,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, options.Access, item.Access)
assert.Equal(t, *options.ProjectAccess.VariableSets, item.ProjectAccess.ProjectVariableSetsPermission)
assert.Equal(t, *options.WorkspaceAccess.Runs, item.WorkspaceAccess.WorkspaceRunsPermission)
}
})
t.Run("with valid options for some custom TeamProject permissions", func(t *testing.T) {
options := TeamProjectAccessAddOptions{
Access: *ProjectAccess(TeamProjectAccessCustom),
Team: tmTest,
Project: pTest,
ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{
Settings: ProjectSettingsPermission(ProjectSettingsPermissionUpdate),
},
WorkspaceAccess: &TeamProjectAccessWorkspacePermissionsOptions{
Runs: WorkspaceRunsPermission(WorkspaceRunsPermissionApply),
},
}
tpa, err := client.TeamProjectAccess.Add(ctx, options)
t.Cleanup(func() {
err := client.TeamProjectAccess.Remove(ctx, tpa.ID)
if err != nil {
t.Logf("error removing team access (%s): %s", tpa.ID, err)
}
})
require.NoError(t, err)
// Get a refreshed view from the API.
refreshed, err := client.TeamProjectAccess.Read(ctx, tpa.ID)
require.NoError(t, err)
for _, item := range []*TeamProjectAccess{
tpa,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, options.Access, item.Access)
assert.Equal(t, *options.ProjectAccess.Settings, item.ProjectAccess.ProjectSettingsPermission)
assert.Equal(t, *options.WorkspaceAccess.Runs, item.WorkspaceAccess.WorkspaceRunsPermission)
}
})
t.Run("with valid options for custom workspace policy overrides permission", func(t *testing.T) {
skipUnlessBeta(t)
options := TeamProjectAccessAddOptions{
Access: *ProjectAccess(TeamProjectAccessCustom),
Team: tmTest,
Project: pTest,
WorkspaceAccess: &TeamProjectAccessWorkspacePermissionsOptions{
Runs: WorkspaceRunsPermission(WorkspaceRunsPermissionApply),
PolicyOverrides: Bool(true),
},
}
tpa, err := client.TeamProjectAccess.Add(ctx, options)
t.Cleanup(func() {
err := client.TeamProjectAccess.Remove(ctx, tpa.ID)
if err != nil {
t.Logf("error removing team access (%s): %s", tpa.ID, err)
}
})
require.NoError(t, err)
// Get a refreshed view from the API.
refreshed, err := client.TeamProjectAccess.Read(ctx, tpa.ID)
require.NoError(t, err)
for _, item := range []*TeamProjectAccess{
tpa,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, options.Access, item.Access)
assert.Equal(t, *options.WorkspaceAccess.Runs, item.WorkspaceAccess.WorkspaceRunsPermission)
assert.Equal(t, true, item.WorkspaceAccess.WorkspacePolicyOverridesPermission)
}
})
t.Run("when the team already has access to the project", func(t *testing.T) {
_, tpaTestCleanup := createTeamProjectAccess(t, client, tmTest, pTest, nil)
defer tpaTestCleanup()
options := TeamProjectAccessAddOptions{
Access: *ProjectAccess(TeamProjectAccessAdmin),
Team: tmTest,
Project: pTest,
}
_, err := client.TeamProjectAccess.Add(ctx, options)
assert.Error(t, err)
})
t.Run("when options is missing access", func(t *testing.T) {
tpa, err := client.TeamProjectAccess.Add(ctx, TeamProjectAccessAddOptions{
Team: tmTest,
Project: pTest,
})
assert.Nil(t, tpa)
assert.Equal(t, err, ErrInvalidTeamProjectAccessType)
})
t.Run("when options is missing team", func(t *testing.T) {
tpa, err := client.TeamProjectAccess.Add(ctx, TeamProjectAccessAddOptions{
Access: *ProjectAccess(TeamProjectAccessAdmin),
Project: pTest,
})
assert.Nil(t, tpa)
assert.Equal(t, err, ErrRequiredTeam)
})
t.Run("when options is missing project", func(t *testing.T) {
tpa, err := client.TeamProjectAccess.Add(ctx, TeamProjectAccessAddOptions{
Access: *ProjectAccess(TeamProjectAccessAdmin),
Team: tmTest,
})
assert.Nil(t, tpa)
assert.Equal(t, err, ErrRequiredProject)
})
t.Run("when invalid custom project permission is provided in options", func(t *testing.T) {
tpa, err := client.TeamProjectAccess.Add(ctx, TeamProjectAccessAddOptions{
Access: *ProjectAccess(TeamProjectAccessCustom),
Team: tmTest,
Project: pTest,
ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{
Teams: ProjectTeamsPermission(badIdentifier),
},
})
assert.Nil(t, tpa)
assert.Error(t, err)
})
t.Run("when invalid access is provided in options", func(t *testing.T) {
tpa, err := client.TeamProjectAccess.Add(ctx, TeamProjectAccessAddOptions{
Access: badIdentifier,
Team: tmTest,
Project: pTest,
})
assert.Nil(t, tpa)
assert.Equal(t, err, ErrInvalidTeamProjectAccessType)
})
}
func TestTeamProjectAccessesUpdate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
pTest, pTestCleanup := createProject(t, client, orgTest)
defer pTestCleanup()
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
defer tmTestCleanup()
tpaTest, tpaTestCleanup := createTeamProjectAccess(t, client, tmTest, pTest, orgTest)
defer tpaTestCleanup()
t.Run("with valid attributes", func(t *testing.T) {
options := TeamProjectAccessUpdateOptions{
Access: ProjectAccess(TeamProjectAccessRead),
}
tpa, err := client.TeamProjectAccess.Update(ctx, tpaTest.ID, options)
require.NoError(t, err)
assert.Equal(t, tpa.Access, TeamProjectAccessRead)
})
t.Run("with valid custom permissions attributes for all permissions", func(t *testing.T) {
options := TeamProjectAccessUpdateOptions{
Access: ProjectAccess(TeamProjectAccessCustom),
ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{
Settings: ProjectSettingsPermission(ProjectSettingsPermissionUpdate),
Teams: ProjectTeamsPermission(ProjectTeamsPermissionManage),
},
WorkspaceAccess: &TeamProjectAccessWorkspacePermissionsOptions{
Runs: WorkspaceRunsPermission(WorkspaceRunsPermissionPlan),
SentinelMocks: WorkspaceSentinelMocksPermission(WorkspaceSentinelMocksPermissionNone),
StateVersions: WorkspaceStateVersionsPermission(WorkspaceStateVersionsPermissionReadOutputs),
Variables: WorkspaceVariablesPermission(WorkspaceVariablesPermissionRead),
Create: Bool(false),
Locking: Bool(false),
Move: Bool(false),
Delete: Bool(true),
RunTasks: Bool(true),
},
}
tpa, err := client.TeamProjectAccess.Update(ctx, tpaTest.ID, options)
require.NoError(t, err)
require.NotNil(t, options.ProjectAccess)
require.NotNil(t, options.WorkspaceAccess)
assert.Equal(t, tpa.Access, TeamProjectAccessCustom)
assert.Equal(t, *options.ProjectAccess.Teams, tpa.ProjectAccess.ProjectTeamsPermission)
assert.Equal(t, *options.ProjectAccess.Settings, tpa.ProjectAccess.ProjectSettingsPermission)
assert.Equal(t, *options.WorkspaceAccess.Runs, tpa.WorkspaceAccess.WorkspaceRunsPermission)
assert.Equal(t, *options.WorkspaceAccess.SentinelMocks, tpa.WorkspaceAccess.WorkspaceSentinelMocksPermission)
assert.Equal(t, *options.WorkspaceAccess.StateVersions, tpa.WorkspaceAccess.WorkspaceStateVersionsPermission)
assert.Equal(t, *options.WorkspaceAccess.Variables, tpa.WorkspaceAccess.WorkspaceVariablesPermission)
assert.Equal(t, false, tpa.WorkspaceAccess.WorkspaceCreatePermission)
assert.Equal(t, false, tpa.WorkspaceAccess.WorkspaceLockingPermission)
assert.Equal(t, false, tpa.WorkspaceAccess.WorkspaceMovePermission)
assert.Equal(t, true, tpa.WorkspaceAccess.WorkspaceDeletePermission)
assert.Equal(t, true, tpa.WorkspaceAccess.WorkspaceRunTasksPermission)
})
t.Run("with valid custom permissions attributes for variable sets permissions", func(t *testing.T) {
// create tpaCustomTest to verify unupdated attributes stay the same for custom permissions
// because going from admin to read to custom changes the values of all custom permissions
tm2Test, tm2TestCleanup := createTeam(t, client, orgTest)
defer tm2TestCleanup()
TpaOptions := TeamProjectAccessAddOptions{
Access: *ProjectAccess(TeamProjectAccessCustom),
Team: tm2Test,
Project: pTest,
}
tpaCustomTest, err := client.TeamProjectAccess.Add(ctx, TpaOptions)
require.NoError(t, err)
options := TeamProjectAccessUpdateOptions{
Access: ProjectAccess(TeamProjectAccessCustom),
ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{
VariableSets: ProjectVariableSetsPermission(ProjectVariableSetsPermissionRead),
},
WorkspaceAccess: &TeamProjectAccessWorkspacePermissionsOptions{
Create: Bool(false),
},
}
tpa, err := client.TeamProjectAccess.Update(ctx, tpaCustomTest.ID, options)
require.NoError(t, err)
require.NotNil(t, options.ProjectAccess)
require.NotNil(t, options.WorkspaceAccess)
assert.Equal(t, *options.ProjectAccess.VariableSets, tpa.ProjectAccess.ProjectVariableSetsPermission)
assert.Equal(t, false, tpa.WorkspaceAccess.WorkspaceCreatePermission)
// assert that other attributes remain the same
assert.Equal(t, tpaCustomTest.ProjectAccess.ProjectSettingsPermission, tpa.ProjectAccess.ProjectSettingsPermission)
assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceLockingPermission, tpa.WorkspaceAccess.WorkspaceLockingPermission)
assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceMovePermission, tpa.WorkspaceAccess.WorkspaceMovePermission)
assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceDeletePermission, tpa.WorkspaceAccess.WorkspaceDeletePermission)
assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceRunsPermission, tpa.WorkspaceAccess.WorkspaceRunsPermission)
assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceSentinelMocksPermission, tpa.WorkspaceAccess.WorkspaceSentinelMocksPermission)
assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceStateVersionsPermission, tpa.WorkspaceAccess.WorkspaceStateVersionsPermission)
})
t.Run("with valid custom permissions attributes for some permissions", func(t *testing.T) {
// create tpaCustomTest to verify unupdated attributes stay the same for custom permissions
// because going from admin to read to custom changes the values of all custom permissions
tm2Test, tm2TestCleanup := createTeam(t, client, orgTest)
defer tm2TestCleanup()
TpaOptions := TeamProjectAccessAddOptions{
Access: *ProjectAccess(TeamProjectAccessCustom),
Team: tm2Test,
Project: pTest,
}
tpaCustomTest, err := client.TeamProjectAccess.Add(ctx, TpaOptions)
require.NoError(t, err)
options := TeamProjectAccessUpdateOptions{
Access: ProjectAccess(TeamProjectAccessCustom),
ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{
Teams: ProjectTeamsPermission(ProjectTeamsPermissionManage),
},
WorkspaceAccess: &TeamProjectAccessWorkspacePermissionsOptions{
Create: Bool(false),
},
}
tpa, err := client.TeamProjectAccess.Update(ctx, tpaCustomTest.ID, options)
require.NoError(t, err)
require.NotNil(t, options.ProjectAccess)
require.NotNil(t, options.WorkspaceAccess)
assert.Equal(t, *options.ProjectAccess.Teams, tpa.ProjectAccess.ProjectTeamsPermission)
assert.Equal(t, false, tpa.WorkspaceAccess.WorkspaceCreatePermission)
// assert that other attributes remain the same
assert.Equal(t, tpaCustomTest.ProjectAccess.ProjectSettingsPermission, tpa.ProjectAccess.ProjectSettingsPermission)
assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceLockingPermission, tpa.WorkspaceAccess.WorkspaceLockingPermission)
assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceMovePermission, tpa.WorkspaceAccess.WorkspaceMovePermission)
assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceDeletePermission, tpa.WorkspaceAccess.WorkspaceDeletePermission)
assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceRunsPermission, tpa.WorkspaceAccess.WorkspaceRunsPermission)
assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceSentinelMocksPermission, tpa.WorkspaceAccess.WorkspaceSentinelMocksPermission)
assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceStateVersionsPermission, tpa.WorkspaceAccess.WorkspaceStateVersionsPermission)
})
t.Run("with invalid custom permissions attributes", func(t *testing.T) {
options := TeamProjectAccessUpdateOptions{
Access: ProjectAccess(TeamProjectAccessCustom),
ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{
Teams: ProjectTeamsPermission(badIdentifier),
},
}
tpa, err := client.TeamProjectAccess.Update(ctx, tpaTest.ID, options)
assert.Nil(t, tpa)
assert.Error(t, err)
})
}
func TestTeamProjectAccessesRemove(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
pTest, pTestCleanup := createProject(t, client, orgTest)
defer pTestCleanup()
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
defer tmTestCleanup()
tpaTest, _ := createTeamProjectAccess(t, client, tmTest, pTest, orgTest)
t.Run("with valid options", func(t *testing.T) {
err := client.TeamProjectAccess.Remove(ctx, tpaTest.ID)
require.NoError(t, err)
// Try loading the project - it should fail.
_, err = client.TeamProjectAccess.Read(ctx, tpaTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the team access does not exist", func(t *testing.T) {
err := client.TeamProjectAccess.Remove(ctx, tpaTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the team access ID is invalid", func(t *testing.T) {
err := client.TeamProjectAccess.Remove(ctx, badIdentifier)
assert.Equal(t, err, ErrInvalidTeamProjectAccessID)
})
}
================================================
FILE: team_token.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ TeamTokens = (*teamTokens)(nil)
// TeamTokens describes all the team token related methods that the
// Terraform Enterprise API supports.
//
// TFE API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/team-tokens
type TeamTokens interface {
// Create a new team token using the legacy creation behavior, which creates a token without a description
// or regenerates the existing, descriptionless token.
Create(ctx context.Context, teamID string) (*TeamToken, error)
// CreateWithOptions creates a team token, with options. If no description is provided, it uses the legacy
// creation behavior, which regenerates the descriptionless token if it already exists. Otherwise, it create
// a new token with the given unique description, allowing for the creation of multiple team tokens.
CreateWithOptions(ctx context.Context, teamID string, options TeamTokenCreateOptions) (*TeamToken, error)
// Read a team token by its team ID.
Read(ctx context.Context, teamID string) (*TeamToken, error)
// Read a team token by its token ID.
ReadByID(ctx context.Context, teamID string) (*TeamToken, error)
// List an organization's team tokens.
List(ctx context.Context, organizationID string, options *TeamTokenListOptions) (*TeamTokenList, error)
// Delete a team token by its team ID.
Delete(ctx context.Context, teamID string) error
// Delete a team token by its token ID.
DeleteByID(ctx context.Context, tokenID string) error
}
// teamTokens implements TeamTokens.
type teamTokens struct {
client *Client
}
// TeamToken represents a Terraform Enterprise team token.
type TeamToken struct {
ID string `jsonapi:"primary,authentication-tokens"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Description *string `jsonapi:"attr,description"`
LastUsedAt time.Time `jsonapi:"attr,last-used-at,iso8601"`
Token string `jsonapi:"attr,token"`
ExpiredAt time.Time `jsonapi:"attr,expired-at,iso8601"`
CreatedBy *CreatedByChoice `jsonapi:"polyrelation,created-by"`
Team *Team `jsonapi:"relation,team"`
}
// TeamTokenCreateOptions contains the options for creating a team token.
type TeamTokenCreateOptions struct {
// Optional: The token's expiration date.
// This feature is available in TFE release v202305-1 and later
ExpiredAt *time.Time `jsonapi:"attr,expired-at,iso8601,omitempty"`
// Optional: The token's description, which must unique per team.
// This feature is considered BETA, SUBJECT TO CHANGE, and likely unavailable to most users.
Description *string `jsonapi:"attr,description,omitempty"`
}
// TeamTokenListOptions contains the options for listing team tokens.
type TeamTokenListOptions struct {
ListOptions
// Optional: A query string used to filter team tokens by
// a specified team name.
Query string `url:"q,omitempty"`
// Optional: Allows sorting the team tokens by "team-name",
// "created-by", "expired-at", and "last-used-at"
Sort string `url:"sort,omitempty"`
}
// TeamTokenList represents a list of team tokens.
type TeamTokenList struct {
*Pagination
Items []*TeamToken
}
// Create a new team token using the legacy creation behavior, which creates a token without a description
// or regenerates the existing, descriptionless token.
func (s *teamTokens) Create(ctx context.Context, teamID string) (*TeamToken, error) {
return s.CreateWithOptions(ctx, teamID, TeamTokenCreateOptions{})
}
// CreateWithOptions creates a team token, with options. If no description is provided, it uses the legacy
// creation behavior, which regenerates the descriptionless token if it already exists. Otherwise, it create
// a new token with the given unique description, allowing for the creation of multiple team tokens.
func (s *teamTokens) CreateWithOptions(ctx context.Context, teamID string, options TeamTokenCreateOptions) (*TeamToken, error) {
if !validStringID(&teamID) {
return nil, ErrInvalidTeamID
}
var u string
if options.Description != nil {
u = fmt.Sprintf("teams/%s/authentication-tokens", url.PathEscape(teamID))
} else {
u = fmt.Sprintf("teams/%s/authentication-token", url.PathEscape(teamID))
}
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
tt := &TeamToken{}
err = req.Do(ctx, tt)
if err != nil {
return nil, err
}
return tt, err
}
// Read a team token by its team ID.
func (s *teamTokens) Read(ctx context.Context, teamID string) (*TeamToken, error) {
if !validStringID(&teamID) {
return nil, ErrInvalidTeamID
}
u := fmt.Sprintf("teams/%s/authentication-token", url.PathEscape(teamID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
tt := &TeamToken{}
err = req.Do(ctx, tt)
if err != nil {
return nil, err
}
return tt, err
}
// Read a team token by its token ID.
func (s *teamTokens) ReadByID(ctx context.Context, tokenID string) (*TeamToken, error) {
if !validStringID(&tokenID) {
return nil, ErrInvalidTokenID
}
u := fmt.Sprintf(AuthenticationTokensPath, url.PathEscape(tokenID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
tt := &TeamToken{}
err = req.Do(ctx, tt)
if err != nil {
return nil, err
}
return tt, err
}
// List an organization's team tokens with the option to filter by team name.
func (s *teamTokens) List(ctx context.Context, organizationID string, options *TeamTokenListOptions) (*TeamTokenList, error) {
if !validStringID(&organizationID) {
return nil, ErrInvalidOrg
}
u := fmt.Sprintf("organizations/%s/team-tokens", url.PathEscape(organizationID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
tt := &TeamTokenList{}
err = req.Do(ctx, tt)
if err != nil {
return nil, err
}
return tt, err
}
// Delete a team token by its team ID.
func (s *teamTokens) Delete(ctx context.Context, teamID string) error {
if !validStringID(&teamID) {
return ErrInvalidTeamID
}
u := fmt.Sprintf("teams/%s/authentication-token", url.PathEscape(teamID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// Delete a team token by its token ID.
func (s *teamTokens) DeleteByID(ctx context.Context, tokenID string) error {
if !validStringID(&tokenID) {
return ErrInvalidTokenID
}
u := fmt.Sprintf(AuthenticationTokensPath, url.PathEscape(tokenID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
================================================
FILE: team_token_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTeamTokensCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
tmTest, tmTestCleanup := createTeam(t, client, nil)
defer tmTestCleanup()
var tmToken string
t.Run("with valid options", func(t *testing.T) {
tt, err := client.TeamTokens.Create(ctx, tmTest.ID)
require.NoError(t, err)
require.NotEmpty(t, tt.Token)
require.NotEmpty(t, tt.CreatedBy)
requireExactlyOneNotEmpty(t, tt.CreatedBy.Organization, tt.CreatedBy.Team, tt.CreatedBy.User)
tmToken = tt.Token
})
t.Run("when a token already exists", func(t *testing.T) {
tt, err := client.TeamTokens.Create(ctx, tmTest.ID)
require.NoError(t, err)
require.NotEmpty(t, tt.Token)
assert.NotEqual(t, tmToken, tt.Token)
})
t.Run("without valid team ID", func(t *testing.T) {
tt, err := client.TeamTokens.Create(ctx, badIdentifier)
assert.Nil(t, tt)
assert.Equal(t, err, ErrInvalidTeamID)
})
}
func TestTeamTokens_CreateWithOptions(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
tmTest, tmTestCleanup := createTeam(t, client, nil)
defer tmTestCleanup()
var tmToken string
t.Run("with valid options", func(t *testing.T) {
tt, err := client.TeamTokens.CreateWithOptions(ctx, tmTest.ID, TeamTokenCreateOptions{})
require.NoError(t, err)
require.NotEmpty(t, tt.Token)
tmToken = tt.Token
})
t.Run("when a token already exists", func(t *testing.T) {
tt, err := client.TeamTokens.CreateWithOptions(ctx, tmTest.ID, TeamTokenCreateOptions{})
require.NoError(t, err)
require.NotEmpty(t, tt.Token)
assert.NotEqual(t, tmToken, tt.Token)
})
t.Run("without valid team ID", func(t *testing.T) {
tt, err := client.TeamTokens.CreateWithOptions(ctx, badIdentifier, TeamTokenCreateOptions{})
assert.Nil(t, tt)
assert.Equal(t, err, ErrInvalidTeamID)
})
t.Run("without an expiration date", func(t *testing.T) {
tt, err := client.TeamTokens.CreateWithOptions(ctx, tmTest.ID, TeamTokenCreateOptions{})
require.NoError(t, err)
require.NotEmpty(t, tt.Token)
assert.NotZero(t, tt.ExpiredAt)
expectedExpiry := tt.CreatedAt.AddDate(defaultTokenExpirationYears, 0, 0)
// Allow a small buffer (1 minute) for timestamp precision differences.
assert.WithinDuration(t, expectedExpiry, tt.ExpiredAt, time.Minute)
tmToken = tt.Token
})
t.Run("with an expiration date", func(t *testing.T) {
currentTime := time.Now().UTC().Truncate(time.Second)
oneDayLater := currentTime.Add(24 * time.Hour)
tt, err := client.TeamTokens.CreateWithOptions(ctx, tmTest.ID, TeamTokenCreateOptions{
ExpiredAt: &oneDayLater,
})
require.NoError(t, err)
require.NotEmpty(t, tt.Token)
assert.Equal(t, tt.ExpiredAt, oneDayLater)
tmToken = tt.Token
})
}
func TestTeamTokens_CreateWithOptions_MultipleTokens(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
tmTest, tmTestCleanup := createTeam(t, client, nil)
t.Cleanup(tmTestCleanup)
t.Run("with multiple tokens", func(t *testing.T) {
desc1 := fmt.Sprintf("go-tfe-team-token-test-%s", randomString(t))
tt, err := client.TeamTokens.CreateWithOptions(ctx, tmTest.ID, TeamTokenCreateOptions{
Description: &desc1,
})
require.NoError(t, err)
require.NotEmpty(t, tt.Token)
require.NotNil(t, tt.Description)
require.Equal(t, *tt.Description, desc1)
desc2 := fmt.Sprintf("go-tfe-team-token-test-%s", randomString(t))
tt, err = client.TeamTokens.CreateWithOptions(ctx, tmTest.ID, TeamTokenCreateOptions{
Description: &desc2,
})
require.NoError(t, err)
require.NotEmpty(t, tt.Token)
require.NotNil(t, tt.Description)
require.Equal(t, *tt.Description, desc2)
emptyString := ""
tt, err = client.TeamTokens.CreateWithOptions(ctx, tmTest.ID, TeamTokenCreateOptions{
Description: &emptyString,
})
require.NoError(t, err)
require.NotEmpty(t, tt.Token)
require.NotNil(t, tt.Description)
require.Equal(t, *tt.Description, emptyString)
tt, err = client.TeamTokens.CreateWithOptions(ctx, tmTest.ID, TeamTokenCreateOptions{})
require.NoError(t, err)
require.NotEmpty(t, tt.Token)
require.Nil(t, tt.Description)
})
t.Run("with an expiration date", func(t *testing.T) {
desc := fmt.Sprintf("go-tfe-team-token-test-%s", randomString(t))
currentTime := time.Now().UTC().Truncate(time.Second)
oneDayLater := currentTime.Add(24 * time.Hour)
tt, err := client.TeamTokens.CreateWithOptions(ctx, tmTest.ID, TeamTokenCreateOptions{
Description: &desc,
ExpiredAt: &oneDayLater,
})
require.NoError(t, err)
require.NotEmpty(t, tt.Token)
assert.Equal(t, tt.ExpiredAt, oneDayLater)
require.NotNil(t, tt.Description)
require.Equal(t, *tt.Description, desc)
})
t.Run("without an expiration date", func(t *testing.T) {
desc := fmt.Sprintf("go-tfe-team-token-test-%s", randomString(t))
tt, err := client.TeamTokens.CreateWithOptions(ctx, tmTest.ID, TeamTokenCreateOptions{
Description: &desc,
})
require.NoError(t, err)
require.NotEmpty(t, tt.Token)
assert.NotZero(t, tt.ExpiredAt)
expectedExpiry := tt.CreatedAt.AddDate(defaultTokenExpirationYears, 0, 0)
// Allow a small buffer (1 minute) for timestamp precision differences.
assert.WithinDuration(t, expectedExpiry, tt.ExpiredAt, time.Minute)
require.NotNil(t, tt.Description)
require.Equal(t, *tt.Description, desc)
})
t.Run("when a token already exists with the same description", func(t *testing.T) {
desc := fmt.Sprintf("go-tfe-team-token-test-%s", randomString(t))
tt, err := client.TeamTokens.CreateWithOptions(ctx, tmTest.ID, TeamTokenCreateOptions{
Description: &desc,
})
require.NoError(t, err)
require.NotEmpty(t, tt.Token)
require.NotNil(t, tt.Description)
require.Equal(t, *tt.Description, desc)
tt, err = client.TeamTokens.CreateWithOptions(ctx, tmTest.ID, TeamTokenCreateOptions{
Description: &desc,
})
assert.Nil(t, tt)
assert.Equal(t, err, ErrInvalidDescriptionConflict)
})
t.Run("without valid team ID", func(t *testing.T) {
tt, err := client.TeamTokens.CreateWithOptions(ctx, badIdentifier, TeamTokenCreateOptions{})
assert.Nil(t, tt)
assert.Equal(t, err, ErrInvalidTeamID)
})
}
func TestTeamTokensRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
tmTest, tmTestCleanup := createTeam(t, client, nil)
defer tmTestCleanup()
t.Run("with valid options", func(t *testing.T) {
_, ttTestCleanup := createTeamToken(t, client, tmTest)
tt, err := client.TeamTokens.Read(ctx, tmTest.ID)
require.NoError(t, err)
assert.NotEmpty(t, tt)
require.NotEmpty(t, tt.Team)
assert.Equal(t, tt.Team.ID, tmTest.ID)
ttTestCleanup()
})
t.Run("with an expiration date passed as a valid option", func(t *testing.T) {
currentTime := time.Now().UTC().Truncate(time.Second)
oneDayLater := currentTime.Add(24 * time.Hour)
_, ttTestCleanup := createTeamTokenWithOptions(t, client, tmTest, TeamTokenCreateOptions{ExpiredAt: &oneDayLater})
tt, err := client.TeamTokens.Read(ctx, tmTest.ID)
require.NoError(t, err)
require.NotEmpty(t, tt)
assert.Equal(t, tt.ExpiredAt, oneDayLater)
require.NotEmpty(t, tt.Team)
assert.Equal(t, tt.Team.ID, tmTest.ID)
ttTestCleanup()
})
t.Run("when a token doesn't exists", func(t *testing.T) {
tt, err := client.TeamTokens.Read(ctx, tmTest.ID)
assert.Equal(t, ErrResourceNotFound, err)
assert.Nil(t, tt)
})
t.Run("without valid organization", func(t *testing.T) {
tt, err := client.OrganizationTokens.Read(ctx, badIdentifier)
assert.Nil(t, tt)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
func TestTeamTokensReadByID(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
tmTest, tmTestCleanup := createTeam(t, client, nil)
t.Cleanup(tmTestCleanup)
currentTime := time.Now().UTC().Truncate(time.Second)
oneDayLater := currentTime.Add(24 * time.Hour)
t.Run("with legacy, descriptionless tokens", func(t *testing.T) {
token, ttTestCleanup := createTeamTokenWithOptions(t, client, tmTest, TeamTokenCreateOptions{
ExpiredAt: &oneDayLater,
})
t.Cleanup(ttTestCleanup)
tt, err := client.TeamTokens.ReadByID(ctx, token.ID)
require.NoError(t, err)
require.NotEmpty(t, tt)
assert.Nil(t, tt.Description)
assert.Equal(t, tt.ExpiredAt, oneDayLater)
require.NotEmpty(t, tt.Team)
assert.Equal(t, tt.Team.ID, tmTest.ID)
})
t.Run("with multiple team tokens", func(t *testing.T) {
skipUnlessBeta(t)
desc1 := fmt.Sprintf("go-tfe-team-token-test-%s", randomString(t))
token, ttTestCleanup := createTeamTokenWithOptions(t, client, tmTest, TeamTokenCreateOptions{
Description: &desc1,
})
t.Cleanup(ttTestCleanup)
tt, err := client.TeamTokens.ReadByID(ctx, token.ID)
require.NoError(t, err)
require.NotEmpty(t, tt)
require.NotNil(t, tt.Description)
assert.Equal(t, *tt.Description, desc1)
assert.NotZero(t, tt.ExpiredAt)
expectedExpiry := tt.CreatedAt.AddDate(defaultTokenExpirationYears, 0, 0)
// Allow a small buffer (1 minute) for timestamp precision differences.
assert.WithinDuration(t, expectedExpiry, tt.ExpiredAt, time.Minute)
require.NotEmpty(t, tt.Team)
assert.Equal(t, tt.Team.ID, tmTest.ID)
desc2 := fmt.Sprintf("go-tfe-team-token-test-%s", randomString(t))
tokenWithExpiration, ttTestCleanup2 := createTeamTokenWithOptions(t, client, tmTest, TeamTokenCreateOptions{
ExpiredAt: &oneDayLater,
Description: &desc2,
})
t.Cleanup(ttTestCleanup2)
tt2, err := client.TeamTokens.ReadByID(ctx, tokenWithExpiration.ID)
require.NoError(t, err)
require.NotEmpty(t, tt2)
require.NotNil(t, tt2.Description)
assert.Equal(t, *tt2.Description, desc2)
assert.Equal(t, tt2.ExpiredAt, oneDayLater)
require.NotEmpty(t, tt.Team)
assert.Equal(t, tt.Team.ID, tmTest.ID)
})
t.Run("when a token doesn't exists", func(t *testing.T) {
tt, err := client.TeamTokens.ReadByID(ctx, "nonexistent-token-id")
assert.Equal(t, ErrResourceNotFound, err)
assert.Nil(t, tt)
})
}
func TestTeamTokensList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
org, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
// Create a team with a token
team1, tmTestCleanup1 := createTeam(t, client, org)
t.Cleanup(tmTestCleanup1)
currentTime := time.Now().UTC().Truncate(time.Second)
oneDayLater := currentTime.Add(24 * time.Hour)
token1, ttTestCleanup := createTeamTokenWithOptions(t, client, team1, TeamTokenCreateOptions{
ExpiredAt: &oneDayLater,
})
t.Cleanup(ttTestCleanup)
// Create a second team with a token that has a later expiration date
team2, tmTestCleanup2 := createTeam(t, client, org)
t.Cleanup(tmTestCleanup2)
twoDaysLater := currentTime.Add(48 * time.Hour)
token2, ttTestCleanup := createTeamTokenWithOptions(t, client, team2, TeamTokenCreateOptions{
ExpiredAt: &twoDaysLater,
})
t.Cleanup(ttTestCleanup)
t.Run("with team tokens across multiple teams", func(t *testing.T) {
tokens, err := client.TeamTokens.List(ctx, org.Name, nil)
require.NoError(t, err)
require.NotNil(t, tokens)
require.Len(t, tokens.Items, 2)
require.ElementsMatch(t, []string{token1.ID, token2.ID}, []string{tokens.Items[0].ID, tokens.Items[1].ID})
})
t.Run("with filtering by team name", func(t *testing.T) {
tokens, err := client.TeamTokens.List(ctx, org.Name, &TeamTokenListOptions{
Query: team1.Name,
})
require.NoError(t, err)
require.NotNil(t, tokens)
require.Len(t, tokens.Items, 1)
require.Equal(t, token1.ID, tokens.Items[0].ID)
})
t.Run("with sorting", func(t *testing.T) {
tokens, err := client.TeamTokens.List(ctx, org.Name, &TeamTokenListOptions{
Sort: "expired-at",
})
require.NoError(t, err)
require.NotNil(t, tokens)
require.Len(t, tokens.Items, 2)
require.Equal(t, []string{token1.ID, token2.ID}, []string{tokens.Items[0].ID, tokens.Items[1].ID})
tokens, err = client.TeamTokens.List(ctx, org.Name, &TeamTokenListOptions{
Sort: "-expired-at",
})
require.NoError(t, err)
require.NotNil(t, tokens)
require.Len(t, tokens.Items, 2)
require.Equal(t, []string{token2.ID, token1.ID}, []string{tokens.Items[0].ID, tokens.Items[1].ID})
})
t.Run("with multiple team tokens in a single team", func(t *testing.T) {
skipUnlessBeta(t)
desc1 := fmt.Sprintf("go-tfe-team-token-test-%s", randomString(t))
multiToken1, ttTestCleanup := createTeamTokenWithOptions(t, client, team1, TeamTokenCreateOptions{
Description: &desc1,
})
t.Cleanup(ttTestCleanup)
desc2 := fmt.Sprintf("go-tfe-team-token-test-%s", randomString(t))
multiToken2, ttTestCleanup := createTeamTokenWithOptions(t, client, team1, TeamTokenCreateOptions{
Description: &desc2,
})
t.Cleanup(ttTestCleanup)
tokens, err := client.TeamTokens.List(ctx, org.Name, nil)
require.NoError(t, err)
require.NotNil(t, tokens)
require.Len(t, tokens.Items, 4)
actualIDs := []string{}
for _, token := range tokens.Items {
actualIDs = append(actualIDs, token.ID)
}
require.ElementsMatch(t, []string{token1.ID, token2.ID, multiToken1.ID, multiToken2.ID},
actualIDs)
})
}
func TestTeamTokensDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
tmTest, tmTestCleanup := createTeam(t, client, nil)
defer tmTestCleanup()
createTeamToken(t, client, tmTest)
t.Run("with valid options", func(t *testing.T) {
err := client.TeamTokens.Delete(ctx, tmTest.ID)
require.NoError(t, err)
})
t.Run("when a token does not exist", func(t *testing.T) {
err := client.TeamTokens.Delete(ctx, tmTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("without valid team ID", func(t *testing.T) {
err := client.TeamTokens.Delete(ctx, badIdentifier)
assert.Equal(t, err, ErrInvalidTeamID)
})
}
func TestTeamTokensDeleteByID(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
tmTest, tmTestCleanup := createTeam(t, client, nil)
t.Cleanup(tmTestCleanup)
t.Run("with legacy, descriptionless tokens", func(t *testing.T) {
token, _ := createTeamToken(t, client, tmTest)
err := client.TeamTokens.DeleteByID(ctx, token.ID)
require.NoError(t, err)
})
t.Run("with multiple team tokens", func(t *testing.T) {
skipUnlessBeta(t)
desc1 := fmt.Sprintf("go-tfe-team-token-test-%s", randomString(t))
token1, _ := createTeamTokenWithOptions(t, client, tmTest, TeamTokenCreateOptions{
Description: &desc1,
})
desc2 := fmt.Sprintf("go-tfe-team-token-test-%s", randomString(t))
token2, _ := createTeamTokenWithOptions(t, client, tmTest, TeamTokenCreateOptions{
Description: &desc2,
})
err := client.TeamTokens.DeleteByID(ctx, token1.ID)
require.NoError(t, err)
err = client.TeamTokens.DeleteByID(ctx, token2.ID)
require.NoError(t, err)
})
t.Run("when a token does not exist", func(t *testing.T) {
err := client.TeamTokens.DeleteByID(ctx, "nonexistent-token-id")
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("with invalid token ID", func(t *testing.T) {
err := client.TeamTokens.DeleteByID(ctx, badIdentifier)
assert.Equal(t, err, ErrInvalidTokenID)
})
}
================================================
FILE: test-fixtures/archive-dir/bar.txt
================================================
bar
================================================
FILE: test-fixtures/archive-dir/exe
================================================
================================================
FILE: test-fixtures/archive-dir/foo.txt
================================================
foo
================================================
FILE: test-fixtures/archive-dir/sub/zip.txt
================================================
zip
================================================
FILE: test-fixtures/config-version/main.tf
================================================
# Copyright IBM Corp. 2018, 2025
# SPDX-License-Identifier: MPL-2.0
resource "null_resource" "foo" {}
================================================
FILE: test-fixtures/config-version-with-test/main.tf
================================================
# Copyright IBM Corp. 2018, 2025
# SPDX-License-Identifier: MPL-2.0
variable "wait_time" {
type = string
default = "0s"
}
resource "null_resource" "foo" {}
resource "time_sleep" "wait_5_seconds" {
depends_on = [null_resource.foo]
create_duration = var.wait_time
}
resource "null_resource" "bar" {
depends_on = [time_sleep.wait_5_seconds]
}
================================================
FILE: test-fixtures/config-version-with-test/main.tftest.hcl
================================================
# Copyright IBM Corp. 2018, 2025
# SPDX-License-Identifier: MPL-2.0
run "test" {}
================================================
FILE: test-fixtures/json-state/state.json
================================================
{
"format_version": "1.0",
"terraform_version": "1.2.0",
"values": {
"outputs": {
"a-decimal": {
"sensitive": false,
"value": 1000.1,
"type": "number"
},
"a-false-bool": {
"sensitive": false,
"value": false,
"type": "bool"
},
"a-list": {
"sensitive": false,
"value": [
"example",
"1001",
"1000.1"
],
"type": [
"list",
"string"
]
},
"a-long-string": {
"sensitive": false,
"value": "The private integer of the main server instance is where you want to go when you have the most fun in every Terraform instance you can see in the world that you live in except for dogs because they don't run servers in the same place that humans do.",
"type": "string"
},
"a-object": {
"sensitive": false,
"value": {
"bar": 1000.1,
"example": 1001
},
"type": [
"object",
{
"bar": "number",
"example": "number"
}
]
},
"a-sensitive-value": {
"sensitive": true,
"value": "hopefully you cannot see me",
"type": "string"
},
"a-string": {
"sensitive": false,
"value": "example string",
"type": "string"
},
"a-true-bool": {
"sensitive": false,
"value": true,
"type": "bool"
},
"a-tuple": {
"sensitive": false,
"value": [
1,
"example"
],
"type": [
"tuple",
[
"number",
"string"
]
]
},
"an-int": {
"sensitive": false,
"value": 1001,
"type": "number"
},
"escapes": {
"sensitive": false,
"value": "line 1\nline 2\n\\\\\\\\\n",
"type": "string"
},
"myoutput": {
"sensitive": false,
"value": {
"nesting1": {
"nesting2": {
"nesting3": "4263891374290101092"
}
}
},
"type": [
"object",
{
"nesting1": [
"object",
{
"nesting2": [
"object",
{
"nesting3": "string"
}
]
}
]
}
]
},
"random": {
"sensitive": false,
"value": "8b3086889a9ef7a5",
"type": "string"
}
},
"root_module": {
"resources": [
{
"address": "null_resource.test",
"mode": "managed",
"type": "null_resource",
"name": "test",
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "4263891374290101092",
"triggers": {
"hello": "wat3"
}
},
"sensitive_values": {
"triggers": {}
}
},
{
"address": "random_id.random",
"mode": "managed",
"type": "random_id",
"name": "random",
"provider_name": "registry.terraform.io/hashicorp/random",
"schema_version": 0,
"values": {
"b64_std": "izCGiJqe96U=",
"b64_url": "izCGiJqe96U",
"byte_length": 8,
"dec": "10029664291421878181",
"hex": "8b3086889a9ef7a5",
"id": "izCGiJqe96U",
"keepers": {
"uuid": "437a1415-932b-9f74-c214-184d88215353"
},
"prefix": null
},
"sensitive_values": {
"keepers": {}
}
}
]
}
}
}
================================================
FILE: test-fixtures/json-state-outputs/everything.json
================================================
{
"a-decimal": {
"sensitive": false,
"value": 1000.1,
"type": "number"
},
"a-false-bool": {
"sensitive": false,
"value": false,
"type": "bool"
},
"a-list": {
"sensitive": false,
"value": [
"example",
"1001",
"1000.1"
],
"type": [
"list",
"string"
]
},
"a-long-string": {
"sensitive": false,
"value": "The private integer of the main server instance is where you want to go when you have the most fun in every Terraform instance you can see in the world that you live in except for dogs because they don't run servers in the same place that humans do.",
"type": "string"
},
"a-object": {
"sensitive": false,
"value": {
"bar": 1000.1,
"example": 1001
},
"type": [
"object",
{
"bar": "number",
"example": "number"
}
]
},
"a-sensitive-value": {
"sensitive": true,
"value": "hopefully you cannot see me",
"type": "string"
},
"a-string": {
"sensitive": false,
"value": "example string",
"type": "string"
},
"a-true-bool": {
"sensitive": false,
"value": true,
"type": "bool"
},
"a-tuple": {
"sensitive": false,
"value": [
1,
"example"
],
"type": [
"tuple",
[
"number",
"string"
]
]
},
"an-int": {
"sensitive": false,
"value": 1001,
"type": "number"
},
"escapes": {
"sensitive": false,
"value": "line 1\nline 2\n\\\\\\\\\n",
"type": "string"
},
"myoutput": {
"sensitive": false,
"value": {
"nesting1": {
"nesting2": {
"nesting3": "4263891374290101092"
}
}
},
"type": [
"object",
{
"nesting1": [
"object",
{
"nesting2": [
"object",
{
"nesting3": "string"
}
]
}
]
}
]
},
"random": {
"sensitive": false,
"value": "8b3086889a9ef7a5",
"type": "string"
}
}
================================================
FILE: test-fixtures/policy-set-version/enforce-mandatory-tags.sentinel
================================================
# This policy is a sample policy that has a list of tags and
# has a rule to confirm the length of the tags.
# # Copyright IBM Corp. 2018, 2025
# SPDX-License-Identifier: MPL-2.0
# List of environment tags
tags = [
"Production",
"Staging",
]
# Main rule
main = rule {
length(tags) is 2
}
================================================
FILE: test-fixtures/policy-set-version/sentinel.hcl
================================================
# Copyright IBM Corp. 2018, 2025
# SPDX-License-Identifier: MPL-2.0
policy "enforce-mandatory-tags" {
source = "./enforce-mandatory-tags.sentinel"
enforcement_level = "hard-mandatory"
}
================================================
FILE: test-fixtures/stack-source/.terraform-version
================================================
1.10.0-alpha20240807
================================================
FILE: test-fixtures/stack-source/components.tfstack.hcl
================================================
# Copyright IBM Corp. 2018, 2025
# SPDX-License-Identifier: MPL-2.0
variable "prefix" {
type = string
}
variable "instances" {
type = number
}
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.5.1"
}
null = {
source = "hashicorp/null"
version = "~> 3.2.2"
}
}
provider "random" "this" {}
provider "null" "this" {}
component "pet" {
source = "./pet"
inputs = {
prefix = var.prefix
}
providers = {
random = provider.random.this
}
}
component "nulls" {
source = "./nulls"
inputs = {
pet = component.pet.name
instances = var.instances
}
providers = {
null = provider.null.this
}
}
================================================
FILE: test-fixtures/stack-source/deployments.tfdeploy.hcl
================================================
# Copyright IBM Corp. 2018, 2025
# SPDX-License-Identifier: MPL-2.0
deployment "simple" {
inputs = {
prefix = "simple"
instances = 1
}
}
deployment "complex" {
inputs = {
prefix = "complex"
instances = 3
}
}
================================================
FILE: test-fixtures/stack-source/nulls/main.tf
================================================
# Copyright IBM Corp. 2018, 2025
# SPDX-License-Identifier: MPL-2.0
terraform {
required_providers {
null = {
source = "hashicorp/null"
version = "3.1.1"
}
}
}
variable "pet" {
type = string
}
variable "instances" {
type = number
}
resource "null_resource" "this" {
count = var.instances
triggers = {
pet = var.pet
}
}
output "ids" {
value = [for n in null_resource.this: n.id]
}
================================================
FILE: test-fixtures/stack-source/pet/main.tf
================================================
# Copyright IBM Corp. 2018, 2025
# SPDX-License-Identifier: MPL-2.0
terraform {
required_providers {
random = {
source = "hashicorp/random"
version = "3.3.2"
}
}
}
variable "prefix" {
type = string
}
resource "random_pet" "this" {
prefix = var.prefix
length = 3
}
output "name" {
value = random_pet.this.id
}
================================================
FILE: test-fixtures/state-version/terraform.tfstate
================================================
{
"version": 4,
"terraform_version": "1.3.6",
"serial": 5,
"lineage": "8094ef40-1dbd-95cd-1f60-bb25d84d883b",
"outputs": {
"test_output_list_string": {
"value": [
"us-west-1a"
],
"type": [
"list",
"string"
]
},
"test_output_string": {
"value": "9023256633839603543",
"type": "string",
"sensitive": true
},
"test_output_tuple_number": {
"value": [
1,
2
],
"type": [
"tuple",
[
"number",
"number"
]
]
},
"test_output_tuple_string": {
"value": [
"one",
"two"
],
"type": [
"tuple",
[
"string",
"string"
]
]
},
"test_output_object": {
"value": {
"foo": "bar"
},
"type": [
"object",
{
"foo": "string"
}
]
},
"test_output_number": {
"value": 5,
"type": "number"
},
"test_output_bool": {
"value": true,
"type": "bool"
}
},
"resources": [
{
"module": "module.media_bucket",
"mode": "managed",
"type": "aws_s3_bucket_public_access_block",
"name": "this",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"index_key": 0,
"schema_version": 0,
"attributes": {
"block_public_acls": true,
"block_public_policy": true,
"bucket": "1234-edited-videos",
"id": "1234-edited-videos",
"ignore_public_acls": true,
"restrict_public_buckets": true
},
"sensitive_attributes": [],
"private": "XXXX=="
}
]
}
],
"check_results": null
}
================================================
FILE: test_config.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
type TestConfig struct {
TestsEnabled bool `jsonapi:"attr,tests-enabled"`
AgentExecutionMode *string `jsonapi:"attr,agent-execution-mode,omitempty"`
AgentPoolID *string `jsonapi:"attr,agent-pool-id,omitempty"`
}
================================================
FILE: test_run.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"io"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ TestRuns = (*testRuns)(nil)
// TestRuns describes all the test run related methods that the Terraform
// Enterprise API supports.
//
// **Note: These methods are still in BETA and subject to change.**
type TestRuns interface {
// List all the test runs for a given private registry module.
List(ctx context.Context, moduleID RegistryModuleID, options *TestRunListOptions) (*TestRunList, error)
// Read a test run by its ID.
Read(ctx context.Context, moduleID RegistryModuleID, testRunID string) (*TestRun, error)
// Create a new test run with the given options.
Create(ctx context.Context, options TestRunCreateOptions) (*TestRun, error)
// Logs retrieves the logs for a test run by its ID.
Logs(ctx context.Context, moduleID RegistryModuleID, testRunID string) (io.Reader, error)
// Cancel a test run by its ID.
Cancel(ctx context.Context, moduleID RegistryModuleID, testRunID string) error
// ForceCancel a test run by its ID.
ForceCancel(ctx context.Context, moduleID RegistryModuleID, testRunID string) error
}
// testRuns implements TestRuns.
type testRuns struct {
client *Client
}
// TestRunStatus represents the status of a test run.
type TestRunStatus string
// List all available test run statuses.
const (
TestRunPending TestRunStatus = "pending"
TestRunQueued TestRunStatus = "queued"
TestRunRunning TestRunStatus = "running"
TestRunErrored TestRunStatus = "errored"
TestRunCanceled TestRunStatus = "canceled"
TestRunFinished TestRunStatus = "finished"
)
// TestStatus represents the status of an individual test within an overall test
// run.
type TestStatus string
// List all available test statuses.
const (
TestPending TestStatus = "pending"
TestSkip TestStatus = "skip"
TestPass TestStatus = "pass"
TestFail TestStatus = "fail"
TestError TestStatus = "error"
)
// TestRun represents a Terraform Enterprise test run.
type TestRun struct {
ID string `jsonapi:"primary,test-runs"`
Status TestRunStatus `jsonapi:"attr,status"`
StatusTimestamps TestRunStatusTimestamps `jsonapi:"attr,status-timestamps"`
TestStatus TestStatus `jsonapi:"attr,test-status"`
TestsPassed int `jsonapi:"attr,tests-passed"`
TestsFailed int `jsonapi:"attr,tests-failed"`
TestsErrored int `jsonapi:"attr,tests-errored"`
TestsSkipped int `jsonapi:"attr,tests-skipped"`
LogReadURL string `jsonapi:"attr,log-read-url"`
// Relations
ConfigurationVersion *ConfigurationVersion `jsonapi:"relation,configuration-version"`
RegistryModule *RegistryModule `jsonapi:"relation,registry-module"`
}
// TestRunStatusTimestamps holds the timestamps for individual test run
// statuses.
type TestRunStatusTimestamps struct {
CanceledAt time.Time `jsonapi:"attr,canceled-at,rfc3339"`
ErroredAt time.Time `jsonapi:"attr,errored-at,rfc3339"`
FinishedAt time.Time `jsonapi:"attr,finished-at,rfc3339"`
ForceCanceledAt time.Time `jsonapi:"attr,force-canceled-at,rfc3339"`
QueuedAt time.Time `jsonapi:"attr,queued-at,rfc3339"`
StartedAt time.Time `jsonapi:"attr,started-at,rfc3339"`
}
// TestRunCreateOptions represents the options for creating a run.
type TestRunCreateOptions struct {
// Type is a public field utitilized by JSON:API to set the resource type
// via the field tag. It is not a user-defined value and does not need to
// be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,test-runs"`
// If non-empty, requests that only a subset of testing files within the
// ConfigurationVersion should be executed.
Filters []string `jsonapi:"attr,filters,omitempty"`
// Specifies the directory within the ConfigurationVersion that test files
// should be loaded from. Defaults to "tests" if empty.
TestDirectory *string `jsonapi:"attr,test-directory,omitempty"`
// Verbose prints out the plan and state files for each run block that is
// executed by this TestRun.
Verbose *bool `jsonapi:"attr,verbose,omitempty"`
// Parallelism controls the number of parallel operations to execute within a single test run.
Parallelism *int `jsonapi:"attr,parallelism,omitempty"`
// Variables allows you to specify terraform input variables for
// a particular run, prioritized over variables defined on the workspace.
Variables []*RunVariable `jsonapi:"attr,variables,omitempty"`
// ConfigurationVersion specifies the configuration version to use for this
// test run.
ConfigurationVersion *ConfigurationVersion `jsonapi:"relation,configuration-version"`
// RegistryModule specifies the registry module this test run should be
// assigned to.
RegistryModule *RegistryModule `jsonapi:"relation,registry-module"`
}
// TestRunList represents a list of test runs.
type TestRunList struct {
*Pagination
Items []*TestRun
}
// TestRunListOptions represents the options for listing runs.
type TestRunListOptions struct {
ListOptions
}
// List all the test runs for a given private registry module.
func (s *testRuns) List(ctx context.Context, moduleID RegistryModuleID, options *TestRunListOptions) (*TestRunList, error) {
if err := moduleID.valid(); err != nil {
return nil, err
}
req, err := s.client.NewRequest("GET", testRunsPath(moduleID), options)
if err != nil {
return nil, err
}
trl := &TestRunList{}
err = req.Do(ctx, trl)
if err != nil {
return nil, err
}
return trl, nil
}
// Read a test run by its ID.
func (s *testRuns) Read(ctx context.Context, moduleID RegistryModuleID, testRunID string) (*TestRun, error) {
if err := moduleID.valid(); err != nil {
return nil, err
}
if !validStringID(&testRunID) {
return nil, ErrInvalidTestRunID
}
u := fmt.Sprintf("%s/%s", testRunsPath(moduleID), url.PathEscape(testRunID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
tr := &TestRun{}
err = req.Do(ctx, tr)
if err != nil {
return nil, err
}
return tr, nil
}
// Create a new test run with the given options.
func (s *testRuns) Create(ctx context.Context, options TestRunCreateOptions) (*TestRun, error) {
if err := options.valid(); err != nil {
return nil, err
}
moduleID := RegistryModuleID{
Organization: options.RegistryModule.Organization.Name,
Name: options.RegistryModule.Name,
Provider: options.RegistryModule.Provider,
Namespace: options.RegistryModule.Namespace,
RegistryName: options.RegistryModule.RegistryName,
}
if err := moduleID.valid(); err != nil {
return nil, err
}
if err := options.valid(); err != nil {
return nil, err
}
req, err := s.client.NewRequest("POST", testRunsPath(moduleID), &options)
if err != nil {
return nil, err
}
tr := &TestRun{}
err = req.Do(ctx, tr)
if err != nil {
return nil, err
}
return tr, nil
}
// Logs retrieves the logs for a test run by its ID.
func (s *testRuns) Logs(ctx context.Context, moduleID RegistryModuleID, testRunID string) (io.Reader, error) {
if err := moduleID.valid(); err != nil {
return nil, err
}
if !validStringID(&testRunID) {
return nil, ErrInvalidTestRunID
}
tr, err := s.Read(ctx, moduleID, testRunID)
if err != nil {
return nil, err
}
if tr.LogReadURL == "" {
return nil, fmt.Errorf("test run %s does not have a log URL", testRunID)
}
u, err := url.Parse(tr.LogReadURL)
if err != nil {
return nil, fmt.Errorf("invalid log URL: %w", err)
}
done := func() (bool, error) {
tr, err := s.Read(ctx, moduleID, testRunID)
if err != nil {
return false, err
}
switch tr.Status {
case TestRunErrored, TestRunCanceled, TestRunFinished:
return true, nil
default:
return false, nil
}
}
return &LogReader{
client: s.client,
ctx: ctx,
done: done,
logURL: u,
}, nil
}
// Cancel a test run by its ID.
func (s *testRuns) Cancel(ctx context.Context, moduleID RegistryModuleID, testRunID string) error {
if err := moduleID.valid(); err != nil {
return err
}
if !validStringID(&testRunID) {
return ErrInvalidTestRunID
}
u := fmt.Sprintf("%s/%s/cancel", testRunsPath(moduleID), url.PathEscape(testRunID))
req, err := s.client.NewRequest("POST", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// ForceCancel a test run by its ID.
func (s *testRuns) ForceCancel(ctx context.Context, moduleID RegistryModuleID, testRunID string) error {
if err := moduleID.valid(); err != nil {
return err
}
if !validStringID(&testRunID) {
return ErrInvalidTestRunID
}
u := fmt.Sprintf("%s/%s/force-cancel", testRunsPath(moduleID), url.PathEscape(testRunID))
req, err := s.client.NewRequest("POST", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o TestRunCreateOptions) valid() error {
if o.ConfigurationVersion == nil {
return ErrInvalidConfigVersionID
}
if o.RegistryModule == nil {
return ErrRequiredRegistryModule
}
if o.RegistryModule.Organization == nil {
return ErrRequiredOrg
}
return nil
}
func testRunsPath(moduleID RegistryModuleID) string {
return fmt.Sprintf("organizations/%s/tests/registry-modules/%s/%s/%s/%s/test-runs",
url.PathEscape(moduleID.Organization),
url.PathEscape(string(moduleID.RegistryName)),
url.PathEscape(moduleID.Namespace),
url.PathEscape(moduleID.Name),
url.PathEscape(moduleID.Provider))
}
================================================
FILE: test_run_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTestRunsList_RunDependent(t *testing.T) {
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
rmTest, registryModuleTestCleanup := createBranchBasedRegistryModuleWithTests(t, client, orgTest)
defer registryModuleTestCleanup()
id := RegistryModuleID{
Organization: orgTest.Name,
Name: rmTest.Name,
Provider: rmTest.Provider,
Namespace: rmTest.Namespace,
RegistryName: rmTest.RegistryName,
}
trTest1, trTestCleanup1 := createTestRun(t, client, rmTest)
trTest2, trTestCleanup2 := createTestRun(t, client, rmTest)
defer trTestCleanup1()
defer trTestCleanup2()
t.Run("without list options", func(t *testing.T) {
trl, err := client.TestRuns.List(ctx, id, nil)
var found []string
for _, r := range trl.Items {
found = append(found, r.ID)
}
require.NoError(t, err)
assert.Contains(t, found, trTest1.ID)
assert.Contains(t, found, trTest2.ID)
assert.Equal(t, 1, trl.CurrentPage)
assert.Equal(t, 2, trl.TotalCount)
})
t.Run("empty list options", func(t *testing.T) {
trl, err := client.TestRuns.List(ctx, id, &TestRunListOptions{})
var found []string
for _, r := range trl.Items {
found = append(found, r.ID)
}
require.NoError(t, err)
assert.Contains(t, found, trTest1.ID)
assert.Contains(t, found, trTest2.ID)
assert.Equal(t, 1, trl.CurrentPage)
assert.Equal(t, 2, trl.TotalCount)
})
t.Run("with page size", func(t *testing.T) {
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
trl, err := client.TestRuns.List(ctx, id, &TestRunListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, trl.Items)
assert.Equal(t, 999, trl.CurrentPage)
assert.Equal(t, 2, trl.TotalCount)
})
}
func TestTestRunsRead_RunDependent(t *testing.T) {
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
rmTest, registryModuleTestCleanup := createBranchBasedRegistryModuleWithTests(t, client, orgTest)
defer registryModuleTestCleanup()
id := RegistryModuleID{
Organization: orgTest.Name,
Name: rmTest.Name,
Provider: rmTest.Provider,
Namespace: rmTest.Namespace,
RegistryName: rmTest.RegistryName,
}
trTest, trTestCleanup := createTestRun(t, client, rmTest)
defer trTestCleanup()
t.Run("when the test run exists", func(t *testing.T) {
tr, err := client.TestRuns.Read(ctx, id, trTest.ID)
require.NoError(t, err)
require.Equal(t, trTest.ID, tr.ID)
})
t.Run("when the test run does not exist", func(t *testing.T) {
_, err := client.TestRuns.Read(ctx, id, "trun-NoTaReAlId")
require.Error(t, err)
require.Equal(t, ErrResourceNotFound, err)
})
}
func TestTestRunsCreate(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
rmTest, rmTestCleanup := createBranchBasedRegistryModuleWithTests(t, client, orgTest)
defer rmTestCleanup()
cvTest, cvTestCleanup := createUploadedTestRunConfigurationVersion(t, client, rmTest)
defer cvTestCleanup()
t.Run("with a configuration version", func(t *testing.T) {
options := TestRunCreateOptions{
ConfigurationVersion: cvTest,
RegistryModule: rmTest,
}
_, err := client.TestRuns.Create(ctx, options)
require.NoError(t, err)
})
p := 10
t.Run("with a custom parallelism version", func(t *testing.T) {
options := TestRunCreateOptions{
ConfigurationVersion: cvTest,
RegistryModule: rmTest,
Parallelism: &p,
}
_, err := client.TestRuns.Create(ctx, options)
require.NoError(t, err)
})
t.Run("without a configuration version", func(t *testing.T) {
options := TestRunCreateOptions{
RegistryModule: rmTest,
}
_, err := client.TestRuns.Create(ctx, options)
require.Equal(t, ErrInvalidConfigVersionID, err)
})
t.Run("without a module", func(t *testing.T) {
options := TestRunCreateOptions{
ConfigurationVersion: cvTest,
}
_, err := client.TestRuns.Create(ctx, options)
require.Equal(t, ErrRequiredRegistryModule, err)
})
t.Run("without an organization", func(t *testing.T) {
rm := &RegistryModule{
ID: rmTest.ID,
Name: rmTest.Name,
Provider: rmTest.Provider,
RegistryName: rmTest.RegistryName,
Namespace: rmTest.Namespace,
NoCode: rmTest.NoCode,
Permissions: rmTest.Permissions,
Status: rmTest.Status,
VCSRepo: rmTest.VCSRepo,
VersionStatuses: rmTest.VersionStatuses,
CreatedAt: rmTest.CreatedAt,
UpdatedAt: rmTest.UpdatedAt,
Organization: nil, // Leave this as nil.
}
options := TestRunCreateOptions{
ConfigurationVersion: cvTest,
RegistryModule: rm,
}
_, err := client.TestRuns.Create(ctx, options)
require.Equal(t, ErrRequiredOrg, err)
})
}
func TestTestRunsLogs_RunDependent(t *testing.T) {
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
rmTest, rmTestCleanup := createBranchBasedRegistryModuleWithTests(t, client, orgTest)
defer rmTestCleanup()
id := RegistryModuleID{
Organization: orgTest.Name,
Name: rmTest.Name,
Provider: rmTest.Provider,
Namespace: rmTest.Namespace,
RegistryName: rmTest.RegistryName,
}
tr, trCleanup := createTestRun(t, client, rmTest)
defer trCleanup()
t.Run("when the log exists", func(t *testing.T) {
waitUntilTestRunStatus(t, client, id, tr, TestRunFinished, 15)
logReader, err := client.TestRuns.Logs(ctx, id, tr.ID)
require.NoError(t, err)
logs, err := io.ReadAll(logReader)
require.NoError(t, err)
assert.Contains(t, string(logs), "Success!")
})
t.Run("when the log does not exist", func(t *testing.T) {
logs, err := client.TestRuns.Logs(ctx, id, "notreal")
assert.Nil(t, logs)
assert.Error(t, err)
})
}
func TestTestRunsCancel_RunDependent(t *testing.T) {
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
rmTest, rmTestCleanup := createBranchBasedRegistryModuleWithTests(t, client, orgTest)
defer rmTestCleanup()
id := RegistryModuleID{
Organization: orgTest.Name,
Name: rmTest.Name,
Provider: rmTest.Provider,
Namespace: rmTest.Namespace,
RegistryName: rmTest.RegistryName,
}
tr, trCleanup := createTestRun(t, client, rmTest, &RunVariable{
Key: "wait_time",
Value: "5s", // Create a long-running test run that we'll have time to cancel.
})
defer trCleanup()
t.Run("when the run exists", func(t *testing.T) {
err := client.TestRuns.Cancel(ctx, id, tr.ID)
require.NoError(t, err)
})
/* TODO: Enable force cancel test when supported.
t.Run("can force cancel", func(t *testing.T) {
var err error
for i := 1; ; i++ {
tr, err = client.TestRuns.Read(ctx, id, tr.ID)
require.NoError(t, err)
// TODO: Check if we can force cancel yet, not available in the
// API yet.
if i > 30 {
t.Fatal("Timeout waiting for run to be canceled")
}
time.Sleep(time.Second)
}
})
*/
t.Run("when the run does not exist", func(t *testing.T) {
err := client.TestRuns.Cancel(ctx, id, "notreal")
assert.Equal(t, err, ErrResourceNotFound)
})
}
================================================
FILE: test_variables.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ TestVariables = (*testVariables)(nil)
// Variables describes all the variable related methods that the Terraform
// Enterprise API supports.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/private-registry/tests
type TestVariables interface {
// List all the test variables associated with the given module.
List(ctx context.Context, moduleID RegistryModuleID, options *VariableListOptions) (*VariableList, error)
// Read a test variable by its ID.
Read(ctx context.Context, moduleID RegistryModuleID, variableID string) (*Variable, error)
// Create is used to create a new variable.
Create(ctx context.Context, moduleID RegistryModuleID, options VariableCreateOptions) (*Variable, error)
// Update values of an existing variable.
Update(ctx context.Context, moduleID RegistryModuleID, variableID string, options VariableUpdateOptions) (*Variable, error)
// Delete a variable by its ID.
Delete(ctx context.Context, moduleID RegistryModuleID, variableID string) error
}
// variables implements Variables.
type testVariables struct {
client *Client
}
// List all the variables associated with the given module.
func (s *testVariables) List(ctx context.Context, moduleID RegistryModuleID, options *VariableListOptions) (*VariableList, error) {
if err := moduleID.valid(); err != nil {
return nil, err
}
req, err := s.client.NewRequest("GET", testVarsPath(moduleID), options)
if err != nil {
return nil, err
}
vl := &VariableList{}
err = req.Do(ctx, vl)
if err != nil {
return nil, err
}
return vl, nil
}
// Read a variable by its ID.
func (s *testVariables) Read(ctx context.Context, moduleID RegistryModuleID, variableID string) (*Variable, error) {
if err := moduleID.valid(); err != nil {
return nil, err
}
if !validStringID(&variableID) {
return nil, ErrInvalidVariableID
}
u := fmt.Sprintf("%s/%s", testVarsPath(moduleID), url.PathEscape(variableID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
v := &Variable{}
err = req.Do(ctx, v)
if err != nil {
return nil, err
}
return v, err
}
// Create is used to create a new variable.
func (s *testVariables) Create(ctx context.Context, moduleID RegistryModuleID, options VariableCreateOptions) (*Variable, error) {
if err := moduleID.valid(); err != nil {
return nil, err
}
if err := options.valid(); err != nil {
return nil, err
}
req, err := s.client.NewRequest("POST", testVarsPath(moduleID), &options)
if err != nil {
return nil, err
}
v := &Variable{}
err = req.Do(ctx, v)
if err != nil {
return nil, err
}
return v, nil
}
// Update values of an existing variable.
func (s *testVariables) Update(ctx context.Context, moduleID RegistryModuleID, variableID string, options VariableUpdateOptions) (*Variable, error) {
if err := moduleID.valid(); err != nil {
return nil, err
}
if !validStringID(&variableID) {
return nil, ErrInvalidVariableID
}
u := fmt.Sprintf("%s/%s", testVarsPath(moduleID), url.PathEscape(variableID))
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
v := &Variable{}
err = req.Do(ctx, v)
if err != nil {
return nil, err
}
return v, nil
}
// Delete a variable by its ID.
func (s *testVariables) Delete(ctx context.Context, moduleID RegistryModuleID, variableID string) error {
if err := moduleID.valid(); err != nil {
return err
}
if !validStringID(&variableID) {
return ErrInvalidVariableID
}
u := fmt.Sprintf("%s/%s", testVarsPath(moduleID), url.PathEscape(variableID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func testVarsPath(moduleID RegistryModuleID) string {
return fmt.Sprintf("organizations/%s/tests/registry-modules/%s/%s/%s/%s/vars",
url.PathEscape(moduleID.Organization),
url.PathEscape(string(moduleID.RegistryName)),
url.PathEscape(moduleID.Namespace),
url.PathEscape(moduleID.Name),
url.PathEscape(moduleID.Provider))
}
================================================
FILE: test_variables_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTestVariablesList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
rmTest, registryModuleTestCleanup := createBranchBasedRegistryModule(t, client, orgTest)
defer registryModuleTestCleanup()
id := RegistryModuleID{
Organization: orgTest.Name,
Name: rmTest.Name,
Provider: rmTest.Provider,
Namespace: rmTest.Namespace,
RegistryName: rmTest.RegistryName,
}
tv1, tvCleanup1 := createTestVariable(t, client, rmTest)
tv2, tvCleanup2 := createTestVariable(t, client, rmTest)
defer tvCleanup1()
defer tvCleanup2()
t.Run("without list options", func(t *testing.T) {
trl, err := client.TestVariables.List(ctx, id, nil)
var found []string
for _, r := range trl.Items {
found = append(found, r.ID)
}
require.NoError(t, err)
assert.Contains(t, found, tv1.ID)
assert.Contains(t, found, tv2.ID)
assert.Equal(t, 1, trl.CurrentPage)
assert.Equal(t, 2, trl.TotalCount)
})
t.Run("empty list options", func(t *testing.T) {
trl, err := client.TestVariables.List(ctx, id, &VariableListOptions{})
var found []string
for _, r := range trl.Items {
found = append(found, r.ID)
}
require.NoError(t, err)
assert.Contains(t, found, tv1.ID)
assert.Contains(t, found, tv2.ID)
assert.Equal(t, 1, trl.CurrentPage)
assert.Equal(t, 2, trl.TotalCount)
})
t.Run("with page size", func(t *testing.T) {
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
tvl, err := client.TestVariables.List(ctx, id, &VariableListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, tvl.Items)
assert.Equal(t, 999, tvl.CurrentPage)
assert.Equal(t, 2, tvl.TotalCount)
})
}
func TestTestVariablesRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
rmTest, registryModuleTestCleanup := createBranchBasedRegistryModule(t, client, orgTest)
defer registryModuleTestCleanup()
id := RegistryModuleID{
Organization: orgTest.Name,
Name: rmTest.Name,
Provider: rmTest.Provider,
Namespace: rmTest.Namespace,
RegistryName: rmTest.RegistryName,
}
tv, tvCleanup := createTestVariable(t, client, rmTest)
defer tvCleanup()
t.Run("when the variable exists", func(t *testing.T) {
v, err := client.TestVariables.Read(ctx, id, tv.ID)
require.NoError(t, err)
assert.Equal(t, tv.ID, v.ID)
assert.Equal(t, tv.Category, v.Category)
assert.Equal(t, tv.HCL, v.HCL)
assert.Equal(t, tv.Key, v.Key)
assert.Equal(t, tv.Sensitive, v.Sensitive)
assert.Equal(t, tv.Value, v.Value)
assert.Equal(t, tv.VersionID, v.VersionID)
})
t.Run("when the variable does not exist", func(t *testing.T) {
v, err := client.TestVariables.Read(ctx, id, "nonexisting")
assert.Nil(t, v)
assert.Equal(t, ErrResourceNotFound, err)
})
t.Run("without a valid module ID", func(t *testing.T) {
v, err := client.TestVariables.Read(ctx, RegistryModuleID{}, tv.ID)
assert.Nil(t, v)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
func TestTestVariablesCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
rmTest, registryModuleTestCleanup := createBranchBasedRegistryModule(t, client, orgTest)
defer registryModuleTestCleanup()
id := RegistryModuleID{
Organization: orgTest.Name,
Name: rmTest.Name,
Provider: rmTest.Provider,
Namespace: rmTest.Namespace,
RegistryName: rmTest.RegistryName,
}
t.Run("with valid options", func(t *testing.T) {
options := VariableCreateOptions{
Key: String(randomKeyValue(t)),
Value: String(randomStringWithoutSpecialChar(t)),
Category: Category(CategoryEnv),
Description: String("testing"),
}
v, err := client.TestVariables.Create(ctx, id, options)
require.NoError(t, err)
assert.NotEmpty(t, v.ID)
assert.Equal(t, *options.Key, v.Key)
assert.Equal(t, *options.Value, v.Value)
assert.Equal(t, *options.Description, v.Description)
assert.Equal(t, *options.Category, v.Category)
assert.NotEmpty(t, v.VersionID)
})
t.Run("when options has an empty string value", func(t *testing.T) {
options := VariableCreateOptions{
Key: String(randomKeyValue(t)),
Value: String(""),
Description: String("testing"),
Category: Category(CategoryEnv),
}
v, err := client.TestVariables.Create(ctx, id, options)
require.NoError(t, err)
assert.NotEmpty(t, v.ID)
assert.Equal(t, *options.Key, v.Key)
assert.Equal(t, *options.Value, v.Value)
assert.Equal(t, *options.Description, v.Description)
assert.Equal(t, *options.Category, v.Category)
assert.NotEmpty(t, v.VersionID)
})
t.Run("when options has an empty string description", func(t *testing.T) {
options := VariableCreateOptions{
Key: String(randomKeyValue(t)),
Value: String(randomStringWithoutSpecialChar(t)),
Description: String(""),
Category: Category(CategoryEnv),
}
v, err := client.TestVariables.Create(ctx, id, options)
require.NoError(t, err)
assert.NotEmpty(t, v.ID)
assert.Equal(t, *options.Key, v.Key)
assert.Equal(t, *options.Value, v.Value)
assert.Equal(t, *options.Description, v.Description)
assert.Equal(t, *options.Category, v.Category)
assert.NotEmpty(t, v.VersionID)
})
t.Run("when options has a too-long description", func(t *testing.T) {
options := VariableCreateOptions{
Key: String(randomKeyValue(t)),
Value: String(randomStringWithoutSpecialChar(t)),
Description: String("tortor aliquam nulla go lint is fussy about spelling cras fermentum odio eu feugiat pretium nibh ipsum consequat nisl vel pretium lectus quam id leo in vitae turpis massa sed elementum tempus egestas sed sed risus pretium quam vulputate dignissim suspendisse in est ante in nibh mauris cursus mattis molestie a iaculis at erat pellentesque adipiscing commodo elit at imperdiet dui accumsan sit amet nulla redacted morbi tempus iaculis urna id volutpat lacus laoreet non curabitur gravida arcu ac tortor dignissim convallis aenean et tortor"),
Category: Category(CategoryEnv),
}
_, err := client.TestVariables.Create(ctx, id, options)
assert.Error(t, err)
})
t.Run("when options is missing value", func(t *testing.T) {
options := VariableCreateOptions{
Key: String(randomKeyValue(t)),
Category: Category(CategoryEnv),
}
v, err := client.TestVariables.Create(ctx, id, options)
require.NoError(t, err)
assert.NotEmpty(t, v.ID)
assert.Equal(t, *options.Key, v.Key)
assert.Equal(t, "", v.Value)
assert.Equal(t, *options.Category, v.Category)
assert.NotEmpty(t, v.VersionID)
})
t.Run("when options is missing key", func(t *testing.T) {
options := VariableCreateOptions{
Value: String(randomStringWithoutSpecialChar(t)),
Category: Category(CategoryEnv),
}
_, err := client.TestVariables.Create(ctx, id, options)
assert.Equal(t, err, ErrRequiredKey)
})
t.Run("when options has an empty key", func(t *testing.T) {
options := VariableCreateOptions{
Key: String(""),
Value: String(randomStringWithoutSpecialChar(t)),
Category: Category(CategoryEnv),
}
_, err := client.TestVariables.Create(ctx, id, options)
assert.Equal(t, err, ErrRequiredKey)
})
t.Run("when options is missing category", func(t *testing.T) {
options := VariableCreateOptions{
Key: String(randomKeyValue(t)),
Value: String(randomStringWithoutSpecialChar(t)),
}
_, err := client.TestVariables.Create(ctx, id, options)
assert.Equal(t, err, ErrRequiredCategory)
})
}
func TestTestVariablesUpdate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
rmTest, registryModuleTestCleanup := createBranchBasedRegistryModule(t, client, orgTest)
defer registryModuleTestCleanup()
id := RegistryModuleID{
Organization: orgTest.Name,
Name: rmTest.Name,
Provider: rmTest.Provider,
Namespace: rmTest.Namespace,
RegistryName: rmTest.RegistryName,
}
vTest, tvCleanup1 := createTestVariable(t, client, rmTest)
defer tvCleanup1()
t.Run("without any changes", func(t *testing.T) {
v, err := client.TestVariables.Update(ctx, id, vTest.ID, VariableUpdateOptions{})
require.NoError(t, err)
assert.Equal(t, vTest.ID, v.ID)
assert.Equal(t, vTest.Key, v.Key)
assert.Equal(t, vTest.Value, v.Value)
assert.Equal(t, vTest.Description, v.Description)
assert.Equal(t, vTest.Category, v.Category)
assert.Equal(t, vTest.HCL, v.HCL)
assert.Equal(t, vTest.Sensitive, v.Sensitive)
assert.NotEqual(t, vTest.VersionID, v.VersionID)
})
t.Run("with valid options", func(t *testing.T) {
options := VariableUpdateOptions{
Key: String("newname"),
Value: String("newvalue"),
HCL: Bool(true),
}
v, err := client.TestVariables.Update(ctx, id, vTest.ID, options)
require.NoError(t, err)
assert.Equal(t, *options.Key, v.Key)
assert.Equal(t, *options.HCL, v.HCL)
assert.Equal(t, *options.Value, v.Value)
assert.NotEqual(t, vTest.VersionID, v.VersionID)
})
t.Run("when updating a subset of values", func(t *testing.T) {
options := VariableUpdateOptions{
Key: String("someothername"),
HCL: Bool(false),
}
v, err := client.TestVariables.Update(ctx, id, vTest.ID, options)
require.NoError(t, err)
assert.Equal(t, *options.Key, v.Key)
assert.Equal(t, *options.HCL, v.HCL)
assert.NotEqual(t, vTest.VersionID, v.VersionID)
})
t.Run("with sensitive set", func(t *testing.T) {
options := VariableUpdateOptions{
Sensitive: Bool(true),
}
v, err := client.TestVariables.Update(ctx, id, vTest.ID, options)
require.NoError(t, err)
assert.Equal(t, *options.Sensitive, v.Sensitive)
assert.Empty(t, v.Value) // Because its now sensitive
assert.NotEqual(t, vTest.VersionID, v.VersionID)
})
t.Run("with invalid variable ID", func(t *testing.T) {
_, err := client.TestVariables.Update(ctx, id, badIdentifier, VariableUpdateOptions{})
assert.Equal(t, err, ErrInvalidVariableID)
})
}
func TestTestVariablesDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
rmTest, registryModuleTestCleanup := createBranchBasedRegistryModule(t, client, orgTest)
defer registryModuleTestCleanup()
id := RegistryModuleID{
Organization: orgTest.Name,
Name: rmTest.Name,
Provider: rmTest.Provider,
Namespace: rmTest.Namespace,
RegistryName: rmTest.RegistryName,
}
vTest, _ := createTestVariable(t, client, rmTest)
t.Run("with valid options", func(t *testing.T) {
err := client.TestVariables.Delete(ctx, id, vTest.ID)
require.NoError(t, err)
})
t.Run("with non existing variable ID", func(t *testing.T) {
err := client.TestVariables.Delete(ctx, id, "nonexisting")
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("with invalid variable ID", func(t *testing.T) {
err := client.TestVariables.Delete(ctx, id, badIdentifier)
assert.Equal(t, err, ErrInvalidVariableID)
})
}
================================================
FILE: tfe.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"log"
"math/rand"
"net/http"
"net/url"
"os"
"reflect"
"sort"
"strconv"
"strings"
"time"
"github.com/google/go-querystring/query"
cleanhttp "github.com/hashicorp/go-cleanhttp"
retryablehttp "github.com/hashicorp/go-retryablehttp"
"github.com/hashicorp/jsonapi"
"golang.org/x/time/rate"
slug "github.com/hashicorp/go-slug"
)
const (
_userAgent = "go-tfe"
_headerRateLimit = "X-RateLimit-Limit"
_headerRateReset = "X-RateLimit-Reset"
_headerAppName = "TFP-AppName"
_headerAPIVersion = "TFP-API-Version"
_headerTFEVersion = "X-TFE-Version"
_headerTFENumericVersion = "X-TFE-Current-Version"
_includeQueryParam = "include"
DefaultAddress = "https://app.terraform.io"
DefaultBasePath = "/api/v2/"
DefaultRegistryPath = "/api/registry/"
// PingEndpoint is a no-op API endpoint used to configure the rate limiter
PingEndpoint = "ping"
ContentTypeJSONAPI = "application/vnd.api+json"
)
// RetryLogHook allows a function to run before each retry.
type RetryLogHook func(attemptNum int, resp *http.Response)
// Config provides configuration details to the API client.
type Config struct {
// The address of the Terraform Enterprise API.
Address string
// The base path on which the API is served.
BasePath string
// The base path for the Registry API
RegistryBasePath string
// API token used to access the Terraform Enterprise API.
Token string
// Headers that will be added to every request.
Headers http.Header
// A custom HTTP client to use.
HTTPClient *http.Client
// RetryLogHook is invoked each time a request is retried.
RetryLogHook RetryLogHook
// RetryServerErrors enables the retry logic in the client.
RetryServerErrors bool
}
// DefaultConfig returns a default config structure.
func DefaultConfig() *Config {
config := &Config{
Address: os.Getenv("TFE_ADDRESS"),
BasePath: DefaultBasePath,
RegistryBasePath: DefaultRegistryPath,
Token: os.Getenv("TFE_TOKEN"),
Headers: make(http.Header),
HTTPClient: cleanhttp.DefaultPooledClient(),
RetryServerErrors: false,
}
// Set the default address if none is given.
if config.Address == "" {
if host := os.Getenv("TFE_HOSTNAME"); host != "" {
config.Address = fmt.Sprintf("https://%s", host)
} else {
config.Address = DefaultAddress
}
}
// Set the default user agent.
config.Headers.Set("User-Agent", _userAgent)
return config
}
// Client is the Terraform Enterprise API client. It provides the basic
// connectivity and configuration for accessing the TFE API
type Client struct {
baseURL *url.URL
registryBaseURL *url.URL
token string
headers http.Header
http *retryablehttp.Client
limiter *rate.Limiter
retryLogHook RetryLogHook
retryServerErrors bool
remoteAPIVersion string
remoteTFEVersion string
remoteTFENumericVersion string
appName string
Admin Admin
Agents Agents
AgentPools AgentPools
AgentTokens AgentTokens
Applies Applies
AuditTrails AuditTrails
AWSOIDCConfigurations AWSOIDCConfigurations
GCPOIDCConfigurations GCPOIDCConfigurations
AzureOIDCConfigurations AzureOIDCConfigurations
VaultOIDCConfigurations VaultOIDCConfigurations
Comments Comments
ConfigurationVersions ConfigurationVersions
CostEstimates CostEstimates
GHAInstallations GHAInstallations
GPGKeys GPGKeys
NotificationConfigurations NotificationConfigurations
OAuthClients OAuthClients
OAuthTokens OAuthTokens
OrganizationAuditConfigurations OrganizationAuditConfigurations
OrganizationMemberships OrganizationMemberships
Organizations Organizations
OrganizationTags OrganizationTags
OrganizationTokens OrganizationTokens
Plans Plans
PlanExports PlanExports
Policies Policies
PolicyChecks PolicyChecks
PolicyEvaluations PolicyEvaluations
PolicySetOutcomes PolicySetOutcomes
PolicySetParameters PolicySetParameters
PolicySetVersions PolicySetVersions
PolicySets PolicySets
QueryRuns QueryRuns
RegistryModules RegistryModules
RegistryNoCodeModules RegistryNoCodeModules
RegistryProviders RegistryProviders
RegistryProviderPlatforms RegistryProviderPlatforms
RegistryProviderVersions RegistryProviderVersions
ReservedTagKeys ReservedTagKeys
Runs Runs
RunEvents RunEvents
RunTasks RunTasks
RunTasksIntegration RunTasksIntegration
RunTriggers RunTriggers
SSHKeys SSHKeys
Stacks Stacks
HYOKConfigurations HYOKConfigurations
HYOKCustomerKeyVersions HYOKCustomerKeyVersions
HYOKEncryptedDataKeys HYOKEncryptedDataKeys
StackConfigurations StackConfigurations
StackConfigurationSummaries StackConfigurationSummaries
StackDeployments StackDeployments
StackDeploymentGroups StackDeploymentGroups
StackDeploymentGroupSummaries StackDeploymentGroupSummaries
StackDeploymentRuns StackDeploymentRuns
StackDeploymentSteps StackDeploymentSteps
StackDiagnostics StackDiagnostics
StackStates StackStates
StateVersionOutputs StateVersionOutputs
StateVersions StateVersions
TaskResults TaskResults
TaskStages TaskStages
Teams Teams
TeamAccess TeamAccesses
TeamMembers TeamMembers
TeamProjectAccess TeamProjectAccesses
TeamTokens TeamTokens
TestRuns TestRuns
TestVariables TestVariables
Users Users
UserTokens UserTokens
Variables Variables
VariableSets VariableSets
VariableSetVariables VariableSetVariables
Workspaces Workspaces
WorkspaceResources WorkspaceResources
WorkspaceRunTasks WorkspaceRunTasks
Projects Projects
Meta Meta
}
// Admin is the the Terraform Enterprise Admin API. It provides access to site
// wide admin settings. These are only available for Terraform Enterprise and
// do not function against HCP Terraform
type Admin struct {
Organizations AdminOrganizations
Workspaces AdminWorkspaces
Runs AdminRuns
TerraformVersions AdminTerraformVersions
OPAVersions AdminOPAVersions
SentinelVersions AdminSentinelVersions
Users AdminUsers
Settings *AdminSettings
}
// Meta contains any HCP Terraform APIs which provide data about the API itself.
type Meta struct {
IPRanges IPRanges
}
// doForeignPUTRequest performs a PUT request using the specific data body. The Content-Type
// header is set to application/octet-stream but no Authentication header is sent. No response
// body is decoded.
func (c *Client) doForeignPUTRequest(ctx context.Context, foreignURL string, data io.Reader) error {
u, err := url.Parse(foreignURL)
if err != nil {
return fmt.Errorf("specified URL was not valid: %w", err)
}
reqHeaders := make(http.Header)
reqHeaders.Set("Accept", "application/json, */*")
reqHeaders.Set("Content-Type", "application/octet-stream")
req, err := retryablehttp.NewRequest("PUT", u.String(), data)
if err != nil {
return err
}
// Set the default headers.
for k, v := range c.headers {
req.Header[k] = v
}
// Set the request specific headers.
for k, v := range reqHeaders {
req.Header[k] = v
}
request := &ClientRequest{
retryableRequest: req,
http: c.http,
Header: req.Header,
}
return request.DoJSON(ctx, nil)
}
// NewRequest performs some basic API request preparation based on the method
// specified. For GET requests, the reqBody is encoded as query parameters.
// For DELETE, PATCH, and POST requests, the request body is serialized as JSONAPI.
// For PUT requests, the request body is sent as a stream of bytes.
func (c *Client) NewRequest(method, path string, reqBody any) (*ClientRequest, error) {
return c.NewRequestWithAdditionalQueryParams(method, path, reqBody, nil)
}
// NewRequestWithAdditionalQueryParams performs some basic API request
// preparation based on the method specified. For GET requests, the reqBody is
// encoded as query parameters. For DELETE, PATCH, and POST requests, the
// request body is serialized as JSONAPI. For PUT requests, the request body is
// sent as a stream of bytes. Additional query parameters can be added to the
// request as a string map. Note that if a key exists in both the reqBody and
// additionalQueryParams, the value in additionalQueryParams will be used.
func (c *Client) NewRequestWithAdditionalQueryParams(method, path string, reqBody any, additionalQueryParams map[string][]string) (*ClientRequest, error) {
var u *url.URL
var err error
if strings.Contains(path, "/api/registry/") {
u, err = c.registryBaseURL.Parse(path)
if err != nil {
return nil, err
}
} else {
u, err = c.baseURL.Parse(path)
if err != nil {
return nil, err
}
}
// Will contain combined query values from path parsing and
// additionalQueryParams parameter
q := make(url.Values)
// Create a request specific headers map.
reqHeaders := make(http.Header)
reqHeaders.Set("Authorization", "Bearer "+c.token)
var body any
switch method {
case "GET":
reqHeaders.Set("Accept", ContentTypeJSONAPI)
// Encode the reqBody as query parameters
if reqBody != nil {
q, err = query.Values(reqBody)
if err != nil {
return nil, err
}
}
case "DELETE", "PATCH", "POST":
reqHeaders.Set("Accept", ContentTypeJSONAPI)
reqHeaders.Set("Content-Type", ContentTypeJSONAPI)
if reqBody != nil {
if body, err = serializeRequestBody(reqBody); err != nil {
return nil, err
}
}
case "PUT":
reqHeaders.Set("Accept", "application/json")
reqHeaders.Set("Content-Type", "application/octet-stream")
body = reqBody
}
for k, v := range u.Query() {
q[k] = v
}
for k, v := range additionalQueryParams {
q[k] = v
}
u.RawQuery = encodeQueryParams(q)
req, err := retryablehttp.NewRequest(method, u.String(), body)
if err != nil {
return nil, err
}
// Set the default headers.
for k, v := range c.headers {
req.Header[k] = v
}
// Set the request specific headers.
for k, v := range reqHeaders {
req.Header[k] = v
}
return &ClientRequest{
retryableRequest: req,
http: c.http,
limiter: c.limiter,
Header: req.Header,
}, nil
}
// NewClient creates a new Terraform Enterprise API client.
func NewClient(cfg *Config) (*Client, error) {
config := DefaultConfig()
// Layer in the provided config for any non-blank values.
if cfg != nil { // nolint
if cfg.Address != "" {
config.Address = cfg.Address
}
if cfg.BasePath != "" {
config.BasePath = cfg.BasePath
}
if cfg.RegistryBasePath != "" {
config.RegistryBasePath = cfg.RegistryBasePath
}
if cfg.Token != "" {
config.Token = cfg.Token
}
for k, v := range cfg.Headers {
config.Headers[k] = v
}
if cfg.HTTPClient != nil {
config.HTTPClient = cfg.HTTPClient
}
if cfg.RetryLogHook != nil {
config.RetryLogHook = cfg.RetryLogHook
}
config.RetryServerErrors = cfg.RetryServerErrors
}
// Parse the address to make sure its a valid URL.
baseURL, err := url.Parse(config.Address)
if err != nil {
return nil, fmt.Errorf("invalid address: %w", err)
}
baseURL.Path = config.BasePath
if !strings.HasSuffix(baseURL.Path, "/") {
baseURL.Path += "/"
}
registryURL, err := url.Parse(config.Address)
if err != nil {
return nil, fmt.Errorf("invalid address: %w", err)
}
registryURL.Path = config.RegistryBasePath
if !strings.HasSuffix(registryURL.Path, "/") {
registryURL.Path += "/"
}
// This value must be provided by the user.
if config.Token == "" {
return nil, fmt.Errorf("missing API token")
}
// Create the client.
client := &Client{
baseURL: baseURL,
registryBaseURL: registryURL,
token: config.Token,
headers: config.Headers,
retryLogHook: config.RetryLogHook,
retryServerErrors: config.RetryServerErrors,
}
client.http = &retryablehttp.Client{
Backoff: client.retryHTTPBackoff,
CheckRetry: client.retryHTTPCheck,
ErrorHandler: retryablehttp.PassthroughErrorHandler,
HTTPClient: config.HTTPClient,
RetryWaitMin: 100 * time.Millisecond,
RetryWaitMax: 400 * time.Millisecond,
RetryMax: 30,
}
meta, err := client.getRawAPIMetadata()
if err != nil {
return nil, err
}
// Configure the rate limiter.
client.configureLimiter(meta.RateLimit)
// Save the API version so we can return it from the RemoteAPIVersion
// method later.
client.remoteAPIVersion = meta.APIVersion
// Save the TFE version
client.remoteTFEVersion = meta.TFEVersion
// Save the TFE Numeric version
client.remoteTFENumericVersion = meta.TFENumericVersion
// Save the app name
client.appName = meta.AppName
// Create Admin
client.Admin = Admin{
Organizations: &adminOrganizations{client: client},
Workspaces: &adminWorkspaces{client: client},
Runs: &adminRuns{client: client},
Settings: newAdminSettings(client),
TerraformVersions: &adminTerraformVersions{client: client},
OPAVersions: &adminOPAVersions{client: client},
SentinelVersions: &adminSentinelVersions{client: client},
Users: &adminUsers{client: client},
}
// Create the services.
client.AgentPools = &agentPools{client: client}
client.Agents = &agents{client: client}
client.AgentTokens = &agentTokens{client: client}
client.Applies = &applies{client: client}
client.AuditTrails = &auditTrails{client: client}
client.AWSOIDCConfigurations = &awsOIDCConfigurations{client: client}
client.GCPOIDCConfigurations = &gcpOIDCConfigurations{client: client}
client.AzureOIDCConfigurations = &azureOIDCConfigurations{client: client}
client.VaultOIDCConfigurations = &vaultOIDCConfigurations{client: client}
client.Comments = &comments{client: client}
client.ConfigurationVersions = &configurationVersions{client: client}
client.CostEstimates = &costEstimates{client: client}
client.GHAInstallations = &gHAInstallations{client: client}
client.GPGKeys = &gpgKeys{client: client}
client.RegistryNoCodeModules = ®istryNoCodeModules{client: client}
client.NotificationConfigurations = ¬ificationConfigurations{client: client}
client.OAuthClients = &oAuthClients{client: client}
client.OAuthTokens = &oAuthTokens{client: client}
client.OrganizationMemberships = &organizationMemberships{client: client}
client.Organizations = &organizations{client: client}
client.OrganizationTags = &organizationTags{client: client}
client.OrganizationTokens = &organizationTokens{client: client}
client.OrganizationAuditConfigurations = &organizationAuditConfigurations{client: client}
client.PlanExports = &planExports{client: client}
client.Plans = &plans{client: client}
client.Policies = &policies{client: client}
client.PolicyChecks = &policyChecks{client: client}
client.PolicyEvaluations = &policyEvaluation{client: client}
client.PolicySetOutcomes = &policySetOutcome{client: client}
client.PolicySetParameters = &policySetParameters{client: client}
client.PolicySets = &policySets{client: client}
client.PolicySetVersions = &policySetVersions{client: client}
client.Projects = &projects{client: client}
client.QueryRuns = &queryRuns{client: client}
client.RegistryModules = ®istryModules{client: client}
client.RegistryProviderPlatforms = ®istryProviderPlatforms{client: client}
client.RegistryProviders = ®istryProviders{client: client}
client.RegistryProviderVersions = ®istryProviderVersions{client: client}
client.ReservedTagKeys = &reservedTagKeys{client: client}
client.Runs = &runs{client: client}
client.RunEvents = &runEvents{client: client}
client.RunTasks = &runTasks{client: client}
client.RunTasksIntegration = &runTaskIntegration{client: client}
client.RunTriggers = &runTriggers{client: client}
client.SSHKeys = &sshKeys{client: client}
client.Stacks = &stacks{client: client}
client.HYOKConfigurations = &hyokConfigurations{client: client}
client.HYOKCustomerKeyVersions = &hyokCustomerKeyVersions{client: client}
client.HYOKEncryptedDataKeys = &hyokEncryptedDataKeys{client: client}
client.StackConfigurations = &stackConfigurations{client: client}
client.StackConfigurationSummaries = &stackConfigurationSummaries{client: client}
client.StackDeployments = &stackDeployments{client: client}
client.StackDeploymentGroups = &stackDeploymentGroups{client: client}
client.StackDeploymentGroupSummaries = &stackDeploymentGroupSummaries{client: client}
client.StackDeploymentRuns = &stackDeploymentRuns{client: client}
client.StackDeploymentSteps = &stackDeploymentSteps{client: client}
client.StackDiagnostics = &stackDiagnostics{client: client}
client.StackStates = &stackStates{client: client}
client.StateVersionOutputs = &stateVersionOutputs{client: client}
client.StateVersions = &stateVersions{client: client}
client.TaskResults = &taskResults{client: client}
client.TaskStages = &taskStages{client: client}
client.TeamAccess = &teamAccesses{client: client}
client.TeamMembers = &teamMembers{client: client}
client.TeamProjectAccess = &teamProjectAccesses{client: client}
client.Teams = &teams{client: client}
client.TeamTokens = &teamTokens{client: client}
client.TestRuns = &testRuns{client: client}
client.TestVariables = &testVariables{client: client}
client.Users = &users{client: client}
client.UserTokens = &userTokens{client: client}
client.Variables = &variables{client: client}
client.VariableSets = &variableSets{client: client}
client.VariableSetVariables = &variableSetVariables{client: client}
client.WorkspaceRunTasks = &workspaceRunTasks{client: client}
client.Workspaces = &workspaces{client: client}
client.WorkspaceResources = &workspaceResources{client: client}
client.Meta = Meta{
IPRanges: &ipRanges{client: client},
}
client.StackDeploymentRuns = &stackDeploymentRuns{client: client}
return client, nil
}
// AppName returns the name of the instance.
func (c Client) AppName() string {
return c.appName
}
// IsCloud returns true if the client is configured against a HCP Terraform
// instance.
//
// Whether an instance is HCP Terraform or Terraform Enterprise is derived from the TFP-AppName header.
func (c Client) IsCloud() bool {
return c.appName == "HCP Terraform"
}
// IsEnterprise returns true if the client is configured against a Terraform
// Enterprise instance.
//
// Whether an instance is HCP Terraform or TFE is derived from the TFP-AppName header. Note:
// not all TFE releases include this header in API responses.
func (c Client) IsEnterprise() bool {
return !c.IsCloud()
}
// RemoteAPIVersion returns the server's declared API version string.
//
// A HCP Terraform or Enterprise API server returns its API version in an
// HTTP header field in all responses. The NewClient function saves the
// version number returned in its initial setup request and RemoteAPIVersion
// returns that cached value.
//
// The API protocol calls for this string to be a dotted-decimal version number
// like 2.3.0, where the first number indicates the API major version while the
// second indicates a minor version which may have introduced some
// backward-compatible additional features compared to its predecessor.
//
// Explicit API versioning was added to the HCP Terraform and Enterprise
// APIs as a later addition, so older servers will not return version
// information. In that case, this function returns an empty string as the
// version.
func (c Client) RemoteAPIVersion() string {
return c.remoteAPIVersion
}
// BaseURL returns the base URL as configured in the client
func (c Client) BaseURL() url.URL {
return *c.baseURL
}
// BaseRegistryURL returns the registry base URL as configured in the client
func (c Client) BaseRegistryURL() url.URL {
return *c.registryBaseURL
}
// SetFakeRemoteAPIVersion allows setting a given string as the client's remoteAPIVersion,
// overriding the value pulled from the API header during client initialization.
//
// This is intended for use in tests, when you may want to configure your TFE client to
// return something different than the actual API version in order to test error handling.
func (c *Client) SetFakeRemoteAPIVersion(fakeAPIVersion string) {
c.remoteAPIVersion = fakeAPIVersion
}
// RemoteTFEVersion returns the server's declared TFE monthly version string.
//
// A Terraform Enterprise API server includes its current version in an
// HTTP header field in all responses. This value is saved by the client
// during the initial setup request and RemoteTFEVersion returns that cached
// value. This function returns an empty string for any Terraform Enterprise version
// earlier than v202208-3 and for HCP Terraform.
func (c Client) RemoteTFEVersion() string {
return c.remoteTFEVersion
}
// RemoteTFENumericVersion returns the server's declared TFE version string.
//
// A Terraform Enterprise API server includes its current numeric version in an
// HTTP header field in all responses. This value is saved by the client
// during the initial setup request and RemoteTFENumericVersion returns that cached
// value. This function returns an empty string for any Terraform Enterprise version
// earlier than 1.0.3 and for HCP Terraform.
func (c Client) RemoteTFENumericVersion() string {
return c.remoteTFENumericVersion
}
// RetryServerErrors configures the retry HTTP check to also retry
// unexpected errors or requests that failed with a server error.
func (c *Client) RetryServerErrors(retry bool) {
c.retryServerErrors = retry
}
// retryHTTPCheck provides a callback for Client.CheckRetry which
// will retry both rate limit (429) and server (>= 500) errors.
func (c *Client) retryHTTPCheck(ctx context.Context, resp *http.Response, err error) (bool, error) {
if ctx.Err() != nil {
return false, ctx.Err()
}
if err != nil {
return c.retryServerErrors, err
}
if resp.StatusCode == 429 || (c.retryServerErrors && resp.StatusCode >= 500) {
return true, nil
}
return false, nil
}
// retryHTTPBackoff provides a generic callback for Client.Backoff which
// will pass through all calls based on the status code of the response.
func (c *Client) retryHTTPBackoff(minimum, maximum time.Duration, attemptNum int, resp *http.Response) time.Duration {
if c.retryLogHook != nil {
c.retryLogHook(attemptNum, resp)
}
// Use the rate limit backoff function when we are rate limited.
if resp != nil && resp.StatusCode == 429 {
return rateLimitBackoff(minimum, maximum, resp)
}
// Set custom duration's when we experience a service interruption.
minimum = 700 * time.Millisecond
maximum = 900 * time.Millisecond
return retryablehttp.LinearJitterBackoff(minimum, maximum, attemptNum, resp)
}
// rateLimitBackoff provides a callback for Client.Backoff which will use the
// X-RateLimit_Reset header to determine the time to wait. We add some jitter
// to prevent a thundering herd.
//
// minimum and maximum are mainly used for bounding the jitter that will be added to
// the reset time retrieved from the headers. But if the final wait time is
// less than minimum, minimum will be used instead.
func rateLimitBackoff(minimum, maximum time.Duration, resp *http.Response) time.Duration {
// rnd is used to generate pseudo-random numbers.
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
// First create some jitter bounded by the min and max durations.
jitter := time.Duration(rnd.Float64() * float64(maximum-minimum))
if resp != nil && resp.Header.Get(_headerRateReset) != "" {
v := resp.Header.Get(_headerRateReset)
reset, err := strconv.ParseFloat(v, 64)
if err != nil {
log.Fatal(err)
}
// Only update min if the given time to wait is longer
if reset > 0 && time.Duration(reset*1e9) > minimum {
minimum = time.Duration(reset * 1e9)
}
}
return minimum + jitter
}
type rawAPIMetadata struct {
// APIVersion is the raw API version string reported by the server in the
// TFP-API-Version response header, or an empty string if that header
// field was not included in the response.
APIVersion string
// TFEVersion is the raw TFE monthly version string reported by the server in the
// X-TFE-Version response header, or an empty string if that header
// field was not included in the response.
TFEVersion string
// TFENumericVersion is the raw TFE Numeric version string reported by the server in the
// X-TFE-Current-Version response header, or an empty string if that header
// field was not included in the response.
TFENumericVersion string
// RateLimit is the raw API version string reported by the server in the
// X-RateLimit-Limit response header, or an empty string if that header
// field was not included in the response.
RateLimit string
// AppName is either 'HCP Terraform' or 'Terraform Enterprise'
AppName string
}
func (c *Client) getRawAPIMetadata() (rawAPIMetadata, error) {
var meta rawAPIMetadata
// Create a new request.
u, err := c.baseURL.Parse(PingEndpoint)
if err != nil {
return meta, err
}
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return meta, err
}
// Attach the default headers.
for k, v := range c.headers {
req.Header[k] = v
}
req.Header.Set("Accept", ContentTypeJSONAPI)
req.Header.Set("Authorization", "Bearer "+c.token)
// Make a single request to retrieve the rate limit headers.
resp, err := c.http.HTTPClient.Do(req)
if err != nil {
return meta, err
}
resp.Body.Close() //nolint:errcheck
meta.APIVersion = resp.Header.Get(_headerAPIVersion)
meta.RateLimit = resp.Header.Get(_headerRateLimit)
meta.TFEVersion = resp.Header.Get(_headerTFEVersion)
meta.TFENumericVersion = resp.Header.Get(_headerTFENumericVersion)
meta.AppName = resp.Header.Get(_headerAppName)
return meta, nil
}
// configureLimiter configures the rate limiter.
func (c *Client) configureLimiter(rawLimit string) {
// Set default values for when rate limiting is disabled.
limit := rate.Inf
burst := 0
if v := rawLimit; v != "" {
if rateLimit, err := strconv.ParseFloat(v, 64); rateLimit > 0 {
if err != nil {
log.Fatal(err)
}
// Configure the limit and burst using a split of 2/3 for the limit and
// 1/3 for the burst. This enables clients to burst 1/3 of the allowed
// calls before the limiter kicks in. The remaining calls will then be
// spread out evenly using intervals of time.Second / limit which should
// prevent hitting the rate limit.
limit = rate.Limit(rateLimit * 0.66)
burst = int(rateLimit * 0.33)
}
}
// Create a new limiter using the calculated values.
c.limiter = rate.NewLimiter(limit, burst)
}
// encodeQueryParams encodes the values into "URL encoded" form
// ("bar=baz&foo=quux") sorted by key. This version behaves as url.Values
// Encode, except that it encodes certain keys as comma-separated values instead
// of using multiple keys.
func encodeQueryParams(v url.Values) string {
if v == nil {
return ""
}
var buf strings.Builder
keys := make([]string, 0, len(v))
for k := range v {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
vs := v[k]
if len(vs) > 1 && validSliceKey(k) {
val := strings.Join(vs, ",")
vs = vs[:0]
vs = append(vs, val)
}
keyEscaped := url.QueryEscape(k)
for _, v := range vs {
if buf.Len() > 0 {
buf.WriteByte('&')
}
buf.WriteString(keyEscaped)
buf.WriteByte('=')
buf.WriteString(url.QueryEscape(v))
}
}
return buf.String()
}
// decodeQueryParams types an object and converts the struct fields into
// Query Parameters, which can be used with NewRequestWithAdditionalQueryParams
// Note that a field without a `url` annotation will be converted into a query
// parameter. Use url:"-" to ignore struct fields.
func decodeQueryParams(v any) (url.Values, error) {
if v == nil {
return make(url.Values, 0), nil
}
return query.Values(v)
}
// serializeRequestBody serializes the given ptr or ptr slice into a JSON
// request. It automatically uses jsonapi or json serialization, depending
// on the body type's tags.
func serializeRequestBody(v interface{}) (interface{}, error) {
// The body can be a slice of pointers or a pointer. In either
// case we want to choose the serialization type based on the
// individual record type. To determine that type, we need
// to either follow the pointer or examine the slice element type.
// There are other theoretical possibilities (e. g. maps,
// non-pointers) but they wouldn't work anyway because the
// json-api library doesn't support serializing other things.
var modelType reflect.Type
bodyType := reflect.TypeOf(v)
switch bodyType.Kind() {
case reflect.Slice:
sliceElem := bodyType.Elem()
if sliceElem.Kind() != reflect.Ptr {
return nil, ErrInvalidRequestBody
}
modelType = sliceElem.Elem()
case reflect.Ptr:
modelType = reflect.ValueOf(v).Elem().Type()
default:
return nil, ErrInvalidRequestBody
}
// Infer whether the request uses jsonapi or regular json
// serialization based on how the fields are tagged.
jsonAPIFields := 0
jsonFields := 0
for i := 0; i < modelType.NumField(); i++ {
structField := modelType.Field(i)
if structField.Tag.Get("jsonapi") != "" {
jsonAPIFields++
}
if structField.Tag.Get("json") != "" {
jsonFields++
}
}
if jsonAPIFields > 0 && jsonFields > 0 {
// Defining a struct with both json and jsonapi tags doesn't
// make sense, because a struct can only be serialized
// as one or another. If this does happen, it's a bug
// in the library that should be fixed at development time
return nil, ErrInvalidStructFormat
}
if jsonFields > 0 {
return json.Marshal(v)
}
buf := bytes.NewBuffer(nil)
if err := jsonapi.MarshalPayloadWithoutIncluded(buf, v); err != nil {
return nil, err
}
return buf, nil
}
func unmarshalResponse(responseBody io.Reader, model interface{}) error {
// Get the value of model so we can test if it's a struct.
dst := reflect.Indirect(reflect.ValueOf(model))
// Return an error if model is not a struct or an io.Writer.
if dst.Kind() != reflect.Struct {
return fmt.Errorf("%v must be a struct or an io.Writer", dst)
}
// Try to get the Items and Pagination struct fields.
items := dst.FieldByName("Items")
// Unmarshal a single value if model does not contain the
// Items and Pagination struct fields.
if !items.IsValid() {
return jsonapi.UnmarshalPayload(responseBody, model)
}
// Return an error if model.Items is not a slice.
if items.Type().Kind() != reflect.Slice {
return ErrItemsMustBeSlice
}
// Create a temporary buffer and copy all the read data into it.
body := bytes.NewBuffer(nil)
reader := io.TeeReader(responseBody, body)
// Unmarshal as a list of values as model.Items is a slice.
raw, err := jsonapi.UnmarshalManyPayload(reader, items.Type().Elem())
if err != nil {
return err
}
// Make a new slice to hold the results.
sliceType := reflect.SliceOf(items.Type().Elem())
result := reflect.MakeSlice(sliceType, 0, len(raw))
// Add all of the results to the new slice.
for _, v := range raw {
result = reflect.Append(result, reflect.ValueOf(v))
}
// Pointer-swap the result.
items.Set(result)
pagination := dst.FieldByName("Pagination")
paginationWithoutTotals := dst.FieldByName("PaginationNextPrev")
// As we are getting a list of values, we need to decode
// the pagination details out of the response body.
// Pointer-swap the decoded pagination details.
if paginationWithoutTotals.IsValid() {
p, err := parsePaginationWithoutTotal(body)
if err != nil {
return err
}
paginationWithoutTotals.Set(reflect.ValueOf(p))
} else if pagination.IsValid() {
p, err := parsePagination(body)
if err != nil {
return err
}
pagination.Set(reflect.ValueOf(p))
}
return nil
}
// ListOptions is used to specify pagination options when making API requests.
// Pagination allows breaking up large result sets into chunks, or "pages".
type ListOptions struct {
// The page number to request. The results vary based on the PageSize.
PageNumber int `url:"page[number],omitempty"`
// The number of elements returned in a single page.
PageSize int `url:"page[size],omitempty"`
}
// PaginationNextPrev is used to return the pagination details of an API request.
type PaginationNextPrev struct {
CurrentPage int `json:"current-page"`
PreviousPage int `json:"prev-page"`
NextPage int `json:"next-page"`
}
// Pagination is used to return the pagination details of an API request including TotalCount.
type Pagination struct {
CurrentPage int `json:"current-page"`
PreviousPage int `json:"prev-page"`
NextPage int `json:"next-page"`
TotalCount int `json:"total-count"`
TotalPages int `json:"total-pages"`
}
func parsePaginationWithoutTotal(body io.Reader) (*PaginationNextPrev, error) {
var raw struct {
Meta struct {
Pagination PaginationNextPrev `jsonapi:"pagination"`
} `jsonapi:"meta"`
}
// JSON decode the raw response.
if err := json.NewDecoder(body).Decode(&raw); err != nil {
return &PaginationNextPrev{}, err
}
return &raw.Meta.Pagination, nil
}
func parsePagination(body io.Reader) (*Pagination, error) {
var raw struct {
Meta struct {
Pagination Pagination `jsonapi:"pagination"`
} `jsonapi:"meta"`
}
// JSON decode the raw response.
if err := json.NewDecoder(body).Decode(&raw); err != nil {
return &Pagination{}, err
}
return &raw.Meta.Pagination, nil
}
// checkResponseCode refines typical API errors into more specific errors
// if possible. It returns nil if the response code < 400
func checkResponseCode(r *http.Response) error {
if r.StatusCode >= 200 && r.StatusCode <= 399 {
return nil
}
var errs []string
var err error
switch r.StatusCode {
case 400:
errs, err = decodeErrorPayload(r)
if err != nil {
return err
}
if errorPayloadContains(errs, "include parameter") {
return ErrInvalidIncludeValue
}
return errors.New(strings.Join(errs, "\n"))
case 401:
return ErrUnauthorized
case 404:
return ErrResourceNotFound
case 409:
switch {
case strings.HasSuffix(r.Request.URL.Path, "actions/lock"):
return ErrWorkspaceLocked
case strings.HasSuffix(r.Request.URL.Path, "actions/unlock"):
errs, err = decodeErrorPayload(r)
if err != nil {
return err
}
if errorPayloadContains(errs, "is locked by Run") {
return ErrWorkspaceLockedByRun
}
if errorPayloadContains(errs, "is locked by Team") {
return ErrWorkspaceLockedByTeam
}
if errorPayloadContains(errs, "is locked by User") {
return ErrWorkspaceLockedByUser
}
return ErrWorkspaceNotLocked
case strings.HasSuffix(r.Request.URL.Path, "actions/force-unlock"):
return ErrWorkspaceNotLocked
case strings.HasSuffix(r.Request.URL.Path, "actions/safe-delete"):
errs, err = decodeErrorPayload(r)
if err != nil {
return err
}
if errorPayloadContains(errs, "locked") {
return ErrWorkspaceLockedCannotDelete
}
if errorPayloadContains(errs, "being processed") {
return ErrWorkspaceStillProcessing
}
return ErrWorkspaceNotSafeToDelete
}
}
errs, err = decodeErrorPayload(r)
if err != nil {
return err
}
return errors.New(strings.Join(errs, "\n"))
}
func decodeErrorPayload(r *http.Response) ([]string, error) {
// Decode the error payload.
var errs []string
body, err := io.ReadAll(r.Body)
if err != nil {
return errs, errors.New(r.Status)
}
// attempt JSON:API error payloads unwrapping
errPayload := &jsonapi.ErrorsPayload{}
if err := json.Unmarshal(body, errPayload); err == nil && len(errPayload.Errors) > 0 {
for _, e := range errPayload.Errors {
if e.Detail == "" {
errs = append(errs, e.Title)
} else {
errs = append(errs, fmt.Sprintf("%s\n\n%s", e.Title, e.Detail))
}
}
return errs, nil
}
// attempt JSON error payloads unwrapping: like {"errors":["..."]}.
var rawErrs struct {
Errors []string `json:"errors"`
}
if err := json.Unmarshal(body, &rawErrs); err == nil && len(rawErrs.Errors) > 0 {
return rawErrs.Errors, nil
}
return errs, errors.New(r.Status)
}
func errorPayloadContains(payloadErrors []string, match string) bool {
for _, e := range payloadErrors {
if strings.Contains(e, match) {
return true
}
}
return false
}
func packContents(path string) (*bytes.Buffer, error) {
body := bytes.NewBuffer(nil)
file, err := os.Stat(path)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return body, fmt.Errorf(`failed to find files under the path "%v": %w`, path, err)
}
return body, fmt.Errorf(`unable to upload files from the path "%v": %w`, path, err)
}
if !file.Mode().IsDir() {
return body, ErrMissingDirectory
}
_, errSlug := slug.Pack(path, body, true)
if errSlug != nil {
return body, errSlug
}
return body, nil
}
func validSliceKey(key string) bool {
return key == _includeQueryParam || strings.Contains(key, "filter[")
}
================================================
FILE: tfe_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"github.com/hashicorp/jsonapi"
"github.com/stretchr/testify/assert"
"golang.org/x/time/rate"
)
func TestClient_newClient(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", ContentTypeJSONAPI)
w.Header().Set("X-RateLimit-Limit", "30")
w.Header().Set("TFP-API-Version", "34.21.9")
w.Header().Set("X-TFE-Version", "202205-1")
w.Header().Set("X-TFE-Current-Version", "1.1.0")
if enterpriseEnabled() {
w.Header().Set("TFP-AppName", "Terraform Enterprise")
} else {
w.Header().Set("TFP-AppName", "HCP Terraform")
}
w.WriteHeader(204) // We query the configured ping URL which should return a 204.
}))
defer ts.Close()
cfg := &Config{
HTTPClient: ts.Client(),
}
t.Run("uses env vars if values are missing", func(t *testing.T) {
defer setupEnvVars("abcd1234", ts.URL)()
client, err := NewClient(cfg)
if err != nil {
t.Fatal(err)
}
if client.token != "abcd1234" {
t.Fatalf("unexpected token: %q", client.token)
}
if client.baseURL.String() != ts.URL+DefaultBasePath {
t.Fatalf("unexpected address: %q", client.baseURL.String())
}
})
t.Run("fails if token is empty", func(t *testing.T) {
defer setupEnvVars("", "")()
_, err := NewClient(cfg)
if err == nil || err.Error() != "missing API token" {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run("makes a new client with good settings", func(t *testing.T) {
config := &Config{
Address: ts.URL,
Token: "abcd1234",
HTTPClient: ts.Client(),
}
client, err := NewClient(config)
if err != nil {
t.Fatal(err)
}
if config.Address+DefaultBasePath != client.baseURL.String() {
t.Fatalf("unexpected client address %q", client.baseURL.String())
}
if config.Token != client.token {
t.Fatalf("unexpected client token %q", client.token)
}
if ts.Client() != client.http.HTTPClient {
t.Fatal("unexpected HTTP client value")
}
if want := "34.21.9"; client.RemoteAPIVersion() != want {
t.Errorf("unexpected remote API version %q; want %q", client.RemoteAPIVersion(), want)
}
if want := "202205-1"; client.RemoteTFEVersion() != want {
t.Errorf("unexpected remote TFE monthly version %q; want %q", client.RemoteTFEVersion(), want)
}
if want := "1.1.0"; client.RemoteTFENumericVersion() != want {
t.Errorf("unexpected remote TFE numeric version %q; want %q", client.RemoteTFENumericVersion(), want)
}
if enterpriseEnabled() {
assert.True(t, client.IsEnterprise())
} else {
assert.True(t, client.IsCloud())
}
client.SetFakeRemoteAPIVersion("1.0")
if want := "1.0"; client.RemoteAPIVersion() != want {
t.Errorf("unexpected remote API version %q; want %q", client.RemoteAPIVersion(), want)
}
})
}
func TestClient_defaultConfig(t *testing.T) {
t.Parallel()
t.Run("with no environment variables", func(t *testing.T) {
defer setupEnvVars("", "")()
os.Unsetenv("TFE_HOSTNAME")
config := DefaultConfig()
assert.Equal(t, config.Address, DefaultAddress)
assert.Equal(t, config.Token, "")
assert.NotNil(t, config.HTTPClient)
})
t.Run("with environment variables", func(t *testing.T) {
t.Run("with TFE_ADDRESS set", func(t *testing.T) {
defer setupEnvVars("abcd1234", "https://mytfe.local")()
client := DefaultConfig()
assert.Equal(t, client.Address, "https://mytfe.local")
})
t.Run("with TFE_HOSTNAME set", func(t *testing.T) {
defer setupEnvVars("abcd1234", "")()
os.Setenv("TFE_HOSTNAME", "iloveterraform.io")
client := DefaultConfig()
assert.Equal(t, client.Address, "https://iloveterraform.io")
os.Unsetenv("TFE_HOSTNAME")
})
})
}
func TestClient_headers(t *testing.T) {
t.Parallel()
testedCalls := 0
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
testedCalls++
if testedCalls == 1 {
w.Header().Set("Content-Type", ContentTypeJSONAPI)
w.Header().Set("X-RateLimit-Limit", "30")
w.WriteHeader(204) // We query the configured ping URL which should return a 204.
return
}
if r.Header.Get("Accept") != ContentTypeJSONAPI {
t.Fatalf("unexpected accept header: %q", r.Header.Get("Accept"))
}
if r.Header.Get("Authorization") != "Bearer dummy-token" {
t.Fatalf("unexpected authorization header: %q", r.Header.Get("Authorization"))
}
if r.Header.Get("My-Custom-Header") != "foobar" {
t.Fatalf("unexpected custom header: %q", r.Header.Get("My-Custom-Header"))
}
if r.Header.Get("Terraform-Version") != "0.11.9" {
t.Fatalf("unexpected Terraform version header: %q", r.Header.Get("Terraform-Version"))
}
if r.Header.Get("User-Agent") != "go-tfe" {
t.Fatalf("unexpected user agent header: %q", r.Header.Get("User-Agent"))
}
}))
defer ts.Close()
cfg := &Config{
Address: ts.URL,
Token: "dummy-token",
Headers: make(http.Header),
HTTPClient: ts.Client(),
}
// Set some custom header.
cfg.Headers.Set("My-Custom-Header", "foobar")
cfg.Headers.Set("Terraform-Version", "0.11.9")
// This one should be overridden!
cfg.Headers.Set("Authorization", "bad-token")
client, err := NewClient(cfg)
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
// Make a few calls so we can check they all send the expected headers.
client.Organizations.List(ctx, nil)
client.Plans.Logs(ctx, "plan-123456789")
client.Runs.Apply(ctx, "run-123456789", RunApplyOptions{})
client.Workspaces.Lock(ctx, "ws-123456789", WorkspaceLockOptions{})
client.Workspaces.Read(ctx, "organization", "workspace")
if testedCalls != 6 {
t.Fatalf("expected 6 tested calls, got: %d", testedCalls)
}
}
func TestClient_userAgent(t *testing.T) {
t.Parallel()
testedCalls := 0
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
testedCalls++
if testedCalls == 1 {
w.Header().Set("Content-Type", ContentTypeJSONAPI)
w.Header().Set("X-RateLimit-Limit", "30")
w.WriteHeader(204) // We query the configured ping URL which should return a 204.
return
}
if r.Header.Get("User-Agent") != "hashicorp" {
t.Fatalf("unexpected user agent header: %q", r.Header.Get("User-Agent"))
}
}))
defer ts.Close()
cfg := &Config{
Address: ts.URL,
Token: "dummy-token",
Headers: make(http.Header),
HTTPClient: ts.Client(),
}
// Set a custom user agent.
cfg.Headers.Set("User-Agent", "hashicorp")
client, err := NewClient(cfg)
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
// Make a few calls so we can check they all send the expected headers.
client.Organizations.List(ctx, nil)
client.Plans.Logs(ctx, "plan-123456789")
client.Runs.Apply(ctx, "run-123456789", RunApplyOptions{})
client.Workspaces.Lock(ctx, "ws-123456789", WorkspaceLockOptions{})
client.Workspaces.Read(ctx, "organization", "workspace")
if testedCalls != 6 {
t.Fatalf("expected 6 tested calls, got: %d", testedCalls)
}
}
type JSONAPIBody struct {
StrAttr string `jsonapi:"attr,str_attr"`
}
type JSONPlainBody struct {
StrAttr string `json:"str_attr"`
}
type InvalidBody struct {
Attr1 string `json:"attr1"`
Attr2 string `jsonapi:"attr,attr2"`
}
func TestClient_requestBodySerialization(t *testing.T) {
t.Parallel()
t.Run("jsonapi request", func(t *testing.T) {
body := JSONAPIBody{StrAttr: "foo"}
requestBody, err := createRequest(&body)
if err != nil {
t.Fatal(err)
}
unmarshalledRequestBody := JSONAPIBody{}
err = jsonapi.UnmarshalPayload(bytes.NewReader(requestBody), &unmarshalledRequestBody)
if err != nil {
t.Fatal(err)
}
if unmarshalledRequestBody.StrAttr != body.StrAttr {
t.Fatal("Request serialized incorrectly")
}
})
t.Run("jsonapi slice of pointers request", func(t *testing.T) {
var body []*JSONAPIBody
body = append(body, &JSONAPIBody{StrAttr: "foo"})
requestBody, err := createRequest(body)
if err != nil {
t.Fatal(err)
}
// The jsonapi library doesn't support unmarshalling bulk objects,
// so for this test we deserialize to the jsonapi intermediate
// format and validate it manually
parsedResponse := new(jsonapi.ManyPayload)
err = json.Unmarshal(requestBody, &parsedResponse)
if err != nil {
t.Fatal(err)
}
if len(parsedResponse.Data) != 1 || parsedResponse.Data[0].Attributes["str_attr"] != "foo" {
t.Fatal("Request serialized incorrectly")
}
})
t.Run("plain json request", func(t *testing.T) {
body := JSONPlainBody{StrAttr: "foo"}
requestBody, err := createRequest(&body)
if err != nil {
t.Fatal(err)
}
unmarshalledRequestBody := JSONPlainBody{}
err = json.Unmarshal(requestBody, &unmarshalledRequestBody)
if err != nil {
t.Fatal(err)
}
if unmarshalledRequestBody.StrAttr != body.StrAttr {
t.Fatal("Request serialized incorrectly")
}
})
t.Run("plain json slice of pointers request", func(t *testing.T) {
var body []*JSONPlainBody
body = append(body, &JSONPlainBody{StrAttr: "foo"})
requestBody, err := createRequest(body)
if err != nil {
t.Fatal(err)
}
var unmarshalledRequestBody []*JSONPlainBody
err = json.Unmarshal(requestBody, &unmarshalledRequestBody)
if err != nil {
t.Fatal(err)
}
if len(unmarshalledRequestBody) != 1 || unmarshalledRequestBody[0].StrAttr != body[0].StrAttr {
t.Fatal("Request serialized incorrectly")
}
})
t.Run("nil request", func(t *testing.T) {
requestBody, err := createRequest(nil)
if err != nil {
t.Fatal(err)
}
if len(requestBody) != 0 {
t.Fatal("nil request serialized incorrectly")
}
})
t.Run("invalid struct request", func(t *testing.T) {
body := InvalidBody{}
_, err := createRequest(&body)
if err == nil || err != ErrInvalidStructFormat {
t.Fatalf("unexpected error: %v", err)
}
})
expectedErr := "go-tfe bug: DELETE/PATCH/POST body must be nil, ptr, or ptr slice"
t.Run("non-pointer request", func(t *testing.T) {
body := InvalidBody{}
_, err := createRequest(body)
if err == nil || err.Error() != expectedErr {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run("slice of non-pointer request", func(t *testing.T) {
body := []InvalidBody{{}}
_, err := createRequest(body)
if err == nil || err.Error() != expectedErr {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run("map request", func(t *testing.T) {
body := make(map[string]string)
_, err := createRequest(body)
if err == nil || err.Error() != expectedErr {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run("string request", func(t *testing.T) {
body := "foo"
_, err := createRequest(body)
if err == nil || err.Error() != expectedErr {
t.Fatalf("unexpected error: %v", err)
}
})
}
func createRequest(v interface{}) ([]byte, error) {
config := DefaultConfig()
config.Token = "dummy"
client, err := NewClient(config)
if err != nil {
return nil, err
}
request, err := client.NewRequest("POST", "/bar", v)
if err != nil {
return nil, err
}
body, err := request.retryableRequest.BodyBytes()
if err != nil {
return nil, err
}
return body, nil
}
func TestClient_configureLimiter(t *testing.T) {
t.Parallel()
rateLimit := ""
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", ContentTypeJSONAPI)
w.Header().Set("X-RateLimit-Limit", rateLimit)
w.WriteHeader(204) // We query the configured ping URL which should return a 204.
}))
defer ts.Close()
cfg := &Config{
Address: ts.URL,
Token: "dummy-token",
HTTPClient: ts.Client(),
}
cases := map[string]struct {
rate string
limit rate.Limit
burst int
}{
"no-value": {
rate: "",
limit: rate.Inf,
burst: 0,
},
"limit-0": {
rate: "0",
limit: rate.Inf,
burst: 0,
},
"limit-30": {
rate: "30",
limit: rate.Limit(19.8),
burst: 9,
},
"limit-100": {
rate: "100",
limit: rate.Limit(66),
burst: 33,
},
}
for name, tc := range cases {
// First set the test rate limit.
rateLimit = tc.rate
client, err := NewClient(cfg)
if err != nil {
t.Fatal(err)
}
if client.limiter.Limit() != tc.limit {
t.Fatalf("test %s expected limit %f, got: %f", name, tc.limit, client.limiter.Limit())
}
if client.limiter.Burst() != tc.burst {
t.Fatalf("test %s expected burst %d, got: %d", name, tc.burst, client.limiter.Burst())
}
}
}
func TestClient_retryHTTPCheck(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", ContentTypeJSONAPI)
w.Header().Set("X-RateLimit-Limit", "30")
w.WriteHeader(204) // We query the configured ping URL which should return a 204.
}))
defer ts.Close()
cfg := &Config{
Address: ts.URL,
Token: "dummy-token",
HTTPClient: ts.Client(),
}
connErr := errors.New("connection error")
cases := map[string]struct {
resp *http.Response
err error
retryServerErrors bool
checkOK bool
checkErr error
}{
"429-no-server-errors": {
resp: &http.Response{StatusCode: 429},
err: nil,
checkOK: true,
checkErr: nil,
},
"429-with-server-errors": {
resp: &http.Response{StatusCode: 429},
err: nil,
retryServerErrors: true,
checkOK: true,
checkErr: nil,
},
"500-no-server-errors": {
resp: &http.Response{StatusCode: 500},
err: nil,
checkOK: false,
checkErr: nil,
},
"500-with-server-errors": {
resp: &http.Response{StatusCode: 500},
err: nil,
retryServerErrors: true,
checkOK: true,
checkErr: nil,
},
"err-no-server-errors": {
err: connErr,
checkOK: false,
checkErr: connErr,
},
"err-with-server-errors": {
err: connErr,
retryServerErrors: true,
checkOK: true,
checkErr: connErr,
},
}
ctx := context.Background()
for name, tc := range cases {
client, err := NewClient(cfg)
if err != nil {
t.Fatal(err)
}
client.RetryServerErrors(tc.retryServerErrors)
checkOK, checkErr := client.retryHTTPCheck(ctx, tc.resp, tc.err)
if checkOK != tc.checkOK {
t.Fatalf("test %s expected checkOK %t, got: %t", name, tc.checkOK, checkOK)
}
if checkErr != tc.checkErr {
t.Fatalf("test %s expected checkErr %v, got: %v", name, tc.checkErr, checkErr)
}
}
}
func TestClient_retryHTTPBackoff(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", ContentTypeJSONAPI)
w.Header().Set("X-RateLimit-Limit", "30")
w.WriteHeader(204) // We query the configured ping URL which should return a 204.
}))
defer ts.Close()
var attempts int
retryLogHook := func(attemptNum int, resp *http.Response) {
attempts++
}
cfg := &Config{
Address: ts.URL,
Token: "dummy-token",
HTTPClient: ts.Client(),
RetryLogHook: retryLogHook,
}
client, err := NewClient(cfg)
if err != nil {
t.Fatal(err)
}
retries := 3
resp := &http.Response{StatusCode: 500}
for i := 0; i < retries; i++ {
client.retryHTTPBackoff(time.Second, time.Second, i, resp)
}
if attempts != retries {
t.Fatalf("expected %d log hook callbacks, got: %d callbacks", retries, attempts)
}
}
func setupEnvVars(token, address string) func() {
origToken := os.Getenv("TFE_TOKEN")
origAddress := os.Getenv("TFE_ADDRESS")
os.Setenv("TFE_TOKEN", token)
os.Setenv("TFE_ADDRESS", address)
return func() {
os.Setenv("TFE_TOKEN", origToken)
os.Setenv("TFE_ADDRESS", origAddress)
}
}
================================================
FILE: tfe_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type tfeAPI struct {
ID string `jsonapi:"primary,tfe"`
Name string `jsonapi:"attr,name"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Enabled bool `jsonapi:"attr,enabled"`
Emails []string `jsonapi:"attr,emails"`
Status tfeAPIStatus `jsonapi:"attr,status"`
StatusTimestamps tfeAPITimestamps `jsonapi:"attr,status-timestamps"`
DeliveryResponses []tfeAPIDeliveryResponse `jsonapi:"attr,delivery-responses"`
}
type tfeAPIDeliveryResponse struct {
Body string `jsonapi:"attr,body"`
Code int `jsonapi:"attr,code"`
}
type tfeAPIStatus string
type tfeAPITimestamps struct {
QueuedAt time.Time `jsonapi:"attr,queued-at,rfc3339"`
}
const (
tfeAPIStatusNormal tfeAPIStatus = "normal"
)
func Test_unmarshalResponse(t *testing.T) {
t.Parallel()
t.Run("unmarshal properly formatted json", func(t *testing.T) {
// This structure is intended to include multiple possible fields and
// formats that are valid for JSON:API
data := map[string]interface{}{
"data": map[string]interface{}{
"type": "tfe",
"id": "1",
"attributes": map[string]interface{}{
"name": "terraform",
"created-at": "2016-08-17T08:27:12Z",
"enabled": true,
"status": tfeAPIStatusNormal,
"emails": []string{"test@hashicorp.com"},
"delivery-responses": []interface{}{
map[string]interface{}{
"body": "",
"code": 200,
},
map[string]interface{}{
"body": "",
"code": 300,
},
},
"status-timestamps": map[string]string{
"queued-at": "2020-03-16T23:15:59+00:00",
},
},
},
}
byteData, errMarshal := json.Marshal(data)
require.NoError(t, errMarshal)
responseBody := bytes.NewReader(byteData)
unmarshalledRequestBody := tfeAPI{}
err := unmarshalResponse(responseBody, &unmarshalledRequestBody)
require.NoError(t, err)
queuedParsedTime, err := time.Parse(time.RFC3339, "2020-03-16T23:15:59+00:00")
require.NoError(t, err)
assert.Equal(t, unmarshalledRequestBody.ID, "1")
assert.Equal(t, unmarshalledRequestBody.Name, "terraform")
assert.Equal(t, unmarshalledRequestBody.Status, tfeAPIStatusNormal)
assert.Equal(t, len(unmarshalledRequestBody.Emails), 1)
assert.Equal(t, unmarshalledRequestBody.Emails[0], "test@hashicorp.com")
assert.Equal(t, unmarshalledRequestBody.StatusTimestamps.QueuedAt, queuedParsedTime)
assert.NotEmpty(t, unmarshalledRequestBody.DeliveryResponses)
assert.Equal(t, len(unmarshalledRequestBody.DeliveryResponses), 2)
assert.Equal(t, unmarshalledRequestBody.DeliveryResponses[0].Body, "")
assert.Equal(t, unmarshalledRequestBody.DeliveryResponses[0].Code, 200)
assert.Equal(t, unmarshalledRequestBody.DeliveryResponses[1].Body, "")
assert.Equal(t, unmarshalledRequestBody.DeliveryResponses[1].Code, 300)
assert.Equal(t, unmarshalledRequestBody.Enabled, true)
})
t.Run("can only unmarshal Items that are slices", func(t *testing.T) {
responseBody := bytes.NewReader([]byte(""))
malformattedItemStruct := struct {
*Pagination
Items int
}{
Items: 1,
}
err := unmarshalResponse(responseBody, &malformattedItemStruct)
require.Error(t, err)
assert.Equal(t, err, ErrItemsMustBeSlice)
})
t.Run("can only unmarshal a struct", func(t *testing.T) {
payload := "random"
responseBody := bytes.NewReader([]byte(payload))
notStruct := "not a struct"
err := unmarshalResponse(responseBody, notStruct)
assert.Error(t, err)
assert.EqualError(t, err, fmt.Sprintf("%v must be a struct or an io.Writer", notStruct))
})
}
func Test_decodeErrorPayload(t *testing.T) {
t.Parallel()
t.Run("with jsonapi errors payload", func(t *testing.T) {
resp := &http.Response{
Status: "400 Bad Request",
StatusCode: http.StatusBadRequest,
Body: io.NopCloser(bytes.NewBufferString(
`{"errors":[{"title":"org name invalid","detail":"Org name is invalid"}]}`,
)),
}
errs, err := decodeErrorPayload(resp)
require.NoError(t, err)
require.Len(t, errs, 1)
assert.Equal(t, "org name invalid\n\nOrg name is invalid", errs[0])
})
t.Run("with regular json errors payload", func(t *testing.T) {
resp := &http.Response{
Status: "400 Bad Request",
StatusCode: http.StatusBadRequest,
Body: io.NopCloser(bytes.NewBufferString(
`{"errors":["Unsupported GPG Key algorithm. Supported key algorithms are [RSA, DSA]"]}`,
)),
}
errs, err := decodeErrorPayload(resp)
require.NoError(t, err)
require.Len(t, errs, 1)
assert.Equal(t, "Unsupported GPG Key algorithm. Supported key algorithms are [RSA, DSA]", errs[0])
})
t.Run("with non-json error body", func(t *testing.T) {
resp := &http.Response{
Status: "400 Bad Request",
StatusCode: http.StatusBadRequest,
Body: io.NopCloser(bytes.NewBufferString("this is not json")),
}
errs, err := decodeErrorPayload(resp)
require.EqualError(t, err, "400 Bad Request")
assert.Empty(t, errs)
})
}
func Test_checkResponseCode(t *testing.T) {
t.Parallel()
t.Run("returns regular json error message", func(t *testing.T) {
resp := &http.Response{
Status: "400 Bad Request",
StatusCode: http.StatusBadRequest,
Body: io.NopCloser(bytes.NewBufferString(
`{"errors":["Unsupported GPG Key algorithm. Supported key algorithms are [RSA, DSA]"]}`,
)),
}
err := checkResponseCode(resp)
require.EqualError(t, err, "Unsupported GPG Key algorithm. Supported key algorithms are [RSA, DSA]")
})
t.Run("still maps invalid include message", func(t *testing.T) {
resp := &http.Response{
Status: "400 Bad Request",
StatusCode: http.StatusBadRequest,
Body: io.NopCloser(bytes.NewBufferString(
`{"errors":["include parameter is invalid"]}`,
)),
}
err := checkResponseCode(resp)
assert.ErrorIs(t, err, ErrInvalidIncludeValue)
})
}
func Test_BaseURL(t *testing.T) {
t.Parallel()
client, err := NewClient(&Config{
Address: "https://example.com",
BasePath: "api/v99",
})
require.NoError(t, err)
url := client.BaseURL()
assert.Equal(t, "https://example.com/api/v99/", url.String())
}
func Test_DefaultBaseURL(t *testing.T) {
t.Parallel()
client, err := NewClient(&Config{
Address: "https://example.com",
})
require.NoError(t, err)
url := client.BaseURL()
assert.Equal(t, "https://example.com/api/v2/", url.String())
}
func Test_DefaultRegistryBaseURL(t *testing.T) {
t.Parallel()
client, err := NewClient(&Config{
Address: "https://example.com",
})
require.NoError(t, err)
url := client.BaseRegistryURL()
assert.Equal(t, "https://example.com/api/registry/", url.String())
}
func Test_RegistryBaseURL(t *testing.T) {
t.Parallel()
client, err := NewClient(&Config{
Address: "https://example.com",
RegistryBasePath: "/api/registry99",
})
require.NoError(t, err)
url := client.BaseRegistryURL()
assert.Equal(t, "https://example.com/api/registry99/", url.String())
}
func Test_EncodeQueryParams(t *testing.T) {
t.Parallel()
t.Run("with no listOptions and therefore no include field defined", func(t *testing.T) {
urlVals := map[string][]string{
"include": {},
}
requestURLquery := encodeQueryParams(urlVals)
assert.Equal(t, requestURLquery, "")
})
t.Run("with listOptions setting multiple include options", func(t *testing.T) {
urlVals := map[string][]string{
"include": {"workspace", "cost_estimate"},
}
requestURLquery := encodeQueryParams(urlVals)
assert.Equal(t, requestURLquery, "include=workspace%2Ccost_estimate")
})
}
func Test_RegistryBasePath(t *testing.T) {
t.Parallel()
client, err := NewClient(&Config{
Token: "foo",
})
require.NoError(t, err)
t.Run("ensures client creates a request with registry base path", func(t *testing.T) {
path := "/api/registry/some/path/to/resource"
req, err := client.NewRequest("GET", path, nil)
require.NoError(t, err)
expected := os.Getenv("TFE_ADDRESS") + path
assert.Equal(t, req.retryableRequest.URL.String(), expected)
})
}
func Test_NewRequest(t *testing.T) {
t.Parallel()
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/get_request_with_query_param":
val := r.URL.Query().Get("include")
if val != "workspace,cost_estimate" {
t.Fatalf("unexpected include value: %q", val)
}
w.WriteHeader(http.StatusOK)
return
case "/api/v2/ping":
w.WriteHeader(http.StatusOK)
return
default:
t.Fatalf("unexpected request: %s", r.URL.String())
}
}))
t.Cleanup(func() {
testServer.Close()
})
client, err := NewClient(&Config{
Address: testServer.URL,
})
require.NoError(t, err)
t.Run("allows path to include query params", func(t *testing.T) {
request, err := client.NewRequest("GET", "/get_request_with_query_param?include=workspace,cost_estimate", nil)
require.NoError(t, err)
ctx := context.Background()
err = request.DoJSON(ctx, nil)
require.NoError(t, err)
})
}
func Test_NewRequestWithAdditionalQueryParams(t *testing.T) {
t.Parallel()
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/get_request_include":
val := r.URL.Query().Get("include")
if val != "workspace,cost_estimate" {
t.Fatalf("unexpected include value: %q", val)
}
w.WriteHeader(http.StatusOK)
return
case "/get_request_include_extra":
val := r.URL.Query().Get("include")
if val != "workspace,cost_estimate" {
t.Fatalf("unexpected include value: expected %q, got %q", "extra,workspace,cost_estimate", val)
}
extra := r.URL.Query().Get("extra")
if extra != "value" {
t.Fatalf("unexpected extra value: expected %q, got %q", "value", extra)
}
w.WriteHeader(http.StatusOK)
return
case "/get_request_include_raw":
extra := r.URL.Query().Get("Name")
if extra != "yes" {
t.Fatalf("unexpected query: %s", r.URL.RawQuery)
}
w.WriteHeader(http.StatusOK)
return
case "/delete_with_query":
extra := r.URL.Query().Get("extra")
if extra != "value" {
t.Fatalf("unexpected query: expected %q, got %q", "value", extra)
}
w.WriteHeader(http.StatusOK)
return
case "/api/v2/ping":
w.WriteHeader(http.StatusOK)
return
default:
t.Fatalf("unexpected request: %s", r.URL.String())
}
}))
t.Cleanup(func() {
testServer.Close()
})
client, err := NewClient(&Config{
Address: testServer.URL,
})
require.NoError(t, err)
t.Run("with additional query parameters", func(t *testing.T) {
request, err := client.NewRequestWithAdditionalQueryParams("GET", "/get_request_include", nil, map[string][]string{
"include": {"workspace", "cost_estimate"},
})
require.NoError(t, err)
ctx := context.Background()
err = request.DoJSON(ctx, nil)
require.NoError(t, err)
})
type extra struct {
Extra string `url:"extra"`
}
// json-encoded structs use the field name as the query parameter name
type raw struct {
Name string `json:"extra"`
}
t.Run("GET request with req attr and additional request attributes", func(t *testing.T) {
request, err := client.NewRequestWithAdditionalQueryParams("GET", "/get_request_include_extra", &extra{Extra: "value"}, map[string][]string{
"include": {"workspace", "cost_estimate"},
})
require.NoError(t, err)
ctx := context.Background()
err = request.DoJSON(ctx, nil)
require.NoError(t, err)
})
t.Run("DELETE request with additional request attributes", func(t *testing.T) {
request, err := client.NewRequestWithAdditionalQueryParams("DELETE", "/delete_with_query", nil, map[string][]string{
"extra": {"value"},
})
require.NoError(t, err)
ctx := context.Background()
err = request.DoJSON(ctx, nil)
require.NoError(t, err)
})
t.Run("GET request with other kinds of annotations", func(t *testing.T) {
request, err := client.NewRequestWithAdditionalQueryParams("GET", "/get_request_include_raw", &raw{Name: "yes"}, nil)
require.NoError(t, err)
ctx := context.Background()
err = request.DoJSON(ctx, nil)
require.NoError(t, err)
})
}
================================================
FILE: type_helpers.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"time"
"github.com/hashicorp/jsonapi"
)
// Access returns a pointer to the given team access type.
func Access(v AccessType) *AccessType {
return &v
}
func AgentExecutionModePtr(v AgentExecutionMode) *AgentExecutionMode {
return &v
}
// ProjectAccess returns a pointer to the given team access project type.
func ProjectAccess(v TeamProjectAccessType) *TeamProjectAccessType {
return &v
}
// ProjectSettingsPermission returns a pointer to the given team access project type.
func ProjectSettingsPermission(v ProjectSettingsPermissionType) *ProjectSettingsPermissionType {
return &v
}
// ProjectTeamsPermission returns a pointer to the given team access project type.
func ProjectTeamsPermission(v ProjectTeamsPermissionType) *ProjectTeamsPermissionType {
return &v
}
// ProjectVariableSetsPermission returns a pointer to the given team access project type.
func ProjectVariableSetsPermission(v ProjectVariableSetsPermissionType) *ProjectVariableSetsPermissionType {
return &v
}
// WorkspaceRunsPermission returns a pointer to the given team access project type.
func WorkspaceRunsPermission(v WorkspaceRunsPermissionType) *WorkspaceRunsPermissionType {
return &v
}
// WorkspaceSentinelMocksPermission returns a pointer to the given team access project type.
func WorkspaceSentinelMocksPermission(v WorkspaceSentinelMocksPermissionType) *WorkspaceSentinelMocksPermissionType {
return &v
}
// WorkspaceStateVersionsPermission returns a pointer to the given team access project type.
func WorkspaceStateVersionsPermission(v WorkspaceStateVersionsPermissionType) *WorkspaceStateVersionsPermissionType {
return &v
}
// WorkspaceStateVersionsPermission returns a pointer to the given team access project type.
func WorkspaceVariablesPermission(v WorkspaceVariablesPermissionType) *WorkspaceVariablesPermissionType {
return &v
}
// RunsPermission returns a pointer to the given team runs permission type.
func RunsPermission(v RunsPermissionType) *RunsPermissionType {
return &v
}
// VariablesPermission returns a pointer to the given team variables permission type.
func VariablesPermission(v VariablesPermissionType) *VariablesPermissionType {
return &v
}
// StateVersionsPermission returns a pointer to the given team state versions permission type.
func StateVersionsPermission(v StateVersionsPermissionType) *StateVersionsPermissionType {
return &v
}
// SentinelMocksPermission returns a pointer to the given team Sentinel mocks permission type.
func SentinelMocksPermission(v SentinelMocksPermissionType) *SentinelMocksPermissionType {
return &v
}
// AuthPolicy returns a pointer to the given authentication poliy.
func AuthPolicy(v AuthPolicyType) *AuthPolicyType {
return &v
}
// Bool returns a pointer to the given bool
func Bool(v bool) *bool {
return &v
}
// Category returns a pointer to the given category type.
func Category(v CategoryType) *CategoryType {
return &v
}
// EnforcementMode returns a pointer to the given enforcement level.
func EnforcementMode(v EnforcementLevel) *EnforcementLevel {
return &v
}
// Int returns a pointer to the given int.
func Int(v int) *int {
return &v
}
// Int64 returns a pointer to the given int64.
func Int64(v int64) *int64 {
return &v
}
// NotificationDestination returns a pointer to the given notification configuration destination type
func NotificationDestination(v NotificationDestinationType) *NotificationDestinationType {
return &v
}
// PlanExportType returns a pointer to the given plan export data type.
func PlanExportType(v PlanExportDataType) *PlanExportDataType {
return &v
}
// ServiceProvider returns a pointer to the given service provider type.
func ServiceProvider(v ServiceProviderType) *ServiceProviderType {
return &v
}
// SMTPAuthValue returns a pointer to a given smtp auth type.
func SMTPAuthValue(v SMTPAuthType) *SMTPAuthType {
return &v
}
// String returns a pointer to the given string.
func String(v string) *string {
return &v
}
// SAMLProvider returns a pointer to the given SAML provider type.
func SAMLProvider(v SAMLProviderType) *SAMLProviderType {
return &v
}
func NullableBool(v bool) jsonapi.NullableAttr[bool] {
return jsonapi.NewNullableAttrWithValue[bool](v)
}
func NullBool() jsonapi.NullableAttr[bool] {
return jsonapi.NewNullNullableAttr[bool]()
}
func NullableTime(v time.Time) jsonapi.NullableAttr[time.Time] {
return jsonapi.NewNullableAttrWithValue[time.Time](v)
}
func NullTime() jsonapi.NullableAttr[time.Time] {
return jsonapi.NewNullNullableAttr[time.Time]()
}
// Ptr returns a pointer to the given value of any type.
func Ptr[T any](v T) *T {
return &v
}
================================================
FILE: user.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
)
// Compile-time proof of interface implementation.
var _ Users = (*users)(nil)
// Users describes all the user related methods that the Terraform
// Enterprise API supports.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/account
type Users interface {
// ReadCurrent reads the details of the currently authenticated user.
ReadCurrent(ctx context.Context) (*User, error)
// UpdateCurrent updates attributes of the currently authenticated user.
UpdateCurrent(ctx context.Context, options UserUpdateOptions) (*User, error)
}
// users implements Users.
type users struct {
client *Client
}
// User represents a Terraform Enterprise user.
type User struct {
ID string `jsonapi:"primary,users"`
AvatarURL string `jsonapi:"attr,avatar-url"`
Email string `jsonapi:"attr,email"`
IsServiceAccount bool `jsonapi:"attr,is-service-account"`
TwoFactor *TwoFactor `jsonapi:"attr,two-factor"`
UnconfirmedEmail string `jsonapi:"attr,unconfirmed-email"`
Username string `jsonapi:"attr,username"`
V2Only bool `jsonapi:"attr,v2-only"`
// Deprecated: IsSiteAdmin was deprecated in v202406 and will be removed in a future version of Terraform Enterprise
IsSiteAdmin *bool `jsonapi:"attr,is-site-admin"`
IsAdmin *bool `jsonapi:"attr,is-admin"`
IsSsoLogin *bool `jsonapi:"attr,is-sso-login"`
Permissions *UserPermissions `jsonapi:"attr,permissions"`
// Relations
// AuthenticationTokens *AuthenticationTokens `jsonapi:"relation,authentication-tokens"`
}
// UserPermissions represents the user permissions.
type UserPermissions struct {
CanCreateOrganizations bool `jsonapi:"attr,can-create-organizations"`
CanChangeEmail bool `jsonapi:"attr,can-change-email"`
CanChangeUsername bool `jsonapi:"attr,can-change-username"`
CanManageUserTokens bool `jsonapi:"attr,can-manage-user-tokens"`
CanView2FaSettings bool `jsonapi:"attr,can-view2fa-settings"`
CanManageHcpAccount bool `jsonapi:"attr,can-manage-hcp-account"`
}
// TwoFactor represents the organization permissions.
type TwoFactor struct {
Enabled bool `jsonapi:"attr,enabled"`
Verified bool `jsonapi:"attr,verified"`
}
// UserUpdateOptions represents the options for updating a user.
type UserUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,users"`
// Optional: New username.
Username *string `jsonapi:"attr,username,omitempty"`
// Optional: New email address (must be consumed afterwards to take effect).
Email *string `jsonapi:"attr,email,omitempty"`
}
// ReadCurrent reads the details of the currently authenticated user.
func (s *users) ReadCurrent(ctx context.Context) (*User, error) {
req, err := s.client.NewRequest("GET", "account/details", nil)
if err != nil {
return nil, err
}
u := &User{}
err = req.Do(ctx, u)
if err != nil {
return nil, err
}
return u, nil
}
// UpdateCurrent updates attributes of the currently authenticated user.
func (s *users) UpdateCurrent(ctx context.Context, options UserUpdateOptions) (*User, error) {
req, err := s.client.NewRequest("PATCH", "account/update", &options)
if err != nil {
return nil, err
}
u := &User{}
err = req.Do(ctx, u)
if err != nil {
return nil, err
}
return u, nil
}
================================================
FILE: user_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUsersReadCurrent(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
u, err := client.Users.ReadCurrent(ctx)
require.NoError(t, err)
assert.NotEmpty(t, u.ID)
assert.NotEmpty(t, u.AvatarURL)
assert.NotEmpty(t, u.Username)
t.Run("two factor options are decoded", func(t *testing.T) {
assert.NotNil(t, u.TwoFactor)
})
t.Run("permissions are decoded", func(t *testing.T) {
assert.NotNil(t, u.Permissions)
})
}
func TestUsersUpdate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
uTest, err := client.Users.ReadCurrent(ctx)
require.NoError(t, err)
// Make sure we reset the current user when we're done.
defer func() {
_, err := client.Users.UpdateCurrent(ctx, UserUpdateOptions{
Email: String(uTest.Email),
Username: String(uTest.Username),
})
if err != nil {
t.Logf("Error updating user: %s", err)
}
}()
t.Run("without any options", func(t *testing.T) {
_, err := client.Users.UpdateCurrent(ctx, UserUpdateOptions{})
require.NoError(t, err)
u, err := client.Users.ReadCurrent(ctx)
require.NoError(t, err)
assert.Equal(t, u, uTest)
})
t.Run("with a new username", func(t *testing.T) {
_, err := client.Users.UpdateCurrent(ctx, UserUpdateOptions{
Username: String("NewTestUsername"),
})
require.NoError(t, err)
u, err := client.Users.ReadCurrent(ctx)
require.NoError(t, err)
assert.Equal(t, "NewTestUsername", u.Username)
})
t.Run("with a new email address", func(t *testing.T) {
_, err := client.Users.UpdateCurrent(ctx, UserUpdateOptions{
Email: String("newtestemail@hashicorp.com"),
})
require.NoError(t, err)
u, err := client.Users.ReadCurrent(ctx)
email := ""
if u.UnconfirmedEmail != "" {
email = u.UnconfirmedEmail
} else if u.Email != "" {
email = u.Email
} else {
t.Fatalf("cannot test with user %q because both email and unconfirmed email are empty", u.ID)
}
require.NoError(t, err)
assert.Equal(t, "newtestemail@hashicorp.com", email)
})
t.Run("with invalid email address", func(t *testing.T) {
u, err := client.Users.UpdateCurrent(ctx, UserUpdateOptions{
Email: String("notamailaddress"),
})
assert.Nil(t, u)
assert.Error(t, err)
})
}
================================================
FILE: user_token.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ UserTokens = (*userTokens)(nil)
// UserTokens describes all the user token related methods that the
// HCP Terraform and Terraform Enterprise API supports.
//
// TFE API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/user-tokens
type UserTokens interface {
// List all the tokens of the given user ID.
List(ctx context.Context, userID string) (*UserTokenList, error)
// Create a new user token
Create(ctx context.Context, userID string, options UserTokenCreateOptions) (*UserToken, error)
// Read a user token by its ID.
Read(ctx context.Context, tokenID string) (*UserToken, error)
// Delete a user token by its ID.
Delete(ctx context.Context, tokenID string) error
}
// userTokens implements UserTokens.
type userTokens struct {
client *Client
}
// UserTokenList is a list of tokens for the given user ID.
type UserTokenList struct {
*Pagination
Items []*UserToken
}
// CreatedByChoice is a choice type struct that represents the possible values
// within a polymorphic relation. If a value is available, exactly one field
// will be non-nil.
type CreatedByChoice struct {
Organization *Organization
Team *Team
User *User
}
// UserToken represents a Terraform Enterprise user token.
type UserToken struct {
ID string `jsonapi:"primary,authentication-tokens"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Description string `jsonapi:"attr,description"`
LastUsedAt time.Time `jsonapi:"attr,last-used-at,iso8601"`
Token string `jsonapi:"attr,token"`
ExpiredAt time.Time `jsonapi:"attr,expired-at,iso8601"`
CreatedBy *CreatedByChoice `jsonapi:"polyrelation,created-by"`
}
// UserTokenCreateOptions contains the options for creating a user token.
type UserTokenCreateOptions struct {
Description string `jsonapi:"attr,description,omitempty"`
// Optional: The token's expiration date.
// This feature is available in TFE release v202305-1 and later
ExpiredAt *time.Time `jsonapi:"attr,expired-at,iso8601,omitempty"`
}
// Create a new user token
func (s *userTokens) Create(ctx context.Context, userID string, options UserTokenCreateOptions) (*UserToken, error) {
if !validStringID(&userID) {
return nil, ErrInvalidUserID
}
u := fmt.Sprintf("users/%s/authentication-tokens", url.PathEscape(userID))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
ut := &UserToken{}
err = req.Do(ctx, ut)
if err != nil {
return nil, err
}
return ut, err
}
// List shows existing user tokens
func (s *userTokens) List(ctx context.Context, userID string) (*UserTokenList, error) {
if !validStringID(&userID) {
return nil, ErrInvalidUserID
}
u := fmt.Sprintf("users/%s/authentication-tokens", url.PathEscape(userID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
tl := &UserTokenList{}
err = req.Do(ctx, tl)
if err != nil {
return nil, err
}
return tl, err
}
// Read a user token by its ID.
func (s *userTokens) Read(ctx context.Context, tokenID string) (*UserToken, error) {
if !validStringID(&tokenID) {
return nil, ErrInvalidTokenID
}
u := fmt.Sprintf(AuthenticationTokensPath, url.PathEscape(tokenID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
tt := &UserToken{}
err = req.Do(ctx, tt)
if err != nil {
return nil, err
}
return tt, err
}
// Delete a user token by its ID.
func (s *userTokens) Delete(ctx context.Context, tokenID string) error {
if !validStringID(&tokenID) {
return ErrInvalidTokenID
}
u := fmt.Sprintf(AuthenticationTokensPath, url.PathEscape(tokenID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
================================================
FILE: user_token_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestUserTokens_List tests listing user tokens
func TestUserTokens_List(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
user, err := client.Users.ReadCurrent(ctx)
if err != nil {
t.Fatal(err)
}
token, cleanupFunc := createToken(t, client, user)
defer cleanupFunc()
t.Run("listing existing tokens", func(t *testing.T) {
ctx := context.Background()
tl, err := client.UserTokens.List(ctx, user.ID)
require.NoError(t, err)
var found bool
for _, j := range tl.Items {
if j.ID == token.ID {
found = true
break
}
}
if !found {
t.Fatalf("token (%s) not found in token list", token.ID)
}
})
}
// TestUserTokens_Create tests basic creation of user tokens
func TestUserTokens_Create(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
user, err := client.Users.ReadCurrent(ctx)
if err != nil {
t.Fatal(err)
}
// collect the created tokens for revoking after the test
var tokens []string
defer func(t *testing.T) {
for _, token := range tokens {
err := client.UserTokens.Delete(ctx, token)
if err != nil {
t.Fatalf("Error deleting token in cleanup:%s", err)
}
}
}(t)
t.Run("create token with no description", func(t *testing.T) {
token, err := client.UserTokens.Create(ctx, user.ID, UserTokenCreateOptions{})
tokens = append(tokens, token.ID)
if err != nil {
t.Fatal(err)
}
})
t.Run("create token with description", func(t *testing.T) {
token, err := client.UserTokens.Create(ctx, user.ID, UserTokenCreateOptions{
Description: fmt.Sprintf("go-tfe-user-token-test-%s", randomString(t)),
})
tokens = append(tokens, token.ID)
if err != nil {
t.Fatal(err)
}
})
t.Run("create token without an expiration date", func(t *testing.T) {
token, err := client.UserTokens.Create(ctx, user.ID, UserTokenCreateOptions{})
tokens = append(tokens, token.ID)
if err != nil {
t.Fatal(err)
}
assert.NotZero(t, token.ExpiredAt)
expectedExpiry := token.CreatedAt.AddDate(defaultTokenExpirationYears, 0, 0)
// Allow a small buffer (1 minute) for timestamp precision differences.
assert.WithinDuration(t, expectedExpiry, token.ExpiredAt, time.Minute)
})
t.Run("create token with an expiration date", func(t *testing.T) {
currentTime := time.Now().UTC().Truncate(time.Second)
oneDayLater := currentTime.Add(24 * time.Hour)
token, err := client.UserTokens.Create(ctx, user.ID, UserTokenCreateOptions{
ExpiredAt: &oneDayLater,
})
tokens = append(tokens, token.ID)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, token.ExpiredAt, oneDayLater)
})
}
// TestUserTokens_Read tests basic creation of user tokens
func TestUserTokens_Read(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
user, err := client.Users.ReadCurrent(ctx)
if err != nil {
t.Fatal(err)
}
token, tokenCleanupFunc := createToken(t, client, user)
defer tokenCleanupFunc()
t.Run("read token", func(t *testing.T) {
to, err := client.UserTokens.Read(ctx, token.ID)
if err != nil {
t.Fatalf("expected to read token (%s), got error: %s", token.ID, err)
}
// The initial API call to create a token will return a value in the token
// object. Empty that out for comparison
token.Token = ""
assert.Equal(t, token, to)
requireExactlyOneNotEmpty(t, token.CreatedBy.Organization, token.CreatedBy.Team, token.CreatedBy.User)
})
}
// createToken is a helper method to create a valid token for a given user,
// which returns both the token and a function to revoke it
func createToken(t *testing.T, client *Client, user *User) (*UserToken, func()) {
t.Helper()
ctx := context.Background()
if user == nil {
t.Fatal("Nil user in createToken")
}
token, err := client.UserTokens.Create(ctx, user.ID, UserTokenCreateOptions{
Description: fmt.Sprintf("go-tfe-user-token-test-%s", randomString(t)),
})
if err != nil {
t.Fatal(err)
}
return token, func() {
if err := client.UserTokens.Delete(ctx, token.ID); err != nil {
t.Errorf("Error destroying token! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"Token: %s\nError: %s", token.ID, err)
}
}
}
================================================
FILE: validations.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"net/mail"
"regexp"
version "github.com/hashicorp/go-version"
)
// A regular expression used to validate common string ID patterns.
var reStringID = regexp.MustCompile(`^[^/\s]+$`)
// validEmail checks if the given input is a correct email
func validEmail(v string) bool {
_, err := mail.ParseAddress(v)
return err == nil
}
// validString checks if the given input is present and non-empty.
func validString(v *string) bool {
return v != nil && *v != ""
}
// validStringID checks if the given string pointer is non-nil and
// contains a typical string identifier.
func validStringID(v *string) bool {
return v != nil && reStringID.MatchString(*v)
}
// validVersion checks if the given input is a valid version.
func validVersion(v string) bool {
_, err := version.NewVersion(v)
return err == nil
}
================================================
FILE: validations_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestValidStringID(t *testing.T) {
t.Parallel()
type testCase struct {
externalID *string
expectedValue bool
}
unifiedTeamID := "iam.group:kmpwhkwf6tkgWzgJKPcP"
unifiedProjectID := "616f63c1-3ef5-46a5-b5e8-6d1d86c3f93f"
nonUnifiedID := "prj-AywVvpbtLQTcwf8K"
invalidID := "test/with-a-slash"
invalidIDWithSpace := "test with-space"
cases := map[string]testCase{
"external-id-is-nil": {externalID: nil, expectedValue: false},
"external-id-is-empty-string": {externalID: new(string), expectedValue: false},
"external-id-is-invalid-with-slash": {externalID: &invalidID, expectedValue: false},
"external-id-is-invalid-with-space": {externalID: &invalidIDWithSpace, expectedValue: false},
"external-id-is-unified-team-id": {externalID: &unifiedTeamID, expectedValue: true},
"external-id-is-unified-project-id": {externalID: &unifiedProjectID, expectedValue: true},
"external-id-is-non-unified": {externalID: &nonUnifiedID, expectedValue: true},
}
for name, tcase := range cases {
t.Run(name, func(tt *testing.T) {
actual := validStringID(tcase.externalID)
assert.Equal(tt, tcase.expectedValue, actual)
})
}
}
================================================
FILE: variable.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ Variables = (*variables)(nil)
// Variables describes all the variable related methods that the Terraform
// Enterprise API supports.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/workspace-variables
type Variables interface {
// List all the variables associated with the given workspace (doesn't include variables inherited from varsets).
List(ctx context.Context, workspaceID string, options *VariableListOptions) (*VariableList, error)
// ListAll all the variables associated with the given workspace including variables inherited from varsets.
ListAll(ctx context.Context, workspaceID string, options *VariableListOptions) (*VariableList, error)
// Create is used to create a new variable.
Create(ctx context.Context, workspaceID string, options VariableCreateOptions) (*Variable, error)
// Read a variable by its ID.
Read(ctx context.Context, workspaceID string, variableID string) (*Variable, error)
// Update values of an existing variable.
Update(ctx context.Context, workspaceID string, variableID string, options VariableUpdateOptions) (*Variable, error)
// Delete a variable by its ID.
Delete(ctx context.Context, workspaceID string, variableID string) error
}
// variables implements Variables.
type variables struct {
client *Client
}
// CategoryType represents a category type.
type CategoryType string
// List all available categories.
const (
CategoryEnv CategoryType = "env"
CategoryPolicySet CategoryType = "policy-set"
CategoryTerraform CategoryType = "terraform"
)
// VariableList represents a list of variables.
type VariableList struct {
*Pagination
Items []*Variable
}
// Variable represents a Terraform Enterprise variable.
type Variable struct {
ID string `jsonapi:"primary,vars"`
Key string `jsonapi:"attr,key"`
Value string `jsonapi:"attr,value"`
Description string `jsonapi:"attr,description"`
Category CategoryType `jsonapi:"attr,category"`
HCL bool `jsonapi:"attr,hcl"`
Sensitive bool `jsonapi:"attr,sensitive"`
VersionID string `jsonapi:"attr,version-id"`
// Relations
Workspace *Workspace `jsonapi:"relation,configurable"`
}
// VariableListOptions represents the options for listing variables.
type VariableListOptions struct {
ListOptions
}
// VariableCreateOptions represents the options for creating a new variable.
type VariableCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,vars"`
// Required: The name of the variable.
Key *string `jsonapi:"attr,key"`
// Optional: The value of the variable.
Value *string `jsonapi:"attr,value,omitempty"`
// Optional: The description of the variable.
Description *string `jsonapi:"attr,description,omitempty"`
// Required: Whether this is a Terraform or environment variable.
Category *CategoryType `jsonapi:"attr,category"`
// Optional: Whether to evaluate the value of the variable as a string of HCL code.
HCL *bool `jsonapi:"attr,hcl,omitempty"`
// Optional: Whether the value is sensitive.
Sensitive *bool `jsonapi:"attr,sensitive,omitempty"`
}
// VariableUpdateOptions represents the options for updating a variable.
type VariableUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,vars"`
// The name of the variable.
Key *string `jsonapi:"attr,key,omitempty"`
// The value of the variable.
Value *string `jsonapi:"attr,value,omitempty"`
// The description of the variable.
Description *string `jsonapi:"attr,description,omitempty"`
// Whether this is a Terraform or environment variable.
Category *CategoryType `jsonapi:"attr,category,omitempty"`
// Whether to evaluate the value of the variable as a string of HCL code.
HCL *bool `jsonapi:"attr,hcl,omitempty"`
// Whether the value is sensitive.
Sensitive *bool `jsonapi:"attr,sensitive,omitempty"`
}
// List all the variables associated with the given workspace (doesn't include variables inherited from varsets).
func (s *variables) List(ctx context.Context, workspaceID string, options *VariableListOptions) (*VariableList, error) {
return s.getList(ctx, workspaceID, options, "workspaces/%s/vars")
}
// ListAll the variables associated with the given workspace including variables inherited from varsets.
func (s *variables) ListAll(ctx context.Context, workspaceID string, options *VariableListOptions) (*VariableList, error) {
return s.getList(ctx, workspaceID, options, "workspaces/%s/all-vars")
}
func (s *variables) getList(ctx context.Context, workspaceID string, options *VariableListOptions, path string) (*VariableList, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
u := fmt.Sprintf(path, url.PathEscape(workspaceID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
vl := &VariableList{}
err = req.Do(ctx, vl)
if err != nil {
return nil, err
}
return vl, nil
}
// Create is used to create a new variable.
func (s *variables) Create(ctx context.Context, workspaceID string, options VariableCreateOptions) (*Variable, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("workspaces/%s/vars", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
v := &Variable{}
err = req.Do(ctx, v)
if err != nil {
return nil, err
}
return v, nil
}
// Read a variable by its ID.
func (s *variables) Read(ctx context.Context, workspaceID, variableID string) (*Variable, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
if !validStringID(&variableID) {
return nil, ErrInvalidVariableID
}
u := fmt.Sprintf("workspaces/%s/vars/%s", url.PathEscape(workspaceID), url.PathEscape(variableID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
v := &Variable{}
err = req.Do(ctx, v)
if err != nil {
return nil, err
}
return v, err
}
// Update values of an existing variable.
func (s *variables) Update(ctx context.Context, workspaceID, variableID string, options VariableUpdateOptions) (*Variable, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
if !validStringID(&variableID) {
return nil, ErrInvalidVariableID
}
u := fmt.Sprintf("workspaces/%s/vars/%s", url.PathEscape(workspaceID), url.PathEscape(variableID))
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
v := &Variable{}
err = req.Do(ctx, v)
if err != nil {
return nil, err
}
return v, nil
}
// Delete a variable by its ID.
func (s *variables) Delete(ctx context.Context, workspaceID, variableID string) error {
if !validStringID(&workspaceID) {
return ErrInvalidWorkspaceID
}
if !validStringID(&variableID) {
return ErrInvalidVariableID
}
u := fmt.Sprintf("workspaces/%s/vars/%s", url.PathEscape(workspaceID), url.PathEscape(variableID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o VariableCreateOptions) valid() error {
if !validString(o.Key) {
return ErrRequiredKey
}
if o.Category == nil {
return ErrRequiredCategory
}
return nil
}
================================================
FILE: variable_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestVariablesList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
vTest1, vTestCleanup1 := createVariable(t, client, wTest)
defer vTestCleanup1()
vTest2, vTestCleanup2 := createVariable(t, client, wTest)
defer vTestCleanup2()
t.Run("without list options", func(t *testing.T) {
vl, err := client.Variables.List(ctx, wTest.ID, nil)
require.NoError(t, err)
assert.Contains(t, vl.Items, vTest1)
assert.Contains(t, vl.Items, vTest2)
t.Skip("paging not supported yet in API")
assert.Equal(t, 1, vl.CurrentPage)
assert.Equal(t, 2, vl.TotalCount)
})
t.Run("with list options", func(t *testing.T) {
t.Skip("paging not supported yet in API")
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
vl, err := client.Variables.List(ctx, wTest.ID, &VariableListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, vl.Items)
assert.Equal(t, 999, vl.CurrentPage)
assert.Equal(t, 2, vl.TotalCount)
})
t.Run("when workspace ID is invalid ID", func(t *testing.T) {
vl, err := client.Variables.List(ctx, badIdentifier, nil)
assert.Nil(t, vl)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
}
func TestVariablesListAll(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
prjTest, prjTestCleanup := createProject(t, client, orgTest)
t.Cleanup(prjTestCleanup)
wTest, wTestCleanup := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{
Name: String(randomString(t)),
Project: prjTest,
})
t.Cleanup(wTestCleanup)
orgVarset, orgVarsetCleanup := createVariableSet(t, client, orgTest, VariableSetCreateOptions{})
t.Cleanup(orgVarsetCleanup)
prjVarset, prjVarsetCleanup := createVariableSet(t, client, orgTest, VariableSetCreateOptions{
Parent: &Parent{
Organization: orgTest,
Project: prjTest,
},
})
t.Cleanup(prjVarsetCleanup)
glVar, glVarCleanup := createVariableSetVariable(t, client, orgVarset, VariableSetVariableCreateOptions{
Key: String("key1"),
Value: String("gl_value1"),
Category: Category(CategoryTerraform),
Description: String(randomString(t)),
})
t.Cleanup(glVarCleanup)
glVarOverwrite, glVarOverwriteCleanup := createVariableSetVariable(t, client, orgVarset, VariableSetVariableCreateOptions{
Key: String("key2"),
Value: String("gl_value2"),
Category: Category(CategoryTerraform),
Description: String(randomString(t)),
})
t.Cleanup(glVarOverwriteCleanup)
prjVar, prjVarCleanup := createVariableSetVariable(t, client, prjVarset, VariableSetVariableCreateOptions{
Key: String("key3"),
Value: String("prj_value3"),
Category: Category(CategoryTerraform),
Description: String(randomString(t)),
})
t.Cleanup(prjVarCleanup)
prjVarOverwrite, prjVarOverwriteCleanup := createVariableSetVariable(t, client, prjVarset, VariableSetVariableCreateOptions{
Key: String("key4"),
Value: String("prj_value4"),
Category: Category(CategoryTerraform),
Description: String(randomString(t)),
})
t.Cleanup(prjVarOverwriteCleanup)
wsVar1, wsVar1Cleanup := createVariableWithOptions(t, client, wTest, VariableCreateOptions{
Key: String("key2"),
Value: String("ws_value2"),
Category: Category(CategoryTerraform),
Description: String(randomString(t)),
})
t.Cleanup(wsVar1Cleanup)
wsVar2, wsVar2Cleanup := createVariableWithOptions(t, client, wTest, VariableCreateOptions{
Key: String("key4"),
Value: String("ws_value4"),
Category: Category(CategoryTerraform),
Description: String(randomString(t)),
})
t.Cleanup(wsVar2Cleanup)
wsVar3, wsVar3Cleanup := createVariableWithOptions(t, client, wTest, VariableCreateOptions{
Key: String("key5"),
Value: String("ws_value5"),
Category: Category(CategoryTerraform),
Description: String(randomString(t)),
})
t.Cleanup(wsVar3Cleanup)
applyVariableSetToWorkspace(t, client, orgVarset.ID, wTest.ID)
applyVariableSetToWorkspace(t, client, prjVarset.ID, wTest.ID)
t.Run("when /workspaces/{external_id}/all-vars API is called", func(t *testing.T) {
vl, err := client.Variables.ListAll(ctx, wTest.ID, nil)
require.NoError(t, err)
assert.NotNilf(t, vl, "expected to get a non-empty variables list")
variableIDToValueMap := make(map[string]string)
for _, variable := range vl.Items {
variableIDToValueMap[variable.ID] = variable.Value
}
assert.Equal(t, len(vl.Items), 5)
assert.NotContains(t, variableIDToValueMap, glVarOverwrite.ID)
assert.NotContains(t, variableIDToValueMap, prjVarOverwrite.ID)
assert.Contains(t, variableIDToValueMap, glVar.ID)
assert.Contains(t, variableIDToValueMap, prjVar.ID)
assert.Contains(t, variableIDToValueMap, wsVar1.ID)
assert.Contains(t, variableIDToValueMap, wsVar2.ID)
assert.Contains(t, variableIDToValueMap, wsVar3.ID)
assert.Equal(t, glVar.Value, variableIDToValueMap[glVar.ID])
assert.Equal(t, prjVar.Value, variableIDToValueMap[prjVar.ID])
assert.Equal(t, wsVar1.Value, variableIDToValueMap[wsVar1.ID])
assert.Equal(t, wsVar2.Value, variableIDToValueMap[wsVar2.ID])
assert.Equal(t, wsVar3.Value, variableIDToValueMap[wsVar3.ID])
})
t.Run("when workspace ID is invalid ID", func(t *testing.T) {
vl, err := client.Variables.ListAll(ctx, badIdentifier, nil)
assert.Nilf(t, vl, "expected variables list to be nil")
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
}
func TestVariablesCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
wTest, wTestCleanup := createWorkspace(t, client, nil)
defer wTestCleanup()
t.Run("with valid options", func(t *testing.T) {
options := VariableCreateOptions{
Key: String(randomString(t)),
Value: String(randomString(t)),
Category: Category(CategoryTerraform),
Description: String(randomString(t)),
}
v, err := client.Variables.Create(ctx, wTest.ID, options)
require.NoError(t, err)
// Refresh workspace once the variable is created.
reWorkspace, err := client.Workspaces.ReadByID(ctx, wTest.ID)
require.NoError(t, err)
assert.NotEmpty(t, v.ID)
assert.Equal(t, *options.Key, v.Key)
assert.Equal(t, *options.Value, v.Value)
assert.Equal(t, *options.Description, v.Description)
assert.Equal(t, *options.Category, v.Category)
// The workspace isn't returned correcly by the API.
// assert.Equal(t, *options.Workspace, v.Workspace)
assert.NotEmpty(t, v.VersionID)
// Validate that the same Variable is now listed in Workspace relations.
assert.NotEmpty(t, reWorkspace.Variables)
assert.Equal(t, reWorkspace.Variables[0].ID, v.ID)
})
t.Run("when options has an empty string value", func(t *testing.T) {
options := VariableCreateOptions{
Key: String(randomString(t)),
Value: String(""),
Description: String(randomString(t)),
Category: Category(CategoryTerraform),
}
v, err := client.Variables.Create(ctx, wTest.ID, options)
require.NoError(t, err)
assert.NotEmpty(t, v.ID)
assert.Equal(t, *options.Key, v.Key)
assert.Equal(t, *options.Value, v.Value)
assert.Equal(t, *options.Description, v.Description)
assert.Equal(t, *options.Category, v.Category)
assert.NotEmpty(t, v.VersionID)
})
t.Run("when options has an empty string description", func(t *testing.T) {
options := VariableCreateOptions{
Key: String(randomString(t)),
Value: String(randomString(t)),
Description: String(""),
Category: Category(CategoryTerraform),
}
v, err := client.Variables.Create(ctx, wTest.ID, options)
require.NoError(t, err)
assert.NotEmpty(t, v.ID)
assert.Equal(t, *options.Key, v.Key)
assert.Equal(t, *options.Value, v.Value)
assert.Equal(t, *options.Description, v.Description)
assert.Equal(t, *options.Category, v.Category)
assert.NotEmpty(t, v.VersionID)
})
t.Run("when options has a too-long description", func(t *testing.T) {
options := VariableCreateOptions{
Key: String(randomString(t)),
Value: String(randomString(t)),
Description: String("tortor aliquam nulla go lint is fussy about spelling cras fermentum odio eu feugiat pretium nibh ipsum consequat nisl vel pretium lectus quam id leo in vitae turpis massa sed elementum tempus egestas sed sed risus pretium quam vulputate dignissim suspendisse in est ante in nibh mauris cursus mattis molestie a iaculis at erat pellentesque adipiscing commodo elit at imperdiet dui accumsan sit amet nulla redacted morbi tempus iaculis urna id volutpat lacus laoreet non curabitur gravida arcu ac tortor dignissim convallis aenean et tortor"),
Category: Category(CategoryTerraform),
}
_, err := client.Variables.Create(ctx, wTest.ID, options)
assert.Error(t, err)
})
t.Run("when options is missing value", func(t *testing.T) {
options := VariableCreateOptions{
Key: String(randomString(t)),
Category: Category(CategoryTerraform),
}
v, err := client.Variables.Create(ctx, wTest.ID, options)
require.NoError(t, err)
assert.NotEmpty(t, v.ID)
assert.Equal(t, *options.Key, v.Key)
assert.Equal(t, "", v.Value)
assert.Equal(t, *options.Category, v.Category)
assert.NotEmpty(t, v.VersionID)
})
t.Run("when options is missing key", func(t *testing.T) {
options := VariableCreateOptions{
Value: String(randomString(t)),
Category: Category(CategoryTerraform),
}
_, err := client.Variables.Create(ctx, wTest.ID, options)
assert.Equal(t, err, ErrRequiredKey)
})
t.Run("when options has an empty key", func(t *testing.T) {
options := VariableCreateOptions{
Key: String(""),
Value: String(randomString(t)),
Category: Category(CategoryTerraform),
}
_, err := client.Variables.Create(ctx, wTest.ID, options)
assert.Equal(t, err, ErrRequiredKey)
})
t.Run("when options is missing category", func(t *testing.T) {
options := VariableCreateOptions{
Key: String(randomString(t)),
Value: String(randomString(t)),
}
_, err := client.Variables.Create(ctx, wTest.ID, options)
assert.Equal(t, err, ErrRequiredCategory)
})
t.Run("when workspace ID is invalid", func(t *testing.T) {
options := VariableCreateOptions{
Key: String(randomString(t)),
Value: String(randomString(t)),
Category: Category(CategoryTerraform),
}
_, err := client.Variables.Create(ctx, badIdentifier, options)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
}
func TestVariablesRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
vTest, vTestCleanup := createVariable(t, client, nil)
defer vTestCleanup()
t.Run("when the variable exists", func(t *testing.T) {
v, err := client.Variables.Read(ctx, vTest.Workspace.ID, vTest.ID)
require.NoError(t, err)
assert.Equal(t, vTest.ID, v.ID)
assert.Equal(t, vTest.Category, v.Category)
assert.Equal(t, vTest.HCL, v.HCL)
assert.Equal(t, vTest.Key, v.Key)
assert.Equal(t, vTest.Sensitive, v.Sensitive)
assert.Equal(t, vTest.Value, v.Value)
assert.Equal(t, vTest.VersionID, v.VersionID)
})
t.Run("when the variable does not exist", func(t *testing.T) {
v, err := client.Variables.Read(ctx, vTest.Workspace.ID, "nonexisting")
assert.Nil(t, v)
assert.Equal(t, ErrResourceNotFound, err)
})
t.Run("without a valid workspace ID", func(t *testing.T) {
v, err := client.Variables.Read(ctx, badIdentifier, vTest.ID)
assert.Nil(t, v)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
t.Run("without a valid variable ID", func(t *testing.T) {
v, err := client.Variables.Read(ctx, vTest.Workspace.ID, badIdentifier)
assert.Nil(t, v)
assert.Equal(t, err, ErrInvalidVariableID)
})
}
func TestVariablesUpdate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
vTest, vTestCleanup := createVariable(t, client, nil)
defer vTestCleanup()
t.Run("with valid options", func(t *testing.T) {
options := VariableUpdateOptions{
Key: String("newname"),
Value: String("newvalue"),
HCL: Bool(true),
}
v, err := client.Variables.Update(ctx, vTest.Workspace.ID, vTest.ID, options)
require.NoError(t, err)
assert.Equal(t, *options.Key, v.Key)
assert.Equal(t, *options.HCL, v.HCL)
assert.Equal(t, *options.Value, v.Value)
assert.NotEqual(t, vTest.VersionID, v.VersionID)
})
t.Run("when updating a subset of values", func(t *testing.T) {
options := VariableUpdateOptions{
Key: String("someothername"),
HCL: Bool(false),
}
v, err := client.Variables.Update(ctx, vTest.Workspace.ID, vTest.ID, options)
require.NoError(t, err)
assert.Equal(t, *options.Key, v.Key)
assert.Equal(t, *options.HCL, v.HCL)
assert.NotEqual(t, vTest.VersionID, v.VersionID)
})
t.Run("with sensitive set", func(t *testing.T) {
options := VariableUpdateOptions{
Sensitive: Bool(true),
}
v, err := client.Variables.Update(ctx, vTest.Workspace.ID, vTest.ID, options)
require.NoError(t, err)
assert.Equal(t, *options.Sensitive, v.Sensitive)
assert.Empty(t, v.Value) // Because its now sensitive
assert.NotEqual(t, vTest.VersionID, v.VersionID)
})
t.Run("with category set", func(t *testing.T) {
category := CategoryEnv
options := VariableUpdateOptions{
Category: &category,
}
v, err := client.Variables.Update(ctx, vTest.Workspace.ID, vTest.ID, options)
require.NoError(t, err)
assert.Equal(t, *options.Category, v.Category)
assert.NotEqual(t, vTest.VersionID, v.VersionID)
})
t.Run("without any changes", func(t *testing.T) {
vTest, vTestCleanup := createVariable(t, client, nil)
defer vTestCleanup()
v, err := client.Variables.Update(ctx, vTest.Workspace.ID, vTest.ID, VariableUpdateOptions{})
require.NoError(t, err)
assert.Equal(t, vTest.ID, v.ID)
assert.Equal(t, vTest.Key, v.Key)
assert.Equal(t, vTest.Value, v.Value)
assert.Equal(t, vTest.Description, v.Description)
assert.Equal(t, vTest.Category, v.Category)
assert.Equal(t, vTest.HCL, v.HCL)
assert.Equal(t, vTest.Sensitive, v.Sensitive)
assert.NotEqual(t, vTest.VersionID, v.VersionID)
})
t.Run("with invalid variable ID", func(t *testing.T) {
_, err := client.Variables.Update(ctx, badIdentifier, vTest.ID, VariableUpdateOptions{})
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
t.Run("with invalid variable ID", func(t *testing.T) {
_, err := client.Variables.Update(ctx, vTest.Workspace.ID, badIdentifier, VariableUpdateOptions{})
assert.Equal(t, err, ErrInvalidVariableID)
})
}
func TestVariablesDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
wTest, wTestCleanup := createWorkspace(t, client, nil)
defer wTestCleanup()
vTest, _ := createVariable(t, client, wTest)
t.Run("with valid options", func(t *testing.T) {
err := client.Variables.Delete(ctx, wTest.ID, vTest.ID)
require.NoError(t, err)
})
t.Run("with non existing variable ID", func(t *testing.T) {
err := client.Variables.Delete(ctx, wTest.ID, "nonexisting")
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("with invalid workspace ID", func(t *testing.T) {
err := client.Variables.Delete(ctx, badIdentifier, vTest.ID)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
t.Run("with invalid variable ID", func(t *testing.T) {
err := client.Variables.Delete(ctx, wTest.ID, badIdentifier)
assert.Equal(t, err, ErrInvalidVariableID)
})
}
================================================
FILE: variable_set.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ VariableSets = (*variableSets)(nil)
// VariableSets describes all the Variable Set related methods that the
// Terraform Enterprise API supports.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/variable-sets
type VariableSets interface {
// List all the variable sets within an organization.
List(ctx context.Context, organization string, options *VariableSetListOptions) (*VariableSetList, error)
// ListForWorkspace gets the associated variable sets for a workspace.
ListForWorkspace(ctx context.Context, workspaceID string, options *VariableSetListOptions) (*VariableSetList, error)
// ListForProject gets the associated variable sets for a project.
ListForProject(ctx context.Context, projectID string, options *VariableSetListOptions) (*VariableSetList, error)
// Create is used to create a new variable set.
Create(ctx context.Context, organization string, options *VariableSetCreateOptions) (*VariableSet, error)
// Read a variable set by its ID.
Read(ctx context.Context, variableSetID string, options *VariableSetReadOptions) (*VariableSet, error)
// Update an existing variable set.
Update(ctx context.Context, variableSetID string, options *VariableSetUpdateOptions) (*VariableSet, error)
// Delete a variable set by ID.
Delete(ctx context.Context, variableSetID string) error
// Apply variable set to workspaces in the supplied list.
ApplyToWorkspaces(ctx context.Context, variableSetID string, options *VariableSetApplyToWorkspacesOptions) error
// Remove variable set from workspaces in the supplied list.
RemoveFromWorkspaces(ctx context.Context, variableSetID string, options *VariableSetRemoveFromWorkspacesOptions) error
// Apply variable set to projects in the supplied list.
ApplyToProjects(ctx context.Context, variableSetID string, options VariableSetApplyToProjectsOptions) error
// Remove variable set from projects in the supplied list.
RemoveFromProjects(ctx context.Context, variableSetID string, options VariableSetRemoveFromProjectsOptions) error
// Apply variable set to stacks in the supplied list.
ApplyToStacks(ctx context.Context, variableSetID string, options *VariableSetApplyToStacksOptions) error
// Remove variable set from stacks in the supplied list.
RemoveFromStacks(ctx context.Context, variableSetID string, options *VariableSetRemoveFromStacksOptions) error
// Update list of workspaces to which the variable set is applied to match the supplied list.
UpdateWorkspaces(ctx context.Context, variableSetID string, options *VariableSetUpdateWorkspacesOptions) (*VariableSet, error)
// Update list of stacks to which the variable set is applied to match the supplied list.
UpdateStacks(ctx context.Context, variableSetID string, options *VariableSetUpdateStacksOptions) (*VariableSet, error)
}
// variableSets implements VariableSets.
type variableSets struct {
client *Client
}
// VariableSetList represents a list of variable sets.
type VariableSetList struct {
*Pagination
Items []*VariableSet
}
// Parent represents the variable set's parent (currently only organizations and projects are supported).
// This relation is considered BETA, SUBJECT TO CHANGE, and likely unavailable to most users.
type Parent struct {
Organization *Organization
Project *Project
}
// VariableSet represents a Terraform Enterprise variable set.
type VariableSet struct {
ID string `jsonapi:"primary,varsets"`
Name string `jsonapi:"attr,name"`
Description string `jsonapi:"attr,description"`
Global bool `jsonapi:"attr,global"`
Priority bool `jsonapi:"attr,priority"`
// Relations
Organization *Organization `jsonapi:"relation,organization"`
// Optional: Parent represents the variable set's parent (currently only organizations and projects are supported).
// This relation is considered BETA, SUBJECT TO CHANGE, and likely unavailable to most users.
Parent *Parent `jsonapi:"polyrelation,parent"`
Workspaces []*Workspace `jsonapi:"relation,workspaces,omitempty"`
Projects []*Project `jsonapi:"relation,projects,omitempty"`
Stacks []*Stack `jsonapi:"relation,stacks,omitempty"`
Variables []*VariableSetVariable `jsonapi:"relation,vars,omitempty"`
}
// A list of relations to include. See available resources
// https://developer.hashicorp.com/terraform/enterprise/api-docs/admin/organizations#available-related-resources
type VariableSetIncludeOpt string
const (
VariableSetWorkspaces VariableSetIncludeOpt = "workspaces"
VariableSetProjects VariableSetIncludeOpt = "projects"
VariableSetStacks VariableSetIncludeOpt = "stacks"
VariableSetVars VariableSetIncludeOpt = "vars"
)
// VariableSetListOptions represents the options for listing variable sets.
type VariableSetListOptions struct {
ListOptions
Include string `url:"include"`
// Optional: A query string used to filter variable sets.
// Any variable sets with a name partially matching this value will be returned.
Query string `url:"q,omitempty"`
}
// VariableSetCreateOptions represents the options for creating a new variable set within in a organization.
type VariableSetCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,varsets"`
// The name of the variable set.
// Affects variable precedence when there are conflicts between Variable Sets
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/variable-sets#apply-variable-set-to-workspaces
Name *string `jsonapi:"attr,name"`
// A description to provide context for the variable set.
Description *string `jsonapi:"attr,description,omitempty"`
// If true the variable set is considered in all runs in the organization.
Global *bool `jsonapi:"attr,global,omitempty"`
// If true the variables in the set override any other variable values set
// in a more specific scope including values set on the command line.
Priority *bool `jsonapi:"attr,priority,omitempty"`
// Optional: Parent represents the variable set's parent (currently only organizations and projects are supported).
// This relation is considered BETA, SUBJECT TO CHANGE, and likely unavailable to most users.
Parent *Parent `jsonapi:"polyrelation,parent"`
}
// VariableSetReadOptions represents the options for reading variable sets.
type VariableSetReadOptions struct {
Include *[]VariableSetIncludeOpt `url:"include,omitempty"`
}
// VariableSetUpdateOptions represents the options for updating a variable set.
type VariableSetUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,varsets"`
// The name of the variable set.
// Affects variable precedence when there are conflicts between Variable Sets
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/variable-sets#apply-variable-set-to-workspaces
Name *string `jsonapi:"attr,name,omitempty"`
// A description to provide context for the variable set.
Description *string `jsonapi:"attr,description,omitempty"`
// If true the variable set is considered in all runs in the organization.
Global *bool `jsonapi:"attr,global,omitempty"`
// If true the variables in the set override any other variable values set
// in a more specific scope including values set on the command line.
Priority *bool `jsonapi:"attr,priority,omitempty"`
}
// VariableSetApplyToWorkspacesOptions represents the options for applying variable sets to workspaces.
type VariableSetApplyToWorkspacesOptions struct {
// The workspaces to apply the variable set to (additive).
Workspaces []*Workspace
}
// VariableSetRemoveFromWorkspacesOptions represents the options for removing variable sets from workspaces.
type VariableSetRemoveFromWorkspacesOptions struct {
// The workspaces to remove the variable set from.
Workspaces []*Workspace
}
// VariableSetApplyToProjectsOptions represents the options for applying variable sets to projects.
type VariableSetApplyToProjectsOptions struct {
// The projects to apply the variable set to (additive).
Projects []*Project
}
// VariableSetApplyToStacksOptions represents the options for applying variable sets to stacks.
type VariableSetApplyToStacksOptions struct {
// The stacks to apply the variable set to (additive).
Stacks []*Stack
}
// VariableSetRemoveFromProjectsOptions represents the options for removing variable sets from projects.
type VariableSetRemoveFromProjectsOptions struct {
// The projects to remove the variable set from.
Projects []*Project
}
// VariableSetRemoveFromStacksOptions represents the options for removing variable sets from stacks.
type VariableSetRemoveFromStacksOptions struct {
// The stacks to remove the variable set from.
Stacks []*Stack
}
// VariableSetUpdateWorkspacesOptions represents a subset of update options specifically for applying variable sets to workspaces
type VariableSetUpdateWorkspacesOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,varsets"`
// The workspaces to be applied to. An empty set means remove all applied
Workspaces []*Workspace `jsonapi:"relation,workspaces"`
}
// VariableSetUpdateStacksOptions represents a subset of update options specifically for applying variable sets to stacks
type VariableSetUpdateStacksOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,varsets"`
// The stacks to be applied to. An empty set means remove all applied
Stacks []*Stack `jsonapi:"relation,stacks"`
}
type privateVariableSetUpdateWorkspacesOptions struct {
Type string `jsonapi:"primary,varsets"`
Global bool `jsonapi:"attr,global"`
Workspaces []*Workspace `jsonapi:"relation,workspaces"`
}
type privateVariableSetUpdateStacksOptions struct {
Type string `jsonapi:"primary,varsets"`
Global bool `jsonapi:"attr,global"`
Stacks []*Stack `jsonapi:"relation,stacks"`
}
// List all Variable Sets in the organization
func (s *variableSets) List(ctx context.Context, organization string, options *VariableSetListOptions) (*VariableSetList, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if options != nil {
if err := options.valid(); err != nil {
return nil, err
}
}
u := fmt.Sprintf("organizations/%s/varsets", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
vl := &VariableSetList{}
err = req.Do(ctx, vl)
if err != nil {
return nil, err
}
return vl, nil
}
// ListForWorkspace gets the associated variable sets for a workspace.
func (s *variableSets) ListForWorkspace(ctx context.Context, workspaceID string, options *VariableSetListOptions) (*VariableSetList, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
if options != nil {
if err := options.valid(); err != nil {
return nil, err
}
}
u := fmt.Sprintf("workspaces/%s/varsets", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
vl := &VariableSetList{}
err = req.Do(ctx, vl)
if err != nil {
return nil, err
}
return vl, nil
}
// ListForProject gets the associated variable sets for a project.
func (s *variableSets) ListForProject(ctx context.Context, projectID string, options *VariableSetListOptions) (*VariableSetList, error) {
if !validStringID(&projectID) {
return nil, ErrInvalidProjectID
}
if options != nil {
if err := options.valid(); err != nil {
return nil, err
}
}
u := fmt.Sprintf("projects/%s/varsets", url.PathEscape(projectID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
vl := &VariableSetList{}
err = req.Do(ctx, vl)
if err != nil {
return nil, err
}
return vl, nil
}
// Create is used to create a new variable set.
func (s *variableSets) Create(ctx context.Context, organization string, options *VariableSetCreateOptions) (*VariableSet, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("organizations/%s/varsets", url.PathEscape(organization))
req, err := s.client.NewRequest("POST", u, options)
if err != nil {
return nil, err
}
vl := &VariableSet{}
err = req.Do(ctx, vl)
if err != nil {
return nil, err
}
return vl, nil
}
// Read is used to inspect a given variable set based on ID
func (s *variableSets) Read(ctx context.Context, variableSetID string, options *VariableSetReadOptions) (*VariableSet, error) {
if !validStringID(&variableSetID) {
return nil, ErrInvalidVariableSetID
}
u := fmt.Sprintf("varsets/%s", url.PathEscape(variableSetID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
vs := &VariableSet{}
err = req.Do(ctx, vs)
if err != nil {
return nil, err
}
return vs, err
}
// Update an existing variable set.
func (s *variableSets) Update(ctx context.Context, variableSetID string, options *VariableSetUpdateOptions) (*VariableSet, error) {
if !validStringID(&variableSetID) {
return nil, ErrInvalidVariableSetID
}
u := fmt.Sprintf("varsets/%s", url.PathEscape(variableSetID))
req, err := s.client.NewRequest("PATCH", u, options)
if err != nil {
return nil, err
}
v := &VariableSet{}
err = req.Do(ctx, v)
if err != nil {
return nil, err
}
return v, nil
}
// Delete a variable set by its ID.
func (s *variableSets) Delete(ctx context.Context, variableSetID string) error {
if !validStringID(&variableSetID) {
return ErrInvalidVariableSetID
}
u := fmt.Sprintf("varsets/%s", url.PathEscape(variableSetID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// Apply variable set to workspaces in the supplied list.
// Note: this method will return an error if the variable set has global = true.
func (s *variableSets) ApplyToWorkspaces(ctx context.Context, variableSetID string, options *VariableSetApplyToWorkspacesOptions) error {
if !validStringID(&variableSetID) {
return ErrInvalidVariableSetID
}
if err := options.valid(); err != nil {
return err
}
u := fmt.Sprintf("varsets/%s/relationships/workspaces", url.PathEscape(variableSetID))
req, err := s.client.NewRequest("POST", u, options.Workspaces)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// Remove variable set from workspaces in the supplied list.
// Note: this method will return an error if the variable set has global = true.
func (s *variableSets) RemoveFromWorkspaces(ctx context.Context, variableSetID string, options *VariableSetRemoveFromWorkspacesOptions) error {
if !validStringID(&variableSetID) {
return ErrInvalidVariableSetID
}
if err := options.valid(); err != nil {
return err
}
u := fmt.Sprintf("varsets/%s/relationships/workspaces", url.PathEscape(variableSetID))
req, err := s.client.NewRequest("DELETE", u, options.Workspaces)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// ApplyToProjects applies the variable set to projects in the supplied list.
// This method will return an error if the variable set has global = true.
func (s variableSets) ApplyToProjects(ctx context.Context, variableSetID string, options VariableSetApplyToProjectsOptions) error {
if !validStringID(&variableSetID) {
return ErrInvalidVariableSetID
}
if err := options.valid(); err != nil {
return err
}
u := fmt.Sprintf("varsets/%s/relationships/projects", url.PathEscape(variableSetID))
req, err := s.client.NewRequest("POST", u, options.Projects)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// RemoveFromProjects removes the variable set from projects in the supplied list.
// This method will return an error if the variable set has global = true.
func (s variableSets) RemoveFromProjects(ctx context.Context, variableSetID string, options VariableSetRemoveFromProjectsOptions) error {
if !validStringID(&variableSetID) {
return ErrInvalidVariableSetID
}
if err := options.valid(); err != nil {
return err
}
u := fmt.Sprintf("varsets/%s/relationships/projects", url.PathEscape(variableSetID))
req, err := s.client.NewRequest("DELETE", u, options.Projects)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// ApplyToStacks applies the variable set to stacks in the supplied list.
// This method will return an error if the variable set has global = true.
func (s *variableSets) ApplyToStacks(ctx context.Context, variableSetID string, options *VariableSetApplyToStacksOptions) error {
if !validStringID(&variableSetID) {
return ErrInvalidVariableSetID
}
if err := options.valid(); err != nil {
return err
}
u := fmt.Sprintf("varsets/%s/relationships/stacks", url.PathEscape(variableSetID))
req, err := s.client.NewRequest("POST", u, options.Stacks)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (s *variableSets) RemoveFromStacks(ctx context.Context, variableSetID string, options *VariableSetRemoveFromStacksOptions) error {
if !validStringID(&variableSetID) {
return ErrInvalidVariableSetID
}
if err := options.valid(); err != nil {
return err
}
u := fmt.Sprintf("varsets/%s/relationships/stacks", url.PathEscape(variableSetID))
req, err := s.client.NewRequest("DELETE", u, options.Stacks)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// Update variable set to be applied to only the workspaces in the supplied list.
func (s *variableSets) UpdateWorkspaces(ctx context.Context, variableSetID string, options *VariableSetUpdateWorkspacesOptions) (*VariableSet, error) {
if err := options.valid(); err != nil {
return nil, err
}
// Use private struct to ensure global is set to false when applying to workspaces
o := privateVariableSetUpdateWorkspacesOptions{
Global: bool(false),
Workspaces: options.Workspaces,
}
// We force inclusion of workspaces as that is the primary data for which we are concerned with confirming changes.
u := fmt.Sprintf("varsets/%s?include=%s", url.PathEscape(variableSetID), VariableSetWorkspaces)
req, err := s.client.NewRequest("PATCH", u, &o)
if err != nil {
return nil, err
}
v := &VariableSet{}
err = req.Do(ctx, v)
if err != nil {
return nil, err
}
return v, nil
}
// Update variable set to be applied to only the stacks in the supplied list.
func (s *variableSets) UpdateStacks(ctx context.Context, variableSetID string, options *VariableSetUpdateStacksOptions) (*VariableSet, error) {
if err := options.valid(); err != nil {
return nil, err
}
// Use private struct to ensure global is set to false when applying to stacks
o := privateVariableSetUpdateStacksOptions{
Global: bool(false),
Stacks: options.Stacks,
}
// We force inclusion of stacks as that is the primary data for which we are concerned with confirming changes.
u := fmt.Sprintf("varsets/%s?include=%s", url.PathEscape(variableSetID), VariableSetStacks)
req, err := s.client.NewRequest("PATCH", u, &o)
if err != nil {
return nil, err
}
v := &VariableSet{}
err = req.Do(ctx, v)
if err != nil {
return nil, err
}
return v, nil
}
func (o *VariableSetListOptions) valid() error {
return nil
}
func (o *VariableSetCreateOptions) valid() error {
if o == nil {
return nil
}
if !validString(o.Name) {
return ErrRequiredName
}
if o.Global == nil {
return ErrRequiredGlobalFlag
}
return nil
}
func (o *VariableSetApplyToWorkspacesOptions) valid() error {
for _, s := range o.Workspaces {
if !validStringID(&s.ID) {
return ErrRequiredWorkspaceID
}
}
return nil
}
func (o *VariableSetRemoveFromWorkspacesOptions) valid() error {
for _, s := range o.Workspaces {
if !validStringID(&s.ID) {
return ErrRequiredWorkspaceID
}
}
return nil
}
func (o *VariableSetApplyToProjectsOptions) valid() error {
for _, s := range o.Projects {
if !validStringID(&s.ID) {
return ErrRequiredProjectID
}
}
return nil
}
func (o VariableSetRemoveFromProjectsOptions) valid() error {
for _, s := range o.Projects {
if !validStringID(&s.ID) {
return ErrRequiredProjectID
}
}
return nil
}
func (o VariableSetApplyToStacksOptions) valid() error {
for _, s := range o.Stacks {
if !validStringID(&s.ID) {
return ErrRequiredStackID
}
}
return nil
}
func (o VariableSetRemoveFromStacksOptions) valid() error {
for _, s := range o.Stacks {
if !validStringID(&s.ID) {
return ErrRequiredStackID
}
}
return nil
}
func (o *VariableSetUpdateWorkspacesOptions) valid() error {
if o == nil || o.Workspaces == nil {
return ErrRequiredWorkspacesList
}
return nil
}
func (o *VariableSetUpdateStacksOptions) valid() error {
if o == nil || o.Stacks == nil {
return ErrRequiredStacksList
}
return nil
}
================================================
FILE: variable_set_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestVariableSetsList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
vsTest1, vsTestCleanup1 := createVariableSet(t, client, orgTest, VariableSetCreateOptions{})
t.Cleanup(vsTestCleanup1)
vsTest2, vsTestCleanup2 := createVariableSet(t, client, orgTest, VariableSetCreateOptions{})
t.Cleanup(vsTestCleanup2)
t.Run("without list options", func(t *testing.T) {
vsl, err := client.VariableSets.List(ctx, orgTest.Name, nil)
require.NoError(t, err)
require.NotEmpty(t, vsl.Items)
assert.Contains(t, vsl.Items, vsTest1)
assert.Contains(t, vsl.Items, vsTest2)
assert.Equal(t, 1, vsl.CurrentPage)
assert.Equal(t, 2, vsl.TotalCount)
})
t.Run("with list options", func(t *testing.T) {
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
vsl, err := client.VariableSets.List(ctx, orgTest.Name, &VariableSetListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, vsl.Items)
assert.Equal(t, 999, vsl.CurrentPage)
assert.Equal(t, 2, vsl.TotalCount)
})
t.Run("when Organization name is an invalid ID", func(t *testing.T) {
vsl, err := client.VariableSets.List(ctx, badIdentifier, nil)
assert.Nil(t, vsl)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("with query parameter", func(t *testing.T) {
vsl, err := client.VariableSets.List(ctx, orgTest.Name, &VariableSetListOptions{
Query: vsTest2.Name,
})
require.NoError(t, err)
assert.Len(t, vsl.Items, 1)
assert.Equal(t, vsTest2.ID, vsl.Items[0].ID)
})
}
func TestVariableSetsListForWorkspace(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
workspaceTest, workspaceTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(workspaceTestCleanup)
vsTest1, vsTestCleanup1 := createVariableSet(t, client, orgTest, VariableSetCreateOptions{})
t.Cleanup(vsTestCleanup1)
vsTest2, vsTestCleanup2 := createVariableSet(t, client, orgTest, VariableSetCreateOptions{})
t.Cleanup(vsTestCleanup2)
applyVariableSetToWorkspace(t, client, vsTest1.ID, workspaceTest.ID)
applyVariableSetToWorkspace(t, client, vsTest2.ID, workspaceTest.ID)
t.Run("without list options", func(t *testing.T) {
vsl, err := client.VariableSets.ListForWorkspace(ctx, workspaceTest.ID, nil)
require.NoError(t, err)
require.Len(t, vsl.Items, 2)
ids := []string{vsTest1.ID, vsTest2.ID}
for _, varset := range vsl.Items {
assert.Contains(t, ids, varset.ID)
}
})
t.Run("with list options", func(t *testing.T) {
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
vsl, err := client.VariableSets.ListForWorkspace(ctx, workspaceTest.ID, &VariableSetListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, vsl.Items)
assert.Equal(t, 999, vsl.CurrentPage)
assert.Equal(t, 2, vsl.TotalCount)
})
t.Run("when Workspace ID is an invalid ID", func(t *testing.T) {
vsl, err := client.VariableSets.ListForWorkspace(ctx, badIdentifier, nil)
assert.Nil(t, vsl)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
t.Run("with query parameter", func(t *testing.T) {
vsl, err := client.VariableSets.List(ctx, orgTest.Name, &VariableSetListOptions{
Query: vsTest2.Name,
})
require.NoError(t, err)
assert.Len(t, vsl.Items, 1)
assert.Equal(t, vsTest2.ID, vsl.Items[0].ID)
})
}
func TestVariableSetsListForProject(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
projectTest, projectTestCleanup := createProject(t, client, orgTest)
t.Cleanup(projectTestCleanup)
vsTest1, vsTestCleanup1 := createVariableSet(t, client, orgTest, VariableSetCreateOptions{})
t.Cleanup(vsTestCleanup1)
vsTest2, vsTestCleanup2 := createVariableSet(t, client, orgTest, VariableSetCreateOptions{})
t.Cleanup(vsTestCleanup2)
applyVariableSetToProject(t, client, vsTest1.ID, projectTest.ID)
applyVariableSetToProject(t, client, vsTest2.ID, projectTest.ID)
t.Run("without list options", func(t *testing.T) {
vsl, err := client.VariableSets.ListForProject(ctx, projectTest.ID, nil)
require.NoError(t, err)
require.Len(t, vsl.Items, 2)
ids := []string{vsTest1.ID, vsTest2.ID}
for _, varset := range vsl.Items {
assert.Contains(t, ids, varset.ID)
}
})
t.Run("with list options", func(t *testing.T) {
vsl, err := client.VariableSets.ListForProject(ctx, projectTest.ID, &VariableSetListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, vsl.Items)
assert.Equal(t, 999, vsl.CurrentPage)
assert.Equal(t, 2, vsl.TotalCount)
})
t.Run("when Project ID is an invalid ID", func(t *testing.T) {
vsl, err := client.VariableSets.ListForProject(ctx, badIdentifier, nil)
assert.Nil(t, vsl)
assert.EqualError(t, err, ErrInvalidProjectID.Error())
})
t.Run("with query parameter", func(t *testing.T) {
vsl, err := client.VariableSets.List(ctx, orgTest.Name, &VariableSetListOptions{
Query: vsTest2.Name,
})
require.NoError(t, err)
assert.Len(t, vsl.Items, 1)
assert.Equal(t, vsTest2.ID, vsl.Items[0].ID)
})
}
func TestVariableSetsCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
t.Run("with valid options", func(t *testing.T) {
options := VariableSetCreateOptions{
Name: String("varset"),
Description: String("a variable set"),
Global: Bool(false),
Priority: Bool(false),
}
vs, err := client.VariableSets.Create(ctx, orgTest.Name, &options)
require.NoError(t, err)
// Get refreshed view from the API
refreshed, err := client.VariableSets.Read(ctx, vs.ID, nil)
require.NoError(t, err)
for _, item := range []*VariableSet{
vs,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, *options.Name, item.Name)
assert.Equal(t, *options.Description, item.Description)
assert.Equal(t, *options.Global, item.Global)
assert.Equal(t, *options.Priority, item.Priority)
}
})
t.Run("when options is missing name", func(t *testing.T) {
vs, err := client.VariableSets.Create(ctx, "foo", &VariableSetCreateOptions{
Global: Bool(true),
})
assert.Nil(t, vs)
assert.EqualError(t, err, ErrRequiredName.Error())
})
t.Run("when options is missing global flag", func(t *testing.T) {
vs, err := client.VariableSets.Create(ctx, "foo", &VariableSetCreateOptions{
Name: String("foo"),
})
assert.Nil(t, vs)
assert.EqualError(t, err, ErrRequiredGlobalFlag.Error())
})
t.Run("when creating project-owned variable set", func(t *testing.T) {
skipUnlessBeta(t)
prjTest, prjTestCleanup := createProject(t, client, orgTest)
t.Cleanup(prjTestCleanup)
options := VariableSetCreateOptions{
Name: String("project-varset"),
Description: String("a project variable set"),
Global: Bool(false),
Parent: &Parent{
Project: prjTest,
},
}
vs, err := client.VariableSets.Create(ctx, orgTest.Name, &options)
require.NoError(t, err)
// Get refreshed view from the API
refreshed, err := client.VariableSets.Read(ctx, vs.ID, nil)
require.NoError(t, err)
for _, item := range []*VariableSet{
vs,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, *options.Name, item.Name)
assert.Equal(t, *options.Description, item.Description)
assert.Equal(t, *options.Global, item.Global)
assert.Equal(t, options.Parent.Project.ID, item.Parent.Project.ID)
}
})
}
func TestVariableSetsRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
vsTest, vsTestCleanup := createVariableSet(t, client, orgTest, VariableSetCreateOptions{})
t.Cleanup(vsTestCleanup)
t.Run("when the variable set exists", func(t *testing.T) {
vs, err := client.VariableSets.Read(ctx, vsTest.ID, nil)
require.NoError(t, err)
assert.Equal(t, vsTest, vs)
})
t.Run("when variable set does not exist", func(t *testing.T) {
vs, err := client.VariableSets.Read(ctx, "nonexisting", nil)
assert.Nil(t, vs)
assert.Error(t, err)
})
t.Run("with parent relationship", func(t *testing.T) {
skipUnlessBeta(t)
vs, err := client.VariableSets.Read(ctx, vsTest.ID, nil)
require.NoError(t, err)
assert.Equal(t, vsTest, vs)
assert.Equal(t, orgTest.Name, vs.Parent.Organization.Name)
})
}
func TestVariableSetsUpdate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
vsTest, _ := createVariableSet(t, client, orgTest, VariableSetCreateOptions{
Name: String("OriginalName"),
Description: String("Original Description"),
Global: Bool(false),
Priority: Bool(false),
})
t.Run("when updating a subset of values", func(t *testing.T) {
options := VariableSetUpdateOptions{
Name: String("UpdatedName"),
Description: String("Updated Description"),
Global: Bool(true),
Priority: Bool(true),
}
vsAfter, err := client.VariableSets.Update(ctx, vsTest.ID, &options)
require.NoError(t, err)
assert.Equal(t, *options.Name, vsAfter.Name)
assert.Equal(t, *options.Description, vsAfter.Description)
assert.Equal(t, *options.Global, vsAfter.Global)
assert.Equal(t, *options.Priority, vsAfter.Priority)
})
t.Run("when options has an invalid variable set ID", func(t *testing.T) {
vsAfter, err := client.VariableSets.Update(ctx, badIdentifier, &VariableSetUpdateOptions{
Name: String("UpdatedName"),
Description: String("Updated Description"),
Global: Bool(true),
})
assert.Nil(t, vsAfter)
assert.EqualError(t, err, ErrInvalidVariableSetID.Error())
})
}
func TestVariableSetsDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
// Do not defer cleanup since the next step in this test is to delete it
vsTest, _ := createVariableSet(t, client, orgTest, VariableSetCreateOptions{})
t.Run("with valid ID", func(t *testing.T) {
err := client.VariableSets.Delete(ctx, vsTest.ID)
require.NoError(t, err)
// Try loading the variable set - it should fail.
_, err = client.VariableSets.Read(ctx, vsTest.ID, nil)
assert.Equal(t, ErrResourceNotFound, err)
})
t.Run("when ID is invalid", func(t *testing.T) {
err := client.VariableSets.Delete(ctx, badIdentifier)
assert.EqualError(t, err, ErrInvalidVariableSetID.Error())
})
}
func TestVariableSetsApplyToAndRemoveFromWorkspaces(t *testing.T) {
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
vsTest, vsTestCleanup := createVariableSet(t, client, orgTest, VariableSetCreateOptions{})
t.Cleanup(vsTestCleanup)
wTest1, wTest1Cleanup := createWorkspace(t, client, orgTest)
defer wTest1Cleanup()
wTest2, wTest2Cleanup := createWorkspace(t, client, orgTest)
defer wTest2Cleanup()
t.Run("with first workspace added", func(t *testing.T) {
options := VariableSetApplyToWorkspacesOptions{
Workspaces: []*Workspace{wTest1},
}
err := client.VariableSets.ApplyToWorkspaces(ctx, vsTest.ID, &options)
require.NoError(t, err)
vsAfter, err := client.VariableSets.Read(ctx, vsTest.ID, nil)
require.NoError(t, err)
// Variable set should be applied to [wTest1]
assert.Equal(t, 1, len(vsAfter.Workspaces))
assert.Equal(t, wTest1.ID, vsAfter.Workspaces[0].ID)
})
t.Run("with second workspace added", func(t *testing.T) {
options := VariableSetApplyToWorkspacesOptions{
Workspaces: []*Workspace{wTest2},
}
err := client.VariableSets.ApplyToWorkspaces(ctx, vsTest.ID, &options)
require.NoError(t, err)
vsAfter, err := client.VariableSets.Read(ctx, vsTest.ID, nil)
require.NoError(t, err)
// Variable set should be applied to [wTest1, wTest2]
assert.Equal(t, 2, len(vsAfter.Workspaces))
wsIDs := []string{vsAfter.Workspaces[0].ID, vsAfter.Workspaces[1].ID}
assert.Contains(t, wsIDs, wTest1.ID)
assert.Contains(t, wsIDs, wTest2.ID)
})
t.Run("with first workspace removed", func(t *testing.T) {
options := VariableSetRemoveFromWorkspacesOptions{
Workspaces: []*Workspace{wTest1},
}
err := client.VariableSets.RemoveFromWorkspaces(ctx, vsTest.ID, &options)
require.NoError(t, err)
vsAfter, err := client.VariableSets.Read(ctx, vsTest.ID, nil)
require.NoError(t, err)
// Variable set should be applied to [wTest2]
assert.Equal(t, 1, len(vsAfter.Workspaces))
assert.Equal(t, wTest2.ID, vsAfter.Workspaces[0].ID)
})
t.Run("when variable set ID is invalid", func(t *testing.T) {
applyOptions := VariableSetApplyToWorkspacesOptions{
Workspaces: []*Workspace{wTest1},
}
err := client.VariableSets.ApplyToWorkspaces(ctx, badIdentifier, &applyOptions)
assert.EqualError(t, err, ErrInvalidVariableSetID.Error())
removeOptions := VariableSetRemoveFromWorkspacesOptions{
Workspaces: []*Workspace{wTest1},
}
err = client.VariableSets.RemoveFromWorkspaces(ctx, badIdentifier, &removeOptions)
assert.EqualError(t, err, ErrInvalidVariableSetID.Error())
})
t.Run("when workspace ID is invalid", func(t *testing.T) {
badWorkspace := &Workspace{
ID: badIdentifier,
}
applyOptions := VariableSetApplyToWorkspacesOptions{
Workspaces: []*Workspace{badWorkspace},
}
err := client.VariableSets.ApplyToWorkspaces(ctx, vsTest.ID, &applyOptions)
assert.EqualError(t, err, ErrRequiredWorkspaceID.Error())
removeOptions := VariableSetRemoveFromWorkspacesOptions{
Workspaces: []*Workspace{badWorkspace},
}
err = client.VariableSets.RemoveFromWorkspaces(ctx, vsTest.ID, &removeOptions)
assert.EqualError(t, err, ErrRequiredWorkspaceID.Error())
})
}
func TestVariableSetsApplyToAndRemoveFromProjects(t *testing.T) {
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
vsTest, vsTestCleanup := createVariableSet(t, client, orgTest, VariableSetCreateOptions{})
t.Cleanup(vsTestCleanup)
prjTest1, prjTest1Cleanup := createProject(t, client, orgTest)
defer prjTest1Cleanup()
prjTest2, prjTest2Cleanup := createProject(t, client, orgTest)
defer prjTest2Cleanup()
t.Run("with first project added", func(t *testing.T) {
options := VariableSetApplyToProjectsOptions{
Projects: []*Project{prjTest1},
}
err := client.VariableSets.ApplyToProjects(ctx, vsTest.ID, options)
require.NoError(t, err)
vsAfter, err := client.VariableSets.Read(ctx, vsTest.ID, nil)
require.NoError(t, err)
// Variable set should be applied to [prjTest1]
assert.Equal(t, 1, len(vsAfter.Projects))
assert.Equal(t, prjTest1.ID, vsAfter.Projects[0].ID)
})
t.Run("with second project added", func(t *testing.T) {
options := VariableSetApplyToProjectsOptions{
Projects: []*Project{prjTest2},
}
err := client.VariableSets.ApplyToProjects(ctx, vsTest.ID, options)
require.NoError(t, err)
vsAfter, err := client.VariableSets.Read(ctx, vsTest.ID, nil)
require.NoError(t, err)
// Variable set should be applied to [prjTest1, prjTest2]
assert.Equal(t, 2, len(vsAfter.Projects))
prjIDs := []string{vsAfter.Projects[0].ID, vsAfter.Projects[1].ID}
assert.Contains(t, prjIDs, prjTest1.ID)
assert.Contains(t, prjIDs, prjTest2.ID)
})
t.Run("with first project removed", func(t *testing.T) {
options := VariableSetRemoveFromProjectsOptions{
Projects: []*Project{prjTest1},
}
err := client.VariableSets.RemoveFromProjects(ctx, vsTest.ID, options)
require.NoError(t, err)
vsAfter, err := client.VariableSets.Read(ctx, vsTest.ID, nil)
require.NoError(t, err)
// Variable set should be applied to [wTest2]
assert.Equal(t, 1, len(vsAfter.Projects))
assert.Equal(t, prjTest2.ID, vsAfter.Projects[0].ID)
})
t.Run("when variable set ID is invalid", func(t *testing.T) {
applyOptions := VariableSetApplyToProjectsOptions{
Projects: []*Project{prjTest1},
}
err := client.VariableSets.ApplyToProjects(ctx, badIdentifier, applyOptions)
assert.EqualError(t, err, ErrInvalidVariableSetID.Error())
removeOptions := VariableSetRemoveFromProjectsOptions{
Projects: []*Project{prjTest1},
}
err = client.VariableSets.RemoveFromProjects(ctx, badIdentifier, removeOptions)
assert.EqualError(t, err, ErrInvalidVariableSetID.Error())
})
t.Run("when project ID is invalid", func(t *testing.T) {
badProject := &Project{
ID: badIdentifier,
}
applyOptions := VariableSetApplyToProjectsOptions{
Projects: []*Project{badProject},
}
err := client.VariableSets.ApplyToProjects(ctx, vsTest.ID, applyOptions)
assert.EqualError(t, err, ErrRequiredProjectID.Error())
removeOptions := VariableSetRemoveFromProjectsOptions{
Projects: []*Project{badProject},
}
err = client.VariableSets.RemoveFromProjects(ctx, vsTest.ID, removeOptions)
assert.EqualError(t, err, ErrRequiredProjectID.Error())
})
}
func TestVariableSetsApplyToAndRemoveFromStacks(t *testing.T) {
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
vsTest, vsTestCleanup := createVariableSet(t, client, orgTest, VariableSetCreateOptions{})
t.Cleanup(vsTestCleanup)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
stackTest1, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "test-stack-1",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
},
Project: &Project{
ID: orgTest.DefaultProject.ID,
},
})
require.NoError(t, err)
t.Cleanup(func() {
if err := client.Stacks.Delete(ctx, stackTest1.ID); err != nil {
t.Logf("Failed to cleanup stack %s: %v", stackTest1.ID, err)
}
})
// Wait for stack to be ready by triggering configuration update
_, err = client.Stacks.FetchLatestFromVcs(ctx, stackTest1.ID)
require.NoError(t, err)
stackTest2, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "test-stack-2",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
},
Project: &Project{
ID: orgTest.DefaultProject.ID,
},
})
require.NoError(t, err)
t.Cleanup(func() {
if err := client.Stacks.Delete(ctx, stackTest2.ID); err != nil {
t.Logf("Failed to cleanup stack %s: %v", stackTest2.ID, err)
}
})
// Wait for stack to be ready by triggering configuration update
_, err = client.Stacks.FetchLatestFromVcs(ctx, stackTest2.ID)
// Don't require this to succeed as it might not be needed
t.Run("with first stack added", func(t *testing.T) {
options := VariableSetApplyToStacksOptions{
Stacks: []*Stack{{ID: stackTest1.ID}},
}
err = client.VariableSets.ApplyToStacks(ctx, vsTest.ID, &options)
require.NoError(t, err)
readOpts := &VariableSetReadOptions{Include: &[]VariableSetIncludeOpt{VariableSetStacks}}
vsAfter, err := client.VariableSets.Read(ctx, vsTest.ID, readOpts)
require.NoError(t, err)
assert.Equal(t, 1, len(vsAfter.Stacks))
assert.Equal(t, stackTest1.ID, vsAfter.Stacks[0].ID)
})
t.Run("with second stack added", func(t *testing.T) {
options := VariableSetApplyToStacksOptions{
Stacks: []*Stack{stackTest2},
}
err := client.VariableSets.ApplyToStacks(ctx, vsTest.ID, &options)
require.NoError(t, err)
readOpts := &VariableSetReadOptions{Include: &[]VariableSetIncludeOpt{VariableSetStacks}}
vsAfter, err := client.VariableSets.Read(ctx, vsTest.ID, readOpts)
require.NoError(t, err)
assert.Equal(t, 2, len(vsAfter.Stacks))
stackIDs := []string{vsAfter.Stacks[0].ID, vsAfter.Stacks[1].ID}
assert.Contains(t, stackIDs, stackTest1.ID)
assert.Contains(t, stackIDs, stackTest2.ID)
})
t.Run("with first stack removed", func(t *testing.T) {
options := VariableSetRemoveFromStacksOptions{
Stacks: []*Stack{stackTest1},
}
err := client.VariableSets.RemoveFromStacks(ctx, vsTest.ID, &options)
require.NoError(t, err)
readOpts := &VariableSetReadOptions{Include: &[]VariableSetIncludeOpt{VariableSetStacks}}
vsAfter, err := client.VariableSets.Read(ctx, vsTest.ID, readOpts)
require.NoError(t, err)
assert.Equal(t, 1, len(vsAfter.Stacks))
assert.Equal(t, stackTest2.ID, vsAfter.Stacks[0].ID)
})
t.Run("when variable set ID is invalid", func(t *testing.T) {
applyOptions := VariableSetApplyToStacksOptions{
Stacks: []*Stack{stackTest1},
}
err := client.VariableSets.ApplyToStacks(ctx, badIdentifier, &applyOptions)
assert.EqualError(t, err, ErrInvalidVariableSetID.Error())
removeOptions := VariableSetRemoveFromStacksOptions{
Stacks: []*Stack{stackTest1},
}
err = client.VariableSets.RemoveFromStacks(ctx, badIdentifier, &removeOptions)
assert.EqualError(t, err, ErrInvalidVariableSetID.Error())
})
t.Run("when stack ID is invalid", func(t *testing.T) {
badStack := &Stack{
ID: badIdentifier,
}
applyOptions := VariableSetApplyToStacksOptions{
Stacks: []*Stack{badStack},
}
err := client.VariableSets.ApplyToStacks(ctx, vsTest.ID, &applyOptions)
assert.EqualError(t, err, ErrRequiredStackID.Error())
removeOptions := VariableSetRemoveFromStacksOptions{
Stacks: []*Stack{badStack},
}
err = client.VariableSets.RemoveFromStacks(ctx, vsTest.ID, &removeOptions)
assert.EqualError(t, err, ErrRequiredStackID.Error())
})
}
func TestVariableSetsUpdateWorkspaces(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
vsTest, vsTestCleanup := createVariableSet(t, client, orgTest, VariableSetCreateOptions{})
t.Cleanup(vsTestCleanup)
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup)
t.Run("with valid workspaces", func(t *testing.T) {
options := VariableSetUpdateWorkspacesOptions{
Workspaces: []*Workspace{wTest},
}
vsAfter, err := client.VariableSets.UpdateWorkspaces(ctx, vsTest.ID, &options)
require.NoError(t, err)
assert.Equal(t, len(options.Workspaces), len(vsAfter.Workspaces))
assert.Equal(t, options.Workspaces[0].ID, vsAfter.Workspaces[0].ID)
options = VariableSetUpdateWorkspacesOptions{
Workspaces: []*Workspace{},
}
vsAfter, err = client.VariableSets.UpdateWorkspaces(ctx, vsTest.ID, &options)
require.NoError(t, err)
assert.Equal(t, len(options.Workspaces), len(vsAfter.Workspaces))
})
}
func TestVariableSetsUpdateStacks(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
vsTest, vsTestCleanup := createVariableSet(t, client, orgTest, VariableSetCreateOptions{})
t.Cleanup(vsTestCleanup)
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)
stackTest, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "test-stack",
VCSRepo: &StackVCSRepoOptions{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
},
Project: &Project{
ID: orgTest.DefaultProject.ID,
},
})
require.NoError(t, err)
t.Cleanup(func() {
if err := client.Stacks.Delete(ctx, stackTest.ID); err != nil {
t.Logf("Failed to cleanup stack %s: %v", stackTest.ID, err)
}
})
// Wait for stack to be ready by triggering configuration update
_, err = client.Stacks.FetchLatestFromVcs(ctx, stackTest.ID)
require.NoError(t, err)
t.Run("with valid stacks", func(t *testing.T) {
options := VariableSetUpdateStacksOptions{
Stacks: []*Stack{stackTest},
}
_, err := client.VariableSets.UpdateStacks(ctx, vsTest.ID, &options)
require.NoError(t, err)
readOpts := &VariableSetReadOptions{Include: &[]VariableSetIncludeOpt{VariableSetStacks}}
vsAfter, err := client.VariableSets.Read(ctx, vsTest.ID, readOpts)
require.NoError(t, err)
assert.Equal(t, len(options.Stacks), len(vsAfter.Stacks))
assert.Equal(t, options.Stacks[0].ID, vsAfter.Stacks[0].ID)
options = VariableSetUpdateStacksOptions{
Stacks: []*Stack{},
}
_, err = client.VariableSets.UpdateStacks(ctx, vsTest.ID, &options)
require.NoError(t, err)
readOpts = &VariableSetReadOptions{Include: &[]VariableSetIncludeOpt{VariableSetStacks}}
vsAfter, err = client.VariableSets.Read(ctx, vsTest.ID, readOpts)
require.NoError(t, err)
assert.Equal(t, len(options.Stacks), len(vsAfter.Stacks))
})
}
================================================
FILE: variable_set_variable.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ VariableSetVariables = (*variableSetVariables)(nil)
// VariableSetVariables describes all variable variable related methods within the scope of
// Variable Sets that the Terraform Enterprise API supports
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/variable-sets#variable-relationships
type VariableSetVariables interface {
// List all variables in the variable set.
List(ctx context.Context, variableSetID string, options *VariableSetVariableListOptions) (*VariableSetVariableList, error)
// Create is used to create a new variable within a given variable set
Create(ctx context.Context, variableSetID string, options *VariableSetVariableCreateOptions) (*VariableSetVariable, error)
// Read a variable by its ID
Read(ctx context.Context, variableSetID string, variableID string) (*VariableSetVariable, error)
// Update valuse of an existing variable
Update(ctx context.Context, variableSetID string, variableID string, options *VariableSetVariableUpdateOptions) (*VariableSetVariable, error)
// Delete a variable by its ID
Delete(ctx context.Context, variableSetID string, variableID string) error
}
type variableSetVariables struct {
client *Client
}
type VariableSetVariableList struct {
*Pagination
Items []*VariableSetVariable
}
type VariableSetVariable struct {
ID string `jsonapi:"primary,vars"`
Key string `jsonapi:"attr,key"`
Value string `jsonapi:"attr,value"`
Description string `jsonapi:"attr,description"`
Category CategoryType `jsonapi:"attr,category"`
HCL bool `jsonapi:"attr,hcl"`
Sensitive bool `jsonapi:"attr,sensitive"`
VersionID string `jsonapi:"attr,version-id"`
// Relations
VariableSet *VariableSet `jsonapi:"relation,varset"`
}
type VariableSetVariableListOptions struct {
ListOptions
}
func (o VariableSetVariableListOptions) valid() error {
return nil
}
// List all variables associated with the given variable set.
func (s *variableSetVariables) List(ctx context.Context, variableSetID string, options *VariableSetVariableListOptions) (*VariableSetVariableList, error) {
if !validStringID(&variableSetID) {
return nil, ErrInvalidVariableSetID
}
if options != nil {
if err := options.valid(); err != nil {
return nil, err
}
}
u := fmt.Sprintf("varsets/%s/relationships/vars", url.PathEscape(variableSetID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
vl := &VariableSetVariableList{}
err = req.Do(ctx, vl)
if err != nil {
return nil, err
}
return vl, nil
}
// VariableSetVariableCreatOptions represents the options for creating a new variable within a variable set
type VariableSetVariableCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,vars"`
// The name of the variable.
Key *string `jsonapi:"attr,key"`
// The value of the variable.
Value *string `jsonapi:"attr,value,omitempty"`
// The description of the variable.
Description *string `jsonapi:"attr,description,omitempty"`
// Whether this is a Terraform or environment variable.
Category *CategoryType `jsonapi:"attr,category"`
// Whether to evaluate the value of the variable as a string of HCL code.
HCL *bool `jsonapi:"attr,hcl,omitempty"`
// Whether the value is sensitive.
Sensitive *bool `jsonapi:"attr,sensitive,omitempty"`
}
func (o VariableSetVariableCreateOptions) valid() error {
if !validString(o.Key) {
return ErrRequiredKey
}
if o.Category == nil {
return ErrRequiredCategory
}
return nil
}
// Create is used to create a new variable.
func (s *variableSetVariables) Create(ctx context.Context, variableSetID string, options *VariableSetVariableCreateOptions) (*VariableSetVariable, error) {
if !validStringID(&variableSetID) {
return nil, ErrInvalidVariableSetID
}
if options != nil {
if err := options.valid(); err != nil {
return nil, err
}
}
u := fmt.Sprintf("varsets/%s/relationships/vars", url.PathEscape(variableSetID))
req, err := s.client.NewRequest("POST", u, options)
if err != nil {
return nil, err
}
v := &VariableSetVariable{}
err = req.Do(ctx, v)
if err != nil {
return nil, err
}
return v, nil
}
// Read a variable by its ID.
func (s *variableSetVariables) Read(ctx context.Context, variableSetID, variableID string) (*VariableSetVariable, error) {
if !validStringID(&variableSetID) {
return nil, ErrInvalidVariableSetID
}
if !validStringID(&variableID) {
return nil, ErrInvalidVariableID
}
u := fmt.Sprintf("varsets/%s/relationships/vars/%s", url.PathEscape(variableSetID), url.PathEscape(variableID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
v := &VariableSetVariable{}
err = req.Do(ctx, v)
if err != nil {
return nil, err
}
return v, err
}
// VariableSetVariableUpdateOptions represents the options for updating a variable.
type VariableSetVariableUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,vars"`
// The name of the variable.
Key *string `jsonapi:"attr,key,omitempty"`
// The value of the variable.
Value *string `jsonapi:"attr,value,omitempty"`
// The description of the variable.
Description *string `jsonapi:"attr,description,omitempty"`
// Whether to evaluate the value of the variable as a string of HCL code.
HCL *bool `jsonapi:"attr,hcl,omitempty"`
// Whether the value is sensitive.
Sensitive *bool `jsonapi:"attr,sensitive,omitempty"`
}
// Update values of an existing variable.
func (s *variableSetVariables) Update(ctx context.Context, variableSetID, variableID string, options *VariableSetVariableUpdateOptions) (*VariableSetVariable, error) {
if !validStringID(&variableSetID) {
return nil, ErrInvalidVariableSetID
}
if !validStringID(&variableID) {
return nil, ErrInvalidVariableID
}
u := fmt.Sprintf("varsets/%s/relationships/vars/%s", url.PathEscape(variableSetID), url.PathEscape(variableID))
req, err := s.client.NewRequest("PATCH", u, options)
if err != nil {
return nil, err
}
v := &VariableSetVariable{}
err = req.Do(ctx, v)
if err != nil {
return nil, err
}
return v, nil
}
// Delete a variable by its ID.
func (s *variableSetVariables) Delete(ctx context.Context, variableSetID, variableID string) error {
if !validStringID(&variableSetID) {
return ErrInvalidVariableSetID
}
if !validStringID(&variableID) {
return ErrInvalidVariableID
}
u := fmt.Sprintf("varsets/%s/relationships/vars/%s", url.PathEscape(variableSetID), url.PathEscape(variableID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
================================================
FILE: variable_set_variable_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestVariableSetVariablesList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
vsTest, vsTestCleanup := createVariableSet(t, client, orgTest, VariableSetCreateOptions{})
defer vsTestCleanup()
vTest1, vTestCleanup1 := createVariableSetVariable(t, client, vsTest, VariableSetVariableCreateOptions{
Key: String("vTest1"),
Value: String("vTest1"),
Category: Category(CategoryTerraform),
})
defer vTestCleanup1()
vTest2, vTestCleanup2 := createVariableSetVariable(t, client, vsTest, VariableSetVariableCreateOptions{
Key: String("vTest2"),
Value: String("vTest2"),
Category: Category(CategoryTerraform),
})
defer vTestCleanup2()
t.Run("without list options", func(t *testing.T) {
vl, err := client.VariableSetVariables.List(ctx, vsTest.ID, nil)
require.NoError(t, err)
require.NotEmpty(t, vl.Items)
assert.Contains(t, vl.Items, vTest1)
assert.Contains(t, vl.Items, vTest2)
t.Run("variable set relationship is deserialized", func(t *testing.T) {
require.NotNil(t, vl.Items[0].VariableSet)
assert.Equal(t, vsTest.ID, vl.Items[0].VariableSet.ID)
})
})
t.Run("with list options", func(t *testing.T) {
t.Skip("paging not supported yet in API")
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
vl, err := client.VariableSetVariables.List(ctx, vsTest.ID, &VariableSetVariableListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, vl.Items)
assert.Equal(t, 999, vl.CurrentPage)
assert.Equal(t, 2, vl.TotalCount)
})
t.Run("when variable set ID is invalid ID", func(t *testing.T) {
vl, err := client.VariableSetVariables.List(ctx, badIdentifier, nil)
assert.Nil(t, vl)
assert.EqualError(t, err, ErrInvalidVariableSetID.Error())
})
}
func TestVariableSetVariablesCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
vsTest, vsTestCleanup := createVariableSet(t, client, orgTest, VariableSetCreateOptions{})
defer vsTestCleanup()
t.Run("with valid options", func(t *testing.T) {
options := VariableSetVariableCreateOptions{
Key: String(randomString(t)),
Value: String(randomString(t)),
Category: Category(CategoryTerraform),
Description: String(randomString(t)),
HCL: Bool(false),
Sensitive: Bool(false),
}
v, err := client.VariableSetVariables.Create(ctx, vsTest.ID, &options)
require.NoError(t, err)
assert.NotEmpty(t, v.ID)
assert.Equal(t, *options.Key, v.Key)
assert.Equal(t, *options.Value, v.Value)
assert.Equal(t, *options.Description, v.Description)
assert.Equal(t, *options.Category, v.Category)
assert.NotEmpty(t, v.VersionID)
})
t.Run("when options has an empty string value", func(t *testing.T) {
options := VariableSetVariableCreateOptions{
Key: String(randomString(t)),
Value: String(""),
Description: String(randomString(t)),
Category: Category(CategoryTerraform),
}
v, err := client.VariableSetVariables.Create(ctx, vsTest.ID, &options)
require.NoError(t, err)
assert.NotEmpty(t, v.ID)
assert.Equal(t, *options.Key, v.Key)
assert.Equal(t, *options.Value, v.Value)
assert.Equal(t, *options.Description, v.Description)
assert.Equal(t, *options.Category, v.Category)
assert.NotEmpty(t, v.VersionID)
})
t.Run("when options has an empty string description", func(t *testing.T) {
options := VariableSetVariableCreateOptions{
Key: String(randomString(t)),
Value: String(randomString(t)),
Description: String(""),
Category: Category(CategoryTerraform),
}
v, err := client.VariableSetVariables.Create(ctx, vsTest.ID, &options)
require.NoError(t, err)
assert.NotEmpty(t, v.ID)
assert.Equal(t, *options.Key, v.Key)
assert.Equal(t, *options.Value, v.Value)
assert.Equal(t, *options.Description, v.Description)
assert.Equal(t, *options.Category, v.Category)
assert.NotEmpty(t, v.VersionID)
})
t.Run("when options has a too-long description", func(t *testing.T) {
options := VariableSetVariableCreateOptions{
Key: String(randomString(t)),
Value: String(randomString(t)),
Description: String("tortor aliquam nulla redacted cras fermentum odio eu feugiat pretium nibh ipsum consequat nisl vel pretium lectus quam id leo in vitae turpis massa sed elementum tempus egestas sed sed risus pretium quam vulputate dignissim suspendisse in est ante in nibh mauris cursus mattis molestie a iaculis at erat pellentesque adipiscing commodo elit at imperdiet dui accumsan sit amet nulla redacted morbi tempus iaculis urna id volutpat lacus laoreet non curabitur gravida arcu ac tortor dignissim convallis aenean et tortor"),
Category: Category(CategoryTerraform),
}
_, err := client.VariableSetVariables.Create(ctx, vsTest.ID, &options)
assert.Error(t, err)
})
t.Run("when options is missing value", func(t *testing.T) {
options := VariableSetVariableCreateOptions{
Key: String(randomString(t)),
Category: Category(CategoryTerraform),
}
v, err := client.VariableSetVariables.Create(ctx, vsTest.ID, &options)
require.NoError(t, err)
assert.NotEmpty(t, v.ID)
assert.Equal(t, *options.Key, v.Key)
assert.Equal(t, "", v.Value)
assert.Equal(t, *options.Category, v.Category)
assert.NotEmpty(t, v.VersionID)
})
t.Run("when options is missing key", func(t *testing.T) {
options := VariableSetVariableCreateOptions{
Value: String(randomString(t)),
Category: Category(CategoryTerraform),
}
_, err := client.VariableSetVariables.Create(ctx, vsTest.ID, &options)
assert.EqualError(t, err, ErrRequiredKey.Error())
})
t.Run("when options has an empty key", func(t *testing.T) {
options := VariableSetVariableCreateOptions{
Key: String(""),
Value: String(randomString(t)),
Category: Category(CategoryTerraform),
}
_, err := client.VariableSetVariables.Create(ctx, vsTest.ID, &options)
assert.EqualError(t, err, ErrRequiredKey.Error())
})
t.Run("when options is missing category", func(t *testing.T) {
options := VariableSetVariableCreateOptions{
Key: String(randomString(t)),
Value: String(randomString(t)),
}
_, err := client.VariableSetVariables.Create(ctx, vsTest.ID, &options)
assert.EqualError(t, err, ErrRequiredCategory.Error())
})
t.Run("when workspace ID is invalid", func(t *testing.T) {
options := VariableSetVariableCreateOptions{
Key: String(randomString(t)),
Value: String(randomString(t)),
Category: Category(CategoryTerraform),
}
_, err := client.VariableSetVariables.Create(ctx, badIdentifier, &options)
assert.EqualError(t, err, ErrInvalidVariableSetID.Error())
})
}
func TestVariableSetVariablesRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
vsTest, vsTestCleanup := createVariableSet(t, client, orgTest, VariableSetCreateOptions{})
defer vsTestCleanup()
vTest, vTestCleanup := createVariableSetVariable(t, client, vsTest, VariableSetVariableCreateOptions{})
defer vTestCleanup()
t.Run("when the variable exists", func(t *testing.T) {
v, err := client.VariableSetVariables.Read(ctx, vsTest.ID, vTest.ID)
require.NoError(t, err)
assert.Equal(t, vTest.ID, v.ID)
assert.Equal(t, vTest.Category, v.Category)
assert.Equal(t, vTest.HCL, v.HCL)
assert.Equal(t, vTest.Key, v.Key)
assert.Equal(t, vTest.Sensitive, v.Sensitive)
assert.Equal(t, vTest.Value, v.Value)
assert.Equal(t, vTest.VersionID, v.VersionID)
})
t.Run("when the variable does not exist", func(t *testing.T) {
v, err := client.VariableSetVariables.Read(ctx, vsTest.ID, "nonexisting")
assert.Nil(t, v)
assert.Equal(t, ErrResourceNotFound, err)
})
t.Run("without a valid variable set ID", func(t *testing.T) {
v, err := client.VariableSetVariables.Read(ctx, badIdentifier, vTest.ID)
assert.Nil(t, v)
assert.EqualError(t, err, ErrInvalidVariableSetID.Error())
})
t.Run("without a valid variable ID", func(t *testing.T) {
v, err := client.VariableSetVariables.Read(ctx, vsTest.ID, badIdentifier)
assert.Nil(t, v)
assert.EqualError(t, err, ErrInvalidVariableID.Error())
})
}
func TestVariableSetVariablesUpdate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
vsTest, vsTestCleanup := createVariableSet(t, client, nil, VariableSetCreateOptions{})
defer vsTestCleanup()
vTest, vTestCleanup := createVariableSetVariable(t, client, vsTest, VariableSetVariableCreateOptions{})
defer vTestCleanup()
t.Run("with valid options", func(t *testing.T) {
options := VariableSetVariableUpdateOptions{
Key: String("newname"),
Value: String("newvalue"),
HCL: Bool(true),
}
v, err := client.VariableSetVariables.Update(ctx, vsTest.ID, vTest.ID, &options)
require.NoError(t, err)
assert.Equal(t, *options.Key, v.Key)
assert.Equal(t, *options.HCL, v.HCL)
assert.Equal(t, *options.Value, v.Value)
assert.NotEqual(t, vTest.VersionID, v.VersionID)
})
t.Run("when updating a subset of values", func(t *testing.T) {
options := VariableSetVariableUpdateOptions{
Key: String("someothername"),
HCL: Bool(false),
}
v, err := client.VariableSetVariables.Update(ctx, vsTest.ID, vTest.ID, &options)
require.NoError(t, err)
assert.Equal(t, *options.Key, v.Key)
assert.Equal(t, *options.HCL, v.HCL)
assert.NotEqual(t, vTest.VersionID, v.VersionID)
})
t.Run("with sensitive set", func(t *testing.T) {
options := VariableSetVariableUpdateOptions{
Sensitive: Bool(true),
}
v, err := client.VariableSetVariables.Update(ctx, vsTest.ID, vTest.ID, &options)
require.NoError(t, err)
assert.Equal(t, *options.Sensitive, v.Sensitive)
assert.Empty(t, v.Value) // Because its now sensitive
assert.NotEqual(t, vTest.VersionID, v.VersionID)
})
t.Run("without any changes", func(t *testing.T) {
vTest, vTestCleanup := createVariableSetVariable(t, client, vsTest, VariableSetVariableCreateOptions{})
defer vTestCleanup()
options := VariableSetVariableUpdateOptions{
Key: String(vTest.Key),
Value: String(vTest.Value),
Description: String(vTest.Description),
Sensitive: Bool(vTest.Sensitive),
HCL: Bool(vTest.HCL),
}
v, err := client.VariableSetVariables.Update(ctx, vsTest.ID, vTest.ID, &options)
require.NoError(t, err)
assert.Equal(t, vTest.ID, v.ID)
assert.Equal(t, vTest.Key, v.Key)
assert.Equal(t, vTest.Value, v.Value)
assert.Equal(t, vTest.Description, v.Description)
assert.Equal(t, vTest.Category, v.Category)
assert.Equal(t, vTest.HCL, v.HCL)
assert.Equal(t, vTest.Sensitive, v.Sensitive)
assert.NotEqual(t, vTest.VersionID, v.VersionID)
})
t.Run("with invalid variable ID", func(t *testing.T) {
_, err := client.VariableSetVariables.Update(ctx, badIdentifier, vTest.ID, nil)
assert.EqualError(t, err, ErrInvalidVariableSetID.Error())
})
t.Run("with invalid variable ID", func(t *testing.T) {
_, err := client.VariableSetVariables.Update(ctx, vsTest.ID, badIdentifier, nil)
assert.EqualError(t, err, ErrInvalidVariableID.Error())
})
}
func TestVariableSetVariablesDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
vsTest, vsTestCleanup := createVariableSet(t, client, nil, VariableSetCreateOptions{})
defer vsTestCleanup()
vTest, _ := createVariableSetVariable(t, client, vsTest, VariableSetVariableCreateOptions{})
t.Run("with valid options", func(t *testing.T) {
err := client.VariableSetVariables.Delete(ctx, vsTest.ID, vTest.ID)
require.NoError(t, err)
})
t.Run("with non existing variable ID", func(t *testing.T) {
err := client.VariableSetVariables.Delete(ctx, vsTest.ID, "nonexisting")
assert.EqualError(t, err, ErrResourceNotFound.Error())
})
t.Run("with invalid workspace ID", func(t *testing.T) {
err := client.VariableSetVariables.Delete(ctx, badIdentifier, vTest.ID)
assert.EqualError(t, err, ErrInvalidVariableSetID.Error())
})
t.Run("with invalid variable ID", func(t *testing.T) {
err := client.VariableSetVariables.Delete(ctx, vsTest.ID, badIdentifier)
assert.EqualError(t, err, ErrInvalidVariableID.Error())
})
}
================================================
FILE: vault_oidc_configuration.go
================================================
package tfe
import (
"context"
"fmt"
"net/url"
)
// VaultOIDCConfigurations describes all the Vault OIDC configuration related methods that the HCP Terraform API supports.
// HCP Terraform API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/hold-your-own-key/oidc-configurations/vault
type VaultOIDCConfigurations interface {
Create(ctx context.Context, organization string, options VaultOIDCConfigurationCreateOptions) (*VaultOIDCConfiguration, error)
Read(ctx context.Context, oidcID string) (*VaultOIDCConfiguration, error)
Update(ctx context.Context, oidcID string, options VaultOIDCConfigurationUpdateOptions) (*VaultOIDCConfiguration, error)
Delete(ctx context.Context, oidcID string) error
}
type vaultOIDCConfigurations struct {
client *Client
}
var _ VaultOIDCConfigurations = &vaultOIDCConfigurations{}
type VaultOIDCConfiguration struct {
ID string `jsonapi:"primary,vault-oidc-configurations"`
Address string `jsonapi:"attr,address"`
RoleName string `jsonapi:"attr,role"`
Namespace string `jsonapi:"attr,namespace"`
JWTAuthPath string `jsonapi:"attr,auth-path"`
TLSCACertificate string `jsonapi:"attr,encoded-cacert"`
Organization *Organization `jsonapi:"relation,organization"`
}
type VaultOIDCConfigurationCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,vault-oidc-configurations"`
// Attributes
Address string `jsonapi:"attr,address"`
RoleName string `jsonapi:"attr,role"`
Namespace string `jsonapi:"attr,namespace"`
JWTAuthPath string `jsonapi:"attr,auth-path"`
TLSCACertificate string `jsonapi:"attr,encoded-cacert"`
}
type VaultOIDCConfigurationUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,vault-oidc-configurations"`
// Attributes
Address *string `jsonapi:"attr,address,omitempty"`
RoleName *string `jsonapi:"attr,role,omitempty"`
Namespace *string `jsonapi:"attr,namespace,omitempty"`
JWTAuthPath *string `jsonapi:"attr,auth-path,omitempty"`
TLSCACertificate *string `jsonapi:"attr,encoded-cacert,omitempty"`
}
func (o *VaultOIDCConfigurationCreateOptions) valid() error {
if o.Address == "" {
return ErrRequiredVaultAddress
}
if o.RoleName == "" {
return ErrRequiredRoleName
}
return nil
}
func (voc *vaultOIDCConfigurations) Create(ctx context.Context, organization string, options VaultOIDCConfigurationCreateOptions) (*VaultOIDCConfiguration, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
req, err := voc.client.NewRequest("POST", fmt.Sprintf("organizations/%s/oidc-configurations", url.PathEscape(organization)), &options)
if err != nil {
return nil, err
}
vaultOIDCConfiguration := &VaultOIDCConfiguration{}
err = req.Do(ctx, vaultOIDCConfiguration)
if err != nil {
return nil, err
}
return vaultOIDCConfiguration, nil
}
func (voc *vaultOIDCConfigurations) Read(ctx context.Context, oidcID string) (*VaultOIDCConfiguration, error) {
req, err := voc.client.NewRequest("GET", fmt.Sprintf(OIDCConfigPathFormat, url.PathEscape(oidcID)), nil)
if err != nil {
return nil, err
}
vaultOIDCConfiguration := &VaultOIDCConfiguration{}
err = req.Do(ctx, vaultOIDCConfiguration)
if err != nil {
return nil, err
}
return vaultOIDCConfiguration, nil
}
func (voc *vaultOIDCConfigurations) Update(ctx context.Context, oidcID string, options VaultOIDCConfigurationUpdateOptions) (*VaultOIDCConfiguration, error) {
if !validStringID(&oidcID) {
return nil, ErrInvalidOIDC
}
req, err := voc.client.NewRequest("PATCH", fmt.Sprintf(OIDCConfigPathFormat, url.PathEscape(oidcID)), &options)
if err != nil {
return nil, err
}
vaultOIDCConfiguration := &VaultOIDCConfiguration{}
err = req.Do(ctx, vaultOIDCConfiguration)
if err != nil {
return nil, err
}
return vaultOIDCConfiguration, nil
}
func (voc *vaultOIDCConfigurations) Delete(ctx context.Context, oidcID string) error {
if !validStringID(&oidcID) {
return ErrInvalidOIDC
}
req, err := voc.client.NewRequest("DELETE", fmt.Sprintf(OIDCConfigPathFormat, url.PathEscape(oidcID)), nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
================================================
FILE: vault_oidc_configuration_integration_test.go
================================================
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// These tests are intended for local execution only, as OIDC configurations for HYOK requires specific conditions.
// To run them locally, follow the instructions outlined in hyok_configuration_integration_test.go
func TestVaultOIDCConfigurationCreateDelete(t *testing.T) {
t.Parallel()
skipHYOKIntegrationTests(t)
client := testClient(t)
ctx := context.Background()
orgTest := testHyokOrganization(t, client)
t.Run("with valid options", func(t *testing.T) {
opts := VaultOIDCConfigurationCreateOptions{
Address: "https://vault.example.com",
RoleName: "vault-role-name",
Namespace: "admin",
JWTAuthPath: "jwt",
TLSCACertificate: randomString(t),
}
oidcConfig, err := client.VaultOIDCConfigurations.Create(ctx, orgTest.Name, opts)
require.NoError(t, err)
require.NotNil(t, oidcConfig)
assert.Equal(t, opts.Address, oidcConfig.Address)
assert.Equal(t, opts.RoleName, oidcConfig.RoleName)
assert.Equal(t, opts.Namespace, oidcConfig.Namespace)
assert.Equal(t, opts.JWTAuthPath, oidcConfig.JWTAuthPath)
// delete the created configuration
err = client.VaultOIDCConfigurations.Delete(ctx, oidcConfig.ID)
require.NoError(t, err)
})
t.Run("missing address", func(t *testing.T) {
opts := VaultOIDCConfigurationCreateOptions{
RoleName: "vault-role-name",
Namespace: "admin",
JWTAuthPath: "jwt",
TLSCACertificate: randomString(t),
}
_, err := client.VaultOIDCConfigurations.Create(ctx, orgTest.Name, opts)
assert.ErrorIs(t, err, ErrRequiredVaultAddress)
})
t.Run("missing role name", func(t *testing.T) {
opts := VaultOIDCConfigurationCreateOptions{
Address: "https://vault.example.com",
Namespace: "admin",
JWTAuthPath: "jwt",
TLSCACertificate: randomString(t),
}
_, err := client.VaultOIDCConfigurations.Create(ctx, orgTest.Name, opts)
assert.ErrorIs(t, err, ErrRequiredRoleName)
})
}
func TestVaultOIDCConfigurationRead(t *testing.T) {
t.Parallel()
skipHYOKIntegrationTests(t)
client := testClient(t)
ctx := context.Background()
orgTest := testHyokOrganization(t, client)
oidcConfig, oidcConfigCleanup := createVaultOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcConfigCleanup)
t.Run("fetch existing configuration", func(t *testing.T) {
fetched, err := client.VaultOIDCConfigurations.Read(ctx, oidcConfig.ID)
require.NoError(t, err)
require.NotEmpty(t, fetched)
})
t.Run("fetching non-existing configuration", func(t *testing.T) {
_, err := client.VaultOIDCConfigurations.Read(ctx, "voidc-notreal")
assert.ErrorIs(t, err, ErrResourceNotFound)
})
}
func TestVaultOIDCConfigurationUpdate(t *testing.T) {
t.Parallel()
skipHYOKIntegrationTests(t)
client := testClient(t)
ctx := context.Background()
orgTest := testHyokOrganization(t, client)
t.Run("update all fields", func(t *testing.T) {
oidcConfig, oidcConfigCleanup := createVaultOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcConfigCleanup)
address := randomString(t)
roleName := randomString(t)
namespace := randomString(t)
jwtAuthPath := randomString(t)
tlscaCertificate := randomString(t)
opts := VaultOIDCConfigurationUpdateOptions{
Address: &address,
RoleName: &roleName,
Namespace: &namespace,
JWTAuthPath: &jwtAuthPath,
TLSCACertificate: &tlscaCertificate,
}
updated, err := client.VaultOIDCConfigurations.Update(ctx, oidcConfig.ID, opts)
require.NoError(t, err)
require.NotEmpty(t, updated)
assert.Equal(t, *opts.Address, updated.Address)
assert.Equal(t, *opts.RoleName, updated.RoleName)
assert.Equal(t, *opts.Namespace, updated.Namespace)
assert.Equal(t, *opts.JWTAuthPath, updated.JWTAuthPath)
assert.Equal(t, *opts.TLSCACertificate, updated.TLSCACertificate)
})
t.Run("address not provided", func(t *testing.T) {
oidcConfig, oidcConfigCleanup := createVaultOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcConfigCleanup)
roleName := randomString(t)
namespace := randomString(t)
jwtAuthPath := randomString(t)
tlscaCertificate := randomString(t)
opts := VaultOIDCConfigurationUpdateOptions{
RoleName: &roleName,
Namespace: &namespace,
JWTAuthPath: &jwtAuthPath,
TLSCACertificate: &tlscaCertificate,
}
updated, err := client.VaultOIDCConfigurations.Update(ctx, oidcConfig.ID, opts)
require.NoError(t, err)
require.NotEmpty(t, updated)
assert.Equal(t, oidcConfig.Address, updated.Address) // not updated
assert.Equal(t, *opts.RoleName, updated.RoleName)
assert.Equal(t, *opts.Namespace, updated.Namespace)
assert.Equal(t, *opts.JWTAuthPath, updated.JWTAuthPath)
assert.Equal(t, *opts.TLSCACertificate, updated.TLSCACertificate)
})
t.Run("role name not provided", func(t *testing.T) {
oidcConfig, oidcConfigCleanup := createVaultOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcConfigCleanup)
address := randomString(t)
namespace := randomString(t)
jwtAuthPath := randomString(t)
tlscaCertificate := randomString(t)
opts := VaultOIDCConfigurationUpdateOptions{
Address: &address,
Namespace: &namespace,
JWTAuthPath: &jwtAuthPath,
TLSCACertificate: &tlscaCertificate,
}
updated, err := client.VaultOIDCConfigurations.Update(ctx, oidcConfig.ID, opts)
require.NoError(t, err)
require.NotEmpty(t, updated)
assert.Equal(t, *opts.Address, updated.Address)
assert.Equal(t, oidcConfig.RoleName, updated.RoleName) // not updated
assert.Equal(t, *opts.Namespace, updated.Namespace)
assert.Equal(t, *opts.JWTAuthPath, updated.JWTAuthPath)
assert.Equal(t, *opts.TLSCACertificate, updated.TLSCACertificate)
})
t.Run("namespace not provided", func(t *testing.T) {
oidcConfig, oidcConfigCleanup := createVaultOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcConfigCleanup)
address := randomString(t)
roleName := randomString(t)
jwtAuthPath := randomString(t)
tlscaCertificate := randomString(t)
opts := VaultOIDCConfigurationUpdateOptions{
Address: &address,
RoleName: &roleName,
JWTAuthPath: &jwtAuthPath,
TLSCACertificate: &tlscaCertificate,
}
updated, err := client.VaultOIDCConfigurations.Update(ctx, oidcConfig.ID, opts)
require.NoError(t, err)
require.NotEmpty(t, updated)
assert.Equal(t, *opts.Address, updated.Address)
assert.Equal(t, *opts.RoleName, updated.RoleName)
assert.Equal(t, oidcConfig.Namespace, updated.Namespace) // not updated
assert.Equal(t, *opts.JWTAuthPath, updated.JWTAuthPath)
assert.Equal(t, *opts.TLSCACertificate, updated.TLSCACertificate)
})
t.Run("JWTAuthPath not provided", func(t *testing.T) {
oidcConfig, oidcConfigCleanup := createVaultOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcConfigCleanup)
address := randomString(t)
roleName := randomString(t)
namespace := randomString(t)
tlscaCertificate := randomString(t)
opts := VaultOIDCConfigurationUpdateOptions{
Address: &address,
RoleName: &roleName,
Namespace: &namespace,
TLSCACertificate: &tlscaCertificate,
}
updated, err := client.VaultOIDCConfigurations.Update(ctx, oidcConfig.ID, opts)
require.NoError(t, err)
require.NotEmpty(t, updated)
assert.Equal(t, *opts.Address, updated.Address)
assert.Equal(t, *opts.RoleName, updated.RoleName)
assert.Equal(t, *opts.Namespace, updated.Namespace)
assert.Equal(t, oidcConfig.JWTAuthPath, updated.JWTAuthPath) // not updated
assert.Equal(t, *opts.TLSCACertificate, updated.TLSCACertificate)
})
t.Run("TLSCACertificate not provided", func(t *testing.T) {
oidcConfig, oidcConfigCleanup := createVaultOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcConfigCleanup)
address := randomString(t)
roleName := randomString(t)
namespace := randomString(t)
jwtAuthPath := randomString(t)
opts := VaultOIDCConfigurationUpdateOptions{
Address: &address,
RoleName: &roleName,
Namespace: &namespace,
JWTAuthPath: &jwtAuthPath,
}
updated, err := client.VaultOIDCConfigurations.Update(ctx, oidcConfig.ID, opts)
require.NoError(t, err)
require.NotEmpty(t, updated)
assert.Equal(t, *opts.Address, updated.Address)
assert.Equal(t, *opts.RoleName, updated.RoleName)
assert.Equal(t, *opts.Namespace, updated.Namespace)
assert.Equal(t, *opts.JWTAuthPath, updated.JWTAuthPath)
assert.Equal(t, oidcConfig.TLSCACertificate, updated.TLSCACertificate) // not updated
})
}
================================================
FILE: workspace.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"io"
"net/url"
"strings"
"time"
"github.com/hashicorp/jsonapi"
)
// Compile-time proof of interface implementation.
var _ Workspaces = (*workspaces)(nil)
// Workspaces describes all the workspace related methods that the Terraform
// Enterprise API supports.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/workspaces
type Workspaces interface {
// List all the workspaces within an organization.
List(ctx context.Context, organization string, options *WorkspaceListOptions) (*WorkspaceList, error)
// Create is used to create a new workspace.
Create(ctx context.Context, organization string, options WorkspaceCreateOptions) (*Workspace, error)
// Read a workspace by its name and organization name.
Read(ctx context.Context, organization string, workspace string) (*Workspace, error)
// ReadWithOptions reads a workspace by name and organization name with given options.
ReadWithOptions(ctx context.Context, organization string, workspace string, options *WorkspaceReadOptions) (*Workspace, error)
// Readme gets the readme of a workspace by its ID.
Readme(ctx context.Context, workspaceID string) (io.Reader, error)
// ReadByID reads a workspace by its ID.
ReadByID(ctx context.Context, workspaceID string) (*Workspace, error)
// ReadByIDWithOptions reads a workspace by its ID with the given options.
ReadByIDWithOptions(ctx context.Context, workspaceID string, options *WorkspaceReadOptions) (*Workspace, error)
// Update settings of an existing workspace.
Update(ctx context.Context, organization string, workspace string, options WorkspaceUpdateOptions) (*Workspace, error)
// UpdateByID updates the settings of an existing workspace.
UpdateByID(ctx context.Context, workspaceID string, options WorkspaceUpdateOptions) (*Workspace, error)
// Delete a workspace by its name.
Delete(ctx context.Context, organization string, workspace string) error
// DeleteByID deletes a workspace by its ID.
DeleteByID(ctx context.Context, workspaceID string) error
// SafeDelete a workspace by its name.
SafeDelete(ctx context.Context, organization string, workspace string) error
// SafeDeleteByID deletes a workspace by its ID.
SafeDeleteByID(ctx context.Context, workspaceID string) error
// RemoveVCSConnection from a workspace.
RemoveVCSConnection(ctx context.Context, organization, workspace string) (*Workspace, error)
// RemoveVCSConnectionByID removes a VCS connection from a workspace.
RemoveVCSConnectionByID(ctx context.Context, workspaceID string) (*Workspace, error)
// Lock a workspace by its ID.
Lock(ctx context.Context, workspaceID string, options WorkspaceLockOptions) (*Workspace, error)
// Unlock a workspace by its ID.
Unlock(ctx context.Context, workspaceID string) (*Workspace, error)
// ForceUnlock a workspace by its ID.
ForceUnlock(ctx context.Context, workspaceID string) (*Workspace, error)
// AssignSSHKey to a workspace.
AssignSSHKey(ctx context.Context, workspaceID string, options WorkspaceAssignSSHKeyOptions) (*Workspace, error)
// UnassignSSHKey from a workspace.
UnassignSSHKey(ctx context.Context, workspaceID string) (*Workspace, error)
// ListRemoteStateConsumers reads the remote state consumers for a workspace.
ListRemoteStateConsumers(ctx context.Context, workspaceID string, options *RemoteStateConsumersListOptions) (*WorkspaceList, error)
// AddRemoteStateConsumers adds remote state consumers to a workspace.
AddRemoteStateConsumers(ctx context.Context, workspaceID string, options WorkspaceAddRemoteStateConsumersOptions) error
// RemoveRemoteStateConsumers removes remote state consumers from a workspace.
RemoveRemoteStateConsumers(ctx context.Context, workspaceID string, options WorkspaceRemoveRemoteStateConsumersOptions) error
// UpdateRemoteStateConsumers updates all the remote state consumers for a workspace
// to match the workspaces in the update options.
UpdateRemoteStateConsumers(ctx context.Context, workspaceID string, options WorkspaceUpdateRemoteStateConsumersOptions) error
// ListTags reads the tags for a workspace.
ListTags(ctx context.Context, workspaceID string, options *WorkspaceTagListOptions) (*TagList, error)
// AddTags appends tags to a workspace
AddTags(ctx context.Context, workspaceID string, options WorkspaceAddTagsOptions) error
// RemoveTags removes tags from a workspace
RemoveTags(ctx context.Context, workspaceID string, options WorkspaceRemoveTagsOptions) error
// ReadDataRetentionPolicy reads a workspace's data retention policy
//
// Deprecated: Use ReadDataRetentionPolicyChoice instead.
// **Note: This functionality is only available in Terraform Enterprise versions v202311-1 and v202312-1.**
ReadDataRetentionPolicy(ctx context.Context, workspaceID string) (*DataRetentionPolicy, error)
// ReadDataRetentionPolicyChoice reads a workspace's data retention policy
// **Note: This functionality is only available in Terraform Enterprise.**
ReadDataRetentionPolicyChoice(ctx context.Context, workspaceID string) (*DataRetentionPolicyChoice, error)
// SetDataRetentionPolicy sets a workspace's data retention policy to delete data older than a certain number of days
//
// Deprecated: Use SetDataRetentionPolicyDeleteOlder instead
// **Note: This functionality is only available in Terraform Enterprise versions v202311-1 and v202312-1.**
SetDataRetentionPolicy(ctx context.Context, workspaceID string, options DataRetentionPolicySetOptions) (*DataRetentionPolicy, error)
// SetDataRetentionPolicyDeleteOlder sets a workspace's data retention policy to delete data older than a certain number of days
// **Note: This functionality is only available in Terraform Enterprise.**
SetDataRetentionPolicyDeleteOlder(ctx context.Context, workspaceID string, options DataRetentionPolicyDeleteOlderSetOptions) (*DataRetentionPolicyDeleteOlder, error)
// SetDataRetentionPolicyDontDelete sets a workspace's data retention policy to explicitly not delete data
// **Note: This functionality is only available in Terraform Enterprise.**
SetDataRetentionPolicyDontDelete(ctx context.Context, workspaceID string, options DataRetentionPolicyDontDeleteSetOptions) (*DataRetentionPolicyDontDelete, error)
// DeleteDataRetentionPolicy deletes a workspace's data retention policy
// **Note: This functionality is only available in Terraform Enterprise.**
DeleteDataRetentionPolicy(ctx context.Context, workspaceID string) error
// ListTagBindings lists all tag bindings associated with the workspace.
ListTagBindings(ctx context.Context, workspaceID string) ([]*TagBinding, error)
// ListEffectiveTagBindings lists all tag bindings associated with the workspace which may be
// either inherited from a project or binded to the workspace itself.
ListEffectiveTagBindings(ctx context.Context, workspaceID string) ([]*EffectiveTagBinding, error)
// AddTagBindings adds or modifies the value of existing tag binding keys for a workspace.
AddTagBindings(ctx context.Context, workspaceID string, options WorkspaceAddTagBindingsOptions) ([]*TagBinding, error)
// DeleteAllTagBindings removes all tag bindings for a workspace.
DeleteAllTagBindings(ctx context.Context, workspaceID string) error
}
// workspaces implements Workspaces.
type workspaces struct {
client *Client
}
// WorkspaceSource represents a source type of a workspace.
type WorkspaceSource string
const (
WorkspaceSourceAPI WorkspaceSource = "tfe-api"
WorkspaceSourceModule WorkspaceSource = "tfe-module"
WorkspaceSourceUI WorkspaceSource = "tfe-ui"
WorkspaceSourceTerraform WorkspaceSource = "terraform"
)
// WorkspaceList represents a list of workspaces.
type WorkspaceList struct {
*Pagination
Items []*Workspace
}
// WorkspaceAddTagBindingsOptions represents the options for adding tag bindings
// to a workspace.
type WorkspaceAddTagBindingsOptions struct {
TagBindings []*TagBinding
}
// LockedByChoice is a choice type struct that represents the possible values
// within a polymorphic relation. If a value is available, exactly one field
// will be non-nil.
type LockedByChoice struct {
Run *Run
User *User
Team *Team
}
// Workspace represents a Terraform Enterprise workspace.
type Workspace struct {
ID string `jsonapi:"primary,workspaces"`
Actions *WorkspaceActions `jsonapi:"attr,actions"`
AllowDestroyPlan bool `jsonapi:"attr,allow-destroy-plan"`
AssessmentsEnabled bool `jsonapi:"attr,assessments-enabled"`
AutoApply bool `jsonapi:"attr,auto-apply"`
AutoApplyRunTrigger bool `jsonapi:"attr,auto-apply-run-trigger"`
AutoDestroyAt jsonapi.NullableAttr[time.Time] `jsonapi:"attr,auto-destroy-at,iso8601,omitempty"`
AutoDestroyActivityDuration jsonapi.NullableAttr[string] `jsonapi:"attr,auto-destroy-activity-duration,omitempty"`
CanQueueDestroyPlan bool `jsonapi:"attr,can-queue-destroy-plan"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Description string `jsonapi:"attr,description"`
Environment string `jsonapi:"attr,environment"`
ExecutionMode string `jsonapi:"attr,execution-mode"`
FileTriggersEnabled bool `jsonapi:"attr,file-triggers-enabled"`
GlobalRemoteState bool `jsonapi:"attr,global-remote-state"`
ProjectRemoteState bool `jsonapi:"attr,project-remote-state"`
InheritsProjectAutoDestroy bool `jsonapi:"attr,inherits-project-auto-destroy"`
Locked bool `jsonapi:"attr,locked"`
MigrationEnvironment string `jsonapi:"attr,migration-environment"`
Name string `jsonapi:"attr,name"`
NoCodeUpgradeAvailable bool `jsonapi:"attr,no-code-upgrade-available"`
Operations bool `jsonapi:"attr,operations"`
Permissions *WorkspacePermissions `jsonapi:"attr,permissions"`
QueueAllRuns bool `jsonapi:"attr,queue-all-runs"`
SpeculativeEnabled bool `jsonapi:"attr,speculative-enabled"`
Source WorkspaceSource `jsonapi:"attr,source"`
SourceName string `jsonapi:"attr,source-name"`
SourceURL string `jsonapi:"attr,source-url"`
StructuredRunOutputEnabled bool `jsonapi:"attr,structured-run-output-enabled"`
TerraformVersion string `jsonapi:"attr,terraform-version"`
TriggerPrefixes []string `jsonapi:"attr,trigger-prefixes"`
TriggerPatterns []string `jsonapi:"attr,trigger-patterns"`
VCSRepo *VCSRepo `jsonapi:"attr,vcs-repo"`
WorkingDirectory string `jsonapi:"attr,working-directory"`
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
ResourceCount int `jsonapi:"attr,resource-count"`
ApplyDurationAverage time.Duration `jsonapi:"attr,apply-duration-average"`
PlanDurationAverage time.Duration `jsonapi:"attr,plan-duration-average"`
PolicyCheckFailures int `jsonapi:"attr,policy-check-failures"`
RunFailures int `jsonapi:"attr,run-failures"`
RunsCount int `jsonapi:"attr,workspace-kpis-runs-count"`
TagNames []string `jsonapi:"attr,tag-names"`
SettingOverwrites *WorkspaceSettingOverwrites `jsonapi:"attr,setting-overwrites"`
HYOKEnabled *bool `jsonapi:"attr,hyok-enabled"`
// Relations
AgentPool *AgentPool `jsonapi:"relation,agent-pool"`
CurrentRun *Run `jsonapi:"relation,current-run"`
CurrentStateVersion *StateVersion `jsonapi:"relation,current-state-version"`
Organization *Organization `jsonapi:"relation,organization"`
SSHKey *SSHKey `jsonapi:"relation,ssh-key"`
Outputs []*WorkspaceOutputs `jsonapi:"relation,outputs"`
Project *Project `jsonapi:"relation,project"`
Tags []*Tag `jsonapi:"relation,tags"`
CurrentConfigurationVersion *ConfigurationVersion `jsonapi:"relation,current-configuration-version,omitempty"`
LockedBy *LockedByChoice `jsonapi:"polyrelation,locked-by"`
Variables []*Variable `jsonapi:"relation,vars"`
TagBindings []*TagBinding `jsonapi:"relation,tag-bindings"`
EffectiveTagBindings []*EffectiveTagBinding `jsonapi:"relation,effective-tag-bindings"`
HYOKEncryptedDataKey *HYOKEncryptedDataKey `jsonapi:"relation,hyok-data-key-for-encryption"`
// Deprecated: Use DataRetentionPolicyChoice instead.
DataRetentionPolicy *DataRetentionPolicy
// **Note: This functionality is only available in Terraform Enterprise.**
DataRetentionPolicyChoice *DataRetentionPolicyChoice `jsonapi:"polyrelation,data-retention-policy"`
// Links
Links map[string]interface{} `jsonapi:"links,omitempty"`
}
type WorkspaceOutputs struct {
ID string `jsonapi:"primary,workspace-outputs"`
Name string `jsonapi:"attr,name"`
Sensitive bool `jsonapi:"attr,sensitive"`
Type string `jsonapi:"attr,output-type"`
Value interface{} `jsonapi:"attr,value"`
}
// workspaceWithReadme is the same as a workspace but it has a readme.
type workspaceWithReadme struct {
ID string `jsonapi:"primary,workspaces"`
Readme *workspaceReadme `jsonapi:"relation,readme"`
}
// workspaceReadme contains the readme of the workspace.
type workspaceReadme struct {
ID string `jsonapi:"primary,workspace-readme"`
RawMarkdown string `jsonapi:"attr,raw-markdown"`
}
// VCSRepo contains the configuration of a VCS integration.
type VCSRepo struct {
Branch string `jsonapi:"attr,branch"`
DisplayIdentifier string `jsonapi:"attr,display-identifier"`
Identifier string `jsonapi:"attr,identifier"`
IngressSubmodules bool `jsonapi:"attr,ingress-submodules"`
OAuthTokenID string `jsonapi:"attr,oauth-token-id"`
GHAInstallationID string `jsonapi:"attr,github-app-installation-id"`
RepositoryHTTPURL string `jsonapi:"attr,repository-http-url"`
ServiceProvider string `jsonapi:"attr,service-provider"`
Tags bool `jsonapi:"attr,tags"`
TagsRegex string `jsonapi:"attr,tags-regex"`
WebhookURL string `jsonapi:"attr,webhook-url"`
SourceDirectory string `jsonapi:"attr,source-directory"`
TagPrefix string `jsonapi:"attr,tag-prefix"`
}
// Note: the fields of this struct are bool pointers instead of bool values, in order to simplify support for
// future TFE versions that support *some but not all* of the inherited defaults that go-tfe knows about.
type WorkspaceSettingOverwrites struct {
ExecutionMode *bool `jsonapi:"attr,execution-mode"`
AgentPool *bool `jsonapi:"attr,agent-pool"`
}
// WorkspaceActions represents the workspace actions.
type WorkspaceActions struct {
IsDestroyable bool `jsonapi:"attr,is-destroyable"`
}
// WorkspacePermissions represents the workspace permissions.
type WorkspacePermissions struct {
CanDestroy bool `jsonapi:"attr,can-destroy"`
CanForceUnlock bool `jsonapi:"attr,can-force-unlock"`
CanLock bool `jsonapi:"attr,can-lock"`
CanManageRunTasks bool `jsonapi:"attr,can-manage-run-tasks"`
CanManageHYOK bool `jsonapi:"attr,can-manage-hyok"`
CanQueueApply bool `jsonapi:"attr,can-queue-apply"`
CanQueueDestroy bool `jsonapi:"attr,can-queue-destroy"`
CanQueueRun bool `jsonapi:"attr,can-queue-run"`
CanReadSettings bool `jsonapi:"attr,can-read-settings"`
CanUnlock bool `jsonapi:"attr,can-unlock"`
CanUpdate bool `jsonapi:"attr,can-update"`
CanUpdateVariable bool `jsonapi:"attr,can-update-variable"`
CanForceDelete *bool `jsonapi:"attr,can-force-delete"` // pointer b/c it will be useful to check if this property exists, as opposed to having it default to false
}
// WSIncludeOpt represents the available options for include query params.
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/workspaces#available-related-resources
type WSIncludeOpt string
const (
WSOrganization WSIncludeOpt = "organization"
WSCurrentConfigVer WSIncludeOpt = "current_configuration_version"
WSCurrentConfigVerIngress WSIncludeOpt = "current_configuration_version.ingress_attributes"
WSCurrentRun WSIncludeOpt = "current_run"
WSCurrentRunPlan WSIncludeOpt = "current_run.plan"
WSCurrentRunConfigVer WSIncludeOpt = "current_run.configuration_version"
WSCurrentrunConfigVerIngress WSIncludeOpt = "current_run.configuration_version.ingress_attributes"
WSEffectiveTagBindings WSIncludeOpt = "effective_tag_bindings"
WSLockedBy WSIncludeOpt = "locked_by"
WSReadme WSIncludeOpt = "readme"
WSOutputs WSIncludeOpt = "outputs"
WSCurrentStateVer WSIncludeOpt = "current-state-version"
WSProject WSIncludeOpt = "project"
)
// WorkspaceReadOptions represents the options for reading a workspace.
type WorkspaceReadOptions struct {
// Optional: A list of relations to include.
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/workspaces#available-related-resources
Include []WSIncludeOpt `url:"include,omitempty"`
}
// WorkspaceListOptions represents the options for listing workspaces.
type WorkspaceListOptions struct {
ListOptions
// Optional: A search string (partial workspace name) used to filter the results.
Search string `url:"search[name],omitempty"`
// Optional: A search string (comma-separated tag names) used to filter the results.
Tags string `url:"search[tags],omitempty"`
// Optional: A search string (comma-separated tag names to exclude) used to filter the results.
ExcludeTags string `url:"search[exclude-tags],omitempty"`
// Optional: A search on substring matching to filter the results.
WildcardName string `url:"search[wildcard-name],omitempty"`
// Optional: A filter string to list all the workspaces linked to a given project id in the organization.
ProjectID string `url:"filter[project][id],omitempty"`
// Optional: A filter string to list all the workspaces filtered by current run status.
CurrentRunStatus string `url:"filter[current-run][status],omitempty"`
// Optional: A filter string to list workspaces filtered by key/value tags.
// These are not annotated and therefore not encoded by go-querystring
TagBindings []*TagBinding
// Optional: A list of relations to include. See available resources https://developer.hashicorp.com/terraform/cloud-docs/api-docs/workspaces#available-related-resources
Include []WSIncludeOpt `url:"include,omitempty"`
// Optional: May sort on "name" (the default) and "current-run.created-at" (which sorts by the time of the current run)
// Prepending a hyphen to the sort parameter will reverse the order (e.g. "-name" to reverse the default order)
Sort string `url:"sort,omitempty"`
}
// WorkspaceCreateOptions represents the options for creating a new workspace.
type WorkspaceCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,workspaces"`
// Required when: execution-mode is set to agent. The ID of the agent pool
// belonging to the workspace's organization. This value must not be specified
// if execution-mode is set to remote or local or if operations is set to true.
AgentPoolID *string `jsonapi:"attr,agent-pool-id,omitempty"`
// Optional: Whether destroy plans can be queued on the workspace.
AllowDestroyPlan *bool `jsonapi:"attr,allow-destroy-plan,omitempty"`
// Optional: Whether to enable health assessments (drift detection etc.) for the workspace.
// Reference: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/workspaces#create-a-workspace
// Requires remote execution mode, HCP Terraform Business entitlement, and a valid agent pool to work
AssessmentsEnabled *bool `jsonapi:"attr,assessments-enabled,omitempty"`
// Optional: Whether to automatically apply changes when a Terraform plan is successful.
AutoApply *bool `jsonapi:"attr,auto-apply,omitempty"`
// Optional: Whether to automatically apply changes for runs that are created by run triggers
// from another workspace.
AutoApplyRunTrigger *bool `jsonapi:"attr,auto-apply-run-trigger,omitempty"`
// Optional: The time after which an automatic destroy run will be queued
AutoDestroyAt jsonapi.NullableAttr[time.Time] `jsonapi:"attr,auto-destroy-at,iso8601,omitempty"`
// Optional: The period of time to wait after workspace activity to trigger a destroy run. The format
// should roughly match a Go duration string limited to days and hours, e.g. "24h" or "1d".
AutoDestroyActivityDuration jsonapi.NullableAttr[string] `jsonapi:"attr,auto-destroy-activity-duration,omitempty"`
// Optional: Whether the workspace inherits auto destroy settings from the project
InheritsProjectAutoDestroy *bool `jsonapi:"attr,inherits-project-auto-destroy,omitempty"`
// Optional: A description for the workspace.
Description *string `jsonapi:"attr,description,omitempty"`
// Optional: Which execution mode to use. Valid values are remote, local, and agent.
// When set to local, the workspace will be used for state storage only.
// This value must not be specified if operations is specified.
// 'agent' execution mode is not available in Terraform Enterprise.
ExecutionMode *string `jsonapi:"attr,execution-mode,omitempty"`
// Optional: Whether to filter runs based on the changed files in a VCS push. If
// enabled, the working directory and trigger prefixes describe a set of
// paths which must contain changes for a VCS push to trigger a run. If
// disabled, any push will trigger a run.
FileTriggersEnabled *bool `jsonapi:"attr,file-triggers-enabled,omitempty"`
GlobalRemoteState *bool `jsonapi:"attr,global-remote-state,omitempty"`
// Optional: Allows the workspace to share remote state at the project level.
// Default is false.
ProjectRemoteState *bool `jsonapi:"attr,project-remote-state,omitempty"`
// Optional: The legacy TFE environment to use as the source of the migration, in the
// form organization/environment. Omit this unless you are migrating a legacy
// environment.
MigrationEnvironment *string `jsonapi:"attr,migration-environment,omitempty"`
// The name of the workspace, which can only include letters, numbers, -,
// and _. This will be used as an identifier and must be unique in the
// organization.
Name *string `jsonapi:"attr,name"`
// DEPRECATED. Whether the workspace will use remote or local execution mode.
// Use ExecutionMode instead.
Operations *bool `jsonapi:"attr,operations,omitempty"`
// Whether to queue all runs. Unless this is set to true, runs triggered by
// a webhook will not be queued until at least one run is manually queued.
QueueAllRuns *bool `jsonapi:"attr,queue-all-runs,omitempty"`
// Whether this workspace allows speculative plans. Setting this to false
// prevents HCP Terraform or the Terraform Enterprise instance from
// running plans on pull requests, which can improve security if the VCS
// repository is public or includes untrusted contributors.
SpeculativeEnabled *bool `jsonapi:"attr,speculative-enabled,omitempty"`
// BETA. A friendly name for the application or client creating this
// workspace. If set, this will be displayed on the workspace as
// "Created via ".
SourceName *string `jsonapi:"attr,source-name,omitempty"`
// BETA. A URL for the application or client creating this workspace. This
// can be the URL of a related resource in another app, or a link to
// documentation or other info about the client.
SourceURL *string `jsonapi:"attr,source-url,omitempty"`
// BETA. Enable the experimental advanced run user interface.
// This only applies to runs using Terraform version 0.15.2 or newer,
// and runs executed using older versions will see the classic experience
// regardless of this setting.
StructuredRunOutputEnabled *bool `jsonapi:"attr,structured-run-output-enabled,omitempty"`
// The version of Terraform to use for this workspace. Upon creating a
// workspace, the latest version is selected unless otherwise specified.
TerraformVersion *string `jsonapi:"attr,terraform-version,omitempty"`
// List of repository-root-relative paths which list all locations to be
// tracked for changes. See FileTriggersEnabled above for more details.
TriggerPrefixes []string `jsonapi:"attr,trigger-prefixes,omitempty"`
// Optional: List of patterns used to match against changed files in order
// to decide whether to trigger a run or not.
TriggerPatterns []string `jsonapi:"attr,trigger-patterns,omitempty"`
// Settings for the workspace's VCS repository. If omitted, the workspace is
// created without a VCS repo. If included, you must specify at least the
// oauth-token-id and identifier keys below.
VCSRepo *VCSRepoOptions `jsonapi:"attr,vcs-repo,omitempty"`
// A relative path that Terraform will execute within. This defaults to the
// root of your repository and is typically set to a subdirectory matching the
// environment when multiple environments exist within the same repository.
WorkingDirectory *string `jsonapi:"attr,working-directory,omitempty"`
// Optional: Enables HYOK in the workspace.
// If set to true, the workspace will be created with HYOK enabled.
// If set to false, the workspace will be created with HYOK disabled.
// If not specified, the workspace will be created with HYOK disabled.
// Note: HYOK is only available in HCP Terraform.
HYOKEnabled *bool `jsonapi:"attr,hyok-enabled,omitempty"`
// A list of tags to attach to the workspace. If the tag does not already
// exist, it is created and added to the workspace.
Tags []*Tag `jsonapi:"relation,tags,omitempty"`
// Optional: Struct of booleans, which indicate whether the workspace
// specifies its own values for various settings. If you mark a setting as
// `false` in this struct, it will clear the workspace's existing value for
// that setting and defer to the default value that its project or
// organization provides.
//
// In general, it's not necessary to mark a setting as `true` in this
// struct; if you provide a literal value for a setting, HCP Terraform will
// automatically update its overwrites field to `true`. If you do choose to
// manually mark a setting as overwritten, you must provide a value for that
// setting at the same time.
SettingOverwrites *WorkspaceSettingOverwritesOptions `jsonapi:"attr,setting-overwrites,omitempty"`
// Associated Project with the workspace. If not provided, default project
// of the organization will be assigned to the workspace.
Project *Project `jsonapi:"relation,project,omitempty"`
// Associated TagBindings of the workspace.
TagBindings []*TagBinding `jsonapi:"relation,tag-bindings,omitempty"`
}
// TODO: move this struct out. VCSRepoOptions is used by workspaces, policy sets, and registry modules
// VCSRepoOptions represents the configuration options of a VCS integration.
type VCSRepoOptions struct {
Branch *string `json:"branch,omitempty"`
Identifier *string `json:"identifier,omitempty"`
IngressSubmodules *bool `json:"ingress-submodules,omitempty"`
OAuthTokenID *string `json:"oauth-token-id,omitempty"`
TagsRegex *string `json:"tags-regex,omitempty"`
GHAInstallationID *string `json:"github-app-installation-id,omitempty"`
}
type WorkspaceSettingOverwritesOptions struct {
// If false, the workspace will defer to its organization or project's DefaultExecutionMode value.
ExecutionMode *bool `json:"execution-mode,omitempty"`
// If false, the workspace will defer to its organization or project's DefaultAgentPool value.
AgentPool *bool `json:"agent-pool,omitempty"`
}
// WorkspaceUpdateOptions represents the options for updating a workspace.
type WorkspaceUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,workspaces"`
// Required when: execution-mode is set to agent. The ID of the agent pool
// belonging to the workspace's organization. This value must not be specified
// if execution-mode is set to remote or local or if operations is set to true.
AgentPoolID *string `jsonapi:"attr,agent-pool-id,omitempty"`
// Optional: Whether destroy plans can be queued on the workspace.
AllowDestroyPlan *bool `jsonapi:"attr,allow-destroy-plan,omitempty"`
// Optional: Whether to enable health assessments (drift detection etc.) for the workspace.
// Reference: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/workspaces#update-a-workspace
// Requires remote execution mode, HCP Terraform Business entitlement, and a valid agent pool to work
AssessmentsEnabled *bool `jsonapi:"attr,assessments-enabled,omitempty"`
// Optional: Whether to automatically apply changes when a Terraform plan is successful.
AutoApply *bool `jsonapi:"attr,auto-apply,omitempty"`
// Optional: Whether to automatically apply changes for runs that are created by run triggers
// from another workspace.
AutoApplyRunTrigger *bool `jsonapi:"attr,auto-apply-run-trigger,omitempty"`
// Optional: The time after which an automatic destroy run will be queued
AutoDestroyAt jsonapi.NullableAttr[time.Time] `jsonapi:"attr,auto-destroy-at,iso8601,omitempty"`
// Optional: The period of time to wait after workspace activity to trigger a destroy run. The format
// should roughly match a Go duration string limited to days and hours, e.g. "24h" or "1d".
AutoDestroyActivityDuration jsonapi.NullableAttr[string] `jsonapi:"attr,auto-destroy-activity-duration,omitempty"`
// Optional: Whether the workspace inherits auto destroy settings from the project
InheritsProjectAutoDestroy *bool `jsonapi:"attr,inherits-project-auto-destroy,omitempty"`
// Optional: A new name for the workspace, which can only include letters, numbers, -,
// and _. This will be used as an identifier and must be unique in the
// organization. Warning: Changing a workspace's name changes its URL in the
// API and UI.
Name *string `jsonapi:"attr,name,omitempty"`
// Optional: A description for the workspace.
Description *string `jsonapi:"attr,description,omitempty"`
// Optional: Which execution mode to use. Valid values are remote, local, and agent.
// When set to local, the workspace will be used for state storage only.
// This value must not be specified if operations is specified.
// 'agent' execution mode is not available in Terraform Enterprise.
ExecutionMode *string `jsonapi:"attr,execution-mode,omitempty"`
// Optional: Whether to filter runs based on the changed files in a VCS push. If
// enabled, the working directory and trigger prefixes describe a set of
// paths which must contain changes for a VCS push to trigger a run. If
// disabled, any push will trigger a run.
FileTriggersEnabled *bool `jsonapi:"attr,file-triggers-enabled,omitempty"`
// Optional:
GlobalRemoteState *bool `jsonapi:"attr,global-remote-state,omitempty"`
// Optional: Allows the workspace to share remote state at the project level.
// Default is false.
ProjectRemoteState *bool `jsonapi:"attr,project-remote-state,omitempty"`
// DEPRECATED. Whether the workspace will use remote or local execution mode.
// Use ExecutionMode instead.
Operations *bool `jsonapi:"attr,operations,omitempty"`
// Optional: Whether to queue all runs. Unless this is set to true, runs triggered by
// a webhook will not be queued until at least one run is manually queued.
QueueAllRuns *bool `jsonapi:"attr,queue-all-runs,omitempty"`
// Optional: Whether this workspace allows speculative plans. Setting this to false
// prevents HCP Terraform or the Terraform Enterprise instance from
// running plans on pull requests, which can improve security if the VCS
// repository is public or includes untrusted contributors.
SpeculativeEnabled *bool `jsonapi:"attr,speculative-enabled,omitempty"`
// BETA. Enable the experimental advanced run user interface.
// This only applies to runs using Terraform version 0.15.2 or newer,
// and runs executed using older versions will see the classic experience
// regardless of this setting.
StructuredRunOutputEnabled *bool `jsonapi:"attr,structured-run-output-enabled,omitempty"`
// Optional: The version of Terraform to use for this workspace.
TerraformVersion *string `jsonapi:"attr,terraform-version,omitempty"`
// Optional: List of repository-root-relative paths which list all locations to be
// tracked for changes. See FileTriggersEnabled above for more details.
TriggerPrefixes []string `jsonapi:"attr,trigger-prefixes,omitempty"`
// Optional: List of patterns used to match against changed files in order
// to decide whether to trigger a run or not.
TriggerPatterns []string `jsonapi:"attr,trigger-patterns,omitempty"`
// Optional: To delete a workspace's existing VCS repo, specify null instead of an
// object. To modify a workspace's existing VCS repo, include whichever of
// the keys below you wish to modify. To add a new VCS repo to a workspace
// that didn't previously have one, include at least the oauth-token-id and
// identifier keys.
VCSRepo *VCSRepoOptions `jsonapi:"attr,vcs-repo,omitempty"`
// Optional: A relative path that Terraform will execute within. This defaults to the
// root of your repository and is typically set to a subdirectory matching
// the environment when multiple environments exist within the same
// repository.
WorkingDirectory *string `jsonapi:"attr,working-directory,omitempty"`
// Optional: Struct of booleans, which indicate whether the workspace
// specifies its own values for various settings. If you mark a setting as
// `false` in this struct, it will clear the workspace's existing value for
// that setting and defer to the default value that its project or
// organization provides.
//
// In general, it's not necessary to mark a setting as `true` in this
// struct; if you provide a literal value for a setting, HCP Terraform will
// automatically update its overwrites field to `true`. If you do choose to
// manually mark a setting as overwritten, you must provide a value for that
// setting at the same time.
SettingOverwrites *WorkspaceSettingOverwritesOptions `jsonapi:"attr,setting-overwrites,omitempty"`
// Optional: Enables HYOK in the workspace.
// If set to true, the workspace will be updated with HYOK enabled.
// This can't be set to false, as HYOK is a one-way operation.
HYOKEnabled *bool `jsonapi:"attr,hyok-enabled,omitempty"`
// Associated Project with the workspace. If not provided, default project
// of the organization will be assigned to the workspace
Project *Project `jsonapi:"relation,project,omitempty"`
// Associated TagBindings of the project. Note that this will replace
// all existing tag bindings.
TagBindings []*TagBinding `jsonapi:"relation,tag-bindings,omitempty"`
}
// WorkspaceLockOptions represents the options for locking a workspace.
type WorkspaceLockOptions struct {
// Specifies the reason for locking the workspace.
Reason *string `jsonapi:"attr,reason,omitempty"`
}
// workspaceRemoveVCSConnectionOptions
type workspaceRemoveVCSConnectionOptions struct {
ID string `jsonapi:"primary,workspaces"`
VCSRepo *VCSRepoOptions `jsonapi:"attr,vcs-repo"`
}
// WorkspaceAssignSSHKeyOptions represents the options to assign an SSH key to
// a workspace.
type WorkspaceAssignSSHKeyOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,workspaces"`
// The SSH key ID to assign.
SSHKeyID *string `jsonapi:"attr,id"`
}
// workspaceUnassignSSHKeyOptions represents the options to unassign an SSH key
// to a workspace.
type workspaceUnassignSSHKeyOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,workspaces"`
// Must be nil to unset the currently assigned SSH key.
SSHKeyID *string `jsonapi:"attr,id"`
}
type RemoteStateConsumersListOptions struct {
ListOptions
}
// WorkspaceAddRemoteStateConsumersOptions represents the options for adding remote state consumers
// to a workspace.
type WorkspaceAddRemoteStateConsumersOptions struct {
// The workspaces to add as remote state consumers to the workspace.
Workspaces []*Workspace
}
// WorkspaceRemoveRemoteStateConsumersOptions represents the options for removing remote state
// consumers from a workspace.
type WorkspaceRemoveRemoteStateConsumersOptions struct {
// The workspaces to remove as remote state consumers from the workspace.
Workspaces []*Workspace
}
// WorkspaceUpdateRemoteStateConsumersOptions represents the options for
// updatintg remote state consumers from a workspace.
type WorkspaceUpdateRemoteStateConsumersOptions struct {
// The workspaces to update remote state consumers for the workspace.
Workspaces []*Workspace
}
type WorkspaceTagListOptions struct {
ListOptions
// A query string used to filter workspace tags.
// Any workspace tag with a name partially matching this value will be returned.
Query *string `url:"name,omitempty"`
}
type WorkspaceAddTagsOptions struct {
Tags []*Tag
}
type WorkspaceRemoveTagsOptions struct {
Tags []*Tag
}
// List all the workspaces within an organization.
func (s *workspaces) List(ctx context.Context, organization string, options *WorkspaceListOptions) (*WorkspaceList, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
var tagFilters map[string][]string
if options != nil {
tagFilters = encodeTagFiltersAsParams(options.TagBindings)
}
// Encode parameters that cannot be encoded by go-querystring
u := fmt.Sprintf("organizations/%s/workspaces", url.PathEscape(organization))
req, err := s.client.NewRequestWithAdditionalQueryParams("GET", u, options, tagFilters)
if err != nil {
return nil, err
}
wl := &WorkspaceList{}
err = req.Do(ctx, wl)
if err != nil {
return nil, err
}
return wl, nil
}
func (s *workspaces) ListTagBindings(ctx context.Context, workspaceID string) ([]*TagBinding, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
u := fmt.Sprintf("workspaces/%s/tag-bindings", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
var list struct {
*Pagination
Items []*TagBinding
}
err = req.Do(ctx, &list)
if err != nil {
return nil, err
}
return list.Items, nil
}
func (s *workspaces) ListEffectiveTagBindings(ctx context.Context, workspaceID string) ([]*EffectiveTagBinding, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
u := fmt.Sprintf("workspaces/%s/effective-tag-bindings", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
var list struct {
*Pagination
Items []*EffectiveTagBinding
}
err = req.Do(ctx, &list)
if err != nil {
return nil, err
}
return list.Items, nil
}
// AddTagBindings adds or modifies the value of existing tag binding keys for a workspace.
func (s *workspaces) AddTagBindings(ctx context.Context, workspaceID string, options WorkspaceAddTagBindingsOptions) ([]*TagBinding, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("workspaces/%s/tag-bindings", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("PATCH", u, options.TagBindings)
if err != nil {
return nil, err
}
var response = struct {
*Pagination
Items []*TagBinding
}{}
err = req.Do(ctx, &response)
return response.Items, err
}
// DeleteAllTagBindings removes all tag bindings associated with a workspace.
// This method will not remove any inherited tag bindings, which must be
// explicitly removed from the parent project.
func (s *workspaces) DeleteAllTagBindings(ctx context.Context, workspaceID string) error {
if !validStringID(&workspaceID) {
return ErrInvalidWorkspaceID
}
type aliasOpts struct {
Type string `jsonapi:"primary,workspaces"`
TagBindings []*TagBinding `jsonapi:"relation,tag-bindings"`
}
opts := &aliasOpts{
TagBindings: []*TagBinding{},
}
u := fmt.Sprintf("workspaces/%s", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("PATCH", u, opts)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// Create is used to create a new workspace.
func (s *workspaces) Create(ctx context.Context, organization string, options WorkspaceCreateOptions) (*Workspace, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("organizations/%s/workspaces", url.PathEscape(organization))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
w := &Workspace{}
err = req.Do(ctx, w)
if err != nil {
return nil, err
}
return w, nil
}
// Read a workspace by its name and organization name.
func (s *workspaces) Read(ctx context.Context, organization, workspace string) (*Workspace, error) {
return s.ReadWithOptions(ctx, organization, workspace, nil)
}
// ReadWithOptions reads a workspace by name and organization name with given options.
func (s *workspaces) ReadWithOptions(ctx context.Context, organization, workspace string, options *WorkspaceReadOptions) (*Workspace, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if !validStringID(&workspace) {
return nil, ErrInvalidWorkspaceValue
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf(
"organizations/%s/workspaces/%s",
url.PathEscape(organization),
url.PathEscape(workspace),
)
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
w := &Workspace{}
err = req.Do(ctx, w)
if err != nil {
return nil, err
}
// Manually populate the deprecated DataRetentionPolicy field
w.DataRetentionPolicy = w.DataRetentionPolicyChoice.ConvertToLegacyStruct()
// durations come over in ms
w.ApplyDurationAverage *= time.Millisecond
w.PlanDurationAverage *= time.Millisecond
return w, nil
}
// ReadByID reads a workspace by its ID.
func (s *workspaces) ReadByID(ctx context.Context, workspaceID string) (*Workspace, error) {
return s.ReadByIDWithOptions(ctx, workspaceID, nil)
}
// ReadByIDWithOptions reads a workspace by its ID with the given options.
func (s *workspaces) ReadByIDWithOptions(ctx context.Context, workspaceID string, options *WorkspaceReadOptions) (*Workspace, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
u := fmt.Sprintf("workspaces/%s", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
w := &Workspace{}
err = req.Do(ctx, w)
if err != nil {
return nil, err
}
// Manually populate the deprecated DataRetentionPolicy field
if w.DataRetentionPolicyChoice != nil {
w.DataRetentionPolicy = w.DataRetentionPolicyChoice.ConvertToLegacyStruct()
}
// durations come over in ms
w.ApplyDurationAverage *= time.Millisecond
w.PlanDurationAverage *= time.Millisecond
return w, nil
}
// Readme gets the readme of a workspace by its ID.
func (s *workspaces) Readme(ctx context.Context, workspaceID string) (io.Reader, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
u := fmt.Sprintf("workspaces/%s?include=readme", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
r := &workspaceWithReadme{}
err = req.Do(ctx, r)
if err != nil {
return nil, err
}
if r.Readme == nil {
return nil, nil
}
return strings.NewReader(r.Readme.RawMarkdown), nil
}
// Update settings of an existing workspace.
func (s *workspaces) Update(ctx context.Context, organization, workspace string, options WorkspaceUpdateOptions) (*Workspace, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if !validStringID(&workspace) {
return nil, ErrInvalidWorkspaceValue
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf(
"organizations/%s/workspaces/%s",
url.PathEscape(organization),
url.PathEscape(workspace),
)
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
w := &Workspace{}
err = req.Do(ctx, w)
if err != nil {
return nil, err
}
return w, nil
}
// UpdateByID updates the settings of an existing workspace.
func (s *workspaces) UpdateByID(ctx context.Context, workspaceID string, options WorkspaceUpdateOptions) (*Workspace, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
u := fmt.Sprintf("workspaces/%s", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
w := &Workspace{}
err = req.Do(ctx, w)
if err != nil {
return nil, err
}
return w, nil
}
// Delete a workspace by its name.
func (s *workspaces) Delete(ctx context.Context, organization, workspace string) error {
if !validStringID(&organization) {
return ErrInvalidOrg
}
if !validStringID(&workspace) {
return ErrInvalidWorkspaceValue
}
u := fmt.Sprintf(
"organizations/%s/workspaces/%s",
url.PathEscape(organization),
url.PathEscape(workspace),
)
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// DeleteByID deletes a workspace by its ID.
func (s *workspaces) DeleteByID(ctx context.Context, workspaceID string) error {
if !validStringID(&workspaceID) {
return ErrInvalidWorkspaceID
}
u := fmt.Sprintf("workspaces/%s", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// SafeDelete a workspace by its name.
func (s *workspaces) SafeDelete(ctx context.Context, organization, workspace string) error {
if !validStringID(&organization) {
return ErrInvalidOrg
}
if !validStringID(&workspace) {
return ErrInvalidWorkspaceValue
}
u := fmt.Sprintf(
"organizations/%s/workspaces/%s/actions/safe-delete",
url.PathEscape(organization),
url.PathEscape(workspace),
)
req, err := s.client.NewRequest("POST", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// SafeDeleteByID safely deletes a workspace by its ID.
func (s *workspaces) SafeDeleteByID(ctx context.Context, workspaceID string) error {
if !validStringID(&workspaceID) {
return ErrInvalidWorkspaceID
}
u := fmt.Sprintf("workspaces/%s/actions/safe-delete", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("POST", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// RemoveVCSConnection from a workspace.
func (s *workspaces) RemoveVCSConnection(ctx context.Context, organization, workspace string) (*Workspace, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}
if !validStringID(&workspace) {
return nil, ErrInvalidWorkspaceValue
}
u := fmt.Sprintf(
"organizations/%s/workspaces/%s",
url.PathEscape(organization),
url.PathEscape(workspace),
)
req, err := s.client.NewRequest("PATCH", u, &workspaceRemoveVCSConnectionOptions{})
if err != nil {
return nil, err
}
w := &Workspace{}
err = req.Do(ctx, w)
if err != nil {
return nil, err
}
return w, nil
}
// RemoveVCSConnectionByID removes a VCS connection from a workspace.
func (s *workspaces) RemoveVCSConnectionByID(ctx context.Context, workspaceID string) (*Workspace, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
u := fmt.Sprintf("workspaces/%s", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("PATCH", u, &workspaceRemoveVCSConnectionOptions{})
if err != nil {
return nil, err
}
w := &Workspace{}
err = req.Do(ctx, w)
if err != nil {
return nil, err
}
return w, nil
}
// Lock a workspace by its ID.
func (s *workspaces) Lock(ctx context.Context, workspaceID string, options WorkspaceLockOptions) (*Workspace, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
u := fmt.Sprintf("workspaces/%s/actions/lock", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
w := &Workspace{}
err = req.Do(ctx, w)
if err != nil {
return nil, err
}
return w, nil
}
// Unlock a workspace by its ID.
func (s *workspaces) Unlock(ctx context.Context, workspaceID string) (*Workspace, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
u := fmt.Sprintf("workspaces/%s/actions/unlock", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("POST", u, nil)
if err != nil {
return nil, err
}
w := &Workspace{}
err = req.Do(ctx, w)
if err != nil {
if strings.Contains(err.Error(), "latest state version is still pending") {
return nil, ErrWorkspaceLockedStateVersionStillPending
}
return nil, err
}
return w, nil
}
// ForceUnlock a workspace by its ID.
func (s *workspaces) ForceUnlock(ctx context.Context, workspaceID string) (*Workspace, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
u := fmt.Sprintf("workspaces/%s/actions/force-unlock", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("POST", u, nil)
if err != nil {
return nil, err
}
w := &Workspace{}
err = req.Do(ctx, w)
if err != nil {
return nil, err
}
return w, nil
}
// AssignSSHKey to a workspace.
func (s *workspaces) AssignSSHKey(ctx context.Context, workspaceID string, options WorkspaceAssignSSHKeyOptions) (*Workspace, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("workspaces/%s/relationships/ssh-key", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
w := &Workspace{}
err = req.Do(ctx, w)
if err != nil {
return nil, err
}
return w, nil
}
// UnassignSSHKey from a workspace.
func (s *workspaces) UnassignSSHKey(ctx context.Context, workspaceID string) (*Workspace, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
u := fmt.Sprintf("workspaces/%s/relationships/ssh-key", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("PATCH", u, &workspaceUnassignSSHKeyOptions{})
if err != nil {
return nil, err
}
w := &Workspace{}
err = req.Do(ctx, w)
if err != nil {
return nil, err
}
return w, nil
}
// RemoteStateConsumers returns the remote state consumers for a given workspace.
func (s *workspaces) ListRemoteStateConsumers(ctx context.Context, workspaceID string, options *RemoteStateConsumersListOptions) (*WorkspaceList, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
u := fmt.Sprintf("workspaces/%s/relationships/remote-state-consumers", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
wl := &WorkspaceList{}
err = req.Do(ctx, wl)
if err != nil {
return nil, err
}
return wl, nil
}
// AddRemoteStateConsumere adds the remote state consumers to a given workspace.
func (s *workspaces) AddRemoteStateConsumers(ctx context.Context, workspaceID string, options WorkspaceAddRemoteStateConsumersOptions) error {
if !validStringID(&workspaceID) {
return ErrInvalidWorkspaceID
}
if err := options.valid(); err != nil {
return err
}
u := fmt.Sprintf("workspaces/%s/relationships/remote-state-consumers", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("POST", u, options.Workspaces)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// RemoveRemoteStateConsumers removes the remote state consumers for a given workspace.
func (s *workspaces) RemoveRemoteStateConsumers(ctx context.Context, workspaceID string, options WorkspaceRemoveRemoteStateConsumersOptions) error {
if !validStringID(&workspaceID) {
return ErrInvalidWorkspaceID
}
if err := options.valid(); err != nil {
return err
}
u := fmt.Sprintf("workspaces/%s/relationships/remote-state-consumers", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("DELETE", u, options.Workspaces)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// UpdateRemoteStateConsumers removes the remote state consumers for a given workspace.
func (s *workspaces) UpdateRemoteStateConsumers(ctx context.Context, workspaceID string, options WorkspaceUpdateRemoteStateConsumersOptions) error {
if !validStringID(&workspaceID) {
return ErrInvalidWorkspaceID
}
if err := options.valid(); err != nil {
return err
}
u := fmt.Sprintf("workspaces/%s/relationships/remote-state-consumers", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("PATCH", u, options.Workspaces)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// ListTags returns the tags for a given workspace.
func (s *workspaces) ListTags(ctx context.Context, workspaceID string, options *WorkspaceTagListOptions) (*TagList, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
u := fmt.Sprintf("workspaces/%s/relationships/tags", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
tl := &TagList{}
err = req.Do(ctx, tl)
if err != nil {
return nil, err
}
return tl, nil
}
// AddTags adds a list of tags to a workspace.
func (s *workspaces) AddTags(ctx context.Context, workspaceID string, options WorkspaceAddTagsOptions) error {
if !validStringID(&workspaceID) {
return ErrInvalidWorkspaceID
}
if err := options.valid(); err != nil {
return err
}
u := fmt.Sprintf("workspaces/%s/relationships/tags", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("POST", u, options.Tags)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
// RemoveTags removes a list of tags from a workspace.
func (s *workspaces) RemoveTags(ctx context.Context, workspaceID string, options WorkspaceRemoveTagsOptions) error {
if !validStringID(&workspaceID) {
return ErrInvalidWorkspaceID
}
if err := options.valid(); err != nil {
return err
}
u := fmt.Sprintf("workspaces/%s/relationships/tags", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("DELETE", u, options.Tags)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (s *workspaces) ReadDataRetentionPolicy(ctx context.Context, workspaceID string) (*DataRetentionPolicy, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
u := fmt.Sprintf("workspaces/%s/relationships/data-retention-policy", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
dataRetentionPolicy := &DataRetentionPolicy{}
err = req.Do(ctx, dataRetentionPolicy)
if err != nil {
// try to detect known issue where this function is used with TFE >= 202401,
// and direct user towards the V2 function
if drpUnmarshalEr.MatchString(err.Error()) {
return nil, fmt.Errorf("error reading deprecated DataRetentionPolicy, use ReadDataRetentionPolicyChoice instead")
}
return nil, err
}
return dataRetentionPolicy, nil
}
func (s *workspaces) ReadDataRetentionPolicyChoice(ctx context.Context, workspaceID string) (*DataRetentionPolicyChoice, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
// The API to read the drp is workspaces//relationships/data-retention-policy
// However, this API can return multiple "types" (e.g. data-retention-policy-delete-olders, or data-retention-policy-dont-deletes)
// Ideally we would deserialize this directly into the choice type (DataRetentionPolicyChoice)...however, there isn't a way to
// tell the current jsonapi implementation that the direct result of an endpoint could be different types. Relationships can be polymorphic,
// but the direct result of an endpoint can't be (as far as the jsonapi implementation is concerned)
// Instead, we need to figure out the type of the data retention policy first, and deserialize it into the matching model. We
// can then create a choice type manually
ws, err := s.ReadByID(ctx, workspaceID)
if err != nil {
return nil, err
}
// there is no drp (of a known type)
if ws.DataRetentionPolicyChoice == nil || !ws.DataRetentionPolicyChoice.IsPopulated() {
return ws.DataRetentionPolicyChoice, nil
}
u := s.dataRetentionPolicyLink(workspaceID)
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
dataRetentionPolicy := &DataRetentionPolicyChoice{}
// if reading the workspace told us it was a "delete older policy" deserialize into the DeleteOlder portion of the choice model
if ws.DataRetentionPolicyChoice.DataRetentionPolicyDeleteOlder != nil {
deleteOlder := &DataRetentionPolicyDeleteOlder{}
err = req.Do(ctx, deleteOlder)
dataRetentionPolicy.DataRetentionPolicyDeleteOlder = deleteOlder
// if reading the workspace told us it was a "delete older policy" deserialize into the DeleteOlder portion of the choice model
} else if ws.DataRetentionPolicyChoice.DataRetentionPolicyDontDelete != nil {
dontDelete := &DataRetentionPolicyDontDelete{}
err = req.Do(ctx, dontDelete)
dataRetentionPolicy.DataRetentionPolicyDontDelete = dontDelete
} else if ws.DataRetentionPolicyChoice != nil {
legacyDrp := &DataRetentionPolicy{}
err = req.Do(ctx, legacyDrp)
dataRetentionPolicy.DataRetentionPolicy = legacyDrp
}
if err != nil {
return nil, err
}
return dataRetentionPolicy, nil
}
func (s *workspaces) SetDataRetentionPolicy(ctx context.Context, workspaceID string, options DataRetentionPolicySetOptions) (*DataRetentionPolicy, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
u := s.dataRetentionPolicyLink(workspaceID)
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
dataRetentionPolicy := &DataRetentionPolicy{}
err = req.Do(ctx, dataRetentionPolicy)
if err != nil {
return nil, err
}
return dataRetentionPolicy, nil
}
func (s *workspaces) SetDataRetentionPolicyDeleteOlder(ctx context.Context, workspaceID string, options DataRetentionPolicyDeleteOlderSetOptions) (*DataRetentionPolicyDeleteOlder, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
u := s.dataRetentionPolicyLink(workspaceID)
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
dataRetentionPolicy := &DataRetentionPolicyDeleteOlder{}
err = req.Do(ctx, dataRetentionPolicy)
if err != nil {
return nil, err
}
return dataRetentionPolicy, nil
}
func (s *workspaces) SetDataRetentionPolicyDontDelete(ctx context.Context, workspaceID string, options DataRetentionPolicyDontDeleteSetOptions) (*DataRetentionPolicyDontDelete, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
u := s.dataRetentionPolicyLink(workspaceID)
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
dataRetentionPolicy := &DataRetentionPolicyDontDelete{}
err = req.Do(ctx, dataRetentionPolicy)
if err != nil {
return nil, err
}
return dataRetentionPolicy, nil
}
func (s *workspaces) DeleteDataRetentionPolicy(ctx context.Context, workspaceID string) error {
if !validStringID(&workspaceID) {
return ErrInvalidWorkspaceID
}
u := s.dataRetentionPolicyLink(workspaceID)
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o WorkspaceAddTagBindingsOptions) valid() error {
if len(o.TagBindings) == 0 {
return ErrRequiredTagBindings
}
return nil
}
func (o WorkspaceCreateOptions) valid() error {
if !validString(o.Name) {
return ErrRequiredName
}
if !validStringID(o.Name) {
return ErrInvalidName
}
if o.Operations != nil && o.ExecutionMode != nil {
return ErrUnsupportedOperations
}
if o.AgentPoolID != nil && (o.ExecutionMode == nil || *o.ExecutionMode != "agent") {
return ErrRequiredAgentMode
}
if o.AgentPoolID == nil && (o.ExecutionMode != nil && *o.ExecutionMode == "agent") {
return ErrRequiredAgentPoolID
}
if len(o.TriggerPrefixes) > 0 &&
o.TriggerPatterns != nil && len(o.TriggerPatterns) > 0 {
return ErrUnsupportedBothTriggerPatternsAndPrefixes
}
if tagRegexDefined(o.VCSRepo) &&
o.TriggerPatterns != nil && len(o.TriggerPatterns) > 0 {
return ErrUnsupportedBothTagsRegexAndTriggerPatterns
}
if tagRegexDefined(o.VCSRepo) &&
o.TriggerPrefixes != nil && len(o.TriggerPrefixes) > 0 {
return ErrUnsupportedBothTagsRegexAndTriggerPrefixes
}
if tagRegexDefined(o.VCSRepo) &&
o.FileTriggersEnabled != nil && *o.FileTriggersEnabled {
return ErrUnsupportedBothTagsRegexAndFileTriggersEnabled
}
return nil
}
func (o WorkspaceUpdateOptions) valid() error {
if o.Name != nil && !validStringID(o.Name) {
return ErrInvalidName
}
if o.Operations != nil && o.ExecutionMode != nil {
return ErrUnsupportedOperations
}
if o.AgentPoolID == nil && (o.ExecutionMode != nil && *o.ExecutionMode == "agent") {
return ErrRequiredAgentPoolID
}
if len(o.TriggerPrefixes) > 0 &&
o.TriggerPatterns != nil && len(o.TriggerPatterns) > 0 {
return ErrUnsupportedBothTriggerPatternsAndPrefixes
}
if tagRegexDefined(o.VCSRepo) &&
o.TriggerPatterns != nil && len(o.TriggerPatterns) > 0 {
return ErrUnsupportedBothTagsRegexAndTriggerPatterns
}
if tagRegexDefined(o.VCSRepo) &&
o.TriggerPrefixes != nil && len(o.TriggerPrefixes) > 0 {
return ErrUnsupportedBothTagsRegexAndTriggerPrefixes
}
if tagRegexDefined(o.VCSRepo) &&
o.FileTriggersEnabled != nil && *o.FileTriggersEnabled {
return ErrUnsupportedBothTagsRegexAndFileTriggersEnabled
}
return nil
}
func (o WorkspaceAssignSSHKeyOptions) valid() error {
if !validString(o.SSHKeyID) {
return ErrRequiredSHHKeyID
}
if !validStringID(o.SSHKeyID) {
return ErrInvalidSHHKeyID
}
return nil
}
func (o WorkspaceAddRemoteStateConsumersOptions) valid() error {
if o.Workspaces == nil {
return ErrWorkspacesRequired
}
if len(o.Workspaces) == 0 {
return ErrWorkspaceMinLimit
}
return nil
}
func (o WorkspaceRemoveRemoteStateConsumersOptions) valid() error {
if o.Workspaces == nil {
return ErrWorkspacesRequired
}
if len(o.Workspaces) == 0 {
return ErrWorkspaceMinLimit
}
return nil
}
func (o WorkspaceUpdateRemoteStateConsumersOptions) valid() error {
if o.Workspaces == nil {
return ErrWorkspacesRequired
}
if len(o.Workspaces) == 0 {
return ErrWorkspaceMinLimit
}
return nil
}
func (o WorkspaceAddTagsOptions) valid() error {
if len(o.Tags) == 0 {
return ErrMissingTagIdentifier
}
for _, s := range o.Tags {
if s.Name == "" && s.ID == "" {
return ErrMissingTagIdentifier
}
}
return nil
}
func (o WorkspaceRemoveTagsOptions) valid() error {
if len(o.Tags) == 0 {
return ErrMissingTagIdentifier
}
for _, s := range o.Tags {
if s.Name == "" && s.ID == "" {
return ErrMissingTagIdentifier
}
}
return nil
}
func (o *WorkspaceListOptions) valid() error {
return nil
}
func (o *WorkspaceReadOptions) valid() error {
return nil
}
func tagRegexDefined(options *VCSRepoOptions) bool {
if options == nil {
return false
}
if options.TagsRegex != nil && *options.TagsRegex != "" {
return true
}
return false
}
func (s *workspaces) dataRetentionPolicyLink(wsID string) string {
return fmt.Sprintf("workspaces/%s/relationships/data-retention-policy", url.PathEscape(wsID))
}
================================================
FILE: workspace_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"sort"
"strings"
"testing"
"time"
retryablehttp "github.com/hashicorp/go-retryablehttp"
"github.com/hashicorp/jsonapi"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type WorkspaceTableOptions struct {
createOptions *WorkspaceCreateOptions
updateOptions *WorkspaceUpdateOptions
}
type WorkspaceTableTest struct {
scenario string
options *WorkspaceTableOptions
setup func(t *testing.T, options *WorkspaceTableOptions) (w *Workspace, cleanup func())
assertion func(t *testing.T, w *Workspace, options *WorkspaceTableOptions, err error)
}
func TestWorkspacesList_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest1, wTest1Cleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTest1Cleanup)
wTest2, wTest2Cleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTest2Cleanup)
wTest3, wTest3Cleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTest3Cleanup)
t.Run("without list options", func(t *testing.T) {
wl, err := client.Workspaces.List(ctx, orgTest.Name, nil)
require.NoError(t, err)
assert.Contains(t, wl.Items, wTest1)
assert.Contains(t, wl.Items, wTest2)
assert.Equal(t, 1, wl.CurrentPage)
assert.Equal(t, 3, wl.TotalCount)
})
t.Run("with list options", func(t *testing.T) {
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
wl, err := client.Workspaces.List(ctx, orgTest.Name, &WorkspaceListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, wl.Items)
assert.Equal(t, 999, wl.CurrentPage)
assert.Equal(t, 3, wl.TotalCount)
})
t.Run("when sorting by workspace names", func(t *testing.T) {
wl, err := client.Workspaces.List(ctx, orgTest.Name, &WorkspaceListOptions{
Sort: "name",
})
require.NoError(t, err)
require.NotEmpty(t, wl.Items)
require.GreaterOrEqual(t, len(wl.Items), 2)
assert.Equal(t, wl.Items[0].Name < wl.Items[1].Name, true)
})
t.Run("when sorting workspaces on current-run.created-at", func(t *testing.T) {
_, unappliedCleanup1 := createRunUnapplied(t, client, wTest2)
t.Cleanup(unappliedCleanup1)
_, unappliedCleanup2 := createRunUnapplied(t, client, wTest3)
t.Cleanup(unappliedCleanup2)
wl, err := client.Workspaces.List(ctx, orgTest.Name, &WorkspaceListOptions{
Include: []WSIncludeOpt{WSCurrentRun},
Sort: "current-run.created-at",
})
require.NoError(t, err)
require.NotEmpty(t, wl.Items)
require.GreaterOrEqual(t, len(wl.Items), 2)
assert.True(t, wl.Items[1].CurrentRun.CreatedAt.After(wl.Items[0].CurrentRun.CreatedAt))
})
t.Run("when searching a known workspace", func(t *testing.T) {
// Use a known workspace prefix as search attribute. The result
// should be successful and only contain the matching workspace.
wl, err := client.Workspaces.List(ctx, orgTest.Name, &WorkspaceListOptions{
Search: wTest1.Name[:len(wTest1.Name)-5],
})
require.NoError(t, err)
assert.Contains(t, wl.Items, wTest1)
assert.NotContains(t, wl.Items, wTest2)
assert.Equal(t, 1, wl.CurrentPage)
assert.Equal(t, 1, wl.TotalCount)
})
t.Run("when searching using a tag", func(t *testing.T) {
tagName := "tagtest"
// Add the tag to the first workspace for searching.
err := client.Workspaces.AddTags(ctx, wTest1.ID, WorkspaceAddTagsOptions{
Tags: []*Tag{
{
Name: tagName,
},
},
})
require.NoError(t, err)
// The result should be successful and only contain the workspace with the
// new tag.
wl, err := client.Workspaces.List(ctx, orgTest.Name, &WorkspaceListOptions{
Tags: tagName,
})
require.NoError(t, err)
assert.Equal(t, wl.Items[0].ID, wTest1.ID)
assert.Equal(t, 1, wl.CurrentPage)
assert.Equal(t, 1, wl.TotalCount)
})
t.Run("when searching using exclude-tags", func(t *testing.T) {
for wsID, tag := range map[string]string{wTest1.ID: "foo", wTest2.ID: "bar", wTest3.ID: "foo"} {
err := client.Workspaces.AddTags(ctx, wsID, WorkspaceAddTagsOptions{
Tags: []*Tag{
{
Name: tag,
},
},
})
require.NoError(t, err)
}
wl, err := client.Workspaces.List(ctx, orgTest.Name, &WorkspaceListOptions{
ExcludeTags: "foo",
})
require.NoError(t, err)
assert.Contains(t, wl.Items[0].ID, wTest2.ID)
assert.Equal(t, 1, wl.CurrentPage)
assert.Equal(t, 1, wl.TotalCount)
})
t.Run("when searching an unknown workspace", func(t *testing.T) {
// Use a nonexisting workspace name as search attribute. The result
// should be successful, but return no results.
wl, err := client.Workspaces.List(ctx, orgTest.Name, &WorkspaceListOptions{
Search: "nonexisting",
})
require.NoError(t, err)
assert.Empty(t, wl.Items)
assert.Equal(t, 1, wl.CurrentPage)
assert.Equal(t, 0, wl.TotalCount)
})
t.Run("without a valid organization", func(t *testing.T) {
wl, err := client.Workspaces.List(ctx, badIdentifier, nil)
assert.Nil(t, wl)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("with organization included", func(t *testing.T) {
wl, err := client.Workspaces.List(ctx, orgTest.Name, &WorkspaceListOptions{
Include: []WSIncludeOpt{WSOrganization},
})
require.NoError(t, err)
require.NotEmpty(t, wl.Items)
require.NotNil(t, wl.Items[0].Organization)
assert.NotEmpty(t, wl.Items[0].Organization.Email)
})
t.Run("with current-state-version,current-run included", func(t *testing.T) {
_, rCleanup := createRunApply(t, client, wTest1)
t.Cleanup(rCleanup)
wl, err := client.Workspaces.List(ctx, orgTest.Name, &WorkspaceListOptions{
Include: []WSIncludeOpt{WSCurrentStateVer, WSCurrentRun},
})
require.NoError(t, err)
require.NotEmpty(t, wl.Items)
foundWTest1 := false
for _, ws := range wl.Items {
if ws.ID != wTest1.ID {
continue
}
foundWTest1 = true
require.NotNil(t, wl.Items[0].CurrentStateVersion)
assert.NotEmpty(t, wl.Items[0].CurrentStateVersion.DownloadURL)
require.NotNil(t, wl.Items[0].CurrentRun)
assert.NotEmpty(t, wl.Items[0].CurrentRun.Message)
}
assert.True(t, foundWTest1)
})
t.Run("when searching a known substring", func(t *testing.T) {
wildcardSearch := "*-prod"
// should be successful, and return 1 result
wTest, wTestCleanup := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{
Name: String("hashicorp-prod"),
})
t.Cleanup(wTestCleanup)
wl, err := client.Workspaces.List(ctx, orgTest.Name, &WorkspaceListOptions{
WildcardName: wildcardSearch,
})
require.NoError(t, err)
assert.NotEmpty(t, wTest.ID)
assert.Equal(t, 1, wl.TotalCount)
})
t.Run("when wildcard match does not exist", func(t *testing.T) {
wildcardSearch := "*-dev"
// should be successful, but return no results
wTest, wTestCleanup := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{
Name: String("hashicorp-staging"),
})
t.Cleanup(wTestCleanup)
wl, err := client.Workspaces.List(ctx, orgTest.Name, &WorkspaceListOptions{
WildcardName: wildcardSearch,
})
require.NoError(t, err)
assert.NotEmpty(t, wTest.ID)
assert.Equal(t, 0, wl.TotalCount)
})
t.Run("when using a tags filter", func(t *testing.T) {
skipUnlessBeta(t)
w1, wTestCleanup1 := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{
Name: String(randomString(t)),
TagBindings: []*TagBinding{
{Key: "key1", Value: "value1"},
{Key: "key2", Value: "value2a"},
},
})
w2, wTestCleanup2 := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{
Name: String(randomString(t)),
TagBindings: []*TagBinding{
{Key: "key2", Value: "value2b"},
{Key: "key3", Value: "value3"},
},
})
t.Cleanup(wTestCleanup1)
t.Cleanup(wTestCleanup2)
// List all the workspaces under the given tag
wl, err := client.Workspaces.List(ctx, orgTest.Name, &WorkspaceListOptions{
TagBindings: []*TagBinding{
{Key: "key1"},
},
})
assert.NoError(t, err)
assert.Len(t, wl.Items, 1)
assert.Contains(t, wl.Items, w1)
wl2, err := client.Workspaces.List(ctx, orgTest.Name, &WorkspaceListOptions{
TagBindings: []*TagBinding{
{Key: "key2"},
},
})
assert.NoError(t, err)
assert.Len(t, wl2.Items, 2)
assert.Contains(t, wl2.Items, w1, w2)
wl3, err := client.Workspaces.List(ctx, orgTest.Name, &WorkspaceListOptions{
TagBindings: []*TagBinding{
{Key: "key2", Value: "value2b"},
},
})
assert.NoError(t, err)
assert.Len(t, wl3.Items, 1)
assert.Contains(t, wl3.Items, w2)
})
t.Run("when including effective tag bindings", func(t *testing.T) {
skipUnlessBeta(t)
orgTest2, orgTest2Cleanup := createOrganization(t, client)
t.Cleanup(orgTest2Cleanup)
prj, pTestCleanup1 := createProjectWithOptions(t, client, orgTest2, ProjectCreateOptions{
Name: randomStringWithoutSpecialChar(t),
TagBindings: []*TagBinding{
{Key: "key3", Value: "value3"},
},
})
t.Cleanup(pTestCleanup1)
_, wTestCleanup1 := createWorkspaceWithOptions(t, client, orgTest2, WorkspaceCreateOptions{
Name: String(randomString(t)),
Project: prj,
TagBindings: []*TagBinding{
{Key: "key1", Value: "value1"},
{Key: "key2", Value: "value2a"},
},
})
t.Cleanup(wTestCleanup1)
wl, err := client.Workspaces.List(ctx, orgTest2.Name, &WorkspaceListOptions{
Include: []WSIncludeOpt{WSEffectiveTagBindings},
})
require.NoError(t, err)
require.Len(t, wl.Items, 1)
require.Len(t, wl.Items[0].EffectiveTagBindings, 3)
assert.NotEmpty(t, wl.Items[0].EffectiveTagBindings[0].Key)
assert.NotEmpty(t, wl.Items[0].EffectiveTagBindings[0].Value)
assert.NotEmpty(t, wl.Items[0].EffectiveTagBindings[1].Key)
assert.NotEmpty(t, wl.Items[0].EffectiveTagBindings[1].Value)
assert.NotEmpty(t, wl.Items[0].EffectiveTagBindings[2].Key)
assert.NotEmpty(t, wl.Items[0].EffectiveTagBindings[2].Value)
inheritedTagsFound := 0
for _, tag := range wl.Items[0].EffectiveTagBindings {
if tag.Links["inherited-from"] != nil {
inheritedTagsFound += 1
}
}
if inheritedTagsFound != 1 {
t.Fatalf("Expected 1 inherited tag, got %d", inheritedTagsFound)
}
})
t.Run("when using project id filter and project contains workspaces", func(t *testing.T) {
// create a project in the orgTest
p, pTestCleanup := createProject(t, client, orgTest)
defer pTestCleanup()
// create a workspace with project
w, wTestCleanup := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{
Name: String(randomString(t)),
Project: p,
})
defer wTestCleanup()
// List all the workspaces under the given ProjectID
wl, err := client.Workspaces.List(ctx, orgTest.Name, &WorkspaceListOptions{
ProjectID: p.ID,
})
require.NoError(t, err)
assert.Contains(t, wl.Items, w)
})
t.Run("when using project id filter but project contains no workspaces", func(t *testing.T) {
// create a project in the orgTest
p, pTestCleanup := createProject(t, client, orgTest)
defer pTestCleanup()
// List all the workspaces under the given ProjectID
wl, err := client.Workspaces.List(ctx, orgTest.Name, &WorkspaceListOptions{
ProjectID: p.ID,
})
require.NoError(t, err)
assert.Empty(t, wl.Items)
})
t.Run("when filter workspaces by current run status", func(t *testing.T) {
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup)
rn, appliedCleanup := createRunApply(t, client, wTest)
t.Cleanup(appliedCleanup)
wl, err := client.Workspaces.List(ctx, orgTest.Name, &WorkspaceListOptions{
CurrentRunStatus: string(RunApplied),
})
require.NoError(t, err)
require.NotEmpty(t, wl.Items)
require.GreaterOrEqual(t, len(wl.Items), 1)
found := false
for _, ws := range wl.Items {
if ws.ID != wTest.ID {
continue
}
assert.Equal(t, ws.CurrentRun.ID, rn.ID)
found = true
}
assert.True(t, found)
})
}
func TestWorkspacesCreateTableDriven(t *testing.T) {
t.Parallel()
t.Skip("Skipping due to persistent failures - see TF-31172")
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
oc, oaCleanup := createOAuthToken(t, client, orgTest)
t.Cleanup(oaCleanup)
workspaceTableTests := []WorkspaceTableTest{
{
scenario: "when options include vcs-repo",
options: &WorkspaceTableOptions{
createOptions: &WorkspaceCreateOptions{
Name: String("foobar"),
VCSRepo: &VCSRepoOptions{
Identifier: String("hashicorp/terraform-random-module"),
OAuthTokenID: &oc.ID,
Branch: String("main"),
},
},
},
assertion: func(t *testing.T, w *Workspace, options *WorkspaceTableOptions, err error) {
require.NoError(t, err)
require.NotNil(t, w)
require.NotEmpty(t, w.VCSRepo.Identifier)
require.NotEmpty(t, w.VCSRepo.OAuthTokenID)
require.NotEmpty(t, w.VCSRepo.Branch)
wRead, err := client.Workspaces.ReadByID(ctx, w.ID)
require.NoError(t, err)
require.Equal(t, w.VCSRepo.Identifier, wRead.VCSRepo.Identifier)
require.Equal(t, w.VCSRepo.OAuthTokenID, wRead.VCSRepo.OAuthTokenID)
require.Equal(t, w.VCSRepo.Branch, wRead.VCSRepo.Branch)
},
},
{
scenario: "when options include tags-regex",
options: &WorkspaceTableOptions{
createOptions: &WorkspaceCreateOptions{
Name: String("foobar"),
FileTriggersEnabled: Bool(false),
VCSRepo: &VCSRepoOptions{
TagsRegex: String("barfoo")},
},
},
setup: func(t *testing.T, options *WorkspaceTableOptions) (w *Workspace, cleanup func()) {
// Remove the below organization creation and use the one from the outer scope once the feature flag is removed
orgTest, orgTestCleanup := createOrganizationWithOptions(t, client, OrganizationCreateOptions{
Name: String("tst-" + randomString(t)[0:20]),
Email: String(fmt.Sprintf("%s@tfe.local", randomString(t))),
})
w, wTestCleanup := createWorkspaceWithVCS(t, client, orgTest, *options.createOptions)
return w, func() {
t.Cleanup(orgTestCleanup)
t.Cleanup(wTestCleanup)
}
},
assertion: func(t *testing.T, w *Workspace, options *WorkspaceTableOptions, err error) {
assert.Equal(t, *options.createOptions.VCSRepo.TagsRegex, w.VCSRepo.TagsRegex)
// Get a refreshed view from the API.
refreshed, readErr := client.Workspaces.Read(ctx, w.Organization.Name, *options.createOptions.Name)
require.NoError(t, readErr)
for _, item := range []*Workspace{
w,
refreshed,
} {
assert.Equal(t, *options.createOptions.VCSRepo.TagsRegex, item.VCSRepo.TagsRegex)
}
},
},
{
scenario: "when options include both non-empty tags-regex and trigger-patterns error is returned",
options: &WorkspaceTableOptions{
createOptions: &WorkspaceCreateOptions{
Name: String("foobar"),
FileTriggersEnabled: Bool(false),
VCSRepo: &VCSRepoOptions{TagsRegex: String("foobar")},
TriggerPatterns: []string{"/module-1/**/*", "/**/networking/*"},
},
},
assertion: func(t *testing.T, w *Workspace, options *WorkspaceTableOptions, err error) {
assert.Nil(t, w)
assert.EqualError(t, err, ErrUnsupportedBothTagsRegexAndTriggerPatterns.Error())
},
},
{
scenario: "when options include both non-empty tags-regex and trigger-prefixes error is returned",
options: &WorkspaceTableOptions{
createOptions: &WorkspaceCreateOptions{
Name: String("foobar"),
FileTriggersEnabled: Bool(false),
VCSRepo: &VCSRepoOptions{TagsRegex: String("foobar")},
TriggerPrefixes: []string{"/module-1", "/module-2"},
},
},
assertion: func(t *testing.T, w *Workspace, options *WorkspaceTableOptions, err error) {
assert.Nil(t, w)
assert.EqualError(t, err, ErrUnsupportedBothTagsRegexAndTriggerPrefixes.Error())
},
},
{
scenario: "when options include both non-empty tags-regex and file-triggers-enabled as true an error is returned",
options: &WorkspaceTableOptions{
createOptions: &WorkspaceCreateOptions{
Name: String("foobar"),
FileTriggersEnabled: Bool(true),
VCSRepo: &VCSRepoOptions{TagsRegex: String("foobar")},
},
},
assertion: func(t *testing.T, w *Workspace, options *WorkspaceTableOptions, err error) {
assert.Nil(t, w)
assert.EqualError(t, err, ErrUnsupportedBothTagsRegexAndFileTriggersEnabled.Error())
},
},
{
scenario: "when options include both non-empty tags-regex and file-triggers-enabled as false an error is not returned",
options: &WorkspaceTableOptions{
createOptions: &WorkspaceCreateOptions{
Name: String("foobar"),
FileTriggersEnabled: Bool(false),
VCSRepo: &VCSRepoOptions{TagsRegex: String("foobar")},
},
},
setup: func(t *testing.T, options *WorkspaceTableOptions) (w *Workspace, cleanup func()) {
w, wTestCleanup := createWorkspaceWithVCS(t, client, orgTest, *options.createOptions)
return w, func() {
t.Cleanup(wTestCleanup)
}
},
assertion: func(t *testing.T, w *Workspace, options *WorkspaceTableOptions, err error) {
require.NotNil(t, w)
require.NoError(t, err)
},
},
}
for _, tableTest := range workspaceTableTests {
t.Run(tableTest.scenario, func(t *testing.T) {
var workspace *Workspace
var cleanup func()
var err error
if tableTest.setup != nil {
workspace, cleanup = tableTest.setup(t, tableTest.options)
defer cleanup()
} else {
workspace, err = client.Workspaces.Create(ctx, orgTest.Name, *tableTest.options.createOptions)
}
tableTest.assertion(t, workspace, tableTest.options, err)
})
}
}
func TestWorkspacesCreateTableDrivenWithGithubApp(t *testing.T) {
t.Parallel()
gHAInstallationID := os.Getenv("GITHUB_APP_INSTALLATION_ID")
if gHAInstallationID == "" {
t.Skip("Export a valid GITHUB_APP_INSTALLATION_ID before running this test!")
}
client := testClient(t)
ctx := context.Background()
orgTest1, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
workspaceTableTests := []WorkspaceTableTest{
{
scenario: "when options include tags-regex",
options: &WorkspaceTableOptions{
createOptions: &WorkspaceCreateOptions{
Name: String("foobar"),
FileTriggersEnabled: Bool(false),
VCSRepo: &VCSRepoOptions{
TagsRegex: String("barfoo")},
},
},
setup: func(t *testing.T, options *WorkspaceTableOptions) (w *Workspace, cleanup func()) {
// Remove the below organization creation and use the one from the outer scope once the feature flag is removed
orgTest, orgTestCleanup := createOrganizationWithOptions(t, client, OrganizationCreateOptions{
Name: String("tst-" + randomString(t)[0:20]),
Email: String(fmt.Sprintf("%s@tfe.local", randomString(t))),
})
w, wTestCleanup := createWorkspaceWithGithubApp(t, client, orgTest, *options.createOptions)
return w, func() {
t.Cleanup(orgTestCleanup)
t.Cleanup(wTestCleanup)
}
},
assertion: func(t *testing.T, w *Workspace, options *WorkspaceTableOptions, err error) {
assert.Equal(t, *options.createOptions.VCSRepo.TagsRegex, w.VCSRepo.TagsRegex)
// Get a refreshed view from the API.
refreshed, readErr := client.Workspaces.Read(ctx, w.Organization.Name, *options.createOptions.Name)
require.NoError(t, readErr)
for _, item := range []*Workspace{
w,
refreshed,
} {
assert.Equal(t, *options.createOptions.VCSRepo.TagsRegex, item.VCSRepo.TagsRegex)
}
},
},
}
for _, tableTest := range workspaceTableTests {
t.Run(tableTest.scenario, func(t *testing.T) {
var workspace *Workspace
var cleanup func()
var err error
if tableTest.setup != nil {
workspace, cleanup = tableTest.setup(t, tableTest.options)
defer cleanup()
} else {
workspace, err = client.Workspaces.Create(ctx, orgTest1.Name, *tableTest.options.createOptions)
}
tableTest.assertion(t, workspace, tableTest.options, err)
})
}
}
func TestWorkspacesCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
t.Run("with valid project option", func(t *testing.T) {
skipUnlessBeta(t)
options := WorkspaceCreateOptions{
Name: String(fmt.Sprintf("foo-%s", randomString(t))),
AllowDestroyPlan: Bool(false),
AutoApply: Bool(true),
Description: String("qux"),
AssessmentsEnabled: Bool(false),
FileTriggersEnabled: Bool(true),
Operations: Bool(true),
QueueAllRuns: Bool(true),
SpeculativeEnabled: Bool(true),
SourceName: String("my-app"),
SourceURL: String("http://my-app-hostname.io"),
StructuredRunOutputEnabled: Bool(true),
TerraformVersion: String("0.11.0"),
TriggerPrefixes: []string{"/modules", "/shared"},
WorkingDirectory: String("bar/"),
Project: orgTest.DefaultProject,
Tags: []*Tag{
{
Name: "tag1",
},
{
Name: "tag2",
},
},
}
w, err := client.Workspaces.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
// Get a refreshed view from the API.
refreshed, err := client.Workspaces.Read(ctx, orgTest.Name, *options.Name)
require.NoError(t, err)
for _, item := range []*Workspace{
w,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, *options.Name, item.Name)
assert.Equal(t, options.Project.ID, item.Project.ID)
}
})
t.Run("with valid auto-apply-run-trigger option", func(t *testing.T) {
skipIfEnterprise(t)
// FEATURE FLAG: auto-apply-run-trigger
// Once un-flagged, delete this test and add an AutoApplyRunTrigger field
// to the basic "with valid options" test below.
options := WorkspaceCreateOptions{
Name: String(fmt.Sprintf("foo-%s", randomString(t))),
AutoApplyRunTrigger: Bool(true),
}
w, err := client.Workspaces.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
// Get a refreshed view from the API.
refreshed, err := client.Workspaces.Read(ctx, orgTest.Name, *options.Name)
require.NoError(t, err)
for _, item := range []*Workspace{
w,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, *options.Name, item.Name)
assert.Equal(t, *options.AutoApplyRunTrigger, item.AutoApplyRunTrigger)
}
})
t.Run("with valid options", func(t *testing.T) {
options := WorkspaceCreateOptions{
Name: String(fmt.Sprintf("foo-%s", randomString(t))),
AllowDestroyPlan: Bool(true),
AutoApply: Bool(true),
AutoDestroyAt: NullableTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)),
Description: String("qux"),
AssessmentsEnabled: Bool(false),
FileTriggersEnabled: Bool(true),
Operations: Bool(true),
QueueAllRuns: Bool(true),
SpeculativeEnabled: Bool(true),
SourceName: String("my-app"),
SourceURL: String("http://my-app-hostname.io"),
StructuredRunOutputEnabled: Bool(true),
TerraformVersion: String("0.11.0"),
TriggerPrefixes: []string{"/modules", "/shared"},
WorkingDirectory: String("bar/"),
Tags: []*Tag{
{
Name: "tag1",
},
{
Name: "tag2",
},
},
}
w, err := client.Workspaces.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
// Get a refreshed view from the API.
refreshed, err := client.Workspaces.Read(ctx, orgTest.Name, *options.Name)
require.NoError(t, err)
for _, item := range []*Workspace{
w,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Equal(t, *options.Name, item.Name)
assert.Equal(t, *options.Description, item.Description)
assert.Equal(t, *options.AllowDestroyPlan, item.AllowDestroyPlan)
assert.Equal(t, *options.AutoApply, item.AutoApply)
assert.Equal(t, options.AutoDestroyAt, item.AutoDestroyAt)
assert.Equal(t, *options.AssessmentsEnabled, item.AssessmentsEnabled)
assert.Equal(t, *options.FileTriggersEnabled, item.FileTriggersEnabled)
assert.Equal(t, *options.Operations, item.Operations)
assert.Equal(t, *options.QueueAllRuns, item.QueueAllRuns)
assert.Equal(t, *options.SpeculativeEnabled, item.SpeculativeEnabled)
assert.Equal(t, *options.SourceName, item.SourceName)
assert.Equal(t, *options.SourceURL, item.SourceURL)
assert.Equal(t, *options.StructuredRunOutputEnabled, item.StructuredRunOutputEnabled)
assert.Equal(t, options.Tags[0].Name, item.TagNames[0])
assert.Equal(t, options.Tags[1].Name, item.TagNames[1])
assert.Equal(t, *options.TerraformVersion, item.TerraformVersion)
assert.Equal(t, options.TriggerPrefixes, item.TriggerPrefixes)
assert.Equal(t, *options.WorkingDirectory, item.WorkingDirectory)
}
})
t.Run("when options is missing name", func(t *testing.T) {
w, err := client.Workspaces.Create(ctx, "foo", WorkspaceCreateOptions{})
assert.Nil(t, w)
assert.EqualError(t, err, ErrRequiredName.Error())
})
t.Run("when options has an invalid name", func(t *testing.T) {
w, err := client.Workspaces.Create(ctx, "foo", WorkspaceCreateOptions{
Name: String(badIdentifier),
})
assert.Nil(t, w)
assert.EqualError(t, err, ErrInvalidName.Error())
})
t.Run("when options has an invalid organization", func(t *testing.T) {
w, err := client.Workspaces.Create(ctx, badIdentifier, WorkspaceCreateOptions{
Name: String(fmt.Sprintf("foo-%s", randomString(t))),
})
assert.Nil(t, w)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("when options includes both an operations value and an enforcement mode value", func(t *testing.T) {
options := WorkspaceCreateOptions{
Name: String(fmt.Sprintf("foo-%s", randomString(t))),
ExecutionMode: String("remote"),
Operations: Bool(true),
}
w, err := client.Workspaces.Create(ctx, orgTest.Name, options)
assert.Nil(t, w)
assert.Equal(t, err, ErrUnsupportedOperations)
})
t.Run("when an agent pool ID is specified without 'agent' execution mode", func(t *testing.T) {
options := WorkspaceCreateOptions{
Name: String(fmt.Sprintf("foo-%s", randomString(t))),
AgentPoolID: String("apool-xxxxx"),
}
w, err := client.Workspaces.Create(ctx, orgTest.Name, options)
assert.Nil(t, w)
assert.Equal(t, err, ErrRequiredAgentMode)
})
t.Run("when 'agent' execution mode is specified without an an agent pool ID", func(t *testing.T) {
options := WorkspaceCreateOptions{
Name: String(fmt.Sprintf("foo-%s", randomString(t))),
ExecutionMode: String("agent"),
}
w, err := client.Workspaces.Create(ctx, orgTest.Name, options)
assert.Nil(t, w)
assert.Equal(t, err, ErrRequiredAgentPoolID)
})
t.Run("when no execution mode is specified, in an organization with local as default execution mode", func(t *testing.T) {
// Remove the below organization creation and use the one from the outer scope once the feature flag is removed
orgTest, orgTestCleanup := createOrganizationWithOptions(t, client, OrganizationCreateOptions{
Name: String("tst-" + randomString(t)[0:20] + "-ff-on"),
Email: String(fmt.Sprintf("%s@tfe.local", randomString(t))),
DefaultExecutionMode: String("local"),
})
t.Cleanup(orgTestCleanup)
options := WorkspaceCreateOptions{
Name: String(fmt.Sprintf("foo-%s", randomString(t))),
SettingOverwrites: &WorkspaceSettingOverwritesOptions{
ExecutionMode: Bool(false),
},
}
_, err := client.Workspaces.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
// Get a refreshed view from the API.
refreshed, err := client.Workspaces.Read(ctx, orgTest.Name, *options.Name)
require.NoError(t, err)
assert.Equal(t, "local", refreshed.ExecutionMode)
})
t.Run("when an error is returned from the API", func(t *testing.T) {
w, err := client.Workspaces.Create(ctx, "bar", WorkspaceCreateOptions{
Name: String(fmt.Sprintf("bar-%s", randomString(t))),
TerraformVersion: String("nonexisting"),
})
assert.Nil(t, w)
assert.Error(t, err)
})
t.Run("when options include trigger-patterns (behind a feature flag)", func(t *testing.T) {
// Remove the below organization creation and use the one from the outer scope once the feature flag is removed
orgTest, orgTestCleanup := createOrganizationWithOptions(t, client, OrganizationCreateOptions{
Name: String("tst-" + randomString(t)[0:20] + "-ff-on"),
Email: String(fmt.Sprintf("%s@tfe.local", randomString(t))),
})
t.Cleanup(orgTestCleanup)
options := WorkspaceCreateOptions{
Name: String("foobar"),
FileTriggersEnabled: Bool(true),
TriggerPatterns: []string{"/module-1/**/*", "/**/networking/*"},
}
w, err := client.Workspaces.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, options.TriggerPatterns, w.TriggerPatterns)
// Get a refreshed view from the API.
refreshed, err := client.Workspaces.Read(ctx, orgTest.Name, *options.Name)
require.NoError(t, err)
for _, item := range []*Workspace{
w,
refreshed,
} {
assert.Equal(t, options.TriggerPatterns, item.TriggerPatterns)
}
})
t.Run("when options include both non-empty trigger-patterns and trigger-paths error is returned", func(t *testing.T) {
options := WorkspaceCreateOptions{
Name: String(fmt.Sprintf("foobar-%s", randomString(t))),
FileTriggersEnabled: Bool(true),
TriggerPrefixes: []string{"/module-1", "/module-2"},
TriggerPatterns: []string{"/module-1/**/*", "/**/networking/*"},
}
w, err := client.Workspaces.Create(ctx, orgTest.Name, options)
assert.Nil(t, w)
assert.EqualError(t, err, ErrUnsupportedBothTriggerPatternsAndPrefixes.Error())
})
t.Run("when options include trigger-patterns populated and empty trigger-paths workspace is created", func(t *testing.T) {
// Remove the below organization creation and use the one from the outer scope once the feature flag is removed
orgTest, orgTestCleanup := createOrganizationWithOptions(t, client, OrganizationCreateOptions{
Name: String("tst-" + randomString(t)[0:20] + "-ff-on"),
Email: String(fmt.Sprintf("%s@tfe.local", randomString(t))),
})
t.Cleanup(orgTestCleanup)
options := WorkspaceCreateOptions{
Name: String(fmt.Sprintf("foobar-%s", randomString(t))),
FileTriggersEnabled: Bool(true),
TriggerPrefixes: []string{},
TriggerPatterns: []string{"/module-1/**/*", "/**/networking/*"},
}
w, err := client.Workspaces.Create(ctx, orgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, options.TriggerPatterns, w.TriggerPatterns)
})
t.Run("when organization has a default execution mode", func(t *testing.T) {
defaultExecutionOrgTest, defaultExecutionOrgTestCleanup := createOrganizationWithDefaultAgentPool(t, client)
t.Cleanup(defaultExecutionOrgTestCleanup)
t.Run("with setting overwrites set to false, workspace inherits the default execution mode", func(t *testing.T) {
options := WorkspaceCreateOptions{
Name: String(fmt.Sprintf("tst-agent-cody-banks-%s", randomString(t))),
SettingOverwrites: &WorkspaceSettingOverwritesOptions{
ExecutionMode: Bool(false),
AgentPool: Bool(false),
},
}
w, err := client.Workspaces.Create(ctx, defaultExecutionOrgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, "agent", w.ExecutionMode)
})
t.Run("with setting overwrites set to true, workspace ignores the default execution mode", func(t *testing.T) {
options := WorkspaceCreateOptions{
Name: String(fmt.Sprintf("tst-agent-tony-tanks-%s", randomString(t))),
ExecutionMode: String("local"),
SettingOverwrites: &WorkspaceSettingOverwritesOptions{
ExecutionMode: Bool(true),
AgentPool: Bool(true),
},
}
w, err := client.Workspaces.Create(ctx, defaultExecutionOrgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, "local", w.ExecutionMode)
})
t.Run("when explicitly setting execution mode, workspace ignores the default execution mode", func(t *testing.T) {
options := WorkspaceCreateOptions{
Name: String(fmt.Sprintf("tst-remotely-interesting-workspace-%s", randomString(t))),
ExecutionMode: String("remote"),
}
w, err := client.Workspaces.Create(ctx, defaultExecutionOrgTest.Name, options)
require.NoError(t, err)
assert.Equal(t, "remote", w.ExecutionMode)
})
})
t.Run("create workspace with hyok enabled set to false", func(t *testing.T) {
skipHYOKIntegrationTests(t)
// replace the environment variable with a valid organization name that has HYOK permissions
hyokOrganizationName := os.Getenv("HYOK_ORGANIZATION_NAME")
if hyokOrganizationName == "" {
t.Fatal("Export a valid HYOK_ORGANIZATION_NAME before running this test!")
}
workspaceCreateOptions := WorkspaceCreateOptions{
Name: String("go-tfe-test-hyok-enabled-false"),
HYOKEnabled: Bool(false),
}
w, err := client.Workspaces.Create(ctx, hyokOrganizationName, workspaceCreateOptions)
require.NoError(t, err)
assert.NotNil(t, w.HYOKEnabled)
assert.False(t, *w.HYOKEnabled)
err = client.Workspaces.Delete(ctx, hyokOrganizationName, *workspaceCreateOptions.Name)
require.NoError(t, err)
})
t.Run("create workspace with hyok enabled set to true", func(t *testing.T) {
skipHYOKIntegrationTests(t)
// replace the environment variable with a valid organization name that has HYOK permissions
hyokOrganizationName := os.Getenv("HYOK_ORGANIZATION_NAME")
if hyokOrganizationName == "" {
t.Fatal("Export a valid HYOK_ORGANIZATION_NAME before running this test!")
}
workspaceCreateOptions := WorkspaceCreateOptions{
Name: String("go-tfe-test-hyok-enabled-true"),
HYOKEnabled: Bool(true),
}
w, err := client.Workspaces.Create(ctx, hyokOrganizationName, workspaceCreateOptions)
require.NoError(t, err)
assert.NotNil(t, w.HYOKEnabled)
assert.True(t, *w.HYOKEnabled)
err = client.Workspaces.Delete(ctx, hyokOrganizationName, *workspaceCreateOptions.Name)
require.NoError(t, err)
})
}
func TestWorkspacesRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup)
t.Run("when the workspace exists", func(t *testing.T) {
w, err := client.Workspaces.Read(ctx, orgTest.Name, wTest.Name)
require.NoError(t, err)
assert.Equal(t, wTest, w)
assert.True(t, w.Permissions.CanDestroy)
assert.NotEmpty(t, w.Actions)
assert.Equal(t, orgTest.Name, w.Organization.Name)
assert.NotEmpty(t, w.CreatedAt)
assert.NotEmpty(t, wTest.SettingOverwrites)
})
t.Run("links are properly decoded", func(t *testing.T) {
w, err := client.Workspaces.Read(ctx, orgTest.Name, wTest.Name)
require.NoError(t, err)
assert.NotEmpty(t, w.Links["self-html"])
assert.Contains(t, w.Links["self-html"], fmt.Sprintf("/app/%s/workspaces/%s", orgTest.Name, wTest.Name))
assert.NotEmpty(t, w.Links["self"])
assert.Contains(t, w.Links["self"], fmt.Sprintf("/api/v2/organizations/%s/workspaces/%s", orgTest.Name, wTest.Name))
})
t.Run("when the workspace does not exist", func(t *testing.T) {
w, err := client.Workspaces.Read(ctx, orgTest.Name, "nonexisting")
assert.Nil(t, w)
assert.Error(t, err)
})
t.Run("when the organization does not exist", func(t *testing.T) {
w, err := client.Workspaces.Read(ctx, "nonexisting", "nonexisting")
assert.Nil(t, w)
assert.Error(t, err)
})
t.Run("without a valid organization", func(t *testing.T) {
w, err := client.Workspaces.Read(ctx, badIdentifier, wTest.Name)
assert.Nil(t, w)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("without a valid workspace", func(t *testing.T) {
w, err := client.Workspaces.Read(ctx, orgTest.Name, badIdentifier)
assert.Nil(t, w)
assert.EqualError(t, err, ErrInvalidWorkspaceValue.Error())
})
t.Run("when workspace is inheriting the default execution mode", func(t *testing.T) {
defaultExecutionOrgTest, defaultExecutionOrgTestCleanup := createOrganizationWithDefaultAgentPool(t, client)
t.Cleanup(defaultExecutionOrgTestCleanup)
options := WorkspaceCreateOptions{
Name: String(fmt.Sprintf("tst-agent-cody-banks-%s", randomString(t))),
SettingOverwrites: &WorkspaceSettingOverwritesOptions{
ExecutionMode: Bool(false),
AgentPool: Bool(false),
},
}
wDefaultTest, wDefaultTestCleanup := createWorkspaceWithOptions(t, client, defaultExecutionOrgTest, options)
t.Cleanup(wDefaultTestCleanup)
t.Run("and workspace execution mode is default", func(t *testing.T) {
w, err := client.Workspaces.Read(ctx, defaultExecutionOrgTest.Name, wDefaultTest.Name)
assert.NoError(t, err)
assert.NotEmpty(t, w)
assert.Equal(t, defaultExecutionOrgTest.DefaultExecutionMode, w.ExecutionMode)
assert.NotEmpty(t, w.SettingOverwrites)
assert.Equal(t, false, *w.SettingOverwrites.ExecutionMode)
assert.Equal(t, false, *w.SettingOverwrites.ExecutionMode)
})
})
t.Run("read hyok enabled of a workspace", func(t *testing.T) {
skipHYOKIntegrationTests(t)
// replace the environment variable with a valid organization name that has HYOK permissions
hyokOrganizationName := os.Getenv("HYOK_ORGANIZATION_NAME")
if hyokOrganizationName == "" {
t.Fatal("Export a valid HYOK_ORGANIZATION_NAME before running this test!")
}
// replace the environment variable with a valid workspace name that has hyok enabled set to true or false
hyokWorkspaceName := os.Getenv("HYOK_WORKSPACE_NAME")
if hyokWorkspaceName == "" {
t.Fatal("Export a valid HYOK_WORKSPACE_NAME before running this test!")
}
w, err := client.Workspaces.Read(ctx, hyokOrganizationName, hyokWorkspaceName)
require.NoError(t, err)
assert.NotNil(t, w.HYOKEnabled)
})
t.Run("read hyok encrypted data key of a workspace", func(t *testing.T) {
skipHYOKIntegrationTests(t)
// replace the environment variable with a valid organization name that has HYOK permissions
hyokOrganizationName := os.Getenv("HYOK_ORGANIZATION_NAME")
if hyokOrganizationName == "" {
t.Fatal("Export a valid HYOK_ORGANIZATION_NAME before running this test!")
}
// replace the environment variable with a valid workspace name that has hyok encrypted data key
hyokWorkspaceName := os.Getenv("HYOK_WORKSPACE_NAME")
if hyokWorkspaceName == "" {
t.Fatal("Export a valid HYOK_WORKSPACE_NAME before running this test!")
}
w, err := client.Workspaces.Read(ctx, hyokOrganizationName, hyokWorkspaceName)
require.NoError(t, err)
assert.NotEmpty(t, w.HYOKEncryptedDataKey)
})
}
func TestWorkspacesReadSource(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup)
w, err := client.Workspaces.Read(ctx, orgTest.Name, wTest.Name)
require.NoError(t, err)
assert.Equal(t, WorkspaceSourceAPI, w.Source)
}
func TestWorkspacesReadWithOptions(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup)
svTest, svTestCleanup := createStateVersion(t, client, 0, wTest)
t.Cleanup(svTestCleanup)
// give HCP Terraform some time to process the statefile and extract the outputs.
waitForSVOutputs(t, client, svTest.ID)
t.Run("when options to include resource", func(t *testing.T) {
opts := &WorkspaceReadOptions{
Include: []WSIncludeOpt{WSOutputs},
}
w, err := client.Workspaces.ReadWithOptions(ctx, orgTest.Name, wTest.Name, opts)
require.NoError(t, err)
assert.Equal(t, wTest.ID, w.ID)
assert.NotEmpty(t, w.Outputs)
svOutputs, err := client.StateVersions.ListOutputs(ctx, svTest.ID, nil)
require.NoError(t, err)
assert.Len(t, w.Outputs, len(svOutputs.Items))
wsOutputsSensitive := map[string]bool{}
wsOutputsTypes := map[string]string{}
for _, op := range w.Outputs {
wsOutputsSensitive[op.Name] = op.Sensitive
wsOutputsTypes[op.Name] = op.Type
}
for _, svop := range svOutputs.Items {
valSensitive, ok := wsOutputsSensitive[svop.Name]
assert.True(t, ok)
assert.Equal(t, svop.Sensitive, valSensitive)
valType, ok := wsOutputsTypes[svop.Name]
assert.True(t, ok)
assert.Equal(t, svop.Type, valType)
}
})
}
func TestWorkspacesReadWithHistory_RunDependent(t *testing.T) {
client := testClient(t)
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup)
_, rCleanup := createRunApply(t, client, wTest)
t.Cleanup(rCleanup)
_, err := retry(func() (interface{}, error) {
w, err := client.Workspaces.Read(context.Background(), orgTest.Name, wTest.Name)
require.NoError(t, err)
if w.RunsCount != 1 {
return nil, fmt.Errorf("expected %d runs but found %d", 1, w.RunsCount)
}
if w.ResourceCount != 1 {
return nil, fmt.Errorf("expected %d resources but found %d", 1, w.ResourceCount)
}
return w, nil
})
if err != nil {
t.Error(err)
}
}
// If you've set your own GITHUB_POLICY_SET_IDENTIFIER, make sure the readme
// starts with the string: This is a simple test
// Otherwise the test will not pass
func TestWorkspacesReadReadme_RunDependent(t *testing.T) {
t.Skip("Skipping due to persistent failures - see TF-31172")
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wTestCleanup := createWorkspaceWithVCS(t, client, orgTest, WorkspaceCreateOptions{})
t.Cleanup(wTestCleanup)
_, rCleanup := createRunApply(t, client, wTest)
t.Cleanup(rCleanup)
t.Run("when the readme exists", func(t *testing.T) {
w, err := client.Workspaces.Readme(ctx, wTest.ID)
require.NoError(t, err)
require.NotNil(t, w)
readme, err := io.ReadAll(w)
require.NoError(t, err)
require.True(
t,
strings.HasPrefix(string(readme), `This is a simple test`),
"got: %s", readme,
)
})
t.Run("when the readme does not exist", func(t *testing.T) {
w, err := client.Workspaces.Readme(ctx, "nonexisting")
assert.Nil(t, w)
assert.Error(t, err)
})
t.Run("without a valid workspace ID", func(t *testing.T) {
w, err := client.Workspaces.Readme(ctx, badIdentifier)
assert.Nil(t, w)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
}
func TestWorkspacesReadByID(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup)
t.Run("when the workspace exists", func(t *testing.T) {
w, err := client.Workspaces.ReadByID(ctx, wTest.ID)
require.NoError(t, err)
assert.Equal(t, wTest, w)
assert.True(t, w.Permissions.CanDestroy)
assert.Equal(t, orgTest.Name, w.Organization.Name)
assert.NotEmpty(t, w.CreatedAt)
assert.NotEmpty(t, w.Actions)
})
t.Run("when the workspace does not exist", func(t *testing.T) {
w, err := client.Workspaces.ReadByID(ctx, "nonexisting")
assert.Nil(t, w)
assert.Error(t, err)
})
t.Run("without a valid workspace ID", func(t *testing.T) {
w, err := client.Workspaces.ReadByID(ctx, badIdentifier)
assert.Nil(t, w)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
}
func TestWorkspacesAddTagBindings(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
wTest, wCleanup := createWorkspace(t, client, nil)
t.Cleanup(wCleanup)
t.Run("when adding tag bindings to a workspace", func(t *testing.T) {
tagBindings := []*TagBinding{
{Key: "foo", Value: "bar"},
{Key: "baz", Value: "qux"},
}
bindings, err := client.Workspaces.AddTagBindings(ctx, wTest.ID, WorkspaceAddTagBindingsOptions{
TagBindings: tagBindings,
})
require.NoError(t, err)
assert.Len(t, bindings, 2)
assert.Equal(t, tagBindings[0].Key, bindings[0].Key)
assert.Equal(t, tagBindings[0].Value, bindings[0].Value)
assert.Equal(t, tagBindings[1].Key, bindings[1].Key)
assert.Equal(t, tagBindings[1].Value, bindings[1].Value)
})
t.Run("when adding 26 tags", func(t *testing.T) {
tagBindings := []*TagBinding{
{Key: "alpha"},
{Key: "bravo"},
{Key: "charlie"},
{Key: "delta"},
{Key: "echo"},
{Key: "foxtrot"},
{Key: "golf"},
{Key: "hotel"},
{Key: "india"},
{Key: "juliet"},
{Key: "kilo"},
{Key: "lima"},
{Key: "mike"},
{Key: "november"},
{Key: "oscar"},
{Key: "papa"},
{Key: "quebec"},
{Key: "romeo"},
{Key: "sierra"},
{Key: "tango"},
{Key: "uniform"},
{Key: "victor"},
{Key: "whiskey"},
{Key: "xray"},
{Key: "yankee"},
{Key: "zulu"},
}
_, err := client.Workspaces.AddTagBindings(ctx, wTest.ID, WorkspaceAddTagBindingsOptions{
TagBindings: tagBindings,
})
require.Error(t, err, "cannot exceed 10 bindings per resource")
})
}
func TestWorkspaces_DeleteAllTagBindings(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
wTest, wCleanup := createWorkspace(t, client, nil)
t.Cleanup(wCleanup)
tagBindings := []*TagBinding{
{Key: "foo", Value: "bar"},
{Key: "baz", Value: "qux"},
}
_, err := client.Workspaces.AddTagBindings(ctx, wTest.ID, WorkspaceAddTagBindingsOptions{
TagBindings: tagBindings,
})
require.NoError(t, err)
err = client.Workspaces.DeleteAllTagBindings(ctx, wTest.ID)
require.NoError(t, err)
bindings, err := client.Workspaces.ListTagBindings(ctx, wTest.ID)
require.NoError(t, err)
require.Empty(t, bindings)
}
func TestWorkspacesUpdate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
upgradeOrganizationSubscription(t, client, orgTest)
wTest, wCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wCleanup)
t.Run("when updating a subset of values", func(t *testing.T) {
options := WorkspaceUpdateOptions{
Name: String(wTest.Name),
AllowDestroyPlan: Bool(false),
AutoApply: Bool(true),
Operations: Bool(true),
QueueAllRuns: Bool(true),
AssessmentsEnabled: Bool(true),
TerraformVersion: String("0.15.4"),
}
wAfter, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, options)
require.NoError(t, err)
assert.Equal(t, wTest.Name, wAfter.Name)
assert.NotEqual(t, wTest.AllowDestroyPlan, wAfter.AllowDestroyPlan)
assert.NotEqual(t, wTest.AutoApply, wAfter.AutoApply)
assert.NotEqual(t, wTest.QueueAllRuns, wAfter.QueueAllRuns)
assert.NotEqual(t, wTest.AssessmentsEnabled, wAfter.AssessmentsEnabled)
assert.NotEqual(t, wTest.TerraformVersion, wAfter.TerraformVersion)
assert.Equal(t, wTest.WorkingDirectory, wAfter.WorkingDirectory)
})
t.Run("when updating auto-apply-run-trigger", func(t *testing.T) {
skipIfEnterprise(t)
// Feature flag: auto-apply-run-trigger. Once flag is removed, delete
// this test and add the attribute to one generic update test.
options := WorkspaceUpdateOptions{
AutoApplyRunTrigger: Bool(true),
}
wAfter, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, options)
require.NoError(t, err)
assert.Equal(t, wTest.Name, wAfter.Name)
assert.NotEqual(t, wTest.AutoApplyRunTrigger, wAfter.AutoApplyRunTrigger)
})
t.Run("when updating project", func(t *testing.T) {
skipUnlessBeta(t)
kBefore, kTestCleanup := createProject(t, client, orgTest)
defer kTestCleanup()
wBefore, wBeforeCleanup := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{
Name: String(randomString(t)),
Project: kBefore,
})
defer wBeforeCleanup()
options := WorkspaceUpdateOptions{
Name: String(wBefore.Name),
AllowDestroyPlan: Bool(false),
AutoApply: Bool(true),
Operations: Bool(true),
QueueAllRuns: Bool(true),
AssessmentsEnabled: Bool(true),
TerraformVersion: String("0.15.4"),
Project: orgTest.DefaultProject,
}
wAfter, err := client.Workspaces.Update(ctx, orgTest.Name, wBefore.Name, options)
require.NoError(t, err)
require.NotNil(t, wAfter.Project)
require.NotNil(t, orgTest.DefaultProject)
assert.Equal(t, wBefore.Name, wAfter.Name)
assert.Equal(t, wAfter.Project.ID, orgTest.DefaultProject.ID)
})
t.Run("with valid options", func(t *testing.T) {
options := WorkspaceUpdateOptions{
Name: String(randomString(t)),
AllowDestroyPlan: Bool(true),
AutoApply: Bool(false),
AutoDestroyAt: NullableTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)),
FileTriggersEnabled: Bool(true),
Operations: Bool(false),
QueueAllRuns: Bool(false),
SpeculativeEnabled: Bool(true),
Description: String("updated description"),
StructuredRunOutputEnabled: Bool(true),
TerraformVersion: String("0.11.1"),
TriggerPrefixes: []string{"/modules", "/shared"},
WorkingDirectory: String("baz/"),
TagBindings: []*TagBinding{
{Key: "foo", Value: "bar"},
},
}
w, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, options)
require.NoError(t, err)
// Get a refreshed view of the workspace from the API
refreshed, err := client.Workspaces.Read(ctx, orgTest.Name, *options.Name)
require.NoError(t, err)
for _, item := range []*Workspace{
w,
refreshed,
} {
assert.Equal(t, *options.Name, item.Name)
assert.Equal(t, *options.AllowDestroyPlan, item.AllowDestroyPlan)
assert.Equal(t, *options.AutoApply, item.AutoApply)
assert.Equal(t, options.AutoDestroyAt, item.AutoDestroyAt)
assert.Equal(t, *options.FileTriggersEnabled, item.FileTriggersEnabled)
assert.Equal(t, *options.Description, item.Description)
assert.Equal(t, *options.Operations, item.Operations)
assert.Equal(t, *options.QueueAllRuns, item.QueueAllRuns)
assert.Equal(t, *options.SpeculativeEnabled, item.SpeculativeEnabled)
assert.Equal(t, *options.StructuredRunOutputEnabled, item.StructuredRunOutputEnabled)
assert.Equal(t, *options.TerraformVersion, item.TerraformVersion)
assert.Equal(t, options.TriggerPrefixes, item.TriggerPrefixes)
assert.Equal(t, *options.WorkingDirectory, item.WorkingDirectory)
}
if betaFeaturesEnabled() {
bindings, err := client.Workspaces.ListTagBindings(ctx, wTest.ID)
require.NoError(t, err)
assert.Len(t, bindings, 1)
assert.Equal(t, "foo", bindings[0].Key)
assert.Equal(t, "bar", bindings[0].Value)
effectiveBindings, err := client.Workspaces.ListEffectiveTagBindings(ctx, wTest.ID)
require.NoError(t, err)
assert.Len(t, effectiveBindings, 1)
assert.Equal(t, "foo", effectiveBindings[0].Key)
assert.Equal(t, "bar", effectiveBindings[0].Value)
}
})
t.Run("when options includes both an operations value and an enforcement mode value", func(t *testing.T) {
options := WorkspaceUpdateOptions{
ExecutionMode: String("remote"),
Operations: Bool(true),
}
wAfter, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, options)
assert.Nil(t, wAfter)
assert.Equal(t, err, ErrUnsupportedOperations)
})
t.Run("when 'agent' execution mode is specified without an agent pool ID", func(t *testing.T) {
options := WorkspaceUpdateOptions{
ExecutionMode: String("agent"),
}
wAfter, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, options)
assert.Nil(t, wAfter)
assert.Equal(t, err, ErrRequiredAgentPoolID)
})
t.Run("when an error is returned from the api", func(t *testing.T) {
w, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, WorkspaceUpdateOptions{
TerraformVersion: String("nonexisting"),
})
assert.Nil(t, w)
assert.Error(t, err)
})
t.Run("when options has an invalid name", func(t *testing.T) {
w, err := client.Workspaces.Update(ctx, orgTest.Name, badIdentifier, WorkspaceUpdateOptions{})
assert.Nil(t, w)
assert.EqualError(t, err, ErrInvalidWorkspaceValue.Error())
})
t.Run("when options has an invalid organization", func(t *testing.T) {
w, err := client.Workspaces.Update(ctx, badIdentifier, wTest.Name, WorkspaceUpdateOptions{})
assert.Nil(t, w)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("when options include trigger-patterns (behind a feature flag)", func(t *testing.T) {
// Remove the below organization and workspace creation and use the one from the outer scope once the feature flag is removed
orgTest, orgTestCleanup := createOrganizationWithOptions(t, client, OrganizationCreateOptions{
Name: String("tst-" + randomString(t)[0:20] + "-ff-on"),
Email: String(fmt.Sprintf("%s@tfe.local", randomString(t))),
})
t.Cleanup(orgTestCleanup)
wTest, wCleanup := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{
Name: String(randomString(t)),
TriggerPrefixes: []string{"/prefix-1/", "/prefix-2/"},
})
t.Cleanup(wCleanup)
assert.Equal(t, wTest.TriggerPrefixes, []string{"/prefix-1/", "/prefix-2/"}) // Sanity test
options := WorkspaceUpdateOptions{
Name: String("foobar"),
FileTriggersEnabled: Bool(true),
TriggerPatterns: []string{"/module-1/**/*", "/**/networking/*"},
}
w, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, options)
require.NoError(t, err)
// Get a refreshed view from the API.
refreshed, err := client.Workspaces.Read(ctx, orgTest.Name, *options.Name)
require.NoError(t, err)
for _, item := range []*Workspace{
w,
refreshed,
} {
assert.Empty(t, options.TriggerPrefixes)
assert.Equal(t, options.TriggerPatterns, item.TriggerPatterns)
}
})
t.Run("when options include both trigger-patterns and trigger-paths error is returned", func(t *testing.T) {
options := WorkspaceUpdateOptions{
Name: String("foobar"),
FileTriggersEnabled: Bool(true),
TriggerPrefixes: []string{"/module-1", "/module-2"},
TriggerPatterns: []string{"/module-1/**/*", "/**/networking/*"},
}
w, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, options)
assert.Nil(t, w)
assert.EqualError(t, err, ErrUnsupportedBothTriggerPatternsAndPrefixes.Error())
})
t.Run("when options include trigger-patterns populated and empty trigger-paths workspace is updated", func(t *testing.T) {
// Remove the below organization creation and use the one from the outer scope once the feature flag is removed
orgTest, orgTestCleanup := createOrganizationWithOptions(t, client, OrganizationCreateOptions{
Name: String("tst-" + randomString(t)[0:20] + "-ff-on"),
Email: String(fmt.Sprintf("%s@tfe.local", randomString(t))),
})
t.Cleanup(orgTestCleanup)
wTest, wCleanup := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{
Name: String(randomString(t)),
TriggerPatterns: []string{"/pattern-1/**/*", "/pattern-2/**/*"},
})
t.Cleanup(wCleanup)
assert.Equal(t, wTest.TriggerPatterns, []string{"/pattern-1/**/*", "/pattern-2/**/*"}) // Sanity test
options := WorkspaceUpdateOptions{
Name: String("foobar"),
FileTriggersEnabled: Bool(true),
TriggerPrefixes: []string{},
TriggerPatterns: []string{"/module-1/**/*", "/**/networking/*"},
}
w, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, options)
require.NoError(t, err)
// Get a refreshed view from the API.
refreshed, err := client.Workspaces.Read(ctx, orgTest.Name, *options.Name)
require.NoError(t, err)
for _, item := range []*Workspace{
w,
refreshed,
} {
assert.Empty(t, options.TriggerPrefixes)
assert.Equal(t, options.TriggerPatterns, item.TriggerPatterns)
}
})
t.Run("update hyok enabled of a workspace from false to false", func(t *testing.T) {
skipHYOKIntegrationTests(t)
// replace the environment variable with a valid organization name that has HYOK permissions
hyokOrganizationName := os.Getenv("HYOK_ORGANIZATION_NAME")
if hyokOrganizationName == "" {
t.Fatal("Export a valid HYOK_ORGANIZATION_NAME before running this test!")
}
workspaceCreateOptions := WorkspaceCreateOptions{
Name: String("go-tfe-test-hyok-enabled-false"),
HYOKEnabled: Bool(false),
}
w, err := client.Workspaces.Create(ctx, hyokOrganizationName, workspaceCreateOptions)
require.NoError(t, err)
assert.NotNil(t, w.HYOKEnabled)
assert.False(t, *w.HYOKEnabled)
workspaceUpdateOptions := WorkspaceUpdateOptions{
HYOKEnabled: Bool(false),
}
w, err = client.Workspaces.Update(ctx, hyokOrganizationName, w.Name, workspaceUpdateOptions)
require.NoError(t, err)
assert.NotNil(t, w.HYOKEnabled)
assert.False(t, *w.HYOKEnabled)
err = client.Workspaces.Delete(ctx, hyokOrganizationName, *workspaceCreateOptions.Name)
require.NoError(t, err)
})
t.Run("update hyok enabled of a workspace from false to true", func(t *testing.T) {
skipHYOKIntegrationTests(t)
// replace the environment variable with a valid organization name that has HYOK permissions
hyokOrganizationName := os.Getenv("HYOK_ORGANIZATION_NAME")
if hyokOrganizationName == "" {
t.Fatal("Export a valid HYOK_ORGANIZATION_NAME before running this test!")
}
workspaceCreateOptions := WorkspaceCreateOptions{
Name: String("go-tfe-test-hyok-enabled-false"),
HYOKEnabled: Bool(false),
}
w, err := client.Workspaces.Create(ctx, hyokOrganizationName, workspaceCreateOptions)
require.NoError(t, err)
assert.NotNil(t, w.HYOKEnabled)
assert.False(t, *w.HYOKEnabled)
workspaceUpdateOptions := WorkspaceUpdateOptions{
HYOKEnabled: Bool(true),
}
w, err = client.Workspaces.Update(ctx, hyokOrganizationName, w.Name, workspaceUpdateOptions)
require.NoError(t, err)
assert.NotNil(t, w.HYOKEnabled)
assert.True(t, *w.HYOKEnabled)
err = client.Workspaces.Delete(ctx, hyokOrganizationName, *workspaceCreateOptions.Name)
require.NoError(t, err)
})
t.Run("update hyok enabled of a workspace from true to true", func(t *testing.T) {
skipHYOKIntegrationTests(t)
// replace the environment variable with a valid organization name that has HYOK permissions
hyokOrganizationName := os.Getenv("HYOK_ORGANIZATION_NAME")
if hyokOrganizationName == "" {
t.Fatal("Export a valid HYOK_ORGANIZATION_NAME before running this test!")
}
workspaceCreateOptions := WorkspaceCreateOptions{
Name: String("go-tfe-test-hyok-enabled-true"),
HYOKEnabled: Bool(true),
}
w, err := client.Workspaces.Create(ctx, hyokOrganizationName, workspaceCreateOptions)
require.NoError(t, err)
assert.NotNil(t, w.HYOKEnabled)
assert.True(t, *w.HYOKEnabled)
workspaceUpdateOptions := WorkspaceUpdateOptions{
HYOKEnabled: Bool(true),
}
w, err = client.Workspaces.Update(ctx, hyokOrganizationName, w.Name, workspaceUpdateOptions)
require.NoError(t, err)
assert.NotNil(t, w.HYOKEnabled)
assert.True(t, *w.HYOKEnabled)
err = client.Workspaces.Delete(ctx, hyokOrganizationName, *workspaceCreateOptions.Name)
require.NoError(t, err)
})
t.Run("update hyok enabled of a workspace from true to false", func(t *testing.T) {
skipHYOKIntegrationTests(t)
// replace the environment variable with a valid organization name that has HYOK permissions
hyokOrganizationName := os.Getenv("HYOK_ORGANIZATION_NAME")
if hyokOrganizationName == "" {
t.Fatal("Export a valid HYOK_ORGANIZATION_NAME before running this test!")
}
workspaceCreateOptions := WorkspaceCreateOptions{
Name: String("go-tfe-test-hyok-enabled-true"),
HYOKEnabled: Bool(true),
}
w, err := client.Workspaces.Create(ctx, hyokOrganizationName, workspaceCreateOptions)
require.NoError(t, err)
assert.NotNil(t, w.HYOKEnabled)
assert.True(t, *w.HYOKEnabled)
workspaceUpdateOptions := WorkspaceUpdateOptions{
HYOKEnabled: Bool(false),
}
_, err = client.Workspaces.Update(ctx, hyokOrganizationName, w.Name, workspaceUpdateOptions)
require.Error(t, err)
assert.EqualError(t, err, ErrHYOKCannotBeDisabled.Error())
err = client.Workspaces.Delete(ctx, hyokOrganizationName, *workspaceCreateOptions.Name)
require.NoError(t, err)
})
}
func TestWorkspacesUpdateTableDriven(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wCleanup)
workspaceTableTests := []WorkspaceTableTest{
{
scenario: "when options include VCSRepo tags-regex",
options: &WorkspaceTableOptions{
createOptions: &WorkspaceCreateOptions{
Name: String("foobar"),
FileTriggersEnabled: Bool(false),
VCSRepo: &VCSRepoOptions{
TagsRegex: String("barfoo")},
},
updateOptions: &WorkspaceUpdateOptions{
Name: String("foobar"),
FileTriggersEnabled: Bool(false),
VCSRepo: &VCSRepoOptions{TagsRegex: String("foobar")},
},
},
setup: func(t *testing.T, options *WorkspaceTableOptions) (w *Workspace, cleanup func()) {
orgTest, orgTestCleanup := createOrganizationWithOptions(t, client, OrganizationCreateOptions{
Name: String("tst-" + randomString(t)[0:20]),
Email: String(fmt.Sprintf("%s@tfe.local", randomString(t))),
})
wTest, wTestCleanup := createWorkspaceWithVCS(t, client, orgTest, *options.createOptions)
return wTest, func() {
t.Cleanup(orgTestCleanup)
t.Cleanup(wTestCleanup)
}
},
assertion: func(t *testing.T, workspace *Workspace, options *WorkspaceTableOptions, _ error) {
assert.Equal(t, *options.createOptions.VCSRepo.TagsRegex, workspace.VCSRepo.TagsRegex)
assert.Equal(t, workspace.VCSRepo.TagsRegex, *String("barfoo")) // Sanity test
w, err := client.Workspaces.Update(ctx, workspace.Organization.Name, workspace.Name, *options.updateOptions)
require.NoError(t, err)
assert.Equal(t, w.VCSRepo.TagsRegex, *String("foobar")) // Sanity test
// Get a refreshed view from the API.
refreshed, err := client.Workspaces.Read(ctx, workspace.Organization.Name, *options.updateOptions.Name)
require.NoError(t, err)
for _, item := range []*Workspace{
w,
refreshed,
} {
assert.Empty(t, options.updateOptions.TriggerPrefixes)
assert.Empty(t, options.updateOptions.TriggerPatterns, item.TriggerPatterns)
}
},
},
{
scenario: "when options include tags-regex and file-triggers-enabled is true an error is returned",
options: &WorkspaceTableOptions{
updateOptions: &WorkspaceUpdateOptions{
Name: String("foobar"),
FileTriggersEnabled: Bool(true),
VCSRepo: &VCSRepoOptions{TagsRegex: String("foobar")},
},
},
assertion: func(t *testing.T, w *Workspace, options *WorkspaceTableOptions, err error) {
assert.Nil(t, w)
assert.EqualError(t, err, ErrUnsupportedBothTagsRegexAndFileTriggersEnabled.Error())
},
},
{
scenario: "when options include both non-empty tags-regex and file-triggers-enabled an error is returned",
options: &WorkspaceTableOptions{
updateOptions: &WorkspaceUpdateOptions{
Name: String("foobar"),
FileTriggersEnabled: Bool(true),
VCSRepo: &VCSRepoOptions{TagsRegex: String("foobar")},
},
},
assertion: func(t *testing.T, w *Workspace, options *WorkspaceTableOptions, err error) {
assert.Nil(t, w)
assert.EqualError(t, err, ErrUnsupportedBothTagsRegexAndFileTriggersEnabled.Error())
},
},
{
scenario: "when options include both tags-regex and trigger-prefixes an error is returned",
options: &WorkspaceTableOptions{
updateOptions: &WorkspaceUpdateOptions{
Name: String("foobar"),
FileTriggersEnabled: Bool(false),
TriggerPrefixes: []string{"/module-1", "/module-2"},
VCSRepo: &VCSRepoOptions{TagsRegex: String("foobar")},
},
},
assertion: func(t *testing.T, w *Workspace, options *WorkspaceTableOptions, err error) {
assert.Nil(t, w)
assert.EqualError(t, err, ErrUnsupportedBothTagsRegexAndTriggerPrefixes.Error())
},
},
{
scenario: "when options include both tags-regex and trigger-patterns error is returned",
options: &WorkspaceTableOptions{
updateOptions: &WorkspaceUpdateOptions{
Name: String("foobar"),
FileTriggersEnabled: Bool(false),
TriggerPatterns: []string{"/module-1/**/*", "/**/networking/*"},
VCSRepo: &VCSRepoOptions{TagsRegex: String("foobar")},
},
},
assertion: func(t *testing.T, w *Workspace, options *WorkspaceTableOptions, err error) {
assert.Nil(t, w)
assert.EqualError(t, err, ErrUnsupportedBothTagsRegexAndTriggerPatterns.Error())
},
},
}
for _, tableTest := range workspaceTableTests {
t.Run(tableTest.scenario, func(t *testing.T) {
var workspace *Workspace
var cleanup func()
var err error
if tableTest.setup != nil {
workspace, cleanup = tableTest.setup(t, tableTest.options)
defer cleanup()
} else {
workspace, err = client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, *tableTest.options.updateOptions)
}
tableTest.assertion(t, workspace, tableTest.options, err)
})
}
}
func TestWorkspacesUpdateTableDrivenWithGithubApp(t *testing.T) {
t.Parallel()
gHAInstallationID := os.Getenv("GITHUB_APP_INSTALLATION_ID")
if gHAInstallationID == "" {
t.Skip("Export a valid GITHUB_APP_INSTALLATION_ID before running this test!")
}
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wCleanup)
workspaceTableTests := []WorkspaceTableTest{
{
scenario: "when options include VCSRepo tags-regex",
options: &WorkspaceTableOptions{
createOptions: &WorkspaceCreateOptions{
Name: String("foobar"),
FileTriggersEnabled: Bool(false),
VCSRepo: &VCSRepoOptions{
TagsRegex: String("barfoo")},
},
updateOptions: &WorkspaceUpdateOptions{
Name: String("foobar"),
FileTriggersEnabled: Bool(false),
VCSRepo: &VCSRepoOptions{
TagsRegex: String("foobar"),
},
},
},
setup: func(t *testing.T, options *WorkspaceTableOptions) (w *Workspace, cleanup func()) {
orgTest, orgTestCleanup := createOrganizationWithOptions(t, client, OrganizationCreateOptions{
Name: String("tst-" + randomString(t)[0:20]),
Email: String(fmt.Sprintf("%s@tfe.local", randomString(t))),
})
wTest, wTestCleanup := createWorkspaceWithGithubApp(t, client, orgTest, *options.createOptions)
return wTest, func() {
t.Cleanup(orgTestCleanup)
t.Cleanup(wTestCleanup)
}
},
assertion: func(t *testing.T, workspace *Workspace, options *WorkspaceTableOptions, _ error) {
assert.Equal(t, *options.createOptions.VCSRepo.TagsRegex, workspace.VCSRepo.TagsRegex)
assert.Equal(t, workspace.VCSRepo.TagsRegex, *String("barfoo")) // Sanity test
w, err := client.Workspaces.Update(ctx, workspace.Organization.Name, workspace.Name, *options.updateOptions)
require.NoError(t, err)
assert.Equal(t, w.VCSRepo.TagsRegex, *String("foobar")) // Sanity test
},
},
}
for _, tableTest := range workspaceTableTests {
t.Run(tableTest.scenario, func(t *testing.T) {
var workspace *Workspace
var cleanup func()
var err error
if tableTest.setup != nil {
workspace, cleanup = tableTest.setup(t, tableTest.options)
defer cleanup()
} else {
workspace, err = client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, *tableTest.options.updateOptions)
}
tableTest.assertion(t, workspace, tableTest.options, err)
})
}
}
func TestWorkspacesUpdateByID(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wCleanup)
t.Run("when updating a subset of values", func(t *testing.T) {
options := WorkspaceUpdateOptions{
Name: String(wTest.Name),
AllowDestroyPlan: Bool(false),
AutoApply: Bool(true),
Operations: Bool(true),
QueueAllRuns: Bool(true),
TerraformVersion: String("0.10.0"),
}
wAfter, err := client.Workspaces.UpdateByID(ctx, wTest.ID, options)
require.NoError(t, err)
assert.Equal(t, wTest.Name, wAfter.Name)
assert.NotEqual(t, wTest.AllowDestroyPlan, wAfter.AllowDestroyPlan)
assert.NotEqual(t, wTest.AutoApply, wAfter.AutoApply)
assert.NotEqual(t, wTest.QueueAllRuns, wAfter.QueueAllRuns)
assert.NotEqual(t, wTest.TerraformVersion, wAfter.TerraformVersion)
assert.Equal(t, wTest.WorkingDirectory, wAfter.WorkingDirectory)
})
t.Run("with valid options", func(t *testing.T) {
options := WorkspaceUpdateOptions{
Name: String(randomString(t)),
AllowDestroyPlan: Bool(true),
AutoApply: Bool(false),
FileTriggersEnabled: Bool(true),
Operations: Bool(false),
QueueAllRuns: Bool(false),
SpeculativeEnabled: Bool(true),
StructuredRunOutputEnabled: Bool(true),
TerraformVersion: String("0.11.1"),
TriggerPrefixes: []string{"/modules", "/shared"},
WorkingDirectory: String("baz/"),
}
w, err := client.Workspaces.UpdateByID(ctx, wTest.ID, options)
require.NoError(t, err)
// Get a refreshed view of the workspace from the API
refreshed, err := client.Workspaces.Read(ctx, orgTest.Name, *options.Name)
require.NoError(t, err)
for _, item := range []*Workspace{
w,
refreshed,
} {
assert.Equal(t, *options.Name, item.Name)
assert.Equal(t, *options.AllowDestroyPlan, item.AllowDestroyPlan)
assert.Equal(t, *options.AutoApply, item.AutoApply)
assert.Equal(t, *options.FileTriggersEnabled, item.FileTriggersEnabled)
assert.Equal(t, *options.Operations, item.Operations)
assert.Equal(t, *options.QueueAllRuns, item.QueueAllRuns)
assert.Equal(t, *options.SpeculativeEnabled, item.SpeculativeEnabled)
assert.Equal(t, *options.StructuredRunOutputEnabled, item.StructuredRunOutputEnabled)
assert.Equal(t, *options.TerraformVersion, item.TerraformVersion)
assert.Equal(t, options.TriggerPrefixes, item.TriggerPrefixes)
assert.Equal(t, *options.WorkingDirectory, item.WorkingDirectory)
}
})
t.Run("when an error is returned from the api", func(t *testing.T) {
w, err := client.Workspaces.UpdateByID(ctx, wTest.ID, WorkspaceUpdateOptions{
TerraformVersion: String("nonexisting"),
})
assert.Nil(t, w)
assert.Error(t, err)
})
t.Run("without a valid workspace ID", func(t *testing.T) {
w, err := client.Workspaces.UpdateByID(ctx, badIdentifier, WorkspaceUpdateOptions{})
assert.Nil(t, w)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
}
func TestWorkspacesUpdateWithDefaultExecutionMode(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
defaultExecutionOrgTest, defaultExecutionOrgTestCleanup := createOrganizationWithDefaultAgentPool(t, client)
t.Cleanup(defaultExecutionOrgTestCleanup)
wTest, wCleanup := createWorkspace(t, client, defaultExecutionOrgTest)
t.Cleanup(wCleanup)
t.Run("when explicitly setting execution mode, workspace ignores the default execution mode", func(t *testing.T) {
options := WorkspaceUpdateOptions{
ExecutionMode: String("remote"),
}
w, err := client.Workspaces.Update(ctx, defaultExecutionOrgTest.Name, wTest.Name, options)
require.NoError(t, err)
assert.Equal(t, "remote", w.ExecutionMode)
})
t.Run("with setting overwrites set to true, workspace ignores the default execution mode", func(t *testing.T) {
options := WorkspaceUpdateOptions{
ExecutionMode: String("local"),
SettingOverwrites: &WorkspaceSettingOverwritesOptions{
ExecutionMode: Bool(true),
AgentPool: Bool(true),
},
}
w, err := client.Workspaces.Update(ctx, defaultExecutionOrgTest.Name, wTest.Name, options)
require.NoError(t, err)
assert.Equal(t, "local", w.ExecutionMode)
})
t.Run("with setting overwrites set to false, workspace inherits the default execution mode", func(t *testing.T) {
options := WorkspaceUpdateOptions{
SettingOverwrites: &WorkspaceSettingOverwritesOptions{
ExecutionMode: Bool(false),
AgentPool: Bool(false),
},
}
w, err := client.Workspaces.Update(ctx, defaultExecutionOrgTest.Name, wTest.Name, options)
require.NoError(t, err)
assert.Equal(t, "agent", w.ExecutionMode)
})
}
func TestWorkspacesDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
// ignore workspace cleanup b/c it will be destroyed during tests
wTest, _ := createWorkspace(t, client, orgTest)
t.Run("with valid options", func(t *testing.T) {
err := client.Workspaces.Delete(ctx, orgTest.Name, wTest.Name)
require.NoError(t, err)
// Try loading the workspace - it should fail.
_, err = client.Workspaces.Read(ctx, orgTest.Name, wTest.Name)
assert.Equal(t, ErrResourceNotFound, err)
})
t.Run("when organization is invalid", func(t *testing.T) {
err := client.Workspaces.Delete(ctx, badIdentifier, wTest.Name)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("when workspace is invalid", func(t *testing.T) {
err := client.Workspaces.Delete(ctx, orgTest.Name, badIdentifier)
assert.EqualError(t, err, ErrInvalidWorkspaceValue.Error())
})
}
func TestWorkspacesDeleteByID(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
// ignore workspace cleanup b/c it will be destroyed during tests
wTest, _ := createWorkspace(t, client, orgTest)
t.Run("with valid options", func(t *testing.T) {
err := client.Workspaces.DeleteByID(ctx, wTest.ID)
require.NoError(t, err)
// Try loading the workspace - it should fail.
_, err = client.Workspaces.ReadByID(ctx, wTest.ID)
assert.Equal(t, ErrResourceNotFound, err)
})
t.Run("without a valid workspace ID", func(t *testing.T) {
err := client.Workspaces.DeleteByID(ctx, badIdentifier)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
}
func TestCanForceDeletePermission(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wCleanup)
t.Run("workspace permission set includes can-force-delete", func(t *testing.T) {
w, err := client.Workspaces.ReadByID(ctx, wTest.ID)
require.NoError(t, err)
assert.Equal(t, wTest, w)
require.NotNil(t, w.Permissions)
require.NotNil(t, w.Permissions.CanForceDelete)
assert.True(t, *w.Permissions.CanForceDelete)
})
}
func TestWorkspacesSafeDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
// ignore workspace cleanup b/c it will be destroyed during tests
wTest, _ := createWorkspace(t, client, orgTest)
t.Run("with valid options", func(t *testing.T) {
err := client.Workspaces.SafeDelete(ctx, orgTest.Name, wTest.Name)
require.NoError(t, err)
// Try loading the workspace - it should fail.
_, err = client.Workspaces.Read(ctx, orgTest.Name, wTest.Name)
assert.Equal(t, ErrResourceNotFound, err)
})
t.Run("when organization is invalid", func(t *testing.T) {
err := client.Workspaces.SafeDelete(ctx, badIdentifier, wTest.Name)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
t.Run("when workspace is invalid", func(t *testing.T) {
err := client.Workspaces.SafeDelete(ctx, orgTest.Name, badIdentifier)
assert.EqualError(t, err, ErrInvalidWorkspaceValue.Error())
})
t.Run("when workspace is locked", func(t *testing.T) {
wTest, workspaceCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(workspaceCleanup)
w, err := client.Workspaces.Lock(ctx, wTest.ID, WorkspaceLockOptions{})
require.NoError(t, err)
require.True(t, w.Locked)
err = client.Workspaces.SafeDelete(ctx, orgTest.Name, wTest.Name)
assert.True(t, errors.Is(err, ErrWorkspaceLockedCannotDelete))
})
t.Run("when workspace has resources under management", func(t *testing.T) {
wTest, workspaceCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(workspaceCleanup)
_, svTestCleanup := createStateVersion(t, client, 0, wTest)
t.Cleanup(svTestCleanup)
_, err := retry(func() (interface{}, error) {
err := client.Workspaces.SafeDelete(ctx, orgTest.Name, wTest.Name)
if errors.Is(err, ErrWorkspaceStillProcessing) {
return nil, err
}
return nil, nil
})
if err != nil {
t.Fatalf("Workspace still processing after retrying: %s", err)
}
err = client.Workspaces.SafeDelete(ctx, orgTest.Name, wTest.Name)
assert.True(t, errors.Is(err, ErrWorkspaceNotSafeToDelete))
})
}
func TestWorkspacesSafeDeleteByID(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
// ignore workspace cleanup b/c it will be destroyed during tests
wTest, _ := createWorkspace(t, client, orgTest)
t.Run("with valid options", func(t *testing.T) {
err := client.Workspaces.SafeDeleteByID(ctx, wTest.ID)
require.NoError(t, err)
// Try loading the workspace - it should fail.
_, err = client.Workspaces.ReadByID(ctx, wTest.ID)
assert.Equal(t, ErrResourceNotFound, err)
})
t.Run("without a valid workspace ID", func(t *testing.T) {
err := client.Workspaces.SafeDeleteByID(ctx, badIdentifier)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
t.Run("when workspace is locked", func(t *testing.T) {
wTest, workspaceCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(workspaceCleanup)
w, err := client.Workspaces.Lock(ctx, wTest.ID, WorkspaceLockOptions{})
require.NoError(t, err)
require.True(t, w.Locked)
err = client.Workspaces.SafeDeleteByID(ctx, wTest.ID)
assert.True(t, errors.Is(err, ErrWorkspaceLockedCannotDelete))
})
t.Run("when workspace has resources under management", func(t *testing.T) {
wTest, workspaceCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(workspaceCleanup)
_, svTestCleanup := createStateVersion(t, client, 0, wTest)
t.Cleanup(svTestCleanup)
_, err := retryPatiently(func() (interface{}, error) {
err := client.Workspaces.SafeDeleteByID(ctx, wTest.ID)
if errors.Is(err, ErrWorkspaceStillProcessing) {
return nil, err
}
return nil, nil
})
if err != nil {
t.Fatalf("Workspace still processing after retrying: %s", err)
}
err = client.Workspaces.SafeDeleteByID(ctx, wTest.ID)
assert.True(t, errors.Is(err, ErrWorkspaceNotSafeToDelete))
})
}
func TestWorkspacesRemoveVCSConnection(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wTestCleanup := createWorkspaceWithVCS(t, client, orgTest, WorkspaceCreateOptions{})
t.Cleanup(wTestCleanup)
t.Run("remove vcs integration", func(t *testing.T) {
w, err := client.Workspaces.RemoveVCSConnection(ctx, orgTest.Name, wTest.Name)
require.NoError(t, err)
assert.Equal(t, (*VCSRepo)(nil), w.VCSRepo)
})
}
func TestWorkspacesRemoveVCSConnectionByID(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wTestCleanup := createWorkspaceWithVCS(t, client, orgTest, WorkspaceCreateOptions{})
t.Cleanup(wTestCleanup)
t.Run("remove vcs integration", func(t *testing.T) {
w, err := client.Workspaces.RemoveVCSConnectionByID(ctx, wTest.ID)
require.NoError(t, err)
assert.Equal(t, (*VCSRepo)(nil), w.VCSRepo)
})
}
func TestWorkspacesLock(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup)
t.Run("with valid options", func(t *testing.T) {
require.Empty(t, wTest.LockedBy)
w, err := client.Workspaces.Lock(ctx, wTest.ID, WorkspaceLockOptions{})
require.NoError(t, err)
assert.True(t, w.Locked)
require.NoError(t, err)
require.NotEmpty(t, w.LockedBy)
requireExactlyOneNotEmpty(t, w.LockedBy.Run, w.LockedBy.Team, w.LockedBy.User)
})
t.Run("when workspace is already locked", func(t *testing.T) {
_, err := client.Workspaces.Lock(ctx, wTest.ID, WorkspaceLockOptions{})
assert.Equal(t, ErrWorkspaceLocked, err)
})
t.Run("without a valid workspace ID", func(t *testing.T) {
w, err := client.Workspaces.Lock(ctx, badIdentifier, WorkspaceLockOptions{})
assert.Nil(t, w)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
}
func TestWorkspacesUnlock_RunDependent(t *testing.T) {
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup)
w, err := client.Workspaces.Lock(ctx, wTest.ID, WorkspaceLockOptions{})
if err != nil {
orgTestCleanup()
}
require.NoError(t, err)
require.True(t, w.Locked)
t.Run("with valid options", func(t *testing.T) {
w, err := client.Workspaces.Unlock(ctx, wTest.ID)
require.NoError(t, err)
assert.False(t, w.Locked)
})
t.Run("when workspace is already unlocked", func(t *testing.T) {
_, err := client.Workspaces.Unlock(ctx, wTest.ID)
assert.Equal(t, ErrWorkspaceNotLocked, err)
})
t.Run("when a workspace is locked by a run", func(t *testing.T) {
wTest2, wTest2Cleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTest2Cleanup)
_, rTestCleanup := createRun(t, client, wTest2)
t.Cleanup(rTestCleanup)
// Wait for wTest2 to be locked by a run
waitForRunLock(t, client, wTest2.ID)
_, err = client.Workspaces.Unlock(ctx, wTest2.ID)
assert.Equal(t, ErrWorkspaceLockedByRun, err)
})
t.Run("when a workspace is locked by a team", func(t *testing.T) {
wTest2, wTest2Cleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTest2Cleanup)
// Create a new team to lock the workspace
tmTest, tmTestCleanup := createTeam(t, client, orgTest)
defer tmTestCleanup()
ta, err := client.TeamAccess.Add(ctx, TeamAccessAddOptions{
Access: Access(AccessAdmin),
Team: tmTest,
Workspace: wTest2,
})
assert.Nil(t, err)
defer func() {
err := client.TeamAccess.Remove(ctx, ta.ID)
if err != nil {
t.Logf("error removing team access (%s): %s", ta.ID, err)
}
}()
tt, ttTestCleanup := createTeamToken(t, client, tmTest)
defer ttTestCleanup()
// Create a new client with the team token
teamClient := testClient(t)
teamClient.token = tt.Token
// Lock the workspace with the team client
_, err = teamClient.Workspaces.Lock(ctx, wTest2.ID, WorkspaceLockOptions{})
assert.Nil(t, err)
// Attempt to unlock the workspace with the original client
_, err = client.Workspaces.Unlock(ctx, wTest2.ID)
assert.Equal(t, ErrWorkspaceLockedByTeam, err)
})
t.Run("without a valid workspace ID", func(t *testing.T) {
w, err := client.Workspaces.Unlock(ctx, badIdentifier)
assert.Nil(t, w)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
}
func TestWorkspacesForceUnlock(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup)
w, err := client.Workspaces.Lock(ctx, wTest.ID, WorkspaceLockOptions{})
if err != nil {
orgTestCleanup()
}
require.NoError(t, err)
require.True(t, w.Locked)
t.Run("with valid options", func(t *testing.T) {
w, err := client.Workspaces.ForceUnlock(ctx, wTest.ID)
require.NoError(t, err)
assert.False(t, w.Locked)
})
t.Run("when workspace is already unlocked", func(t *testing.T) {
_, err := client.Workspaces.ForceUnlock(ctx, wTest.ID)
assert.Equal(t, ErrWorkspaceNotLocked, err)
})
t.Run("without a valid workspace ID", func(t *testing.T) {
w, err := client.Workspaces.ForceUnlock(ctx, badIdentifier)
assert.Nil(t, w)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
}
func TestWorkspacesAssignSSHKey(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup)
sshKeyTest, sshKeyTestCleanup := createSSHKey(t, client, orgTest)
t.Cleanup(sshKeyTestCleanup)
t.Run("with valid options", func(t *testing.T) {
w, err := client.Workspaces.AssignSSHKey(ctx, wTest.ID, WorkspaceAssignSSHKeyOptions{
SSHKeyID: String(sshKeyTest.ID),
})
require.NoError(t, err)
require.NotNil(t, w.SSHKey)
assert.Equal(t, w.SSHKey.ID, sshKeyTest.ID)
})
t.Run("without an SSH key ID", func(t *testing.T) {
w, err := client.Workspaces.AssignSSHKey(ctx, wTest.ID, WorkspaceAssignSSHKeyOptions{})
assert.Nil(t, w)
assert.Equal(t, err, ErrRequiredSHHKeyID)
})
t.Run("without a valid SSH key ID", func(t *testing.T) {
w, err := client.Workspaces.AssignSSHKey(ctx, wTest.ID, WorkspaceAssignSSHKeyOptions{
SSHKeyID: String(badIdentifier),
})
assert.Nil(t, w)
assert.Equal(t, err, ErrInvalidSHHKeyID)
})
t.Run("without a valid workspace ID", func(t *testing.T) {
w, err := client.Workspaces.AssignSSHKey(ctx, badIdentifier, WorkspaceAssignSSHKeyOptions{
SSHKeyID: String(sshKeyTest.ID),
})
assert.Nil(t, w)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
}
func TestWorkspacesUnassignSSHKey(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup)
sshKeyTest, sshKeyTestCleanup := createSSHKey(t, client, orgTest)
t.Cleanup(sshKeyTestCleanup)
w, err := client.Workspaces.AssignSSHKey(ctx, wTest.ID, WorkspaceAssignSSHKeyOptions{
SSHKeyID: String(sshKeyTest.ID),
})
if err != nil {
orgTestCleanup()
}
require.NoError(t, err)
require.NotNil(t, w.SSHKey)
require.Equal(t, w.SSHKey.ID, sshKeyTest.ID)
t.Run("with valid options", func(t *testing.T) {
w, err := client.Workspaces.UnassignSSHKey(ctx, wTest.ID)
assert.Nil(t, err)
assert.Nil(t, w.SSHKey)
})
t.Run("without a valid workspace ID", func(t *testing.T) {
w, err := client.Workspaces.UnassignSSHKey(ctx, badIdentifier)
assert.Nil(t, w)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
}
func TestWorkspaces_AddRemoteStateConsumers(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup)
// Update workspace to not allow global remote state
options := WorkspaceUpdateOptions{
GlobalRemoteState: Bool(false),
}
wTest, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, options)
require.NoError(t, err)
t.Run("successfully adds a remote state consumer", func(t *testing.T) {
wTestConsumer1, wTestCleanupConsumer1 := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanupConsumer1)
wTestConsumer2, wTestCleanupConsumer2 := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanupConsumer2)
err := client.Workspaces.AddRemoteStateConsumers(ctx, wTest.ID, WorkspaceAddRemoteStateConsumersOptions{
Workspaces: []*Workspace{wTestConsumer1, wTestConsumer2},
})
require.NoError(t, err)
_, err = client.Workspaces.Read(ctx, orgTest.Name, wTest.Name)
require.NoError(t, err)
rsc, err := client.Workspaces.ListRemoteStateConsumers(ctx, wTest.ID, nil)
require.NoError(t, err)
assert.Equal(t, 2, len(rsc.Items))
assert.Contains(t, rsc.Items, wTestConsumer1)
assert.Contains(t, rsc.Items, wTestConsumer2)
})
t.Run("with invalid options", func(t *testing.T) {
err := client.Workspaces.AddRemoteStateConsumers(ctx, wTest.ID, WorkspaceAddRemoteStateConsumersOptions{})
require.Error(t, err)
assert.EqualError(t, err, ErrWorkspacesRequired.Error())
err = client.Workspaces.AddRemoteStateConsumers(ctx, wTest.ID, WorkspaceAddRemoteStateConsumersOptions{
Workspaces: []*Workspace{},
})
require.Error(t, err)
assert.EqualError(t, err, ErrWorkspaceMinLimit.Error())
})
t.Run("without a valid workspace ID", func(t *testing.T) {
err := client.Workspaces.AddRemoteStateConsumers(ctx, badIdentifier, WorkspaceAddRemoteStateConsumersOptions{})
require.Error(t, err)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
}
func TestWorkspaces_RemoveRemoteStateConsumers(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup)
// Update workspace to not allow global remote state
options := WorkspaceUpdateOptions{
GlobalRemoteState: Bool(false),
}
wTest, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, options)
require.NoError(t, err)
t.Run("successfully removes a remote state consumer", func(t *testing.T) {
wTestConsumer1, wTestCleanupConsumer1 := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanupConsumer1)
wTestConsumer2, wTestCleanupConsumer2 := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanupConsumer2)
err := client.Workspaces.AddRemoteStateConsumers(ctx, wTest.ID, WorkspaceAddRemoteStateConsumersOptions{
Workspaces: []*Workspace{wTestConsumer1, wTestConsumer2},
})
require.NoError(t, err)
rsc, err := client.Workspaces.ListRemoteStateConsumers(ctx, wTest.ID, nil)
require.NoError(t, err)
assert.Equal(t, 2, len(rsc.Items))
assert.Contains(t, rsc.Items, wTestConsumer1)
assert.Contains(t, rsc.Items, wTestConsumer2)
err = client.Workspaces.RemoveRemoteStateConsumers(ctx, wTest.ID, WorkspaceRemoveRemoteStateConsumersOptions{
Workspaces: []*Workspace{wTestConsumer1},
})
require.NoError(t, err)
_, err = client.Workspaces.Read(ctx, orgTest.Name, wTest.Name)
require.NoError(t, err)
rsc, err = client.Workspaces.ListRemoteStateConsumers(ctx, wTest.ID, nil)
require.NoError(t, err)
assert.Contains(t, rsc.Items, wTestConsumer2)
assert.Equal(t, 1, len(rsc.Items))
err = client.Workspaces.RemoveRemoteStateConsumers(ctx, wTest.ID, WorkspaceRemoveRemoteStateConsumersOptions{
Workspaces: []*Workspace{wTestConsumer2},
})
require.NoError(t, err)
rsc, err = client.Workspaces.ListRemoteStateConsumers(ctx, wTest.ID, nil)
require.NoError(t, err)
assert.Empty(t, len(rsc.Items))
})
t.Run("with invalid options", func(t *testing.T) {
err := client.Workspaces.RemoveRemoteStateConsumers(ctx, wTest.ID, WorkspaceRemoveRemoteStateConsumersOptions{})
require.Error(t, err)
assert.EqualError(t, err, ErrWorkspacesRequired.Error())
err = client.Workspaces.RemoveRemoteStateConsumers(ctx, wTest.ID, WorkspaceRemoveRemoteStateConsumersOptions{
Workspaces: []*Workspace{},
})
require.Error(t, err)
assert.EqualError(t, err, ErrWorkspaceMinLimit.Error())
})
t.Run("without a valid workspace ID", func(t *testing.T) {
err := client.Workspaces.RemoveRemoteStateConsumers(ctx, badIdentifier, WorkspaceRemoveRemoteStateConsumersOptions{})
require.Error(t, err)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
}
func TestWorkspaces_UpdateRemoteStateConsumers(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup)
// Update workspace to not allow global remote state
options := WorkspaceUpdateOptions{
GlobalRemoteState: Bool(false),
}
wTest, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, options)
require.NoError(t, err)
t.Run("successfully updates a remote state consumer", func(t *testing.T) {
wTestConsumer1, wTestCleanupConsumer1 := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanupConsumer1)
wTestConsumer2, wTestCleanupConsumer2 := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanupConsumer2)
err := client.Workspaces.AddRemoteStateConsumers(ctx, wTest.ID, WorkspaceAddRemoteStateConsumersOptions{
Workspaces: []*Workspace{wTestConsumer1},
})
require.NoError(t, err)
rsc, err := client.Workspaces.ListRemoteStateConsumers(ctx, wTest.ID, nil)
require.NoError(t, err)
assert.Equal(t, 1, len(rsc.Items))
assert.Contains(t, rsc.Items, wTestConsumer1)
err = client.Workspaces.UpdateRemoteStateConsumers(ctx, wTest.ID, WorkspaceUpdateRemoteStateConsumersOptions{
Workspaces: []*Workspace{wTestConsumer2},
})
require.NoError(t, err)
rsc, err = client.Workspaces.ListRemoteStateConsumers(ctx, wTest.ID, nil)
require.NoError(t, err)
assert.Equal(t, 1, len(rsc.Items))
assert.Contains(t, rsc.Items, wTestConsumer2)
})
t.Run("with invalid options", func(t *testing.T) {
err := client.Workspaces.UpdateRemoteStateConsumers(ctx, wTest.ID, WorkspaceUpdateRemoteStateConsumersOptions{})
require.Error(t, err)
assert.EqualError(t, err, ErrWorkspacesRequired.Error())
err = client.Workspaces.UpdateRemoteStateConsumers(ctx, wTest.ID, WorkspaceUpdateRemoteStateConsumersOptions{
Workspaces: []*Workspace{},
})
require.Error(t, err)
assert.EqualError(t, err, ErrWorkspaceMinLimit.Error())
})
t.Run("without a valid workspace ID", func(t *testing.T) {
err := client.Workspaces.UpdateRemoteStateConsumers(ctx, badIdentifier, WorkspaceUpdateRemoteStateConsumersOptions{})
require.Error(t, err)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
}
func TestWorkspaces_AddTags(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup)
options := WorkspaceAddTagsOptions{
Tags: []*Tag{
{
Name: "tag1",
},
{
Name: "tag2",
},
{
Name: "tag3",
},
},
}
t.Run("successfully adds tags", func(t *testing.T) {
err := client.Workspaces.AddTags(ctx, wTest.ID, options)
require.NoError(t, err)
w, err := client.Workspaces.Read(ctx, orgTest.Name, wTest.Name)
require.NoError(t, err)
assert.Equal(t, 3, len(w.TagNames))
assert.Equal(t, w.TagNames, []string{"tag1", "tag2", "tag3"})
err = client.Workspaces.AddTags(ctx, wTest.ID, WorkspaceAddTagsOptions{
Tags: []*Tag{
{
Name: "tag4",
},
},
})
require.NoError(t, err)
w, err = client.Workspaces.Read(ctx, orgTest.Name, wTest.Name)
require.NoError(t, err)
assert.Equal(t, 4, len(w.TagNames))
sort.Strings(w.TagNames)
assert.EqualValues(t, w.TagNames, []string{"tag1", "tag2", "tag3", "tag4"})
wt, err := client.Workspaces.ListTags(ctx, wTest.ID, nil)
require.NoError(t, err)
assert.Equal(t, 4, len(wt.Items))
assert.Equal(t, wt.Items[3].Name, "tag4")
})
t.Run("successfully adds tags by id and name", func(t *testing.T) {
wTest2, wTest2Cleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTest2Cleanup)
// add a tag to another workspace
err := client.Workspaces.AddTags(ctx, wTest2.ID, WorkspaceAddTagsOptions{
Tags: []*Tag{
{
Name: "tagbyid",
},
},
})
require.NoError(t, err)
// get the id of the new tag (may take a moment to show up)
createdTags, err := retryPatientlyIf(
func() (any, error) {
return client.Workspaces.ListTags(ctx, wTest2.ID, nil)
},
func(tl *TagList) bool {
return tl == nil || len(tl.Items) == 0
},
)
require.NoError(t, err)
require.NotNil(t, createdTags)
require.NotEmpty(t, createdTags.Items)
tagID := createdTags.Items[0].ID
// add the tag to our workspace by id
err = client.Workspaces.AddTags(ctx, wTest.ID, WorkspaceAddTagsOptions{
Tags: []*Tag{
{
ID: tagID,
},
},
})
require.NoError(t, err)
// tag is now in our tag list
wt, err := retryPatientlyIf(
func() (any, error) {
return client.Workspaces.ListTags(ctx, wTest.ID, nil)
},
func(tl *TagList) bool {
// wait for the tag to appear
if tl == nil {
return true
}
for _, tag := range tl.Items {
if tag.ID == tagID {
return false
}
}
return true
},
)
require.NoError(t, err)
require.NotNil(t, wt)
// find the tag we added
var addedTag *Tag
for _, tag := range wt.Items {
if tag.ID == tagID {
addedTag = tag
break
}
}
require.NotNil(t, addedTag)
assert.Equal(t, "tagbyid", addedTag.Name)
})
t.Run("with invalid options", func(t *testing.T) {
err := client.Workspaces.AddTags(ctx, wTest.ID, WorkspaceAddTagsOptions{
Tags: []*Tag{},
})
require.Error(t, err)
assert.EqualError(t, err, ErrMissingTagIdentifier.Error())
})
t.Run("without a valid workspace ID", func(t *testing.T) {
err := client.Workspaces.AddTags(ctx, badIdentifier, WorkspaceAddTagsOptions{})
require.Error(t, err)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
}
func TestWorkspaces_RemoveTags(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup)
tags := []*Tag{
{
Name: "tag1",
},
{
Name: "tag2",
},
{
Name: "tag3",
},
}
addOptions := WorkspaceAddTagsOptions{
Tags: tags,
}
removeOptions := WorkspaceRemoveTagsOptions{
Tags: tags[0:2],
}
t.Run("successfully removes tags", func(t *testing.T) {
err := client.Workspaces.AddTags(ctx, wTest.ID, addOptions)
require.NoError(t, err)
w, err := client.Workspaces.Read(ctx, orgTest.Name, wTest.Name)
require.NoError(t, err)
assert.Equal(t, 3, len(w.TagNames))
assert.Equal(t, w.TagNames, []string{"tag1", "tag2", "tag3"})
err = client.Workspaces.RemoveTags(ctx, wTest.ID, removeOptions)
require.NoError(t, err)
w, err = client.Workspaces.Read(ctx, orgTest.Name, wTest.Name)
require.NoError(t, err)
assert.Equal(t, 1, len(w.TagNames))
assert.Equal(t, w.TagNames, []string{"tag3"})
wt, err := client.Workspaces.ListTags(ctx, wTest.ID, nil)
require.NoError(t, err)
assert.Equal(t, 1, len(wt.Items))
assert.EqualValues(t, wt.Items[0].Name, "tag3")
})
t.Run("attempts to remove a tag that doesn't exist", func(t *testing.T) {
err := client.Workspaces.RemoveTags(ctx, wTest.ID, WorkspaceRemoveTagsOptions{
Tags: []*Tag{
{
Name: "NonExistentTag",
},
},
})
require.NoError(t, err)
})
t.Run("with invalid options", func(t *testing.T) {
err := client.Workspaces.RemoveTags(ctx, wTest.ID, WorkspaceRemoveTagsOptions{
Tags: []*Tag{},
})
require.Error(t, err)
assert.EqualError(t, err, ErrMissingTagIdentifier.Error())
})
t.Run("without a valid workspace ID", func(t *testing.T) {
err := client.Workspaces.RemoveTags(ctx, badIdentifier, WorkspaceRemoveTagsOptions{})
require.Error(t, err)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
}
func TestWorkspace_Unmarshal(t *testing.T) {
t.Parallel()
data := map[string]interface{}{
"data": map[string]interface{}{
"type": "workspaces",
"id": "ws-1234",
"attributes": map[string]interface{}{
"name": "my-workspace",
"auto-apply": true,
"created-at": "2020-07-15T23:38:43.821Z",
"resource-count": 2,
"permissions": map[string]interface{}{
"can-update": true,
"can-lock": true,
},
"vcs-repo": map[string]interface{}{
"branch": "main",
"display-identifier": "repo-name",
"identifier": "hashicorp/repo-name",
"ingress-submodules": true,
"oauth-token-id": "token",
"repository-http-url": "github.com",
"service-provider": "github",
"webhook-url": "https://app.terraform.io/webhooks/vcs/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
},
"actions": map[string]interface{}{
"is-destroyable": true,
},
"trigger-prefixes": []string{"prefix-"},
"trigger-patterns": []string{"pattern1/**/*", "pattern2/**/submodule/*"},
},
},
}
byteData, err := json.Marshal(data)
require.NoError(t, err)
responseBody := bytes.NewReader(byteData)
ws := &Workspace{}
err = unmarshalResponse(responseBody, ws)
require.NoError(t, err)
iso8601TimeFormat := "2006-01-02T15:04:05Z"
parsedTime, err := time.Parse(iso8601TimeFormat, "2020-07-15T23:38:43.821Z")
assert.NoError(t, err)
assert.Equal(t, ws.ID, "ws-1234")
assert.Equal(t, ws.Name, "my-workspace")
assert.Equal(t, ws.AutoApply, true)
assert.Equal(t, ws.CreatedAt, parsedTime)
assert.Equal(t, ws.ResourceCount, 2)
assert.Equal(t, ws.Permissions.CanUpdate, true)
assert.Equal(t, ws.Permissions.CanLock, true)
assert.Equal(t, ws.VCSRepo.Branch, "main")
assert.Equal(t, ws.VCSRepo.DisplayIdentifier, "repo-name")
assert.Equal(t, ws.VCSRepo.Identifier, "hashicorp/repo-name")
assert.Equal(t, ws.VCSRepo.IngressSubmodules, true)
assert.Equal(t, ws.VCSRepo.OAuthTokenID, "token")
assert.Equal(t, ws.VCSRepo.RepositoryHTTPURL, "github.com")
assert.Equal(t, ws.VCSRepo.ServiceProvider, "github")
assert.Equal(t, ws.VCSRepo.WebhookURL, "https://app.terraform.io/webhooks/vcs/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
assert.Equal(t, ws.Actions.IsDestroyable, true)
assert.Equal(t, ws.TriggerPrefixes, []string{"prefix-"})
assert.Equal(t, ws.TriggerPatterns, []string{"pattern1/**/*", "pattern2/**/submodule/*"})
}
func TestWorkspaceCreateOptions_Marshal(t *testing.T) {
t.Parallel()
opts := WorkspaceCreateOptions{
AllowDestroyPlan: Bool(true),
Name: String("my-workspace"),
TriggerPrefixes: []string{"prefix-"},
TriggerPatterns: []string{"pattern1/**/*", "pattern2/**/*"},
VCSRepo: &VCSRepoOptions{
Identifier: String("id"),
OAuthTokenID: String("token"),
},
}
reqBody, err := serializeRequestBody(&opts)
require.NoError(t, err)
req, err := retryablehttp.NewRequest("POST", "url", reqBody)
require.NoError(t, err)
bodyBytes, err := req.BodyBytes()
require.NoError(t, err)
expectedBody := `{"data":{"type":"workspaces","attributes":{"allow-destroy-plan":true,"name":"my-workspace","trigger-patterns":["pattern1/**/*","pattern2/**/*"],"trigger-prefixes":["prefix-"],"vcs-repo":{"identifier":"id","oauth-token-id":"token"}}}}
`
assert.Equal(t, expectedBody, string(bodyBytes))
}
func TestWorkspacesRunTasksPermission(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup)
t.Run("when the workspace exists", func(t *testing.T) {
w, err := client.Workspaces.Read(ctx, orgTest.Name, wTest.Name)
require.NoError(t, err)
assert.Equal(t, wTest, w)
assert.True(t, w.Permissions.CanManageRunTasks)
})
}
func TestWorkspacesProjects(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
t.Run("created workspace includes default organization project", func(t *testing.T) {
require.NotNil(t, orgTest.DefaultProject)
require.NotNil(t, wTest.Project)
assert.Equal(t, wTest.Project.ID, orgTest.DefaultProject.ID)
})
t.Run("created workspace includes project ID", func(t *testing.T) {
assert.NotNil(t, wTest.Project.ID)
})
t.Run("read workspace includes project ID", func(t *testing.T) {
workspace, err := client.Workspaces.ReadByID(ctx, wTest.ID)
assert.NoError(t, err)
assert.NotNil(t, workspace.Project.ID)
})
t.Run("list workspace includes project ID", func(t *testing.T) {
workspaces, err := client.Workspaces.List(ctx, orgTest.Name, &WorkspaceListOptions{})
assert.NoError(t, err)
for idx, item := range workspaces.Items {
assert.NotNil(t, item.Project.ID, "No project ID set on workspace %s at idx %d", item.ID, idx)
}
})
}
func TestWorkspace_DataRetentionPolicy(t *testing.T) {
t.Parallel()
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
wTest, wTestCleanup := createWorkspace(t, client, nil)
defer wTestCleanup()
dataRetentionPolicy, err := client.Workspaces.ReadDataRetentionPolicyChoice(ctx, wTest.ID)
assert.Nil(t, err)
require.Nil(t, dataRetentionPolicy)
workspace, err := client.Workspaces.ReadByID(ctx, wTest.ID)
require.NoError(t, err)
require.Nil(t, workspace.DataRetentionPolicy)
require.Nil(t, workspace.DataRetentionPolicyChoice)
t.Run("set and update data retention policy to delete older", func(t *testing.T) {
createdDataRetentionPolicy, err := client.Workspaces.SetDataRetentionPolicyDeleteOlder(ctx, wTest.ID, DataRetentionPolicyDeleteOlderSetOptions{DeleteOlderThanNDays: 33})
require.NoError(t, err)
require.Equal(t, 33, createdDataRetentionPolicy.DeleteOlderThanNDays)
require.Contains(t, createdDataRetentionPolicy.ID, "drp-")
dataRetentionPolicy, err = client.Workspaces.ReadDataRetentionPolicyChoice(ctx, wTest.ID)
require.NoError(t, err)
require.NotNil(t, dataRetentionPolicy.DataRetentionPolicyDeleteOlder)
require.Equal(t, 33, dataRetentionPolicy.DataRetentionPolicyDeleteOlder.DeleteOlderThanNDays)
require.Equal(t, createdDataRetentionPolicy.ID, dataRetentionPolicy.DataRetentionPolicyDeleteOlder.ID)
require.Contains(t, dataRetentionPolicy.DataRetentionPolicyDeleteOlder.ID, "drp-")
workspace, err := client.Workspaces.ReadByID(ctx, wTest.ID)
require.NoError(t, err)
require.Equal(t, dataRetentionPolicy.DataRetentionPolicyDeleteOlder.ID, workspace.DataRetentionPolicyChoice.DataRetentionPolicyDeleteOlder.ID)
// deprecated DataRetentionPolicy field should also have been populated
require.NotNil(t, workspace.DataRetentionPolicy)
require.Equal(t, workspace.DataRetentionPolicy.ID, dataRetentionPolicy.DataRetentionPolicyDeleteOlder.ID)
// try updating the number of days
createdDataRetentionPolicy, err = client.Workspaces.SetDataRetentionPolicyDeleteOlder(ctx, wTest.ID, DataRetentionPolicyDeleteOlderSetOptions{DeleteOlderThanNDays: 1})
require.NoError(t, err)
require.Equal(t, 1, createdDataRetentionPolicy.DeleteOlderThanNDays)
dataRetentionPolicy, err = client.Workspaces.ReadDataRetentionPolicyChoice(ctx, wTest.ID)
require.NoError(t, err)
require.NotNil(t, dataRetentionPolicy.DataRetentionPolicyDeleteOlder)
require.Equal(t, 1, dataRetentionPolicy.DataRetentionPolicyDeleteOlder.DeleteOlderThanNDays)
require.Equal(t, createdDataRetentionPolicy.ID, dataRetentionPolicy.DataRetentionPolicyDeleteOlder.ID)
})
t.Run("set data retention policy to not delete", func(t *testing.T) {
createdDataRetentionPolicy, err := client.Workspaces.SetDataRetentionPolicyDontDelete(ctx, wTest.ID, DataRetentionPolicyDontDeleteSetOptions{})
require.NoError(t, err)
require.Contains(t, createdDataRetentionPolicy.ID, "drp-")
dataRetentionPolicy, err = client.Workspaces.ReadDataRetentionPolicyChoice(ctx, wTest.ID)
require.NoError(t, err)
require.NotNil(t, dataRetentionPolicy.DataRetentionPolicyDontDelete)
require.Equal(t, createdDataRetentionPolicy.ID, dataRetentionPolicy.DataRetentionPolicyDontDelete.ID)
// dont delete policies should leave the legacy DataRetentionPolicy field on workspaces empty
workspace, err := client.Workspaces.ReadByID(ctx, wTest.ID)
require.NoError(t, err)
require.Nil(t, workspace.DataRetentionPolicy)
})
t.Run("change data retention policy type", func(t *testing.T) {
_, err = client.Workspaces.SetDataRetentionPolicyDeleteOlder(ctx, wTest.ID, DataRetentionPolicyDeleteOlderSetOptions{DeleteOlderThanNDays: 45})
require.NoError(t, err)
dataRetentionPolicy, err = client.Workspaces.ReadDataRetentionPolicyChoice(ctx, wTest.ID)
require.NoError(t, err)
require.NotNil(t, dataRetentionPolicy.DataRetentionPolicyDeleteOlder)
require.Equal(t, 45, dataRetentionPolicy.DataRetentionPolicyDeleteOlder.DeleteOlderThanNDays)
require.Nil(t, dataRetentionPolicy.DataRetentionPolicyDontDelete)
_, err = client.Workspaces.SetDataRetentionPolicyDontDelete(ctx, wTest.ID, DataRetentionPolicyDontDeleteSetOptions{})
require.NoError(t, err)
dataRetentionPolicy, err = client.Workspaces.ReadDataRetentionPolicyChoice(ctx, wTest.ID)
require.NoError(t, err)
require.Nil(t, dataRetentionPolicy.DataRetentionPolicyDeleteOlder)
require.NotNil(t, dataRetentionPolicy.DataRetentionPolicyDontDelete)
_, err = client.Workspaces.SetDataRetentionPolicyDeleteOlder(ctx, wTest.ID, DataRetentionPolicyDeleteOlderSetOptions{DeleteOlderThanNDays: 20})
require.NoError(t, err)
dataRetentionPolicy, err = client.Workspaces.ReadDataRetentionPolicyChoice(ctx, wTest.ID)
require.NoError(t, err)
require.NotNil(t, dataRetentionPolicy.DataRetentionPolicyDeleteOlder)
require.Equal(t, 20, dataRetentionPolicy.DataRetentionPolicyDeleteOlder.DeleteOlderThanNDays)
require.Nil(t, dataRetentionPolicy.DataRetentionPolicyDontDelete)
})
t.Run("delete data retention policy", func(t *testing.T) {
err = client.Workspaces.DeleteDataRetentionPolicy(ctx, wTest.ID)
require.NoError(t, err)
dataRetentionPolicy, err = client.Workspaces.ReadDataRetentionPolicyChoice(ctx, wTest.ID)
assert.Nil(t, err)
require.Nil(t, dataRetentionPolicy)
})
}
func TestWorkspacesAutoDestroy(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t)
autoDestroyAt := NullableTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC))
wTest, wCleanup := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{
Name: String(randomString(t)),
AutoDestroyAt: autoDestroyAt,
})
t.Cleanup(wCleanup)
require.Equal(t, autoDestroyAt, wTest.AutoDestroyAt)
// respect default omitempty
w, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, WorkspaceUpdateOptions{
AutoDestroyAt: nil,
})
require.NoError(t, err)
require.NotNil(t, w.AutoDestroyAt)
// explicitly update the value of auto_destroy_at
w, err = client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, WorkspaceUpdateOptions{
AutoDestroyAt: NullableTime(time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC)),
})
require.NoError(t, err)
require.NotNil(t, w.AutoDestroyAt)
require.NotEqual(t, autoDestroyAt, w.AutoDestroyAt)
// disable auto destroy
w, err = client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, WorkspaceUpdateOptions{
AutoDestroyAt: NullTime(),
})
require.NoError(t, err)
require.Nil(t, w.AutoDestroyAt)
}
func TestWorkspacesAutoDestroyDuration(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t)
t.Run("when creating a new workspace with standalone auto destroy settings", func(t *testing.T) {
duration := jsonapi.NewNullableAttrWithValue("14d")
nilDuration := jsonapi.NewNullNullableAttr[string]()
nilAutoDestroy := jsonapi.NewNullNullableAttr[time.Time]()
wTest, wCleanup := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{
Name: String(randomString(t)),
AutoDestroyActivityDuration: duration,
InheritsProjectAutoDestroy: Bool(false),
})
t.Cleanup(wCleanup)
require.Equal(t, duration, wTest.AutoDestroyActivityDuration)
require.NotEqual(t, nilAutoDestroy, wTest.AutoDestroyAt)
require.Equal(t, wTest.InheritsProjectAutoDestroy, false)
w, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, WorkspaceUpdateOptions{
AutoDestroyActivityDuration: nilDuration,
InheritsProjectAutoDestroy: Bool(false),
})
require.NoError(t, err)
require.False(t, w.AutoDestroyActivityDuration.IsSpecified())
require.False(t, w.AutoDestroyAt.IsSpecified())
require.Equal(t, wTest.InheritsProjectAutoDestroy, false)
})
}
func TestWorkspaces_effectiveTagBindingsInheritedFrom(t *testing.T) {
t.Parallel()
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
projTest, projTestCleanup := createProject(t, client, orgTest)
t.Cleanup(projTestCleanup)
ws, wsCleanup := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{
Name: String("mycoolworkspace"),
Project: projTest,
})
t.Cleanup(wsCleanup)
_, err := client.Workspaces.AddTagBindings(ctx, ws.ID, WorkspaceAddTagBindingsOptions{
TagBindings: []*TagBinding{
{
Key: "a",
Value: "1",
},
{
Key: "b",
Value: "2",
},
},
})
require.NoError(t, err)
t.Run("when no tags are inherited from the project", func(t *testing.T) {
effectiveBindings, err := client.Workspaces.ListEffectiveTagBindings(ctx, ws.ID)
require.NoError(t, err)
for _, binding := range effectiveBindings {
require.Nil(t, binding.Links)
}
})
t.Run("when tags are inherited from the project", func(t *testing.T) {
_, err := client.Projects.AddTagBindings(ctx, projTest.ID, ProjectAddTagBindingsOptions{
TagBindings: []*TagBinding{
{
Key: "inherited",
Value: "foo",
},
},
})
require.NoError(t, err)
effectiveBindings, err := client.Workspaces.ListEffectiveTagBindings(ctx, ws.ID)
require.NoError(t, err)
for _, binding := range effectiveBindings {
if binding.Key == "inherited" {
require.NotNil(t, binding.Links)
require.NotNil(t, binding.Links["inherited-from"])
} else {
require.Nil(t, binding.Links)
}
}
})
}
func TestWorkspacesProjectRemoteState(t *testing.T) {
skipUnlessEnterprise(t)
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
// create project
projTest, projTestCleanup := createProject(t, client, orgTest)
t.Cleanup(projTestCleanup)
// create workspace in first project
wTest, wTestCleanup := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{
Name: String(randomString(t)),
Project: projTest,
GlobalRemoteState: Bool(false),
})
t.Cleanup(wTestCleanup)
t.Run("successfully returns remote state consumer list", func(t *testing.T) {
// create consumer workspace in the test project
wTestConsumer1, wTestCleanupConsumer1 := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{
Name: String(randomString(t)),
Project: projTest,
})
t.Cleanup(wTestCleanupConsumer1)
// create another project
projTest2, projTest2Cleanup := createProject(t, client, orgTest)
t.Cleanup(projTest2Cleanup)
// create consumer workspace in the other project
wTestConsumer2, wTestCleanupConsumer2 := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{
Name: String(randomString(t)),
Project: projTest2,
})
t.Cleanup(wTestCleanupConsumer2)
err := client.Workspaces.AddRemoteStateConsumers(ctx, wTest.ID, WorkspaceAddRemoteStateConsumersOptions{
Workspaces: []*Workspace{wTestConsumer1, wTestConsumer2},
})
require.NoError(t, err)
rsc, err := client.Workspaces.ListRemoteStateConsumers(ctx, wTest.ID, nil)
require.NoError(t, err)
// all the consumer workspaces are in the list
assert.Equal(t, 2, len(rsc.Items))
assert.Contains(t, rsc.Items, wTestConsumer1, wTestConsumer2)
// Update workspace to allow project remote state sharing
options := WorkspaceUpdateOptions{
ProjectRemoteState: Bool(true),
}
wTest, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, options)
require.NoError(t, err)
_, err = client.Workspaces.Read(ctx, orgTest.Name, wTest.Name)
require.NoError(t, err)
rsc, err = client.Workspaces.ListRemoteStateConsumers(ctx, wTest.ID, nil)
require.NoError(t, err)
assert.Equal(t, 1, len(rsc.Items))
assert.Contains(t, rsc.Items, wTestConsumer1)
assert.NotContains(t, rsc.Items, wTestConsumer2)
})
t.Run("with invalid options", func(t *testing.T) {
err := client.Workspaces.AddRemoteStateConsumers(ctx, wTest.ID, WorkspaceAddRemoteStateConsumersOptions{})
require.Error(t, err)
assert.EqualError(t, err, ErrWorkspacesRequired.Error())
err = client.Workspaces.AddRemoteStateConsumers(ctx, wTest.ID, WorkspaceAddRemoteStateConsumersOptions{
Workspaces: []*Workspace{},
})
require.Error(t, err)
assert.EqualError(t, err, ErrWorkspaceMinLimit.Error())
// Update workspace to allow project and global remote state sharing
options := WorkspaceUpdateOptions{
ProjectRemoteState: Bool(true),
GlobalRemoteState: Bool(true),
}
_, err = client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, options)
require.Error(t, err)
assert.ErrorContains(t, err, ErrInvalidRemoteStateOptions.Error())
})
t.Run("without a valid workspace ID", func(t *testing.T) {
err := client.Workspaces.AddRemoteStateConsumers(ctx, badIdentifier, WorkspaceAddRemoteStateConsumersOptions{})
require.Error(t, err)
assert.EqualError(t, err, ErrInvalidWorkspaceID.Error())
})
}
================================================
FILE: workspace_resources.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ WorkspaceResources = (*workspaceResources)(nil)
// WorkspaceResources describes all the workspace resources related methods that the Terraform
// Enterprise API supports.
//
// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/workspace-resources
type WorkspaceResources interface {
// List all the workspaces resources within a workspace
List(ctx context.Context, workspaceID string, options *WorkspaceResourceListOptions) (*WorkspaceResourcesList, error)
}
// workspaceResources implements WorkspaceResources.
type workspaceResources struct {
client *Client
}
// WorkspaceResourcesList represents a list of workspace resources.
type WorkspaceResourcesList struct {
*Pagination
Items []*WorkspaceResource
}
// WorkspaceResource represents a Terraform Enterprise workspace resource.
type WorkspaceResource struct {
ID string `jsonapi:"primary,resources"`
Address string `jsonapi:"attr,address"`
Name string `jsonapi:"attr,name"`
CreatedAt string `jsonapi:"attr,created-at"`
UpdatedAt string `jsonapi:"attr,updated-at"`
Module string `jsonapi:"attr,module"`
Provider string `jsonapi:"attr,provider"`
ProviderType string `jsonapi:"attr,provider-type"`
ModifiedByStateVersionID string `jsonapi:"attr,modified-by-state-version-id"`
NameIndex *string `jsonapi:"attr,name-index"`
}
// WorkspaceResourceListOptions represents the options for listing workspace resources.
type WorkspaceResourceListOptions struct {
ListOptions
}
// List all the workspaces resources within a workspace
func (s *workspaceResources) List(ctx context.Context, workspaceID string, options *WorkspaceResourceListOptions) (*WorkspaceResourcesList, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("workspaces/%s/resources", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
wl := &WorkspaceResourcesList{}
err = req.Do(ctx, wl)
if err != nil {
return nil, err
}
return wl, nil
}
func (o *WorkspaceResourceListOptions) valid() error {
return nil
}
================================================
FILE: workspace_resources_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWorkspaceResourcesList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)
wTest, wTestCleanup := createWorkspace(t, client, orgTest)
t.Cleanup(wTestCleanup)
svTest, svTestCleanup := createStateVersion(t, client, 0, wTest)
t.Cleanup(svTestCleanup)
// give HCP Terraform some time to process the statefile and extract the outputs.
waitForSVOutputs(t, client, svTest.ID)
t.Run("without list options", func(t *testing.T) {
// Retry while waiting for workspace resources to be populated.
// This can take some time after the state version is created, so we
// retry the list call until we get non-empty results.
rs, err := retryPatientlyIf(
func() (any, error) {
return client.WorkspaceResources.List(ctx, wTest.ID, nil)
},
func(rs *WorkspaceResourcesList) bool {
return len(rs.Items) == 0
},
)
require.NoError(t, err)
require.NotNil(t, rs)
require.NotNil(t, rs.Items)
require.NotEmpty(t, rs.Items)
assert.Equal(t, 1, len(rs.Items))
assert.Equal(t, 1, rs.CurrentPage)
assert.Equal(t, 1, rs.TotalCount)
assert.Equal(t, "media_bucket.aws_s3_bucket_public_access_block.this[0]", rs.Items[0].Address)
assert.Equal(t, "this", rs.Items[0].Name)
assert.Equal(t, "media_bucket", rs.Items[0].Module)
assert.Equal(t, "hashicorp/aws", rs.Items[0].Provider)
})
t.Run("with list options", func(t *testing.T) {
rs, err := client.WorkspaceResources.List(ctx, wTest.ID, &WorkspaceResourceListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, rs.Items)
assert.Equal(t, 999, rs.CurrentPage)
assert.Equal(t, 1, rs.TotalCount)
})
}
================================================
FILE: workspace_run_task.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation
var _ WorkspaceRunTasks = (*workspaceRunTasks)(nil)
// WorkspaceRunTasks represent all the run task related methods in the context of a workspace that the HCP Terraform and Terraform Enterprise API supports.
type WorkspaceRunTasks interface {
// Add a run task to a workspace
Create(ctx context.Context, workspaceID string, options WorkspaceRunTaskCreateOptions) (*WorkspaceRunTask, error)
// List all run tasks for a workspace
List(ctx context.Context, workspaceID string, options *WorkspaceRunTaskListOptions) (*WorkspaceRunTaskList, error)
// Read a workspace run task by ID
Read(ctx context.Context, workspaceID string, workspaceTaskID string) (*WorkspaceRunTask, error)
// Update a workspace run task by ID
Update(ctx context.Context, workspaceID string, workspaceTaskID string, options WorkspaceRunTaskUpdateOptions) (*WorkspaceRunTask, error)
// Delete a workspace's run task by ID
Delete(ctx context.Context, workspaceID string, workspaceTaskID string) error
}
// workspaceRunTasks implements WorkspaceRunTasks
type workspaceRunTasks struct {
client *Client
}
// WorkspaceRunTask represents a HCP Terraform or Terraform Enterprise run task that belongs to a workspace
type WorkspaceRunTask struct {
ID string `jsonapi:"primary,workspace-tasks"`
EnforcementLevel TaskEnforcementLevel `jsonapi:"attr,enforcement-level"`
// Deprecated: Use Stages property instead.
Stage Stage `jsonapi:"attr,stage"`
Stages []Stage `jsonapi:"attr,stages"`
RunTask *RunTask `jsonapi:"relation,task"`
Workspace *Workspace `jsonapi:"relation,workspace"`
}
// WorkspaceRunTaskList represents a list of workspace run tasks
type WorkspaceRunTaskList struct {
*Pagination
Items []*WorkspaceRunTask
}
// WorkspaceRunTaskListOptions represents the set of options for listing workspace run tasks
type WorkspaceRunTaskListOptions struct {
ListOptions
}
// WorkspaceRunTaskCreateOptions represents the set of options for creating a workspace run task
type WorkspaceRunTaskCreateOptions struct {
Type string `jsonapi:"primary,workspace-tasks"`
// Required: The enforcement level for a run task
EnforcementLevel TaskEnforcementLevel `jsonapi:"attr,enforcement-level"`
// Required: The run task to attach to the workspace
RunTask *RunTask `jsonapi:"relation,task"`
// Deprecated: Use Stages property instead.
Stage *Stage `jsonapi:"attr,stage,omitempty"`
// Optional: The stage to run the task in
Stages *[]Stage `jsonapi:"attr,stages,omitempty"`
}
// WorkspaceRunTaskUpdateOptions represent the set of options for updating a workspace run task.
type WorkspaceRunTaskUpdateOptions struct {
Type string `jsonapi:"primary,workspace-tasks"`
EnforcementLevel TaskEnforcementLevel `jsonapi:"attr,enforcement-level,omitempty"`
// Deprecated: Use Stages property instead.
Stage *Stage `jsonapi:"attr,stage,omitempty"`
// Optional: The stage to run the task in
Stages *[]Stage `jsonapi:"attr,stages,omitempty"`
}
// List all run tasks attached to a workspace
func (s *workspaceRunTasks) List(ctx context.Context, workspaceID string, options *WorkspaceRunTaskListOptions) (*WorkspaceRunTaskList, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
u := fmt.Sprintf("workspaces/%s/tasks", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("GET", u, options)
if err != nil {
return nil, err
}
rl := &internalWorkspaceRunTaskList{}
err = req.Do(ctx, rl)
if err != nil {
return nil, err
}
return rl.ToWorkspaceRunTaskList(), nil
}
// Read a workspace run task by ID
func (s *workspaceRunTasks) Read(ctx context.Context, workspaceID, workspaceTaskID string) (*WorkspaceRunTask, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
if !validStringID(&workspaceTaskID) {
return nil, ErrInvalidWorkspaceRunTaskID
}
u := fmt.Sprintf(
"workspaces/%s/tasks/%s",
url.PathEscape(workspaceID),
url.PathEscape(workspaceTaskID),
)
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
wr := &internalWorkspaceRunTask{}
err = req.Do(ctx, wr)
if err != nil {
return nil, err
}
return wr.ToWorkspaceRunTask(), nil
}
// Create is used to attach a run task to a workspace, or in other words: create a workspace run task. The run task must exist in the workspace's organization.
func (s *workspaceRunTasks) Create(ctx context.Context, workspaceID string, options WorkspaceRunTaskCreateOptions) (*WorkspaceRunTask, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
if err := options.valid(); err != nil {
return nil, err
}
u := fmt.Sprintf("workspaces/%s/tasks", workspaceID)
req, err := s.client.NewRequest("POST", u, &options)
if err != nil {
return nil, err
}
wr := &internalWorkspaceRunTask{}
err = req.Do(ctx, wr)
if err != nil {
return nil, err
}
return wr.ToWorkspaceRunTask(), nil
}
// Update an existing workspace run task by ID
func (s *workspaceRunTasks) Update(ctx context.Context, workspaceID, workspaceTaskID string, options WorkspaceRunTaskUpdateOptions) (*WorkspaceRunTask, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}
if !validStringID(&workspaceTaskID) {
return nil, ErrInvalidWorkspaceRunTaskID
}
u := fmt.Sprintf(
"workspaces/%s/tasks/%s",
url.PathEscape(workspaceID),
url.PathEscape(workspaceTaskID),
)
req, err := s.client.NewRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
wr := &internalWorkspaceRunTask{}
err = req.Do(ctx, wr)
if err != nil {
return nil, err
}
return wr.ToWorkspaceRunTask(), nil
}
// Delete a workspace run task by ID
func (s *workspaceRunTasks) Delete(ctx context.Context, workspaceID, workspaceTaskID string) error {
if !validStringID(&workspaceID) {
return ErrInvalidWorkspaceID
}
if !validStringID(&workspaceTaskID) {
return ErrInvalidWorkspaceRunTaskType
}
u := fmt.Sprintf(
"workspaces/%s/tasks/%s",
url.PathEscape(workspaceID),
url.PathEscape(workspaceTaskID),
)
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return err
}
return req.Do(ctx, nil)
}
func (o *WorkspaceRunTaskCreateOptions) valid() error {
if o.RunTask.ID == "" {
return ErrInvalidRunTaskID
}
return nil
}
================================================
FILE: workspace_run_task_integration_test.go
================================================
// Copyright IBM Corp. 2018, 2025
// SPDX-License-Identifier: MPL-2.0
package tfe
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWorkspaceRunTasksCreate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest)
defer runTaskTestCleanup()
wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest)
defer wkspaceTestCleanup()
t.Run("attach run task to workspace", func(t *testing.T) {
s := []Stage{PrePlan, PostPlan}
wr, err := client.WorkspaceRunTasks.Create(ctx, wkspaceTest.ID, WorkspaceRunTaskCreateOptions{
EnforcementLevel: Mandatory,
Stages: &s,
RunTask: runTaskTest,
})
require.NoError(t, err)
defer func() {
err = client.WorkspaceRunTasks.Delete(ctx, wkspaceTest.ID, wr.ID)
require.NoError(t, err)
}()
assert.NotEmpty(t, wr.ID)
assert.Equal(t, Mandatory, wr.EnforcementLevel)
assert.Equal(t, s[0], wr.Stage)
assert.Equal(t, s, wr.Stages)
t.Run("ensure run task is deserialized properly", func(t *testing.T) {
assert.NotNil(t, wr.RunTask)
assert.NotEmpty(t, wr.RunTask.ID)
})
})
}
func TestWorkspaceRunTasksCreateDeprecated(t *testing.T) {
t.Parallel()
// This test uses the deprecate `stage` attribute
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest)
defer runTaskTestCleanup()
wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest)
defer wkspaceTestCleanup()
t.Run("attach run task to workspace", func(t *testing.T) {
s := PrePlan
wr, err := client.WorkspaceRunTasks.Create(ctx, wkspaceTest.ID, WorkspaceRunTaskCreateOptions{
EnforcementLevel: Mandatory,
Stage: &s,
RunTask: runTaskTest,
})
require.NoError(t, err)
defer func() {
err = client.WorkspaceRunTasks.Delete(ctx, wkspaceTest.ID, wr.ID)
require.NoError(t, err)
}()
assert.NotEmpty(t, wr.ID)
assert.Equal(t, wr.EnforcementLevel, Mandatory)
assert.Equal(t, wr.Stage, s)
t.Run("ensure run task is deserialized properly", func(t *testing.T) {
assert.NotNil(t, wr.RunTask)
assert.NotEmpty(t, wr.RunTask.ID)
})
})
}
func TestWorkspaceRunTasksList(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest)
defer wkspaceTestCleanup()
runTaskTest1, runTaskTest1Cleanup := createRunTask(t, client, orgTest)
defer runTaskTest1Cleanup()
runTaskTest2, runTaskTest2Cleanup := createRunTask(t, client, orgTest)
defer runTaskTest2Cleanup()
_, wrTaskTest1Cleanup := createWorkspaceRunTask(t, client, wkspaceTest, runTaskTest1)
defer wrTaskTest1Cleanup()
_, wrTaskTest2Cleanup := createWorkspaceRunTask(t, client, wkspaceTest, runTaskTest2)
defer wrTaskTest2Cleanup()
t.Run("with no params", func(t *testing.T) {
wrTaskList, err := client.WorkspaceRunTasks.List(ctx, wkspaceTest.ID, nil)
require.NoError(t, err)
assert.NotNil(t, wrTaskList.Items)
assert.Equal(t, len(wrTaskList.Items), 2)
assert.NotEmpty(t, wrTaskList.Items[0].ID)
assert.NotEmpty(t, wrTaskList.Items[0].EnforcementLevel)
})
}
func TestWorkspaceRunTasksRead(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest)
defer wkspaceTestCleanup()
runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest)
defer runTaskTestCleanup()
wrTaskTest, wrTaskTestCleanup := createWorkspaceRunTask(t, client, wkspaceTest, runTaskTest)
defer wrTaskTestCleanup()
t.Run("by ID", func(t *testing.T) {
wr, err := client.WorkspaceRunTasks.Read(ctx, wkspaceTest.ID, wrTaskTest.ID)
require.NoError(t, err)
assert.Equal(t, wrTaskTest.ID, wr.ID)
assert.Equal(t, wrTaskTest.EnforcementLevel, wr.EnforcementLevel)
t.Run("ensure run task is deserialized", func(t *testing.T) {
assert.Equal(t, wr.RunTask.ID, runTaskTest.ID)
})
t.Run("ensure workspace is deserialized", func(t *testing.T) {
assert.Equal(t, wr.Workspace.ID, wkspaceTest.ID)
})
})
}
func TestWorkspaceRunTasksUpdate(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest)
defer wkspaceTestCleanup()
runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest)
defer runTaskTestCleanup()
wrTaskTest, wrTaskTestCleanup := createWorkspaceRunTask(t, client, wkspaceTest, runTaskTest)
defer wrTaskTestCleanup()
t.Run("update task", func(t *testing.T) {
stages := []Stage{PrePlan, PostPlan}
wr, err := client.WorkspaceRunTasks.Update(ctx, wkspaceTest.ID, wrTaskTest.ID, WorkspaceRunTaskUpdateOptions{
EnforcementLevel: Mandatory,
Stages: &stages,
})
require.NoError(t, err)
wr, err = client.WorkspaceRunTasks.Read(ctx, wkspaceTest.ID, wr.ID)
require.NoError(t, err)
assert.Equal(t, Mandatory, wr.EnforcementLevel)
assert.Equal(t, stages, wr.Stages)
assert.Equal(t, PrePlan, wr.Stage)
})
}
func TestWorkspaceRunTasksUpdateDeprecated(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest)
defer wkspaceTestCleanup()
runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest)
defer runTaskTestCleanup()
wrTaskTest, wrTaskTestCleanup := createWorkspaceRunTask(t, client, wkspaceTest, runTaskTest)
defer wrTaskTestCleanup()
t.Run("update task", func(t *testing.T) {
stage := PrePlan
wr, err := client.WorkspaceRunTasks.Update(ctx, wkspaceTest.ID, wrTaskTest.ID, WorkspaceRunTaskUpdateOptions{
EnforcementLevel: Mandatory,
Stage: &stage,
})
require.NoError(t, err)
wr, err = client.WorkspaceRunTasks.Read(ctx, wkspaceTest.ID, wr.ID)
require.NoError(t, err)
assert.Equal(t, wr.EnforcementLevel, Mandatory)
assert.Equal(t, wr.Stage, PrePlan)
})
}
func TestWorkspaceRunTasksDelete(t *testing.T) {
t.Parallel()
client := testClient(t)
ctx := context.Background()
orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
upgradeOrganizationSubscription(t, client, orgTest)
wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest)
defer wkspaceTestCleanup()
runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest)
defer runTaskTestCleanup()
wrTaskTest, _ := createWorkspaceRunTask(t, client, wkspaceTest, runTaskTest)
t.Run("with valid options", func(t *testing.T) {
err := client.WorkspaceRunTasks.Delete(ctx, wkspaceTest.ID, wrTaskTest.ID)
require.NoError(t, err)
_, err = client.WorkspaceRunTasks.Read(ctx, wkspaceTest.ID, wrTaskTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the workspace run task does not exist", func(t *testing.T) {
err := client.WorkspaceRunTasks.Delete(ctx, wkspaceTest.ID, wrTaskTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})
t.Run("when the workspace does not exist", func(t *testing.T) {
err := client.WorkspaceRunTasks.Delete(ctx, "does-not-exist", wrTaskTest.ID)
assert.EqualError(t, err, ErrResourceNotFound.Error())
})
}