├── .copywrite.hcl ├── .github ├── CODEOWNERS ├── actions │ ├── test-copy-state │ │ └── action.yml │ ├── test-copy-workspaces │ │ └── action.yml │ └── test-list │ │ └── action.yml ├── dependabot.yml ├── terraform │ ├── tfe │ │ ├── agents.tf │ │ ├── main.tf │ │ ├── oauth.tf │ │ ├── sshkeys.tf │ │ ├── teams.tf │ │ ├── terraform.tf │ │ ├── variables.tf │ │ └── variablesets.tf │ └── workspace │ │ └── main.tf └── workflows │ ├── README.md │ ├── build.yml │ ├── docs-deploy.yml │ ├── end-to-end-test.yml │ ├── jira-issues.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yaml ├── ADR ├── 0001-initial-adr.md ├── 0002-deprecated-ce-to-tfc.md ├── 0003-ce-to-tfc.md ├── 0004-ce-migration-cli-driven-workspaces copy.md ├── 0005-ce-migration-variables.md ├── 0006-gitlab-vcs.md ├── ADR-template.md ├── ce-to-tfc.md └── index.md ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── ce-migration.md ├── cmd ├── copy │ ├── copy.go │ ├── projects.go │ ├── teams.go │ ├── validation.go │ ├── variablesets.go │ ├── workspaces-agents.go │ ├── workspaces-run-triggers.go │ ├── workspaces-ssh.go │ ├── workspaces-state-sharing.go │ ├── workspaces-states.go │ ├── workspaces-teamaccess.go │ ├── workspaces-vars.go │ ├── workspaces-vcs.go │ └── workspaces.go ├── core │ ├── cleanup.go │ ├── clone.go │ ├── core.go │ ├── createworkspaces.go │ ├── getstate.go │ ├── init-repos.go │ ├── linkvcs.go │ ├── migrate.go │ ├── removebackend.go │ └── uploadstates.go ├── delete │ ├── delete.go │ ├── validation.go │ ├── workspace-vcs.go │ └── workspace.go ├── generate │ ├── config.go │ └── generate.go ├── helper │ ├── helper_logs.go │ ├── helper_time.go │ └── helper_viper.go ├── list │ ├── list.go │ ├── orgs.go │ ├── projects.go │ ├── ssh-keys.go │ ├── teams.go │ ├── vcs-gha.go │ ├── vcs.go │ ├── workspace-filter.go │ └── workspaces.go ├── lock │ ├── lock.go │ └── workspace.go ├── nuke │ ├── nuke.go │ └── workspaces.go ├── root.go └── unlock │ ├── unlock.go │ └── workspace.go ├── go.mod ├── go.sum ├── main.go ├── output └── writer.go ├── site ├── README.md ├── docs │ ├── about │ │ ├── contributing.md │ │ └── purpose.md │ ├── code │ │ ├── future.md │ │ ├── mvp.md │ │ └── project-details.md │ ├── commands │ │ ├── copy.md │ │ ├── copy_projects.md │ │ ├── copy_teams.md │ │ ├── copy_varsets.md │ │ ├── copy_workspace_agents.md │ │ ├── copy_workspace_remote_state_sharing.md │ │ ├── copy_workspace_run_triggers.md │ │ ├── copy_workspace_state.md │ │ ├── copy_workspace_teamaccess.md │ │ ├── copy_workspace_variables.md │ │ ├── copy_workspace_vcs.md │ │ ├── copy_workspaces.md │ │ ├── core.md │ │ ├── core_cleanup.md │ │ ├── core_clone.md │ │ ├── core_create-workspaces.md │ │ ├── core_getstate.md │ │ ├── core_init-repos.md │ │ ├── core_link-vcs.md │ │ ├── core_migrate.md │ │ ├── core_remove-backend.md │ │ ├── core_upload-state.md │ │ ├── delete.md │ │ ├── delete_workspace.md │ │ ├── generate_config.md │ │ ├── list.md │ │ ├── list_orgs.md │ │ ├── list_projects.md │ │ ├── list_ssh.md │ │ ├── list_teams.md │ │ ├── list_vcs.md │ │ └── list_workspaces.md │ ├── configuration_file │ │ └── config_file.md │ ├── faqs.md │ ├── feedback.md │ ├── images │ │ ├── TFE-workspaces.png │ │ ├── TFM-black.png │ │ ├── TFM-white.png │ │ ├── copy_projects.png │ │ ├── copy_teams.png │ │ ├── copy_varsets.png │ │ ├── copy_ws.png │ │ ├── copy_ws_exists.png │ │ ├── copy_ws_remote_state_sharing.png │ │ ├── copy_ws_remote_state_sharing_org_consolidation.png │ │ ├── copy_ws_run_triggers.png │ │ ├── copy_ws_ssh.png │ │ ├── copy_ws_state.png │ │ ├── copy_ws_state_last_x.png │ │ ├── copy_ws_teamaccess.png │ │ ├── copy_ws_vars.png │ │ ├── copy_ws_vcs.png │ │ ├── delete_workspace_id_dst.png │ │ ├── delete_workspace_name_autoapprove.png │ │ ├── delete_workspace_name_dst.png │ │ ├── list_organization_dst.png │ │ ├── list_organization_src.png │ │ ├── list_projects_dst.png │ │ ├── list_projects_json.png │ │ ├── list_projects_src.png │ │ ├── list_ssh_dst.png │ │ ├── list_ssh_src.png │ │ ├── list_teams_dst.png │ │ ├── list_teams_src.png │ │ ├── list_vcs_dst.png │ │ ├── list_vcs_src.png │ │ ├── list_workspaces_dst.png │ │ ├── list_workspaces_dst1.png │ │ ├── list_workspaces_json.png │ │ ├── list_workspaces_src.png │ │ ├── list_workspaces_src1.png │ │ ├── migration-journey.png │ │ ├── tfe_tfm_tfc.png │ │ ├── tfm-history-1.png │ │ ├── tfm-history-2.png │ │ ├── tfm_copy_ws_confirm.png │ │ └── tfm_copy_ws_confirm_autoapprove.png │ ├── index.md │ └── migration │ │ ├── case-studies.md │ │ ├── example-scenario-ce-tfc.md │ │ ├── example-scenario-tfe-tfc.md │ │ ├── journey-tfe-tfc.md │ │ ├── pre-migration-ce-tfc.md │ │ ├── pre-migration-tfe-tfc.md │ │ ├── pre-requisites-ce-tfc.md │ │ ├── pre-requisites-tfe-tfc.md │ │ └── supported-vcs.md └── mkdocs.yml ├── test ├── README.md ├── cleanup │ ├── e2e-nuke.sh │ └── nuke.sh ├── configs │ ├── .e2e-all-projects-test.hcl │ ├── .e2e-all-workspaces-test.hcl │ ├── .e2e-ce-to-tfc-test.hcl │ ├── .e2e-project-list-test.hcl │ ├── .e2e-project-map-test.hcl │ ├── .e2e-workspace-map-test.hcl │ ├── .e2e-workspaces-list-destination-agent-test.hcl │ ├── .e2e-workspaces-list-test.hcl │ ├── .state-test-tfm.hcl │ ├── .unit-test-tfm.hcl │ └── build-configs.sh ├── state │ └── create_states.sh └── terraform │ ├── README.md │ ├── main.tf │ ├── outputs.tf │ ├── outputs.txt │ ├── sample-resources │ └── main.tf │ └── variables.tf ├── tfclient ├── destination-only.go └── tfclient.go ├── tfe-migration.md ├── vcsclients ├── github.go └── gitlab.go └── version └── version.go /.copywrite.hcl: -------------------------------------------------------------------------------- 1 | schema_version = 1 2 | 3 | project { 4 | license = "MPL-2.0" 5 | copyright_year = 2023 6 | 7 | # (OPTIONAL) A list of globs that should not have copyright/license headers. 8 | # Supports doublestar glob patterns for more flexibility in defining which 9 | # files or folders should be ignored 10 | header_ignore = [ 11 | # "vendors/**", 12 | # "**autogen**", 13 | "test/**", 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Code Owners for TFM 2 | 3 | * @jeffmccollum 4 | -------------------------------------------------------------------------------- /.github/actions/test-copy-state/action.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 -------------------------------------------------------------------------------- /.github/actions/test-copy-workspaces/action.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 -------------------------------------------------------------------------------- /.github/actions/test-list/action.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | -------------------------------------------------------------------------------- /.github/terraform/tfe/agents.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | resource "tfe_agent_pool" "source" { 5 | provider = tfe.source 6 | 7 | name = "tfm-ci-testing-src" 8 | } 9 | 10 | resource "tfe_agent_pool" "destination" { 11 | provider = tfe.destination 12 | 13 | name = "tfm-ci-testing-dest" 14 | } -------------------------------------------------------------------------------- /.github/terraform/tfe/oauth.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | resource "tfe_oauth_client" "source" { 5 | provider = tfe.source 6 | 7 | name = "github-hashicorp-services-ci" 8 | api_url = "https://api.github.com" 9 | http_url = "https://github.com" 10 | oauth_token = var.gh_token 11 | service_provider = "github" 12 | } 13 | 14 | resource "tfe_oauth_client" "destination" { 15 | provider = tfe.destination 16 | 17 | name = "github-hashicorp-services-ci" 18 | api_url = "https://api.github.com" 19 | http_url = "https://github.com" 20 | oauth_token = var.gh_token 21 | service_provider = "github" 22 | } -------------------------------------------------------------------------------- /.github/terraform/tfe/sshkeys.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | resource "tfe_ssh_key" "source" { 5 | provider = tfe.source 6 | 7 | name = "tfm-ci-testing-src" 8 | key = < v2" 34 | args: build --clean --skip validate 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | - uses: actions/upload-artifact@v4 38 | with: 39 | name: tfm-artifacts 40 | path: ./dist/* 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/docs-deploy.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Deploy Docs site 3 | 4 | # Controls when the workflow will run 5 | on: 6 | # Triggers the workflow on push or pull request events but only for the "main" branch 7 | push: 8 | branches: 9 | - "main" 10 | paths: 11 | - site/** 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v4 27 | 28 | - name: Copy Release Notes for docs site 29 | uses: canastro/copy-action@master 30 | with: 31 | source: "CHANGELOG.md" 32 | target: "site/docs/about/release_notes.md" 33 | - name: Copy Architectual Decision Records for docs site 34 | uses: canastro/copy-action@master 35 | with: 36 | source: "ADR/0001-initial-adr.md" 37 | target: "site/docs/code/adr.md" 38 | # - name: test copy 39 | # run: ls -l site/docs/about 40 | - name: Deploy MkDocs 41 | # You may pin to the exact commit or the version. 42 | uses: mhausenblas/mkdocs-deploy-gh-pages@1.26 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | CONFIG_FILE: site/mkdocs.yml 46 | EXTRA_PACKAGES: build-base 47 | -------------------------------------------------------------------------------- /.github/workflows/jira-issues.yml: -------------------------------------------------------------------------------- 1 | on: 2 | issues: 3 | types: [opened] 4 | 5 | name: Open Jira Issue 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | name: Jira Issue Opener 11 | steps: 12 | - name: Login 13 | uses: atlassian/gajira-login@v3 14 | env: 15 | JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} 16 | JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} 17 | JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} 18 | 19 | - name: Create 20 | id: create 21 | uses: atlassian/gajira-create@v3 22 | with: 23 | project: ASESP 24 | issuetype: Bug 25 | summary: '${{ github.event.issue.title }}' 26 | description: ${{ github.event.issue.body }} \\ \\ _Created from GitHub Action_ for ${{ github.event.issue.html_url }} 27 | 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version-file: 'go.mod' 23 | cache: true 24 | - name: Run GoReleaser 25 | uses: goreleaser/goreleaser-action@v6 26 | with: 27 | distribution: goreleaser 28 | version: "~> v2" 29 | args: release --clean 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | tfm 3 | .tfm.hcl 4 | terraform_config_metadata.json 5 | 6 | # Local .terraform directories 7 | **/.terraform/* 8 | 9 | # Ignore Dependency Lock File that is generated by the command: terraform init 10 | .terraform.lock.hcl 11 | 12 | # API Payload 13 | **/test/cleanup/create_state_payload.json 14 | 15 | # Vscode 16 | .vscode 17 | 18 | # Goreleaser build dir 19 | dist/ 20 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | before: 4 | hooks: 5 | - go mod tidy 6 | 7 | builds: 8 | - env: 9 | - CGO_ENABLED=0 10 | goos: 11 | - linux 12 | - windows 13 | - darwin 14 | goarch: 15 | - amd64 16 | - arm64 17 | ldflags: 18 | - -s -w -X github.com/hashicorp-services/tfm/version.Version={{ .Version }} 19 | - -s -w -X github.com/hashicorp-services/tfm/version.Build={{ .FullCommit }} 20 | - -s -w -X github.com/hashicorp-services/tfm/version.Date={{ .Date }} 21 | - -s -w -X github.com/hashicorp-services/tfm/version.BuiltBy=goreleaser 22 | 23 | archives: 24 | - format: binary 25 | name_template: >- 26 | {{ .ProjectName }}_{{- tolower .Os }}_{{- if eq .Arch "amd64" }}x86_64{{- else }}{{ .Arch }}{{ end }} 27 | changelog: 28 | sort: asc 29 | filters: 30 | exclude: 31 | - "^docs:" 32 | - "^test:" 33 | - "^site:" -------------------------------------------------------------------------------- /ADR/0001-initial-adr.md: -------------------------------------------------------------------------------- 1 | # ADR #0001: Initial Architectural Decision Records 2 | 3 | | Architectural Decision | Date | Requestor | Approver | Notes | 4 | |---|---|---|---|---| 5 | | Use Go to provide cross compiled binaries | Oct 23 , 2022 | Team | Team | Alternatives were Python scripts OR Bash scripts which could require further dependencies for customer to use | 6 | | Use Cobra for CLI | Oct 23 , 2022 | Team | Team | doormat-cli and tfx use it, it's good enough for us! | 7 | | Use Viper to handle config | Oct 23 , 2022 | Team | Team | | 8 | | Users should be able to provide configuration via Environment variables OR config file | Oct 23 , 2022 | Team | Team | | 9 | | Users should be able to re-run the same command to ensure any changes from source is copied/updates in the destination | Oct 23 , 2022 | Team | Team | | 10 | | Any objects/resources in the destination that already exist, will be skipped. | Oct 23, 2022 | Team | Team | eg If a workspace exists, it will not try to recreate it in the destination | 11 | | Workspace settings will always be updated in the destination from the source. This allow the tool be re-ran if any changes in the source occurs. | Nov, 2022 | Team | Team | | 12 | | | | | | | -------------------------------------------------------------------------------- /ADR/0003-ce-to-tfc.md: -------------------------------------------------------------------------------- 1 | # ADR #0003: Feature - Terraform Community Edition to TFC Migration 2 | 3 | Date: 2024-02-26 4 | 5 | ## Responsible Architect 6 | 7 | Joshua Tracy 8 | 9 | ## Author 10 | 11 | Joshua Tracy 12 | 13 | ## Contributors 14 | 15 | * Joshua Tracy 16 | * Jeff McCollum 17 | * Alex Basista 18 | 19 | ## Lifecycle 20 | 21 | Proof of Concept 22 | 23 | ## Status 24 | 25 | Proposed 26 | 27 | ## Context 28 | 29 | Users need a tool to assist in the migration from terraform community edition managed configurations to Terraform Cloud workspace managed configurations. 30 | 31 | ### Migration Workflow 32 | 33 | Today migration can be accomplished following the following steps: 34 | 35 | - Get the state file 36 | - Create a TFC Workspace 37 | - Push the state file to the TFC workspace 38 | - Link the code to the workspace containing its code somehow 39 | 40 | The above are the 4 high-level steps required for a migration to happen. Each step can be broken down into more steps and complexity can increase quickly based on the terraform configuration setup. 41 | 42 | ### TFM Capabilities 43 | 44 | TFM must be able to do the following: 45 | 46 | - Automate speedup the process of getting the state file. 47 | - Determine the landscape of each terraform configuration. 48 | - Automate and speedup the process of creating TFC workspace. 49 | - Automate and speedup the process of uploading state files to worksapces. 50 | - Automate and speedup tieing together the configuration code with the workspaces containing the respective state. 51 | - Automate and speedup the post migration process of modifying code. 52 | 53 | ### What Kind of Terraform Configurations Exist In the Wild? 54 | 55 | User typically have the following configurations: 56 | 57 | - A single terraform configuration with a configured backend stored in the root of a VCS repository. 58 | - Multiple terraform configurations each with a configured backend stored in multiple directory paths within a VCS repository. 59 | - A single terraform configuration using multiple terraform workspaces with a single backend configuration. 60 | - Multiple terraform configurations each terraform ce workspaces and a single backend configured for each configuration. 61 | 62 | ## Decision 63 | 64 | - A function to assist users in retreiving the terraform configurations will be implemented in the form of `tfm core clone` to allow users to download VCS configurations. This will allow the process of getting the state file to be automated at scale. 65 | - A function to build a metadata file that contains the information about how each cloned VCS repository is configured will be created. The metadata file will contain information about each path within the VCS repository that contains terraform code. 66 | - A function to determine if a VCS repository contains multiple configurations will be created. 67 | - A function to determine if a VCS repository contains configurations using terraform ce workspaces. 68 | - A function will be created to assist users in downloading the state file for every identified terraform configuration. 69 | - A function will be created to assist users in creating TFC workspaces. 70 | - A function will be created to assist users in uploading state files to the created workspaces. 71 | - A function will be created to assist in connecting the VCS repositories to the TFC workspaces. 72 | - A function will be created to assist users in removing the old backend configuration if desired. 73 | - The function to assist users in removing backend configurations will be optional. 74 | - The function to assist users in removing the backend will create a branch and push it to the VCS. 75 | - The function to assist users in removing the backend will NOT create a PR. 76 | - A function to help users cleanup the working environment will be created. 77 | - The tfm command that creates workspaces will create them with an invisible tfm tag to allow `tfm nuke workspaces` to delete all of the workspaces created by tfm. 78 | 79 | ## Consequences 80 | 81 | - Users will be able to quickly migrate multiple terraform configurations within minutes VS days. 82 | - Users will be able to quickly remove unwanted backend code from their configurations post migration. 83 | - Users will not have the ability to choose their workspace names. 84 | - No consideration for CLI driven or API driven workspaces. 85 | - No consideration for configurations using terraform variable files. 86 | - Only GitHub VCS will be supported initially. -------------------------------------------------------------------------------- /ADR/0004-ce-migration-cli-driven-workspaces copy.md: -------------------------------------------------------------------------------- 1 | # ADR #0004: How to Handle CE/OSS to TFC Migration for CLI Driven Workspaces 2 | 3 | Date: 2024-02-29 4 | 5 | ## Responsible Architect 6 | Joshua Tracy 7 | 8 | ## Author 9 | 10 | Joshua Tracy 11 | 12 | ## Contributors 13 | 14 | * Joshua Tracy 15 | * Jeff McCollum 16 | * Alex Basista 17 | 18 | ## Lifecycle 19 | 20 | Pilot 21 | 22 | ## Status 23 | 24 | Accepted 25 | 26 | ## Context 27 | 28 | Not all users that migrate from Terraform communtiy edition managed configurations to Terraform Cloud managed worksapces want to use VCS driven workspaces. In situations where users want to migrate to CLI driven workspaces we should assist those users in configuring a `cloud {}` block automatically. This will help drive migration at scale. 29 | 30 | CLI Driven workspaces require the following configuration at a minimum: 31 | 32 | ``` 33 | terraform { 34 | cloud { 35 | organization = "org-name" 36 | 37 | workspaces { 38 | name = "workspace-name" 39 | } 40 | } 41 | } 42 | ``` 43 | 44 | ## Decision 45 | 46 | - A command will be implemented to assit users in adding the `cloud {}` configuration to terraform code. 47 | - The `cloud {}` block will be automagically populated with the organization name taken from the `.tfm` config file setting `dst_tfc_org` and workspace name will be taken from the workspace name constructed from the metadata file created using `tfm core init-repos`. 48 | - Any instances of `backend {}` will be commented out in favor of the `cloud{}` block. 49 | - A VCS branch will be created, the change commmited, and pushed, but no PR will be created. 50 | 51 | ## Consequences 52 | 53 | Users will be able configure terraform configurations for use with TFC/TFE CLI Driven workspaces at scale. -------------------------------------------------------------------------------- /ADR/0005-ce-migration-variables.md: -------------------------------------------------------------------------------- 1 | # ADR #0006: How to Handle CE/OSS to TFC Migration for Variables 2 | 3 | Date: 2024-02-29 4 | 5 | ## Responsible Architect 6 | Joshua Tracy 7 | 8 | ## Author 9 | 10 | Joshua Tracy 11 | 12 | ## Contributors 13 | 14 | * Joshua Tracy 15 | * Jeff McCollum 16 | * Alex Basista 17 | 18 | ## Lifecycle 19 | 20 | Pilot 21 | 22 | ## Status 23 | 24 | Accepted 25 | 26 | ## Context 27 | 28 | Part of migrating from Terraform Community Edition to Terraform Cloud or Enterprise is also migrating terraform inputs / variables in some fashion. A feature to assist users in making existing `.tfvars` files compatible with TFC/TFE runs or creating variables to be managed into TFC/TFE is required. 29 | 30 | Per [This Link](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/variables) Although Terraform Cloud uses variables from terraform.tfvars, Terraform Enterprise currently ignores this file. A feature to locate all `*.tfvars` files in a given `config_path` could be created. The feature could rename the file to `terraform.auto.tfvars` for consistency across the board between TFC and TFE. 31 | 32 | An additional feature in the form of a command or flad to an existing command could be used to allow users to specify the path to an existing vars file and the go-tfe sdk could be used to create those variables in the TFC/TFE workspace. 33 | 34 | ## Decision 35 | 36 | - A command will be created for converting all `*.tfvars` files to `terraform.auto.tfvars` files. 37 | - The `tfm core init-repos` commmand will be updated to identify and add the `tfvars` files in each `config_path`. 38 | - A flag simliar to `-variables-path /path/to/terraform.tfvars` will be created to convert variables to workspaces managed variables. 39 | 40 | ## Consequences 41 | 42 | Users will be able to modify or create terraform variables for TFC/TFE support at scale. -------------------------------------------------------------------------------- /ADR/0006-gitlab-vcs.md: -------------------------------------------------------------------------------- 1 | # ADR #0000: Title, a short present tense phrase 2 | 3 | Date: YYYY-MM-DD 4 | 5 | ## Responsible Architect 6 | The Architect most closely aligned with this decision. 7 | 8 | ## Author 9 | 10 | The person who wrote this document. 11 | 12 | ## Contributors 13 | 14 | * List the names of the contributors here 15 | * Name 1 16 | * Name 2 17 | * etc 18 | 19 | ## Lifecycle 20 | 21 | POC (Proof of Concept), Pilot, Beta, GA (General Availability), Sunset 22 | 23 | ## Status 24 | 25 | Status of the decision made. A decision may be "Proposed" if the project stakeholders haven't agreed with it yet, or "Accepted" once it is agreed. If a later ADR changes or reverses a decision, it may be marked as "Deprecated" or "Superseded" with a reference to its replacement. Once the decision has been implemented, it should be marked as "Implemented". 26 | 27 | ## Context 28 | 29 | TFM only supports the GitHub VCS as of release 0.8.0. There are multiple VCS providers that are compatible with Terraform Cloud and Enterprise that customers use to store Terraform Community Edition code. TFM needs to support all of the same VCS providers for the `tfm core clone` and `tfm core remove-backend` commands along with any future commands that interact with a VCS provider. We should only aim to support VCS providers that TFC/TFE suport [documented here](https://developer.hashicorp.com/terraform/cloud-docs/vcs#supported-vcs-providers). 30 | 31 | An ADR should be created for each VCS provider as they require different client auth methods and some have unique paths for storing VCS respoitories. 32 | 33 | ### Go-Gitlab 34 | 35 | [A go-gitlab](https://pkg.go.dev/github.com/xanzy/go-gitlab) package exists to assist in developing this feature. 36 | 37 | ### Client Context 38 | 39 | A new file `vcsclients/gitlab.go` should create the gitlab client context for use with tfm functions that interact with GitLab. 40 | 41 | Need create a struct similiar to the following: 42 | 43 | > [!IMPORTANT] 44 | > Gitlab uses groups instead of organizations 45 | 46 | ```go 47 | type ClientContext struct { 48 | GitLabClient *gitlab.Client 49 | GitLabContext context.Context 50 | GitLabToken string 51 | GitLabGroup string 52 | GitLabUsername string 53 | } 54 | ``` 55 | 56 | ### TFM Config File 57 | 58 | The following new configurations will be added and required for use with GitLab: 59 | 60 | - gitlab_token 61 | - gitlab_group 62 | - gitlab_username 63 | 64 | ### Testing 65 | 66 | GitLab offers a free tier that can be used for nightly/weekly testing with GitLab. A service account can be created for implementing a nightly/weekly test for interaction with gitlab. 67 | 68 | ### Changes to Existing Code 69 | 70 | - `tfm core clone` must be modified to clone repos based on VCS. 71 | - We can add an additional config file option `vcs_type` and use that as an input for the clone function. 72 | - The configuration file option `clone_repos_path` needs to be modified to `clone_repo_path`. 73 | - Many functions enforce the requirements of `github_username` `github_token` `github_organization`. Functions should me refactored to be VCS agnostic or, if required, a seprate function should be created for each VCS. 74 | 75 | ## Decision 76 | 77 | - A new `gitlab.go` will be created as part of the `vcsclients` package and will build a context for client creation and use with Viper. 78 | - Additional config file items will be added for GitLab support. 79 | - `clone_repos_path` will be modified to be VCS agnostic. 80 | - Existing functions will be made VCS agnostic. 81 | - A new config file option `vcs_type` will be added to the config file. 82 | 83 | ## Consequences 84 | 85 | - Users will have the ability to use GitLab with the `tfm core clone` and `tfm core remove-backend` commands. 86 | - TFM will be made VCS agnostic for future implementation of supported VCS providers. 87 | -------------------------------------------------------------------------------- /ADR/ADR-template.md: -------------------------------------------------------------------------------- 1 | # ADR #0000: Title, a short present tense phrase 2 | 3 | Date: YYYY-MM-DD 4 | 5 | ## Responsible Architect 6 | The Architect most closely aligned with this decision. 7 | 8 | ## Author 9 | 10 | The person who wrote this document. 11 | 12 | ## Contributors 13 | 14 | * List the names of the contributors here 15 | * Name 1 16 | * Name 2 17 | * etc 18 | 19 | ## Lifecycle 20 | 21 | POC (Proof of Concept), Pilot, Beta, GA (General Availability), Sunset 22 | 23 | ## Status 24 | 25 | Status of the decision made. A decision may be "Proposed" if the project stakeholders haven't agreed with it yet, or "Accepted" once it is agreed. If a later ADR changes or reverses a decision, it may be marked as "Deprecated" or "Superseded" with a reference to its replacement. Once the decision has been implemented, it should be marked as "Implemented". 26 | 27 | ## Context 28 | 29 | This section describes the forces at play, including technological, political, social, and project local. These forces are probably in tension, and should be called out as such. The language in this section is value-neutral. It is simply describing facts. 30 | 31 | ## Decision 32 | 33 | This section describes our response to these forces and what we are actually proposing on doing. It is stated in full sentences, with active voice. "We will ..." 34 | 35 | ## Consequences 36 | 37 | What becomes easier or more difficult because of the decision made. This section describes the resulting context, after applying the decision. All consequences should be listed here, not just the "positive" ones. A particular decision may have positive, negative, and neutral consequences, but all of them affect the team and project in the future. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Build locally, will make ./tfm available to run 3 | # Adds in some build flags to identify the binary in the future 4 | build-local: 5 | go build -v \ 6 | -ldflags="\ 7 | -X 'github.com/hashicorp-services/tfm/version.Version=x.x.x' \ 8 | -X 'github.com/hashicorp-services/tfm/version.Prerelease=alpha' \ 9 | -X 'github.com/hashicorp-services/tfm/version.Build=local' \ 10 | -X 'github.com/hashicorp-services/tfm/version.BuiltBy=$(shell whoami)' \ 11 | -X 'github.com/hashicorp-services/tfm/version.Date=$(shell date)'" 12 | 13 | # Updated go packages (will touch go.mod and go.sum) 14 | update: 15 | go get -u 16 | go mod tidy 17 | 18 | format: 19 | go fmt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tfm 2 | 3 | ![TFM](site/docs/images/TFM-black.png) 4 | 5 | HashiCorp Implementation Services (IS) has identified a need to develop a purpose built tool to assist our engagements and customers during: 6 | 7 | - Terraform open source / community edition / core to Terraform Cloud (TFC) & Terraform Enterprise (TFE) 8 | - TFE to TFC 9 | - TFC to TFE 10 | - 1 TFC Organization to another TFC Organization 11 | - TFE Server / Organization Consolidation 12 | 13 | > [!Warning] 14 | > This CLI does not have official support, but the code owners will work with partners and interested parties to provide assitance when possible. 15 | > Check out our [case studies](https://hashicorp-services.github.io/tfm/migration/case-studies/). 16 | 17 | ## Overview 18 | 19 | This tool has been develop to assist HashiCorp Implementation Services, Partners and Customers with their migrations to HashiCorp services. Having a tool allows us the ability to offer a standardized offering to our customers. 20 | 21 | Check out the full documentation at [https://hashicorp-services.github.io/tfm/](https://hashicorp-services.github.io/tfm/) 22 | 23 | Note: The Terraform Community Edition migration as part of `tfm` has been deprecated in favor of [tf-migrate](https://developer.hashicorp.com/terraform/cloud-docs/migrate/tf-migrate). The CE migration feature has not been removed from `tfm` however it will not be receiving further developments. 24 | 25 | ## Installation 26 | 27 | Binaries are created as part of a release, check out the [Release Page](https://github.com/hashicorp-services/tfm/releases) for the latest version. 28 | 29 | ## Migration Type 30 | 31 | There are differences between Terraform OSS / CE / Core migrations and Terraform Enterprise & Terraform Cloud Migrations. Accordingly this Readme has been split between two pages [tfe migration](./tfe-migration.md) and [ce migration](./ce-migration.md) 32 | 33 | ## tfm in a Pipeline 34 | 35 | `tfm` can be used in a pipeline to automate migrations. There are a few considerations when using `tfm` in this manner. 36 | 37 | - For source and destination credentials use environment variables from the pipeline's secrete manager or other secure means. Don't embed these credentials in `tfm` configuration files 38 | - Several `tfm` commands require confirmation before proceeding, which are listed below. To override these in a pipeline, add the `--autoapprove` flag. 39 | - `copy workspaces` Only when all workspaces are going to be migrated due to no workspace list, or map defined. 40 | - `copy workspaces --state --last` 41 | - `delete workspace` 42 | - `delete workspaces-vcs` 43 | 44 | ## Architectural Decisions Record (ADR) 45 | 46 | An architecture decision record (ADR) is a document that captures an important architecture decision made along with its context and consequences. 47 | 48 | This project will store ADRs in [docs/ADR](docs/ADR/) as a historical record. 49 | 50 | More information about [ADRs](docs/ADR/index.md). 51 | 52 | ## To build 53 | 54 | ```bash 55 | make build-local 56 | ./tfm -v 57 | ``` 58 | 59 | -or- 60 | 61 | ```bash 62 | go run . -v 63 | ``` 64 | 65 | ## To release 66 | 67 | To create a new release of TFM 68 | 69 | - Locally Create a new Tag 70 | - Push tag to Origin 71 | - GitHub workflow `release.yml` will create a build and make the release in GitHub 72 | 73 | ## Reporting Issues 74 | 75 | If you believe you have found a defect in `tfm` or its documentation, use the [GitHub issue tracker](https://github.com/hashicorp-services/tfm/issues) to report the problem to the `tfm` maintainers. 76 | -------------------------------------------------------------------------------- /ce-migration.md: -------------------------------------------------------------------------------- 1 | # TFM - Terraform Open Source / Community Edition to TFC/TFE 2 | 3 | ## Pre-Requisites 4 | 5 | The following prerequisites are used when migrating from terraform community edition (also known as open source) to TFC/TFE managed workspaces. 6 | 7 | - Terraform - Must be installed in the execution environment and available in the path 8 | - A [configuration file](./site/docs/configuration_file/config_file.md) 9 | - Terraform backend authentication credentials must be configured in the execution environment. 10 | - A terraform cloud or enterprise token with the permissions to create workspaces in an organization 11 | - A [supported VCS](./site/docs/migration/supported-vcs.md) token with the permissions to read repositories containing terraform code to be migrated 12 | 13 | ## Config File 14 | 15 | `tfm` utilizes [a config file](./site/docs/configuration_file/config_file.md) OR environment variables. An HCL file with the following is the minimum located at `/home/user/.tfm.hcl` or specified by `--config /path/to/config_file`. Multiple config files can be created to assist with large migrations. 16 | 17 | > [!NOTE] 18 | > Use the `tfm generate config` command to generate a sample configuration for quick editing. 19 | 20 | ```hcl 21 | dst_tfc_hostname="app.terraform.io for TFC or the hostname of your TFE application" 22 | dst_tfc_org="A TFE/TFC organization to create workspaces in" 23 | dst_tfc_token="A TFC/TFE Token with the permissions to create workspaces in the TFC/TFE organization" 24 | vcs_type = " A [supported vcs_type](./site/docs/migration/supported-vcs.md) " 25 | github_token = "A Github token with the permissions to read terraform code repositories you wish to migrate" 26 | github_organization = "The github organization containing terraform code repositories" 27 | github_username = "A github username" 28 | gitlab_username = "A gitlab username" 29 | gitlab_token = "A gitlab token" 30 | gitlab_group = "A gitlab group" 31 | clone_repos_path = "/path/on/local/system/to/clone/repos/to" 32 | ``` 33 | 34 | Additional configurations can be provided to assist in the community edition to TFC/TFE migration: 35 | 36 | ```hcl 37 | commit_message = "A commit message the tfm core remove-backend command uses when removing backend blocks from .tf files and committing the changes back" 38 | commit_author_name = "the name that will appear as the commit author" 39 | commit_author_email = "the email that will appear for the commit author" 40 | vcs_provider_id = "An Oauth ID of a VCS provider connection configured in TFC/TFE" 41 | 42 | # A list of VCS repositories containing terraform code. TFM will clone each repo during the tfm core clone command for migrating opensource/commmunity edition terraform managed code to TFE/TFC. 43 | 44 | Organization. 45 | repos_to_clone = [ 46 | "repo1", 47 | "repo2", 48 | "repo3" 49 | ] 50 | ``` 51 | 52 | ## Environment Variables 53 | 54 | If no config file is found, the following environment variables can be set or used to override existing config file values. 55 | 56 | ```bash 57 | export dst_tfc_hostname="app.terraform.io for TFC or the hostname of your TFE application" 58 | export dst_tfc_org="A TFE/TFC organization to create workspaces in" 59 | export dst_tfc_token="A TFC/TFE Token with the permissions to create workspaces in the TFC/TFE organization" 60 | export github_token="A Github token with the permissions to read terraform code repositories you wish to migrate" 61 | export github_organization="The github organization containing terraform code repositories" 62 | export github_username="A github username" 63 | export clone_repos_path="/path/on/local/system/to/clone/repos/to" 64 | export vcs_type="A [supported VCS](./site/docs/migration/supported-vcs.md)" 65 | export gitlab_username="A gitlab username" 66 | export gitlab_token="A gitlab token" 67 | export gitlab_group="A gitlab group" 68 | ``` 69 | -------------------------------------------------------------------------------- /cmd/copy/copy.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package copy 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // `tfm copy` commands 11 | var CopyCmd = &cobra.Command{ 12 | Use: "copy", 13 | Short: "Copy command", 14 | Long: "Copy objects from Source Organization to Destination Organization", 15 | } 16 | 17 | func init() { 18 | 19 | } 20 | -------------------------------------------------------------------------------- /cmd/copy/validation.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package copy 5 | 6 | import ( 7 | "github.com/hashicorp-services/tfm/cmd/helper" 8 | "github.com/hashicorp-services/tfm/tfclient" 9 | ) 10 | 11 | // Validation function that validates a map is configured correctely in the tfm.hcl file. 12 | // Takes a map's name from the configuration file as a string 13 | func validateMap(c tfclient.ClientContexts, cfgMap string) (bool, map[string]string, error) { 14 | m, err := helper.ViperStringSliceMap(cfgMap) 15 | 16 | if err != nil { 17 | o.AddErrorUserProvided3("Error in", cfgMap, "mapping.") 18 | return false, m, err 19 | } 20 | 21 | if len(m) <= 0 { 22 | o.AddErrorUserProvided3("No", cfgMap, "mapping found in configuration file.") 23 | } else { 24 | o.AddMessageUserProvided("Using map ", cfgMap) 25 | o.AddFormattedMessageCalculated("Found %d mappings in the map.", len(m)) 26 | return true, m, nil 27 | } 28 | 29 | return false, m, nil 30 | } 31 | -------------------------------------------------------------------------------- /cmd/copy/workspaces-agents.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package copy 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/hashicorp-services/tfm/cmd/helper" 10 | "github.com/hashicorp-services/tfm/tfclient" 11 | tfe "github.com/hashicorp/go-tfe" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | // All functions related to copying/assigning agent pools to workspaces 16 | 17 | // Check workspace properties for execution type. 18 | func checkExecution(c tfclient.ClientContexts, ws *tfe.Workspace) bool { 19 | if ws.ExecutionMode == "agent" { 20 | return true 21 | } 22 | return false 23 | } 24 | 25 | // Update workspace execution mode to agent and assign an agent pool ID to a workspace. 26 | func assignAgentPool(c tfclient.ClientContexts, org string, destPoolId string, ws string) (*tfe.Workspace, error) { 27 | 28 | executionMode := "agent" 29 | 30 | opts := tfe.WorkspaceUpdateOptions{ 31 | Type: "", 32 | AgentPoolID: &destPoolId, 33 | ExecutionMode: &executionMode, 34 | } 35 | 36 | workspace, err := c.DestinationClient.Workspaces.Update(c.DestinationContext, c.DestinationOrganizationName, ws, opts) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | return workspace, nil 42 | } 43 | 44 | // Main function for --agents flag if there is a map of agent pools 45 | func createAgentPoolAssignmentMap(c tfclient.ClientContexts, agentpools map[string]string) error { 46 | 47 | // for each `sourceID=destID` string in the map, define the source agent pool ID and the target agent pool ID 48 | for key, element := range agentpools { 49 | srcpool := key 50 | destpool := element 51 | 52 | // Get the source workspaces properties 53 | srcWorkspaces, err := getSrcWorkspacesCfg(c) 54 | if err != nil { 55 | return errors.Wrap(err, "failed to list Workspaces from source while checking source agent pool IDs") 56 | } 57 | 58 | // Get/Check if Workspace map exists 59 | wsMapCfg, err := helper.ViperStringSliceMap("workspaces-map") 60 | if err != nil { 61 | fmt.Println("invalid input for workspaces-map") 62 | } 63 | 64 | // For each source workspace with an execution mode of "agent", compare the source agent pool ID to the 65 | // user provided source pool ID. If they match, update the destination workspace with 66 | // the user provided agent pool ID that exists in the destination. 67 | for _, ws := range srcWorkspaces { 68 | isagent := checkExecution(c, ws) 69 | destWorkSpaceName := ws.Name 70 | 71 | // Check if the destination Workspace name differs from the source name 72 | if len(wsMapCfg) > 0 { 73 | destWorkSpaceName = wsMapCfg[ws.Name] 74 | } 75 | 76 | // If the source Workspace execution type is not `agent` then do nothing and inform the user 77 | if !isagent { 78 | o.AddMessageUserProvided("No Agent Pool Assigned to source Workspace: ", ws.Name) 79 | } else { 80 | 81 | // If the source Workspace assigned agent pool ID does not match the one provided by the user on the left side of the `agents-map`, do nothing and inform the user 82 | if ws.AgentPool != nil { 83 | if ws.AgentPool.ID != srcpool { 84 | o.AddFormattedMessageUserProvided2("Workspace %v assigned agent pool ID does not match provided source ID %v. Skipping.", ws.Name, srcpool) 85 | 86 | // If the source Workspace assigned agent pool ID matches the one provided by the user on the left side of the `agents-map`, update the destination Workspace 87 | // with the agent pool ID provided by the user on the right side of the `agents-map` 88 | } else { 89 | o.AddFormattedMessageUserProvided2("Updating destination workspace %v execution mode to type agent and assigning pool ID %v", destWorkSpaceName, destpool) 90 | assignAgentPool(c, c.DestinationOrganizationName, destpool, destWorkSpaceName) 91 | } 92 | } else { 93 | o.AddMessageUserProvided("No Agent Pool Assigned to source Workspace: ", ws.Name) 94 | } 95 | 96 | } 97 | } 98 | } 99 | return nil 100 | } 101 | 102 | // Main function for --agents flag if there is a single agent pool to be assigned to all destination workspaces 103 | func createAgentPoolAssignmentSingle(c tfclient.ClientContexts, agentpool string) error { 104 | 105 | // Get the source workspaces properties 106 | srcWorkspaces, err := getSrcWorkspacesCfg(c) 107 | if err != nil { 108 | return errors.Wrap(err, "failed to list Workspaces from source while checking source agent pool IDs") 109 | } 110 | 111 | // Get/Check if Workspace map exists 112 | wsMapCfg, err := helper.ViperStringSliceMap("workspaces-map") 113 | if err != nil { 114 | fmt.Println("invalid input for workspaces-map") 115 | } 116 | 117 | // For each source workspace update the destination workspace with 118 | // the user provided agent pool ID that exists in the destination. 119 | for _, ws := range srcWorkspaces { 120 | destWorkSpaceName := ws.Name 121 | 122 | // Check if the destination Workspace name differs from the source name 123 | if len(wsMapCfg) > 0 { 124 | destWorkSpaceName = wsMapCfg[ws.Name] 125 | } 126 | 127 | o.AddFormattedMessageUserProvided2("Updating destination workspace %v execution mode to type agent and assigning pool ID %v", destWorkSpaceName, agentpool) 128 | assignAgentPool(c, c.DestinationOrganizationName, agentpool, destWorkSpaceName) 129 | } 130 | return nil 131 | } -------------------------------------------------------------------------------- /cmd/copy/workspaces-ssh.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package copy 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/hashicorp-services/tfm/tfclient" 10 | tfe "github.com/hashicorp/go-tfe" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // All functions related to copying/assigning ssh provider to workspaces 15 | 16 | // Update destination workspace with ssh-key id. 17 | func configureSSHsettings(c tfclient.ClientContexts, org string, sshId string, ws string) (*tfe.Workspace, error) { 18 | 19 | workspace, err := c.DestinationClient.Workspaces.Read(c.DestinationContext, c.DestinationOrganizationName, ws) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | workspaceSSHOptions := tfe.WorkspaceAssignSSHKeyOptions{ 25 | Type: "", 26 | SSHKeyID: &sshId, 27 | } 28 | 29 | workspaceSSH, err := c.DestinationClient.Workspaces.AssignSSHKey(c.DestinationContext, workspace.ID, workspaceSSHOptions) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return workspaceSSH, nil 35 | } 36 | 37 | func createSSHConfiguration(c tfclient.ClientContexts, sshConfig map[string]string) error { 38 | 39 | fmt.Println(sshConfig) 40 | o.AddFormattedMessageCalculated("Found %d ssh mappings in Configuration", len(sshConfig)) 41 | 42 | for key, element := range sshConfig { 43 | srcSsh := key 44 | destSsh := element 45 | 46 | // Get the source workspaces properties 47 | srcWorkspaces, err := getSrcWorkspacesCfg(c) 48 | if err != nil { 49 | return errors.Wrap(err, "failed to list Workspaces from source while checking source SSH-Key IDs") 50 | } 51 | 52 | // For each source workspace with a configured ssh key compare the source SSH ID to the 53 | // user provided SSH ID. If they match, update the matching destination workspace with 54 | // the user provided SSH ID that exists in the destination. 55 | for _, ws := range srcWorkspaces { 56 | 57 | if ws.SSHKey == nil { 58 | o.AddMessageUserProvided("No SSH ID Assigned to source Workspace: ", ws.Name) 59 | } else { 60 | if ws.SSHKey.ID != srcSsh { 61 | o.AddFormattedMessageUserProvided2("Workspace %v configured SSH ID does not match provided source ID %v. Skipping.", ws.Name, srcSsh) 62 | } else { 63 | o.AddFormattedMessageUserProvided2("Updating destination workspace %v SSH ID %v", ws.Name, destSsh) 64 | 65 | configureSSHsettings(c, c.DestinationOrganizationName, destSsh, ws.Name) 66 | } 67 | } 68 | } 69 | } 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /cmd/copy/workspaces-vcs.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package copy 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/hashicorp-services/tfm/cmd/helper" 11 | "github.com/hashicorp-services/tfm/tfclient" 12 | tfe "github.com/hashicorp/go-tfe" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | // All functions related to copying/assigning vcs provider(s) to workspaces 17 | 18 | // Update the destination workspace VCS setttings 19 | func configureVCSsettings(c tfclient.ClientContexts, org string, vcsOptions tfe.VCSRepoOptions, ws string) (*tfe.Workspace, error) { 20 | 21 | workspaceOptions := tfe.WorkspaceUpdateOptions{ 22 | Type: "", 23 | VCSRepo: &vcsOptions, 24 | } 25 | 26 | workspace, err := c.DestinationClient.Workspaces.Update(c.DestinationContext, c.DestinationOrganizationName, ws, workspaceOptions) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | return workspace, nil 32 | } 33 | 34 | // Main function for --vcs flag 35 | func createVCSConfiguration(c tfclient.ClientContexts, vcsConfig map[string]string) error { 36 | 37 | // for each `source-ot-ID=dest-ot-ID` string in the map, define the source vcs ID and the target vcs ID 38 | for key, element := range vcsConfig { 39 | srcvcs := key 40 | destvcs := element 41 | 42 | // Get the source workspaces properties 43 | srcWorkspaces, err := getSrcWorkspacesCfg(c) 44 | if err != nil { 45 | return errors.Wrap(err, "Failed to list Workspaces from source while checking source VCS IDs") 46 | } 47 | 48 | // Get/Check if Workspace map exists 49 | wsMapCfg, err := helper.ViperStringSliceMap("workspaces-map") 50 | if err != nil { 51 | fmt.Println("Invalid input for workspaces-map") 52 | } 53 | 54 | // For each source workspace with a VCS connection, compare the source ID to the 55 | // user provided ID. If they match, update the destination workspace with 56 | // the user provided ID that exists in the destination. 57 | for _, ws := range srcWorkspaces { 58 | destWorkSpaceName := ws.Name 59 | 60 | // Check if the destination Workspace name differs from the source name 61 | if len(wsMapCfg) > 0 { 62 | destWorkSpaceName = wsMapCfg[ws.Name] 63 | } 64 | 65 | // If the source workspace has no VCS assigned, do nothing and inform the user 66 | if ws.VCSRepo == nil { 67 | o.AddMessageUserProvided("No VCS ID Assigned to source Workspace: ", ws.Name) 68 | } else { 69 | 70 | if ws.VCSRepo.OAuthTokenID == srcvcs || ws.VCSRepo.GHAInstallationID == srcvcs { 71 | o.AddFormattedMessageUserProvided2("Updating destination Workspace %v VCS Settings %v", destWorkSpaceName, destvcs) 72 | 73 | vcsConfig := tfe.VCSRepoOptions{ 74 | Branch: &ws.VCSRepo.Branch, 75 | Identifier: &ws.VCSRepo.Identifier, 76 | IngressSubmodules: &ws.VCSRepo.IngressSubmodules, 77 | TagsRegex: &ws.VCSRepo.TagsRegex, 78 | } 79 | 80 | if strings.HasPrefix(destvcs, "ot-") { 81 | vcsConfig.OAuthTokenID = &destvcs 82 | } else if strings.HasPrefix(destvcs, "ghain-") { 83 | vcsConfig.GHAInstallationID = &destvcs 84 | } else { 85 | o.AddFormattedMessageUserProvided2("Invalid destination VCS ID %v for Workspace %v. Skipping.", destvcs, destWorkSpaceName) 86 | continue 87 | } 88 | 89 | configureVCSsettings(c, c.DestinationOrganizationName, vcsConfig, destWorkSpaceName) 90 | } else { 91 | 92 | o.AddFormattedMessageUserProvided2("Workspace %v configured VCS ID does not match provided source ID %v. Skipping.", ws.Name, srcvcs) 93 | } 94 | } 95 | } 96 | } 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /cmd/core/cleanup.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package core 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/spf13/cobra" 12 | "github.com/spf13/viper" 13 | ) 14 | 15 | var cleanupCmd = &cobra.Command{ 16 | Use: "cleanup", 17 | Short: "Removes up all cloned repositories from the clone_repos_path.", 18 | Long: `Deletes all repositories that were cloned into the specified clone path, cleaning up the workspace.`, 19 | RunE: func(cmd *cobra.Command, args []string) error { 20 | clonePath := viper.GetString("clone_repos_path") 21 | if clonePath == "" { 22 | return fmt.Errorf("clone path is not specified in the configuration") 23 | } 24 | 25 | if !autoApprove { 26 | fmt.Printf("Are you sure you want to delete all repositories in %s? [yes/no]: ", clonePath) 27 | var response string 28 | fmt.Scanln(&response) 29 | if response != "yes" { 30 | fmt.Println("Cleanup aborted.") 31 | return nil 32 | } 33 | } 34 | 35 | return cleanupRepos(clonePath) 36 | }, 37 | } 38 | 39 | func init() { 40 | CoreCmd.AddCommand(cleanupCmd) 41 | cleanupCmd.Flags().BoolVar(&autoApprove, "autoapprove", false, "Automatically approve the operation without a confirmation prompt") 42 | } 43 | 44 | func cleanupRepos(clonePath string) error { 45 | dirs, err := os.ReadDir(clonePath) 46 | if err != nil { 47 | return fmt.Errorf("error reading clone path directories: %v", err) 48 | } 49 | 50 | for _, dir := range dirs { 51 | if dir.IsDir() { 52 | dirPath := filepath.Join(clonePath, dir.Name()) 53 | fmt.Printf("Removing directory: %s\n", dirPath) 54 | err := os.RemoveAll(dirPath) 55 | if err != nil { 56 | return fmt.Errorf("failed to remove directory %s: %v", dirPath, err) 57 | } 58 | } 59 | } 60 | 61 | fmt.Println("Cleanup completed successfully.") 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /cmd/core/core.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package core 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var autoApprove bool 11 | 12 | // `tfm core` commands 13 | var CoreCmd = &cobra.Command{ 14 | Use: "core", 15 | Short: "Command used to perform terraform open source (core) to TFE/TFC migration commands", 16 | Long: "Command used to perform terraform open source (core) to TFE/TFC migration commands", 17 | } 18 | 19 | func init() { 20 | 21 | } 22 | -------------------------------------------------------------------------------- /cmd/core/migrate.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package core 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var includeRemoveBackend bool 13 | var includeCommands []string 14 | 15 | var validIncludeCommands = map[string]bool{ 16 | "remove-backend": true, 17 | } 18 | 19 | var migrateCmd = &cobra.Command{ 20 | Use: "migrate", 21 | Short: "Migrates opensource/community edition Terraform code and state to TFE/TFC in 1 continuous workflow.", 22 | Long: `Executes a sequence of commands to clone repositories, get state, create workspaces, 23 | upload state, link VCS, and optionally remove backend configurations as part of the core migration process.`, 24 | RunE: func(cmd *cobra.Command, args []string) error { 25 | // Validate included commands before executing the migration process 26 | for _, includeCmd := range includeCommands { 27 | if _, valid := validIncludeCommands[includeCmd]; !valid { 28 | return fmt.Errorf("invalid command specified in --include: %s", includeCmd) 29 | } 30 | } 31 | 32 | // Check for auto-approval 33 | if !autoApprove { 34 | promptMessage := ` 35 | This command will run all of the commands listed below in order.: 36 | 1. tfm core clone 37 | 2. tfm core init-repos 38 | 3. tfm core getstate 39 | 4. tfm core create-workspaces 40 | 5. tfm core upload-state 41 | 6. tfm core link-vcs 42 | 7. (Optional) If you provided the flag --include remove-backend then tfm core remove-backen will run. 43 | 44 | Are you sure you want to proceed? Type 'yes' to continue: ` 45 | o.AddPassUserProvided(promptMessage) 46 | var response string 47 | _, err := fmt.Scanln(&response) 48 | if err != nil || response != "yes" { 49 | fmt.Println("Operation aborted by the user.") 50 | return nil // Exit if the user does not confirm 51 | } 52 | } 53 | 54 | commonArgs := []string{} 55 | 56 | // Directly invoke the RunE function of each command 57 | if err := CloneCmd.RunE(cmd, commonArgs); err != nil { 58 | return err 59 | } 60 | if err := InitReposCmd.RunE(cmd, commonArgs); err != nil { 61 | return err 62 | } 63 | if err := GetStateCmd.RunE(cmd, commonArgs); err != nil { 64 | return err 65 | } 66 | if err := CreateWorkspacesCmd.RunE(cmd, commonArgs); err != nil { 67 | return err 68 | } 69 | if err := UploadStateCmd.RunE(cmd, commonArgs); err != nil { 70 | return err 71 | } 72 | if err := LinkVCSCmd.RunE(cmd, commonArgs); err != nil { 73 | return err 74 | } 75 | 76 | // Dynamically execute additional commands based on --include flag 77 | for _, includeCmd := range includeCommands { 78 | switch includeCmd { 79 | case "remove-backend": 80 | RemoveBackendCmd.Flags().Set("autoapprove", "true") 81 | if err := RemoveBackendCmd.RunE(cmd, []string{}); err != nil { 82 | return err 83 | } 84 | } 85 | } 86 | 87 | return nil 88 | }, 89 | } 90 | 91 | func init() { 92 | CoreCmd.AddCommand(migrateCmd) 93 | migrateCmd.Flags().BoolVar(&autoApprove, "autoapprove", false, "Automatically approve the operation without a confirmation prompt") 94 | migrateCmd.Flags().StringSliceVar(&includeCommands, "include", nil, "Specify additional commands to include in the migration process (e.g., --include remove-backend)") 95 | } 96 | -------------------------------------------------------------------------------- /cmd/delete/delete.go: -------------------------------------------------------------------------------- 1 | package delete 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var ( 8 | side string 9 | 10 | DeleteCmd = &cobra.Command{ 11 | Use: "delete", 12 | Short: "delete command", 13 | Long: "delete objects in an org. DANGER this will delete things!", 14 | } 15 | ) 16 | 17 | func init() { 18 | 19 | DeleteCmd.PersistentFlags().StringVar(&side, "side", "", "Specify source or destination side to process") 20 | 21 | } 22 | -------------------------------------------------------------------------------- /cmd/delete/validation.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package delete 5 | 6 | import ( 7 | "github.com/hashicorp-services/tfm/cmd/helper" 8 | "github.com/hashicorp-services/tfm/tfclient" 9 | ) 10 | 11 | 12 | // Validation function that validates a map is configured correctly in the tfm.hcl file. 13 | // Takes a map's name from the configuration file as a string 14 | func validateMap(c tfclient.ClientContexts, cfgMap string) (bool, map[string]string, error) { 15 | m, err := helper.ViperStringSliceMap(cfgMap) 16 | 17 | if err != nil { 18 | o.AddErrorUserProvided3("Error in", cfgMap, "mapping.") 19 | return false, m, err 20 | } 21 | 22 | if len(m) <= 0 { 23 | o.AddErrorUserProvided3("No", cfgMap, "mapping found in configuration file.") 24 | } else { 25 | o.AddMessageUserProvided("Using map ", cfgMap) 26 | o.AddFormattedMessageCalculated("Found %d mappings in the map.", len(m)) 27 | return true, m, nil 28 | } 29 | 30 | return false, m, nil 31 | } 32 | -------------------------------------------------------------------------------- /cmd/generate/generate.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package generate 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var ( 11 | GenerateCmd = &cobra.Command{ 12 | Use: "generate", 13 | Short: "generate command for generating .tfm.hcl config template", 14 | Long: "generate a .tfm.hcl file template ", 15 | } 16 | ) 17 | 18 | func init() { 19 | 20 | } 21 | -------------------------------------------------------------------------------- /cmd/helper/helper_logs.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package helper 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | 10 | "github.com/fatih/color" 11 | ) 12 | 13 | // Centralize error handing, simple print message and exit 14 | func LogError(err error, message string) { 15 | fmt.Println() 16 | fmt.Println() 17 | fmt.Println(color.RedString("Error: " + message)) 18 | log.Fatalln(err) 19 | } 20 | 21 | // Warning but dont exit 22 | func LogWarning(err error, message string) { 23 | fmt.Println() 24 | fmt.Println() 25 | fmt.Println(color.YellowString("Error: " + message)) 26 | } 27 | -------------------------------------------------------------------------------- /cmd/helper/helper_time.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package helper 5 | 6 | import ( 7 | "time" 8 | ) 9 | 10 | func Timestamp(time time.Time) string { 11 | // TODO: normalize this to current users timezone 12 | return time.Format("2006-01-02 15:04:05") 13 | } 14 | 15 | // Format date consistently 16 | func FormatDateTime(t time.Time) string { 17 | return t.Format("Mon Jan _2 15:04 2006") 18 | } 19 | -------------------------------------------------------------------------------- /cmd/helper/helper_viper.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package helper 5 | 6 | import ( 7 | "errors" 8 | "strings" 9 | 10 | "github.com/spf13/viper" 11 | ) 12 | 13 | func ViperString(flag string) *string { 14 | if viper.GetString(flag) == "" { 15 | value := "" 16 | return &value 17 | } 18 | value := viper.GetString(flag) 19 | return &value 20 | } 21 | 22 | func ViperInt(flag string) *int { 23 | value := viper.GetInt(flag) 24 | return &value 25 | } 26 | 27 | func ViperBool(flag string) *bool { 28 | if !viper.GetBool(flag) { 29 | value := false 30 | return &value 31 | } 32 | value := viper.GetBool(flag) 33 | return &value 34 | } 35 | 36 | func ViperStringSlice(flag string) []string { 37 | value := viper.GetStringSlice(flag) 38 | if len(value) == 0 { 39 | return []string{} 40 | } 41 | return value 42 | } 43 | 44 | func ViperStringSliceMap(flag string) (map[string]string, error) { 45 | m := make(map[string]string) 46 | values := viper.GetStringSlice(flag) 47 | 48 | for _, v := range values { 49 | // Expecting each value to be in "a=1" format 50 | s := strings.SplitN(v, "=", 2) 51 | if len(s) != 2 { 52 | return nil, errors.New("invalid env var or configuration file.") 53 | } 54 | m[s[0]] = s[1] 55 | s1 := s[0] 56 | s2 := s[1] 57 | 58 | if s1 == "" { 59 | return m, errors.New("invalid input provided on left side of a mapping inside the configuration file") 60 | } 61 | 62 | if s2 == "" { 63 | return m, errors.New("invalid input provided on right side of a mapping inside the configuration file") 64 | } 65 | 66 | } 67 | return m, nil 68 | } 69 | 70 | func ViperMapKeyValuePair(flag string) (string, string, error) { 71 | //m := make(map[string]string) 72 | var s1 string 73 | var s2 string 74 | values := viper.GetStringSlice(flag) 75 | 76 | for _, v := range values { 77 | // Expecting each value to be in "a=1" format 78 | s := strings.SplitN(v, "=", 2) 79 | if len(s) != 2 { 80 | return "", "", errors.New("invalid env var") 81 | } 82 | s1 := s[0] 83 | s2 := s[1] 84 | 85 | if s1 == "" { 86 | return "", "", errors.New("invalid source provided on varsets-map left side") 87 | } 88 | 89 | if s2 == "" { 90 | return "", "", errors.New("invalid destination provided on varsets-map right side") 91 | } 92 | 93 | return s1, s2, nil 94 | } 95 | return s1, s2, nil 96 | } 97 | -------------------------------------------------------------------------------- /cmd/list/list.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package list 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var ( 11 | side string 12 | jsonOut bool 13 | 14 | ListCmd = &cobra.Command{ 15 | Use: "list", 16 | Short: "List command", 17 | Long: "List objects in an org", 18 | } 19 | ) 20 | 21 | func init() { 22 | 23 | ListCmd.PersistentFlags().StringVar(&side, "side", "", "Specify source or destination side to process") 24 | ListCmd.PersistentFlags().BoolVar(&jsonOut, "json", false, "Print the output in JSON format. Only supported with [workspaces, projects]") 25 | } 26 | -------------------------------------------------------------------------------- /cmd/list/orgs.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package list 22 | 23 | import ( 24 | "fmt" 25 | 26 | "github.com/hashicorp-services/tfm/cmd/helper" 27 | "github.com/hashicorp-services/tfm/output" 28 | "github.com/hashicorp-services/tfm/tfclient" 29 | tfe "github.com/hashicorp/go-tfe" 30 | "github.com/logrusorgru/aurora" 31 | "github.com/spf13/cobra" 32 | ) 33 | 34 | var ( 35 | o output.Output 36 | 37 | // `tfm list organization` command 38 | orgListCmd = &cobra.Command{ 39 | Use: "organization", 40 | Aliases: []string{"orgs"}, 41 | Short: "List Organizations", 42 | Long: "List of Organizations.", 43 | Run: func(cmd *cobra.Command, args []string) { 44 | orgList(tfclient.GetClientContexts()) 45 | }, 46 | PostRun: func(cmd *cobra.Command, args []string) { 47 | o.Close() 48 | }, 49 | } 50 | 51 | // `tfm org show org-id` command 52 | // orgShowCmd = &cobra.Command{ 53 | // Use: "show", 54 | // Short: "Show org attributes", 55 | // Long: "Show the attributes of a specific org.", 56 | // Run: func(cmd *cobra.Command, args []string) { 57 | // // return orgShow( 58 | // // viper.GetString("name")) 59 | // fmt.Println(tfclient.Foo()) 60 | // }, 61 | // PostRun: func(cmd *cobra.Command, args []string) { 62 | // o.Close() 63 | // }, 64 | // } 65 | ) 66 | 67 | func init() { 68 | 69 | // Flags().StringP, etc... - the "P" gives us the option for a short hand 70 | 71 | // `tfe-discover organization list` command 72 | //orgListCmd.Flags().BoolP("all", "a", false, "List all? (optional)") 73 | 74 | // `tfe-discover organization show` command 75 | // orgShowCmd.Flags().Int16P("id", "i", 0, "id of foo.") 76 | // orgShowCmd.MarkFlagRequired("name") 77 | // orgShowCmd.Flags().String("name", "n", "name of foo") 78 | 79 | // Add commands 80 | ListCmd.AddCommand(orgListCmd) 81 | // ListCmd.AddCommand(orgShowCmd) 82 | 83 | } 84 | 85 | func orgList(c tfclient.ClientContexts) error { 86 | 87 | allItems := []*tfe.Organization{} 88 | opts := tfe.OrganizationListOptions{ 89 | ListOptions: tfe.ListOptions{ 90 | PageNumber: 1, 91 | PageSize: 100}, 92 | } 93 | 94 | if (ListCmd.Flags().Lookup("side").Value.String() == "source") || (!ListCmd.Flags().Lookup("side").Changed) { 95 | 96 | o.AddMessageUserProvided("List of Organizations at: ", c.SourceHostname) 97 | 98 | for { 99 | items, err := c.SourceClient.Organizations.List(c.SourceContext, &opts) 100 | if err != nil { 101 | helper.LogError(err, "failed to list orgs") 102 | } 103 | 104 | allItems = append(allItems, items.Items...) 105 | 106 | o.AddFormattedMessageCalculated("Found %d Organizations", len(allItems)) 107 | 108 | if items.CurrentPage >= items.TotalPages { 109 | break 110 | } 111 | opts.PageNumber = items.NextPage 112 | } 113 | 114 | o.AddTableHeaders("Name", "Created On", "Email") 115 | for _, i := range allItems { 116 | cr_created_at := helper.FormatDateTime(i.CreatedAt) 117 | 118 | o.AddTableRows(i.Name, cr_created_at, i.Email) 119 | } 120 | } 121 | if ListCmd.Flags().Lookup("side").Value.String() == "destination" { 122 | 123 | o.AddMessageUserProvided("List of Organizations at: ", c.DestinationHostname) 124 | 125 | for { 126 | items, err := c.DestinationClient.Organizations.List(c.DestinationContext, &opts) 127 | if err != nil { 128 | helper.LogError(err, "failed to list orgs") 129 | } 130 | 131 | allItems = append(allItems, items.Items...) 132 | 133 | o.AddFormattedMessageCalculated("Found %d Organizations", len(allItems)) 134 | 135 | if items.CurrentPage >= items.TotalPages { 136 | break 137 | } 138 | opts.PageNumber = items.NextPage 139 | } 140 | 141 | o.AddTableHeaders("Name", "Created On", "Email") 142 | for _, i := range allItems { 143 | cr_created_at := helper.FormatDateTime(i.CreatedAt) 144 | 145 | o.AddTableRows(i.Name, cr_created_at, i.Email) 146 | } 147 | } 148 | 149 | return nil 150 | } 151 | 152 | func orgShow(name string) error { 153 | fmt.Println("Show org with name:", aurora.Bold(name)) 154 | return nil 155 | } 156 | -------------------------------------------------------------------------------- /cmd/list/projects.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package list 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | 10 | "encoding/json" 11 | 12 | "github.com/hashicorp-services/tfm/tfclient" 13 | tfe "github.com/hashicorp/go-tfe" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | var ( 18 | 19 | // `tfm list project` command 20 | projectListCmd = &cobra.Command{ 21 | Use: "projects", 22 | Aliases: []string{"prj"}, 23 | Short: "Projects command", 24 | Long: "List Projects in an org", 25 | Run: func(cmd *cobra.Command, args []string) { 26 | listProjects(tfclient.GetClientContexts(), jsonOut) 27 | }, 28 | PostRun: func(cmd *cobra.Command, args []string) { 29 | o.Close() 30 | }, 31 | } 32 | ) 33 | 34 | func init() { 35 | 36 | // Add commands 37 | ListCmd.AddCommand(projectListCmd) 38 | 39 | } 40 | 41 | func listProjects(c tfclient.ClientContexts, jsonOut bool) error { 42 | 43 | srcProjects := []*tfe.Project{} 44 | projectJSON := make(map[string]interface{}) // Parent JSON object "project-names" 45 | projectNamesAndIDs := []map[string]string{} // project names slice to go inside parent object "project-names" 46 | 47 | opts := tfe.ProjectListOptions{ 48 | ListOptions: tfe.ListOptions{ 49 | PageNumber: 1, 50 | PageSize: 100}, 51 | } 52 | 53 | if (ListCmd.Flags().Lookup("side").Value.String() == "source") || (!ListCmd.Flags().Lookup("side").Changed) { 54 | 55 | if jsonOut == false { 56 | o.AddMessageUserProvided("Getting list of projects from: ", c.SourceHostname) 57 | } 58 | 59 | for { 60 | items, err := c.SourceClient.Projects.List(c.SourceContext, c.SourceOrganizationName, &opts) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | srcProjects = append(srcProjects, items.Items...) 66 | 67 | if jsonOut == false { 68 | o.AddFormattedMessageCalculated("Found %d Projects", len(srcProjects)) 69 | } 70 | 71 | if items.CurrentPage >= items.TotalPages { 72 | break 73 | } 74 | opts.PageNumber = items.NextPage 75 | 76 | } 77 | if jsonOut == false { 78 | o.AddTableHeaders("Name", "ID") 79 | } 80 | for _, i := range srcProjects { 81 | projectInfo := map[string]string{ 82 | "name": i.Name, 83 | "id": i.ID, 84 | } 85 | 86 | if jsonOut { 87 | projectNamesAndIDs = append(projectNamesAndIDs, projectInfo) // Store project name in slice 88 | } 89 | if jsonOut == false { 90 | o.AddTableRows(i.Name, i.ID) 91 | } 92 | } 93 | if jsonOut { 94 | projectJSON["projects"] = projectNamesAndIDs // Assign projects to the "projects" key 95 | 96 | jsonData, err := json.Marshal(projectJSON) 97 | if err != nil { 98 | fmt.Println("Error marshaling projects to JSON:", err) 99 | return err 100 | } 101 | 102 | fmt.Println(string(jsonData)) 103 | } 104 | } 105 | 106 | if ListCmd.Flags().Lookup("side").Value.String() == "destination" { 107 | if jsonOut == false { 108 | o.AddMessageUserProvided("Getting list of projects from: ", c.DestinationHostname) 109 | } 110 | 111 | for { 112 | items, err := c.DestinationClient.Projects.List(c.DestinationContext, c.DestinationOrganizationName, &opts) 113 | if err != nil { 114 | return err 115 | } 116 | 117 | srcProjects = append(srcProjects, items.Items...) 118 | 119 | if jsonOut == false { 120 | o.AddFormattedMessageCalculated("Found %d Projects", len(srcProjects)) 121 | } 122 | 123 | if items.CurrentPage >= items.TotalPages { 124 | break 125 | } 126 | opts.PageNumber = items.NextPage 127 | 128 | } 129 | if jsonOut == false { 130 | o.AddTableHeaders("Name", "ID") 131 | } 132 | 133 | for _, i := range srcProjects { 134 | projectInfo := map[string]string{ 135 | "name": i.Name, 136 | "id": i.ID, 137 | } 138 | 139 | if jsonOut { 140 | projectNamesAndIDs = append(projectNamesAndIDs, projectInfo) // Store project name in slice 141 | } 142 | 143 | if jsonOut == false { 144 | o.AddTableRows(i.Name, i.ID) 145 | } 146 | } 147 | if jsonOut { 148 | projectJSON["projects"] = projectNamesAndIDs // Assign projects to the "project" key 149 | 150 | jsonData, err := json.Marshal(projectJSON) 151 | if err != nil { 152 | fmt.Println("Error marshaling projects to JSON:", err) 153 | return err 154 | } 155 | 156 | fmt.Println(string(jsonData)) 157 | } 158 | } 159 | 160 | return nil 161 | } 162 | 163 | func getProjectName(client *tfe.Client, ctx context.Context, projectId string) (string, error) { 164 | 165 | prj, err := client.Projects.Read(ctx, projectId) 166 | 167 | if err != nil { 168 | return "error reading project", err 169 | } 170 | 171 | return prj.Name, nil 172 | } 173 | -------------------------------------------------------------------------------- /cmd/list/ssh-keys.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package list 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/hashicorp-services/tfm/tfclient" 10 | tfe "github.com/hashicorp/go-tfe" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var ( 15 | 16 | // `tfm list ssh` command 17 | sshListCmd = &cobra.Command{ 18 | Use: "ssh", 19 | Short: "ssh-keys command", 20 | Long: "Lists the ssh-keys for an org", 21 | // RunE: func(cmd *cobra.Command, args []string) error { 22 | // return listTeams( 23 | // tfeclient.GetClientContexts()) 24 | 25 | // }, 26 | Run: func(cmd *cobra.Command, args []string) { 27 | // return orgShow( 28 | // viper.GetString("name")) 29 | listSrcSSHKeys(tfclient.GetClientContexts()) 30 | }, 31 | PostRun: func(cmd *cobra.Command, args []string) { 32 | o.Close() 33 | }, 34 | } 35 | ) 36 | 37 | func init() { 38 | // Flags().StringP, etc... - the "P" gives us the option for a short hand 39 | 40 | // `tfm list ssh all` command 41 | //sshListCmd.Flags().BoolP("all", "a", false, "List all? (optional)") 42 | 43 | // Add commands 44 | ListCmd.AddCommand(sshListCmd) 45 | 46 | } 47 | 48 | func listSrcSSHKeys(c tfclient.ClientContexts) error { 49 | 50 | keys := []*tfe.SSHKey{} 51 | 52 | opts := tfe.SSHKeyListOptions{ 53 | ListOptions: tfe.ListOptions{ 54 | PageNumber: 1, 55 | PageSize: 100, 56 | }, 57 | } 58 | 59 | if (ListCmd.Flags().Lookup("side").Value.String() == "source") || (!ListCmd.Flags().Lookup("side").Changed) { 60 | 61 | o.AddMessageUserProvided("Getting list of SSH keys from: ", c.SourceHostname) 62 | 63 | for { 64 | k, err := c.SourceClient.SSHKeys.List(c.SourceContext, c.SourceOrganizationName, &opts) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | fmt.Println() 70 | keys = append(keys, k.Items...) 71 | 72 | o.AddFormattedMessageCalculated("Found %d SSH keys", len(keys)) 73 | 74 | if k.CurrentPage >= k.TotalPages { 75 | break 76 | } 77 | opts.PageNumber = k.NextPage 78 | 79 | } 80 | o.AddTableHeaders("Key Name", "Key ID") 81 | for _, i := range keys { 82 | 83 | o.AddTableRows(i.Name, i.ID) 84 | 85 | } 86 | } 87 | 88 | if ListCmd.Flags().Lookup("side").Value.String() == "destination" { 89 | 90 | o.AddMessageUserProvided("Getting list of SSH keys from: ", c.DestinationHostname) 91 | 92 | for { 93 | k, err := c.DestinationClient.SSHKeys.List(c.DestinationContext, c.DestinationOrganizationName, &opts) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | fmt.Println() 99 | keys = append(keys, k.Items...) 100 | 101 | o.AddFormattedMessageCalculated("Found %d SSH keys", len(keys)) 102 | 103 | if k.CurrentPage >= k.TotalPages { 104 | break 105 | } 106 | opts.PageNumber = k.NextPage 107 | 108 | } 109 | o.AddTableHeaders("Key Name", "Key ID") 110 | for _, i := range keys { 111 | 112 | o.AddTableRows(i.Name, i.ID) 113 | 114 | } 115 | } 116 | 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /cmd/list/teams.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package list 5 | 6 | import ( 7 | "github.com/hashicorp-services/tfm/tfclient" 8 | "github.com/hashicorp/go-tfe" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var ( 13 | 14 | // `tfm list teams` command 15 | teamsListCmd = &cobra.Command{ 16 | Use: "teams", 17 | Short: "Teams command", 18 | Long: "Act upon Teams in an org", 19 | // RunE: func(cmd *cobra.Command, args []string) error { 20 | // return listTeams( 21 | // tfeclient.GetClientContexts()) 22 | 23 | // }, 24 | Run: func(cmd *cobra.Command, args []string) { 25 | // return orgShow( 26 | // viper.GetString("name")) 27 | listTeams(tfclient.GetClientContexts()) 28 | }, 29 | PostRun: func(cmd *cobra.Command, args []string) { 30 | o.Close() 31 | }, 32 | } 33 | ) 34 | 35 | func init() { 36 | // Flags().StringP, etc... - the "P" gives us the option for a short hand 37 | 38 | // `tfm copy teams all` command 39 | //teamsListCmd.Flags().BoolP("all", "a", false, "List all? (optional)") 40 | 41 | // Add commands 42 | ListCmd.AddCommand(teamsListCmd) 43 | 44 | } 45 | 46 | func listTeams(c tfclient.ClientContexts) error { 47 | 48 | srcTeams := []*tfe.Team{} 49 | 50 | opts := tfe.TeamListOptions{ 51 | ListOptions: tfe.ListOptions{ 52 | PageNumber: 1, 53 | PageSize: 100}, 54 | } 55 | 56 | if (ListCmd.Flags().Lookup("side").Value.String() == "source") || (!ListCmd.Flags().Lookup("side").Changed) { 57 | 58 | o.AddMessageUserProvided("Getting list of teams from: ", c.SourceHostname) 59 | 60 | for { 61 | items, err := c.SourceClient.Teams.List(c.SourceContext, c.SourceOrganizationName, &opts) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | srcTeams = append(srcTeams, items.Items...) 67 | 68 | o.AddFormattedMessageCalculated("Found %d Teams", len(srcTeams)) 69 | 70 | if items.CurrentPage >= items.TotalPages { 71 | break 72 | } 73 | opts.PageNumber = items.NextPage 74 | 75 | } 76 | o.AddTableHeaders("Name") 77 | for _, i := range srcTeams { 78 | 79 | o.AddTableRows(i.Name) 80 | } 81 | } 82 | 83 | if ListCmd.Flags().Lookup("side").Value.String() == "destination" { 84 | o.AddMessageUserProvided("Getting list of teams from: ", c.DestinationHostname) 85 | 86 | for { 87 | items, err := c.DestinationClient.Teams.List(c.DestinationContext, c.DestinationOrganizationName, &opts) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | srcTeams = append(srcTeams, items.Items...) 93 | 94 | o.AddFormattedMessageCalculated("Found %d Teams", len(srcTeams)) 95 | 96 | if items.CurrentPage >= items.TotalPages { 97 | break 98 | } 99 | opts.PageNumber = items.NextPage 100 | 101 | } 102 | o.AddTableHeaders("Name") 103 | for _, i := range srcTeams { 104 | 105 | o.AddTableRows(i.Name) 106 | } 107 | } 108 | 109 | return nil 110 | } 111 | -------------------------------------------------------------------------------- /cmd/list/vcs-gha.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package list 5 | 6 | import ( 7 | "github.com/hashicorp-services/tfm/cmd/helper" 8 | "github.com/hashicorp-services/tfm/tfclient" 9 | tfe "github.com/hashicorp/go-tfe" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var ( 14 | ghaVcsListCmd = &cobra.Command{ 15 | Use: "vcs-gha", 16 | Aliases: []string{"vcs-gha"}, 17 | Short: "List GHA VCS Providers", 18 | Long: "List of GitHub App VCS Providers. Will default to source if no side is specified", 19 | Run: func(cmd *cobra.Command, args []string) { 20 | ghaVcsList(tfclient.GetClientContexts()) 21 | }, 22 | PostRun: func(cmd *cobra.Command, args []string) { 23 | o.Close() 24 | }, 25 | } 26 | ) 27 | 28 | func init() { 29 | ListCmd.AddCommand(ghaVcsListCmd) 30 | } 31 | 32 | // helper functions 33 | func ghaVcsListAllForOrganization(c tfclient.ClientContexts) ([]*tfe.GHAInstallation, error) { 34 | var allGHAItems []*tfe.GHAInstallation 35 | optsGHA := tfe.GHAInstallationListOptions{ 36 | ListOptions: tfe.ListOptions{PageNumber: 1, PageSize: 100}, 37 | } 38 | for { 39 | var ghaItems *tfe.GHAInstallationList 40 | var err error 41 | 42 | if (ListCmd.Flags().Lookup("side").Value.String() == "source") || (!ListCmd.Flags().Lookup("side").Changed) { 43 | ghaItems, err = c.SourceClient.GHAInstallations.List(c.SourceContext, &optsGHA) 44 | } 45 | 46 | if ListCmd.Flags().Lookup("side").Value.String() == "destination" { 47 | ghaItems, err = c.DestinationClient.GHAInstallations.List(c.DestinationContext, &optsGHA) 48 | } 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | allGHAItems = append(allGHAItems, ghaItems.Items...) 54 | 55 | if ghaItems.CurrentPage >= ghaItems.TotalPages { 56 | break 57 | } 58 | optsGHA.PageNumber = ghaItems.NextPage 59 | } 60 | 61 | return allGHAItems, nil 62 | } 63 | 64 | // output functions 65 | func ghaVcsList(c tfclient.ClientContexts) error { 66 | o.AddMessageUserProvided("List vcs for configured Organizations", "") 67 | 68 | var orgGhaVcsList []*tfe.GHAInstallation 69 | var err error 70 | 71 | if (ListCmd.Flags().Lookup("side").Value.String() == "source") || (!ListCmd.Flags().Lookup("side").Changed) { 72 | orgGhaVcsList, err = ghaVcsListAllForOrganization(c) 73 | } 74 | 75 | if ListCmd.Flags().Lookup("side").Value.String() == "destination" { 76 | orgGhaVcsList, err = ghaVcsListAllForOrganization(c) 77 | } 78 | 79 | if err != nil { 80 | helper.LogError(err, "failed to list vcs for organization") 81 | } 82 | 83 | o.AddFormattedMessageCalculated("Found %d vcs", len(orgGhaVcsList)) 84 | 85 | o.AddTableHeaders("Name", "Installation ID", "ID") 86 | for _, i := range orgGhaVcsList { 87 | 88 | // The ID and Installation ID are flipped as they are flipped in the TFE/HCP TF UI, so we are matching the UI instead of the API/SDK 89 | o.AddTableRows(*i.Name, *i.ID, *i.InstallationID) 90 | } 91 | 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /cmd/list/workspace-filter.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package list 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/hashicorp-services/tfm/tfclient" 10 | tfe "github.com/hashicorp/go-tfe" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var ( 15 | searchString string 16 | tagsString string 17 | excludedTags string 18 | wildcardName string 19 | includes []string 20 | 21 | workspaceFilterCmd = &cobra.Command{ 22 | Use: "workspace-filter", 23 | Aliases: []string{"workspace-filter"}, 24 | Short: "Filter workspaces", 25 | Long: "Filter Workspaces. Trying different ways to return workspaces", 26 | Run: func(cmd *cobra.Command, args []string) { 27 | workspaceFilter(tfclient.GetClientContexts()) 28 | }, 29 | PostRun: func(cmd *cobra.Command, args []string) { 30 | o.Close() 31 | }, 32 | } 33 | ) 34 | 35 | func init() { 36 | ListCmd.AddCommand(workspaceFilterCmd) 37 | workspaceFilterCmd.PersistentFlags().StringVar(&searchString, "name", "", "partial workspace name used to filter the results") 38 | workspaceFilterCmd.PersistentFlags().StringVar(&tagsString, "tags", "", "comma-separated tag names used to filter the results") 39 | workspaceFilterCmd.PersistentFlags().StringVar(&excludedTags, "excluded-tags", "", "comma-separated tag names to exclude") 40 | workspaceFilterCmd.PersistentFlags().StringVar(&wildcardName, "wildcard-name", "", "workspace name to match with a wildcard") 41 | workspaceFilterCmd.PersistentFlags().StringSliceVar(&includes, "includes", nil, "Additional relations to include, comma separated, no space") 42 | } 43 | 44 | func workspaceFilter(c tfclient.ClientContexts) error { 45 | 46 | allItems := []*tfe.Workspace{} 47 | 48 | // converts the type of slice from []string to []tfe.WSIncludeOpt Not sure if there is a way to not need this? 49 | workspaceIncludes := make([]tfe.WSIncludeOpt, len(includes)) 50 | for i, v := range includes { 51 | workspaceIncludes[i] = tfe.WSIncludeOpt(v) 52 | } 53 | 54 | workspaceFilterOpts := tfe.WorkspaceListOptions{ 55 | ListOptions: tfe.ListOptions{PageNumber: 1, PageSize: 100}, 56 | Search: searchString, 57 | Tags: tagsString, 58 | ExcludeTags: excludedTags, 59 | WildcardName: wildcardName, 60 | Include: workspaceIncludes, 61 | } 62 | 63 | for { 64 | var items *tfe.WorkspaceList 65 | var err error 66 | 67 | if (ListCmd.Flags().Lookup("side").Value.String() == "source") || (!ListCmd.Flags().Lookup("side").Changed) { 68 | items, err = c.SourceClient.Workspaces.List(c.SourceContext, c.SourceOrganizationName, &workspaceFilterOpts) 69 | } 70 | 71 | if ListCmd.Flags().Lookup("side").Value.String() == "destination" { 72 | items, err = c.DestinationClient.Workspaces.List(c.DestinationContext, c.DestinationOrganizationName, &workspaceFilterOpts) 73 | } 74 | if err != nil { 75 | return nil 76 | } 77 | 78 | allItems = append(allItems, items.Items...) 79 | 80 | if items.CurrentPage >= items.TotalPages { 81 | break 82 | } 83 | workspaceFilterOpts.PageNumber = items.NextPage 84 | } 85 | 86 | fmt.Print(len(allItems)) 87 | 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /cmd/lock/lock.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package lock 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // `tfm lock` commands 11 | var ( 12 | side string 13 | 14 | LockCmd = &cobra.Command{ 15 | Use: "lock", 16 | Short: "Lock", 17 | Long: "Locks objects in source or destination", 18 | } 19 | ) 20 | 21 | func init() { 22 | LockCmd.PersistentFlags().StringVar(&side, "side", "", "Specify source or destination side to process") 23 | } 24 | -------------------------------------------------------------------------------- /cmd/nuke/nuke.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // package nuke 5 | 6 | // import ( 7 | // "github.com/spf13/cobra" 8 | // ) 9 | 10 | // var ( 11 | // NukeCmd = &cobra.Command{ 12 | // Use: "nuke", 13 | // Short: "nuke command", 14 | // Long: "nuke objects in an org. DANGER this will delete things!", 15 | // Hidden: true, 16 | // } 17 | // ) 18 | 19 | // func init() {} 20 | -------------------------------------------------------------------------------- /cmd/nuke/workspaces.go: -------------------------------------------------------------------------------- 1 | // // Copyright (c) HashiCorp, Inc. 2 | // // SPDX-License-Identifier: MPL-2.0 3 | 4 | // package nuke 5 | 6 | // import ( 7 | // "fmt" 8 | // "strings" 9 | 10 | // "github.com/hashicorp-services/tfm/output" 11 | // "github.com/hashicorp-services/tfm/tfclient" 12 | // tfe "github.com/hashicorp/go-tfe" 13 | // "github.com/spf13/cobra" 14 | // ) 15 | 16 | // var ( 17 | // o output.Output 18 | 19 | // // `tfm nuke workspaces` command 20 | // workspacesNukeCmd = &cobra.Command{ 21 | // Use: "workspaces", 22 | // Aliases: []string{"ws"}, 23 | // Short: "Workspaces command", 24 | // Long: "Nuke Workspaces in an org", 25 | // Run: func(cmd *cobra.Command, args []string) { 26 | // nukeWorkspaces(tfclient.GetClientContexts()) 27 | // }, 28 | // PostRun: func(cmd *cobra.Command, args []string) { 29 | // o.Close() 30 | // }, 31 | // } 32 | // ) 33 | 34 | // func init() { 35 | // // Add commands 36 | // NukeCmd.AddCommand(workspacesNukeCmd) 37 | // } 38 | 39 | // func nukeWorkspaces(c tfclient.ClientContexts) error { 40 | 41 | // workspaces := listWorkspaces(c) 42 | // workspacesToNuke := []*tfe.Workspace{} 43 | 44 | // for _, workspace := range workspaces { 45 | // if workspace.SourceName == "tfm" { 46 | // workspacesToNuke = append(workspacesToNuke, workspace) 47 | // } 48 | // } 49 | 50 | // if len(workspacesToNuke) > 0 { 51 | // o.AddFormattedMessageCalculated("Found %d Workspaces created by tfm to remove", len(workspacesToNuke)) 52 | // o.AddTableHeaders("Name", "Description", "ExecutionMode", "VCS Repo", "Locked", "TF Version") 53 | 54 | // for _, i := range workspacesToNuke { 55 | // ws_repo := "" 56 | 57 | // if i.VCSRepo != nil { 58 | // ws_repo = i.VCSRepo.DisplayIdentifier 59 | // } 60 | // o.AddTableRows(i.Name, i.Description, i.ExecutionMode, ws_repo, i.Locked, i.TerraformVersion) 61 | // } 62 | // o.Close() 63 | 64 | // o.AddFormattedMessageCalculatedDanger("Are you sure you want to proceed? %d Workspaces will be deleted!", len(workspacesToNuke)) 65 | // if confirm() { 66 | // for _, i := range workspacesToNuke { 67 | // c.DestinationClient.Workspaces.DeleteByID(c.DestinationContext, i.ID) 68 | // } 69 | 70 | // o.AddFormattedMessageCalculatedDanger("%d Workspaces have been nuked!", len(workspacesToNuke)) 71 | 72 | // } else { 73 | // o.AddPassUserProvided("Nuke Disarmed!") 74 | // } 75 | 76 | // } else { 77 | // fmt.Print("No workspaces created by tfm were found") 78 | // } 79 | 80 | // return nil 81 | // } 82 | 83 | // func listWorkspaces(c tfclient.ClientContexts) []*tfe.Workspace { 84 | 85 | // workspaces := []*tfe.Workspace{} 86 | 87 | // opts := tfe.WorkspaceListOptions{ 88 | // ListOptions: tfe.ListOptions{ 89 | // PageNumber: 1, 90 | // PageSize: 100}, 91 | // } 92 | 93 | // o.AddMessageUserProvided("Getting list of workspaces from: ", c.DestinationHostname) 94 | 95 | // for { 96 | // items, err := c.DestinationClient.Workspaces.List(c.DestinationContext, c.DestinationOrganizationName, &opts) 97 | 98 | // if err != nil { 99 | // fmt.Println("Error With retrieving Workspaces from ", c.DestinationHostname, " : Error ", err) 100 | // // return err 101 | // } 102 | 103 | // workspaces = append(workspaces, items.Items...) 104 | 105 | // if items.CurrentPage >= items.TotalPages { 106 | // break 107 | // } 108 | // opts.PageNumber = items.NextPage 109 | // } 110 | 111 | // return workspaces 112 | // } 113 | 114 | // func confirm() bool { 115 | 116 | // var input string 117 | 118 | // fmt.Printf("Do you want to continue with this operation? [y|n]: ") 119 | 120 | // auto, err := NukeCmd.Flags().GetBool("autoapprove") 121 | 122 | // if err != nil { 123 | // fmt.Println("Error Retrieving autoapprove flag value: ", err) 124 | // } 125 | 126 | // // Check if --autoapprove=false 127 | // if !auto { 128 | // _, err := fmt.Scanln(&input) 129 | // if err != nil { 130 | // panic(err) 131 | // } 132 | // } else { 133 | // input = "y" 134 | // fmt.Println("y(autoapprove=true)") 135 | // } 136 | 137 | // input = strings.ToLower(input) 138 | 139 | // if input == "y" || input == "yes" { 140 | // return true 141 | // } 142 | // return false 143 | // } 144 | -------------------------------------------------------------------------------- /cmd/unlock/unlock.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package unlock 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // `tfm lock` commands 11 | var ( 12 | side string 13 | 14 | UnlockCmd = &cobra.Command{ 15 | Use: "unlock", 16 | Short: "Unlock", 17 | Long: "Unlocks objects in source or destination", 18 | } 19 | ) 20 | 21 | func init() { 22 | UnlockCmd.PersistentFlags().StringVar(&side, "side", "", "Specify source or destination side to process") 23 | } 24 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp-services/tfm 2 | 3 | go 1.23 4 | 5 | toolchain go1.23.3 6 | 7 | require ( 8 | github.com/fatih/color v1.18.0 9 | github.com/jedib0t/go-pretty v4.3.0+incompatible 10 | github.com/logrusorgru/aurora v2.0.3+incompatible 11 | github.com/mitchellh/go-homedir v1.1.0 12 | github.com/spf13/cobra v1.8.1 13 | github.com/spf13/pflag v1.0.5 14 | github.com/spf13/viper v1.19.0 15 | github.com/xanzy/go-gitlab v0.113.0 16 | ) 17 | 18 | require ( 19 | dario.cat/mergo v1.0.1 // indirect 20 | github.com/Microsoft/go-winio v0.6.2 // indirect 21 | github.com/ProtonMail/go-crypto v1.1.2 // indirect 22 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect 23 | github.com/cloudflare/circl v1.5.0 // indirect 24 | github.com/cyphar/filepath-securejoin v0.3.4 // indirect 25 | github.com/emirpasic/gods v1.18.1 // indirect 26 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 27 | github.com/go-git/go-billy/v5 v5.6.0 // indirect 28 | github.com/go-openapi/errors v0.22.0 // indirect 29 | github.com/go-openapi/strfmt v0.23.0 // indirect 30 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 31 | github.com/google/go-querystring v1.1.0 // indirect 32 | github.com/google/uuid v1.6.0 // indirect 33 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 34 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 35 | github.com/hashicorp/go-slug v0.16.4 // indirect 36 | github.com/hashicorp/go-version v1.7.0 // indirect 37 | github.com/hashicorp/jsonapi v1.4.3-0.20250220162346-81a76b606f3e // indirect 38 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 39 | github.com/kevinburke/ssh_config v1.2.0 // indirect 40 | github.com/mattn/go-colorable v0.1.13 // indirect 41 | github.com/mattn/go-isatty v0.0.20 // indirect 42 | github.com/mattn/go-runewidth v0.0.16 // indirect 43 | github.com/oklog/ulid v1.3.1 // indirect 44 | github.com/pjbgf/sha1cd v0.3.0 // indirect 45 | github.com/rivo/uniseg v0.4.7 // indirect 46 | github.com/sagikazarmark/locafero v0.6.0 // indirect 47 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 48 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 49 | github.com/skeema/knownhosts v1.3.0 // indirect 50 | github.com/sourcegraph/conc v0.3.0 // indirect 51 | github.com/xanzy/ssh-agent v0.3.3 // indirect 52 | go.mongodb.org/mongo-driver v1.17.1 // indirect 53 | go.uber.org/multierr v1.11.0 // indirect 54 | golang.org/x/crypto v0.29.0 // indirect 55 | golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect 56 | golang.org/x/net v0.31.0 // indirect 57 | golang.org/x/sync v0.10.0 // indirect 58 | golang.org/x/time v0.10.0 // indirect 59 | gopkg.in/warnings.v0 v0.1.2 // indirect 60 | ) 61 | 62 | require ( 63 | github.com/fsnotify/fsnotify v1.8.0 // indirect 64 | github.com/go-git/go-git/v5 v5.12.0 65 | github.com/google/go-github v17.0.0+incompatible 66 | github.com/hashicorp/go-tfe v1.78.0 67 | github.com/hashicorp/hcl v1.0.0 // indirect 68 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 69 | github.com/magiconair/properties v1.8.7 // indirect 70 | github.com/mitchellh/mapstructure v1.5.0 // indirect 71 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 72 | github.com/pkg/errors v0.9.1 73 | github.com/spf13/afero v1.11.0 // indirect 74 | github.com/spf13/cast v1.7.0 // indirect 75 | github.com/subosito/gotenv v1.6.0 // indirect 76 | golang.org/x/oauth2 v0.24.0 77 | golang.org/x/sys v0.29.0 // indirect 78 | golang.org/x/text v0.20.0 // indirect 79 | gopkg.in/ini.v1 v1.67.0 // indirect 80 | gopkg.in/yaml.v3 v3.0.1 // indirect 81 | ) 82 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import "github.com/hashicorp-services/tfm/cmd" 24 | 25 | func main() { 26 | cmd.Execute() 27 | } 28 | -------------------------------------------------------------------------------- /site/README.md: -------------------------------------------------------------------------------- 1 | # Docs Site Readme 2 | 3 | This directory (site/) on the `main` branch contains all the markdown for the website hosted on a custom domain https://hashicorp-services. 4 | 5 | ## How the Site is Built and Published 6 | 7 | Changes to the `main` branch in the `site` directory will trigger the [docs-deploy.yml](./../.github/workflows/docs-deploy.yml) Github Action. 8 | 9 | This will build the site and push all changes to the `gh-pages` branch, which in turn will trigger the Github action to deploy the site. 10 | 11 | The `CNAME` file **must** be present in the `site/docs/` folder for custom domain to be successful. 12 | This file is also copied into the `gh-pages` branch. 13 | 14 | ## Local Development 15 | 16 | ```sh 17 | mkdocs serve -f site/mkdocs.yml 18 | ``` 19 | 20 | -------------------------------------------------------------------------------- /site/docs/about/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md and CHANGELOG.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Request review from a repo maintainer and work to resolve any feedback/suggestions. 15 | 16 | ## Code of Conduct 17 | 18 | ### Our Pledge 19 | 20 | In the interest of fostering an open and welcoming environment, we as 21 | contributors and maintainers pledge to making participation in our project and 22 | our community a harassment-free experience for everyone, regardless of age, body 23 | size, disability, ethnicity, gender identity and expression, level of experience, 24 | nationality, personal appearance, race, religion, or sexual identity and 25 | orientation. 26 | 27 | ### Our Standards 28 | 29 | Examples of behavior that contributes to creating a positive environment 30 | include: 31 | 32 | * Using welcoming and inclusive language 33 | * Being respectful of differing viewpoints and experiences 34 | * Gracefully accepting constructive criticism 35 | * Focusing on what is best for the community 36 | * Showing empathy towards other community members 37 | 38 | Examples of unacceptable behavior by participants include: 39 | 40 | * The use of sexualized language or imagery and unwelcome sexual attention or 41 | advances 42 | * Trolling, insulting/derogatory comments, and personal or political attacks 43 | * Public or private harassment 44 | * Publishing others' private information, such as a physical or electronic 45 | address, without explicit permission 46 | * Other conduct which could reasonably be considered inappropriate in a 47 | professional setting 48 | 49 | ### Our Responsibilities 50 | 51 | Project maintainers are responsible for clarifying the standards of acceptable 52 | behavior and are expected to take appropriate and fair corrective action in 53 | response to any instances of unacceptable behavior. 54 | 55 | Project maintainers have the right and responsibility to remove, edit, or 56 | reject comments, commits, code, wiki edits, issues, and other contributions 57 | that are not aligned to this Code of Conduct, or to ban temporarily or 58 | permanently any contributor for other behaviors that they deem inappropriate, 59 | threatening, offensive, or harmful. 60 | 61 | ### Scope 62 | 63 | This Code of Conduct applies both within project spaces and in public spaces 64 | when an individual is representing the project or its community. Examples of 65 | representing a project or community include using an official project e-mail 66 | address, posting via an official social media account, or acting as an appointed 67 | representative at an online or offline event. Representation of a project may be 68 | further defined and clarified by project maintainers. 69 | 70 | ### Enforcement 71 | 72 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 73 | reported by contacting the project team. All 74 | complaints will be reviewed and investigated and will result in a response that 75 | is deemed necessary and appropriate to the circumstances. The project team is 76 | obligated to maintain confidentiality with regard to the reporter of an incident. 77 | Further details of specific enforcement policies may be posted separately. 78 | 79 | Project maintainers who do not follow or enforce the Code of Conduct in good 80 | faith may face temporary or permanent repercussions as determined by other 81 | members of the project's leadership. 82 | 83 | ### Attribution 84 | 85 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 86 | available at [http://contributor-covenant.org/version/1/4][version] 87 | 88 | [homepage]: http://contributor-covenant.org 89 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /site/docs/about/purpose.md: -------------------------------------------------------------------------------- 1 | # Why TFM? 2 | 3 | If you are asking why does this CLI exist, read on... 4 | 5 | HashiCorp Implementation Services have been helping customers get started with Terraform Enterprise in the last 5-6 years. As some of these customers have matured and situations have changed, some customers are asking how they would migrate to Terraform Cloud. 6 | 7 | A small group of individuals have already gone through some skunk work engagements for existing customers to migrate from TFE to TFC, however it was very clunky, slow and very custom. At the same time the experience and knowledged gained have insightful on what was needed if a tool was made to assist migrations of this very nature. 8 | 9 | There have been multiple examples in the community of a tool or scripts to assist with migration, however we wanted to provide a CLI binary that could be used in our initial migration services offering as well as left with the customer to continue any future migrations. 10 | 11 | Our migration services program would : 12 | 13 | - teach the customer how to migrate 14 | - plan how they can migrate from TFE to TFC 15 | - assist with a few migrations using our tool 16 | - then allow them to continue migrations in futures for any workspaces that required more time and planning. 17 | 18 | We also had aspirations that this tool could be repeatably used by us or a customer in a CI pipeline of some sort to ensure or keep track of migrations of workspaces from TFE to TFC. 19 | -------------------------------------------------------------------------------- /site/docs/code/future.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | In between engagments, across variouble global timezones, the IS team asynchronysly worked on developing a MVP of `tfm`. 4 | 5 | !!! note "" 6 | Note: The IS Team had to learn Go and other toolsets as we all come from a sys admin and infrastructure engineer background. It has been a massive mind shift and challenge for the group that have embarked on this project. 7 | 8 | ![tfm-history-1](../images/tfm-history-1.png) 9 | 10 | ![tfm-history-2](../images/tfm-history-2.png) 11 | 12 | 13 | ## MVP Pre-Alpha Release 14 | 15 | Our MVP allows a customer to: 16 | 17 | - Migrate Workspaces 1 to 1 from source to destination 18 | - Migrate Workspace settings 19 | - Migrate Workspace States 20 | - Migrate Workspace Variables and Variable Sets 21 | - Migrate Workspace Team Access and Permissions 22 | - Assign VCS connections to Destination Workspaces based on a Map provided in the config file 23 | 24 | 25 | 26 | ## Future Work and Roadmap 27 | 28 | !!! warning "" 29 | Note: We are looking for internal Beta testers to validate the migration process and provide feedback on scenarios our customers may encounter when migrating from TFE to TFC. 30 | 31 | Please reach out to one of our listed [contact methods](#contacts) to get involved. 32 | 33 | 34 | Depending on time and pipeline, we do hope we can continue to progress this project to provide value to our customers. 35 | 36 | Check out our [Github Projects page](https://github.com/orgs/hashicorp-services/projects/6). 37 | 38 | Got an idea for a feature to `tfm`? Submit a [feature request](https://github.com/hashicorp-services/tfm/issues/new?assignees=&labels=&template=feature_request.md&title=)! 39 | 40 | 41 | -------------------------------------------------------------------------------- /site/docs/code/mvp.md: -------------------------------------------------------------------------------- 1 | # MVP 2 | 3 | Our MVP is defined as a **Migration is accomplished by gathering facts/properties about the SOURCE objects and recreating them in the DESTINATION. Migration is not moving data, it is recreating it.** 4 | 5 | ## The MVP tool should have the ability to: 6 | 7 | - Migrate Workspaces 1 to 1 from source to destination 8 | - Migrate Workspace settings 9 | - Migrate Workspace States 10 | - Migrate Workspace Variables and Variable Sets 11 | - Migrate Workspace Team Access and Permissions 12 | - Assign VCS connections to Destination Workspaces based on a Map provided in the config file 13 | 14 | 15 | 16 | ## Roadmap 17 | 18 | There are more features we want to bring om the future. 19 | 20 | Check out our [Road to TFM 1.0 project board](https://github.com/orgs/hashicorp-services/projects/6). -------------------------------------------------------------------------------- /site/docs/code/project-details.md: -------------------------------------------------------------------------------- 1 | # Project Details 2 | 3 | 4 | 5 | ## Repository Layout 6 | Information on how this repository is structured. 7 | 8 | ### Files 9 | 10 | ``` 11 | ├── cmd 12 | │ ├── copy 13 | │ ├── helper 14 | │ ├── list 15 | │ └── root.go 16 | ├── main.go 17 | ├── output 18 | ├── site 19 | ├── test 20 | ├── tfclient 21 | ├── tfm 22 | └── version 23 | ``` 24 | 25 | `main.go` 26 | 27 | - Entry point for the CLI, not much code here 28 | 29 | `go.mod` and `go.sum` 30 | 31 | - Go package dependencies 32 | 33 | `CHANGELOG.md` 34 | 35 | - Repository Changes by release 36 | 37 | `version/version.go` 38 | 39 | - Isolated package to create a struct for Version information 40 | 41 | `cmds/` 42 | 43 | - Directory for all commands/subcommands 44 | 45 | `cmds/copy` 46 | 47 | - each subcommand is placed into it's own directory package 48 | 49 | `cmds/list` 50 | 51 | - each subcommand is placed into it's own directory package 52 | 53 | 54 | `cmds/helper` 55 | 56 | - Package that creates an easy set of functions to cleanly retrieve flag values regardless of how they were set 57 | 58 | 59 | `tfclient` 60 | 61 | - Package for to setup a `go-tfe` source and destination context to interact with the TFC/TFE APIs. 62 | 63 | 64 | `output` 65 | 66 | - Package to assist with outputing information for the user. 67 | 68 | `docs` 69 | 70 | - Directory for documentation about the tool. Powered by MKDocs and hosted on GitHub Pages 71 | 72 | `.gihub/worksflow` 73 | 74 | - Github Action workflows 75 | - `main.tml` automated builds 76 | - `release.yaml` binary release build 77 | - `unit-test.yml` automated features testing pipeline 78 | - `docs-deploy.yml` deployment of TFM Docs to GitHub pages 79 | 80 | 81 | ## Technologies 82 | 83 | ### Language: 84 | - `Go` , chosen to have the ability to cross compile for multiple operating systems. 85 | 86 | ### Go Libraries 87 | - [`cobra`](https://github.com/spf13/cobra), chosen to provide a modern CLI interface experience. 88 | - [`viper`](https://github.com/spf13/viper), chosen to handle configuration files 89 | - [`go-tfe`](https://github.com/hashicorp/go-tfe), Go API client for TFE/TFC 90 | 91 | 92 | ## Architectural Decisions 93 | 94 | See [Architectural Decisions Records](./adr.md) to understand how and why `tfm` has been built. 95 | 96 | 97 | 98 | ## History and Future of TFM 99 | 100 | See [Future Work / Roadmap](./future.md) -------------------------------------------------------------------------------- /site/docs/commands/copy.md: -------------------------------------------------------------------------------- 1 | # Copy 2 | 3 | ![tfm-copy](../images/tfe_tfm_tfc.png) 4 | 5 | Copy or Migrate, this sub commands takes X from source organization and copies (or migrates) it to the destination organization. 6 | 7 | ```sh 8 | # tfm copy -h 9 | 10 | Copy objects from Source Organization to Destination Organization 11 | 12 | Usage: 13 | tfm copy [command] 14 | 15 | Available Commands: 16 | teams Copy Teams 17 | varsets Copy Variable Sets 18 | workspaces Copy Workspaces 19 | 20 | Flags: 21 | -h, --help help for copy 22 | 23 | Global Flags: 24 | --config string Config file, can be used to store common flags, (default is ./.tfm.hcl). 25 | 26 | Use "tfm copy [command] --help" for more information about a command. 27 | ``` 28 | 29 | ## Copy sub commands 30 | 31 | - [`tfm copy teams`](copy_teams.md) 32 | - [`tfm copy varsets`](copy_varsets.md) 33 | - [`tfm copy workspaces`](copy_workspaces.md) 34 | 35 | ## Possible Future copy commands enhancements 36 | 37 | - `tfm copy modules` 38 | - `tfm copy policy-sets` 39 | - `tfm copy workspace --all` 40 | 41 | Got an idea for a feature to `tfm`? Submit a [feature request](https://github.com/hashicorp-services/tfm/issues/new?assignees=&labels=&template=feature_request.md&title=)! 42 | -------------------------------------------------------------------------------- /site/docs/commands/copy_projects.md: -------------------------------------------------------------------------------- 1 | # tfm copy projects 2 | 3 | `tfm copy projects` will take source organization projects and create them in the destination organization. 4 | 5 | ![copy_projects](../images/copy_projects.png) 6 | 7 | ## Copy ALL projects 8 | 9 | `tfm copy projects` will copy all source projects and create them in the destination organization. 10 | 11 | Users will be required to confirm all projects is the desired operation if no `projects` list or `projects-map` map is not found in tfm config file (eg `~/.tfm.hcl`). 12 | 13 | To automate the confirmation, the flag `--autoapprove=true` can be provided during a `tfm` run. 14 | 15 | ## Copy a list of projects 16 | 17 | As part of the HCL config file (`/home/user/.tfm.hcl`), a list of projects from the source TFE can be specified. `tfm` will use this list when running `tfm copy projects` and ensure the project exists or is created in the target. 18 | 19 | ``` terraform 20 | #List of projects to create/check are migrated across to new TFC 21 | "projects" = [ 22 | "appAFrontEnd", 23 | "appABackEnd", 24 | "appBDataLake", 25 | "appBInfra" 26 | ] 27 | 28 | ``` 29 | 30 | ## Rename projects in destination during a copy 31 | 32 | As part of the HCL config file (`/home/user/.tfm.hcl`), a list of `source-project-name=destination-project-name` can be provided. `tfm` will use this list when running `tfm copy project` to look at all projects in the source host and rename the destination project name. 33 | 34 | !!! note "" 35 | *NOTE: Using the 'projects-map' configuration in your HCL config file will take precedence over the other 'projects' list feature which only lists source project names.* 36 | 37 | ```terraform 38 | # A list of source=destination project names. TFM will look at each source project and recreate the project with the specified destination name. 39 | "projects-map"=[ 40 | "projectA=NewProjectA", 41 | "projectZ=NewProjectX" 42 | ] 43 | -------------------------------------------------------------------------------- /site/docs/commands/copy_teams.md: -------------------------------------------------------------------------------- 1 | # tfm copy teams 2 | 3 | `tfm copy teams` will take source organization teams and create them in the destination organization. 4 | 5 | ![copy_teams](../images/copy_teams.png) -------------------------------------------------------------------------------- /site/docs/commands/copy_varsets.md: -------------------------------------------------------------------------------- 1 | # tfm copy varsets 2 | 3 | `tfm copy varsets` will take source organization variable sets and create them in the destination organization. 4 | 5 | 6 | ![copy_varsets](../images/copy_varsets.png) -------------------------------------------------------------------------------- /site/docs/commands/copy_workspace_agents.md: -------------------------------------------------------------------------------- 1 | # tfm copy workspaces --agents 2 | 3 | `tfm copy workspaces --agents` or `tfm copy ws --agent` assigns a workspaces' agents from source to destination org. 4 | 5 | !!! note "" 6 | *NOTE: A agent-pool mapping must be provided in TFM's config file * 7 | 8 | 9 | 10 | As part of the HCL config file (`/home/user/.tfm.hcl`), a list of `source-agent-pool-ID=destination-agent-pool-ID` can be provided. `tfm` will use this list when running `tfm copy workspaces --agents` to look at all workspaces in the source host with the assigned source agent pool ID and assign the matching named workspace in the destination with the mapped destination agent pool ID. 11 | 12 | ```terraform 13 | # A list of source=destination agent pool IDs TFM will look at each workspace in the source for the source agent pool ID and assign the matching workspace in the destination the destination agent pool ID. 14 | agents-map = [ 15 | "apool-DgzkahoomwHsBHcJ=apool-vbrJZKLnPy6aLVxE", 16 | "apool-DgzkahoomwHsBHc3=apool-vbrJZKLnPy6aLVx4", 17 | "apool-DgzkahoomwHsB125=apool-vbrJZKLnPy6adwe3", 18 | "test=beep" 19 | ] 20 | ``` 21 | 22 | -------------------------------------------------------------------------------- /site/docs/commands/copy_workspace_remote_state_sharing.md: -------------------------------------------------------------------------------- 1 | # tfm copy workspaces --remote-state-sharing 2 | 3 | `tfm copy workspaces --remote-state-sharing` is used to set the remote state sharing setting on a workspace. 4 | 5 | ![copy_ws_remote_state_sharing](../images/copy_ws_remote_state_sharing.png) 6 | 7 | This flag is designed for users that want to copy the workspace state sharing setting to the workspaces in the destination. 8 | 9 | A workspace can be configured to share the state org wide or to selected workspaces. By default a workspace is set to being shared to selected workspaces, with no workspaces added. 10 | This flag will go through each workspace on the source organization, check the state sharing setting, and on the destination workspace configure it to be shared org wide, shared to selected workspaces with no workspaces added or shared to selected workspaces, with adding workspaces with matching names from the source workspace to the destination workspace. 11 | 12 | # tfm copy workspaces --remote-state-sharing --consolidate-global 13 | 14 | There is one additional flag that can be used with `--remote-state-sharing` which is `--consolidate-global` 15 | 16 | ![copy_ws_remote_state_sharing_org_consolidation](../images/copy_ws_remote_state_sharing_org_consolidation.png) 17 | 18 | This flag is to be used for companies that are doing an organization consolidation with their migration. For example, a company running TFE may have 10 orgs each with 10 workspaces, but in HCP Terraform they will have 1 orginzation with 100 workspaces. In one of the source orgs, they have 1 workspace that is shared with 9 other workspaces. When tfm migrates this workspace state sharing setting, in the destination org, that workspace would then be shared with 99 workspaces instead of just 9. 19 | 20 | The `--consolidate-global` flag is designed to prevent this from happening. For workspaces that are shared with no workspaces or shared with a list of workspaces that functionality is unchanged from above. However if a workspace is shared org wide tfm will behave differently and perform the following step: 21 | 22 | 1. tfm will first set the destination workspace to being shared with selected workspaces only 23 | 2. List all workspaces in the source org 24 | 3. List all workspaces in the destination org 25 | 4. Filter the two lists to only return workspaces that have the same name as workspaces from the source org 26 | 5. Set that returned list of workspaces to be shared on the workspace in the destination org. 27 | 28 | This will result in the workspaces that are being shared org wide in the source org to only being shared with selected workspaces in the destination org and prevent the oversharing of the state with workspaces that shouldn't be accessing the state. 29 | -------------------------------------------------------------------------------- /site/docs/commands/copy_workspace_run_triggers.md: -------------------------------------------------------------------------------- 1 | # tfm copy workspaces --run-triggers 2 | 3 | `tfm copy workspaces --run-triggers` is used to copy run triggers that have been configured on a source workspace to the destination workspace. 4 | 5 | ![copy_ws_run_triggers](../images/copy_ws_run_triggers.png) 6 | 7 | A workspace can be configured so that when one or more source workspaces have a successful apply of runs, this will trigger a new run in this workspace. By default it will only do an automatic plan that will require confirmation before applying, however the Auto-apply run triggers can be enabled so it will automatically apply a run if changes are detected. 8 | 9 | There is a limitation as of now, that the workspaces can't be changing names between the source and destination. 10 | For example: 11 | Workspace "app-1" is configured to have a run trigger from a workspace named "networking". When this flag is used, tfm will lookup the workspace names that have run triggers assigned on the "app-1" workspace. It will then on the migrated "app-1" workspace in the destination try and create a run trigger for the "app-1" workspace linked to a workspace named "networking" in the destination environment. If the "network" workspace is named differently in the destination tfm won't be able to find a matching workspace and won't create the run trigger 12 | -------------------------------------------------------------------------------- /site/docs/commands/copy_workspace_state.md: -------------------------------------------------------------------------------- 1 | # tfm copy workspaces --state 2 | 3 | `tfm copy workspaces --state` or `tfm copy ws --state` copies a workspaces' states from source to destination org. 4 | 5 | In the event a state file encounters an error when attempting to migrate, TFM will stop migrating state files for that particular workspace and move to the next workspace. 6 | 7 | ![copy_ws_state](../images/copy_ws_state.png) 8 | 9 | # tfm copy workspaces --state --last X 10 | 11 | `tfm copy workspaces --state --last X` or `tfm copy ws --state --last X` copies the last X number of workspaces' states from source to destination org. 12 | 13 | This flag is designed for users who only want to copy the last X number of states from a workspace. 14 | 15 | !!! WARNING "" 16 | **WARNING: This operation should not be ran more than once** 17 | 18 | In the event a state file encounters an error when attempting to migrate, TFM will stop migrating state files for that particular workspace and move to the next workspace. 19 | 20 | ![copy_ws_state_last_x](../images/copy_ws_state_last_x.png) 21 | -------------------------------------------------------------------------------- /site/docs/commands/copy_workspace_teamaccess.md: -------------------------------------------------------------------------------- 1 | # tfm copy workspaces --teamaccess 2 | 3 | `tfm copy workspaces --teamaccess` or `tfm copy ws --teamaccess` copies a workspaces' team access from source to destination org. 4 | 5 | !!! warning "" 6 | *NOTE: Teams must exist in the destination.* 7 | 8 | ![copy_ws_teamaccess](../images/copy_ws_teamaccess.png) 9 | -------------------------------------------------------------------------------- /site/docs/commands/copy_workspace_variables.md: -------------------------------------------------------------------------------- 1 | # tfm copy workspaces --vars 2 | 3 | `tfm copy workspaces --vars` or `tfm copy ws --vars` copies a workspaces' variables from source to destination org. 4 | 5 | !!! note "" 6 | *NOTE: Any sensitive variables will ONLY be created in the destination. These values will need to be populated* 7 | 8 | ![copy_ws_vars](../images/copy_ws_vars.png) 9 | -------------------------------------------------------------------------------- /site/docs/commands/copy_workspace_vcs.md: -------------------------------------------------------------------------------- 1 | # tfm copy workspaces --vcs 2 | 3 | `tfm copy workspaces --vcs` or `tfm copy ws --vcs` assigns a vcs' from source to destination org. 4 | 5 | !!! note "" 6 | *NOTE: A vcs mapping must be provided in TFM's* 7 | 8 | As part of the HCL config file (`/home/user/.tfm.hcl`), a list of `source-vcs-oauth-ID=destination-vcs-oauth-id-ID` can be provided. `tfm` will use this list when running `tfm copy workspaces --vcs` to look at all workspaces in the source host with the assigned source VCS oauth ID and assign the matching named workspace in the destination with the mapped destination VCS oauth ID. 9 | 10 | ```terraform 11 | # A list of source=destination VCS oauth IDs. TFM will look at each workspace in the source for the source VCS oauth ID and assign the matching workspace in the destination with the destination VCS oauth ID. 12 | vcs-map=[ 13 | "ot-5uwu2Kq8mEyLFPzP=ot-coPDFTEr66YZ9X9n", 14 | "ot-gkj2An452kn2flfw=ot-8ALKBaqnvj232GB4", 15 | ] 16 | ``` 17 | 18 | ![copy_ws_vcs](../images/copy_ws_vcs.png) 19 | -------------------------------------------------------------------------------- /site/docs/commands/copy_workspaces.md: -------------------------------------------------------------------------------- 1 | # tfm copy workspaces 2 | 3 | `tfm copy workspaces` or `tfm copy ws` creates a workspaces from source to destination org. 4 | 5 | After the workspaces are created at the destination, the next step is to copy the rest of the workspaces settings such as [state](copy_workspace_state.md), [variables](copy_workspace_variables.md), [team access](copy_workspace_teamaccess.md), [remote state sharing](copy_workspace_remote_state_sharing.md) etc using `tfm copy workspace` flags. 6 | 7 | `tfm copy workspaces -h` 8 | 9 | ```bash 10 | Copy Workspaces from source to destination org 11 | 12 | Usage: 13 | tfm copy workspaces [flags] 14 | 15 | Aliases: 16 | workspaces, ws 17 | 18 | Flags: 19 | --agents Mapping of source Agent Pool IDs to destination Agent Pool IDs in config file 20 | --consolidate-global Consolidate global remote state sharing settings. Must be used with --remote-state-sharing flag 21 | -h, --help help for workspaces 22 | -l, --last int Copy the last X number of state files only. 23 | --lock Lock all source workspaces 24 | --remote-state-sharing Copy remote state sharing settings 25 | --skip-sensitive-vars Skip copying sensitive variables. Must be used with --vars flag 26 | --ssh Mapping of source ssh id to destination ssh id in config file 27 | --state Copy workspace states 28 | --teamaccess Copy workspace Team Access 29 | --unlock Unlock all source workspaces 30 | --vars Copy workspace variables 31 | --vcs Mapping of source vcs Oauth ID to destination vcs Oath in config file 32 | --workspace-id string Specify one single workspace ID to copy to destination 33 | 34 | Global Flags: 35 | --config string Config file, can be used to store common flags, (default is ./.tfm.hcl). 36 | ``` 37 | 38 | ## Copy ALL workspaces 39 | 40 | Without providing any flags, `tfm copy workspaces` will copy all source workspaces and create them in the destination organization. 41 | 42 | Users will be required to confirm all workspaces is the desired operation if no `workspaces` or `workspaces-map` is not found in tfm config file (eg `~/.tfm.hcl`). 43 | 44 | ![tfm_cp_ws_confirm](../images/tfm_copy_ws_confirm.png) 45 | 46 | To automate the confirmation, the flag `--autoapprove=true` can be provided during a `tfm` run. 47 | 48 | ![tfm_cp_ws_confirm_autoapprove](../images/tfm_copy_ws_confirm_autoapprove.png) 49 | 50 | ## Copy a list of workspaces 51 | 52 | As part of the HCL config file (`/home/user/.tfm.hcl`), a list of workspaces from the source TFE can be specified. `tfm` will use this list when running `tfm copy workspaces` and ensure the workspace exists or is created in the target. 53 | 54 | ``` terraform 55 | #List of Workspaces to create/check are migrated across to new TFC 56 | "workspaces" = [ 57 | "appAFrontEnd", 58 | "appABackEnd", 59 | "appBDataLake", 60 | "appBInfra" 61 | ] 62 | 63 | ``` 64 | 65 | ## Rename Workspaces in destination during a copy 66 | 67 | As part of the HCL config file (`/home/user/.tfm.hcl`), a list of `source-workspace-name=destination-workspace-name` can be provided. `tfm` will use this list when running `tfm copy workspace` to look at all workspaces in the source host and rename the destination workspace name. 68 | 69 | !!! note "" 70 | *NOTE: Using the 'workspaces-map' configuration in your HCL config file will take precedence over the other 'workspaces' list feature which only lists source workspace names.* 71 | 72 | ```terraform 73 | # A list of source=destination workspace names. TFM will look at each source workspace and recreate the workspace with the specified destination name. 74 | "workspaces-map"=[ 75 | "tfc-mig-vcs-0=tfc-mig-vcs-0", 76 | "tfc-mig-vcs-1=tfc-mig-vcs-1", 77 | "tfc-mig-vcs-2=tfc-mig-vcs-2", 78 | "tfc-mig-vcs-3=tfc-mig-vcs-30", 79 | "tfc-mig-vcs-4=tfc-mig-vcs-40", 80 | ]s 81 | ``` 82 | 83 | ![copy_ws](../images/copy_ws.png) 84 | 85 | ## Existing Workspaces in Destination 86 | 87 | Any existing workspaces in the destination will be skipped. 88 | 89 | ![copy_ws_exist](../images/copy_ws_exists.png) 90 | 91 | ## Copy Workspaces into Projects 92 | 93 | By default, a workspace will be copied over to the Default Project in the destination (eg TFC). 94 | Users can specify the project ID for the desired project to place all workspaces in the `tfm copy workspace` run. 95 | 96 | Utilize [`tfm list projects --side destination`](../commands/list_projects.md#side-flag) to determine the `project id`. 97 | 98 | Set either the environment variable: 99 | 100 | ```bast 101 | export DST_TFC_PROJECT_ID=prj-XXXX 102 | ``` 103 | 104 | or specify the following in your `~/.tfm.hcl` configuration file. 105 | 106 | ```terraform 107 | dst_tfc_project_id=prj-xxx 108 | ``` 109 | -------------------------------------------------------------------------------- /site/docs/commands/core.md: -------------------------------------------------------------------------------- 1 | # Core 2 | 3 | `tfm core` 4 | 5 | The command used to prefix all commands related to assiting in migrating from terraform open source / community edition (also known as terraform core) to TFC/TFE. 6 | 7 | ```stdout 8 | tfm core -h 9 | Command used to perform terraform open source (core) to TFE/TFC migration commands 10 | 11 | Usage: 12 | tfm core [command] 13 | 14 | Available Commands: 15 | cleanup Removes up all cloned repositories from the clone_repos_path. 16 | clone Clone VCS repositories containing terraform code. 17 | create-workspaces Create TFE/TFC workspaces for each cloned repo in the clone_repos_path that contains a pulled_terraform.tfstate file. 18 | getstate Initialize and get state from terraform repos in the clone_repos_path. 19 | init-repos Scan cloned repositories for Terraform configurations and build metadata 20 | link-vcs Link repos in the clone_repos_path to their corresponding workspaces in TFE/TFC. 21 | migrate Migrates opensource/community edition Terraform code and state to TFE/TFC in 1 continuous workflow. 22 | remove-backend Create a branch, remove Terraform backend configurations from cloned repos in clone_repos_path, commit the changes, and push to the origin. 23 | upload-state Upload .terraform/pulled_terraform.tfstate files from repos cloned into the clone_repos_path to TFE/TFC workspaces. 24 | 25 | Flags: 26 | -h, --help help for core 27 | 28 | Global Flags: 29 | --autoapprove Auto approve the tfm run. --autoapprove=true . false by default 30 | --config string Config file, can be used to store common flags, (default is ~/.tfm.hcl). 31 | --json Print the output in JSON format 32 | ``` 33 | 34 | Got an idea for a feature to `tfm`? Submit a [feature request](https://github.com/hashicorp-services/tfm/issues/new?assignees=&labels=&template=feature_request.md&title=)! -------------------------------------------------------------------------------- /site/docs/commands/core_cleanup.md: -------------------------------------------------------------------------------- 1 | # tfm core cleanup 2 | 3 | `tfm core cleanup` deletes all of the cloned repositories from the `clone_repos_path`. 4 | 5 | # Flags 6 | 7 | `--autoapprove` will automatically approve the operation without confirmation prompt. 8 | -------------------------------------------------------------------------------- /site/docs/commands/core_clone.md: -------------------------------------------------------------------------------- 1 | # tfm core clone 2 | 3 | `tfm core clone` will clone VCS repositories to the local system into the `clone_repos_path` you have defined in the config file. 4 | 5 | ## Supported VCS 6 | 7 | - github 8 | - gitlab 9 | 10 | ## Requirements 11 | 12 | - A [supported](../migration/supported-vcs.md) `vcs_type` must be configured with one of the tfm supported VCS providers in the tfm config file. 13 | 14 | ``` 15 | vcs_type = github 16 | ``` 17 | 18 | ### Credentials 19 | 20 | tfm will used the following configuration file settings to authenticate to VCS and clone the repos: 21 | 22 | The api token must have read access to the repositories to clone them. 23 | 24 | #### github 25 | 26 | ``` 27 | github_token = "api token" 28 | github_organization = "org" 29 | github_username = "username" 30 | ``` 31 | 32 | #### gitlab 33 | 34 | ``` 35 | gitlab_token = "api token" 36 | gitlab_group = "group102109" 37 | gitlab_username = "username" 38 | ``` 39 | 40 | ## Clone a List of Repositories 41 | 42 | Provide a `repos_to_clone` list in the config file of repositories that you would like to clone for migration. 43 | 44 | ```hcl 45 | repos_to_clone = [ 46 | "repos1", 47 | "repo2", 48 | "repo3" 49 | ] 50 | ``` 51 | 52 | ## Clone All Repositories 53 | 54 | Not providing a `repos_to_clone` list will result in tfm attempting to clone every repository in the GitHub org or GitLab group. 55 | -------------------------------------------------------------------------------- /site/docs/commands/core_create-workspaces.md: -------------------------------------------------------------------------------- 1 | # tfm core create-workspaces 2 | 3 | ## Requirements 4 | 5 | - A `terraform_config_metadata.json` must exist in the tfm working directory. Run `tfm core init-repos` to generate one. 6 | - Configure the following credentials in the tfm config file: 7 | 8 | ```hcl 9 | dst_tfc_hostname="app.terraform.io" 10 | dst_tfc_org="organization" 11 | dst_tfc_token="A token with permissions to create TFC/TFE workspaces" 12 | ``` 13 | 14 | ## Create Workspaces 15 | 16 | `tfm core create-workspaces` will use the `terraform_config_metadata.json` config file to create a TFC/TFE workspace in the `dst_tfc_org` defined in the config file for each repository. 17 | 18 | Workspace names are generated using the metadata file in the following format: 19 | 20 | - Config path using terraform ce workspaces: 21 | `repo_name+config_path+workspace_name` 22 | 23 | - Config path without terraform ce workspaces: 24 | `repo_name+config_path` 25 | 26 | As an example, the below metadata would create 2 TFC/TFE workspaces with the names: 27 | 28 | - `isengard-infra-east-primary-newisengard` 29 | - `isengard-infra-east-primary-oldisengard` 30 | 31 | ```json 32 | { 33 | "repo_name": "isengard", 34 | "config_paths": [ 35 | { 36 | "path": "isengard/infra/east/primary", 37 | "workspace_info": { 38 | "uses_workspaces": true, 39 | "workspace_names": [ 40 | "default", 41 | "newisengard", 42 | "oldisengard" 43 | ] 44 | } 45 | }, 46 | ``` 47 | 48 | If a workspace already exists with the name of the repository tfm will return an error and continue on to the next workspace creation attempt. 49 | 50 | 53 | 54 | ## Out of Band Workspace Creation 55 | 56 | You can create workspaces using the terraform tfe provider instead of tfm. As long as the workspace names match the constructed workspace name that tfm is looking for then the state will still be uploaded. 57 | 58 | ## Future Updates 59 | 60 | Future updates are planned to also allow a variable set containing credentials to be assigned to the workspace the the time of creation. This will allow a plan to be run post migration to verify no changes are expected. 61 | -------------------------------------------------------------------------------- /site/docs/commands/core_getstate.md: -------------------------------------------------------------------------------- 1 | # tfm core getstate 2 | 3 | ## Requirements 4 | 5 | - Terraform community edition must be installed and in your path in the environment where this command is run. 6 | - Credentials to authenticate to the configured backend must be configured in the environment where this command is run. 7 | - A `terraform_config_metadata.json` must exist in the tfm working directory. Run `tfm core init-repos` to generate one. 8 | 9 | ## Get State 10 | 11 | `tfm core getstate` will use the `terraform_config_metadata.json` config file to iterate through all of the cloned repositories in the `clone_repos_path` and metadata `config_paths` to download the state files from the backend. 12 | 13 | tfm will use the locally installed terraform binary to perform `terraform init` and `terraform state pull > .terraform/pulled_terraform.tfstate` commands. 14 | 15 | If tfm cannot successfully run a `terraform init` for a cloned repo tfm will return an error and continue with the next repository initilization attempt. 16 | 17 | ## Terraform CE Workspaces 18 | 19 | For any `config_path` with `uses_workspaces: true`, tfm will run `tfm workspace select` for each workspace in the `workspace_names` list and `terraform state pull > .terraform/pulled__terraform.tfstate`. The end result will be multiple state files within the `config_path` for each workspace. 20 | -------------------------------------------------------------------------------- /site/docs/commands/core_init-repos.md: -------------------------------------------------------------------------------- 1 | # tfm core init-repos 2 | 3 | ## Requirements 4 | 5 | - Terraform community edition must be installed and in your path in the environment where this command is run. 6 | - Credentials to authenticate to the configured backend must be configured in the environment where this command is run. 7 | 8 | ## Init Repos 9 | `tfm core init-repos` will iterate through the cloned repositories in the `clone_repos_path` and build a `terraform_config_metadata.json` file that contains information about how the repositories are configured. It will determine all paths within the repo that contain terraform configurations and if terraform CE workspaces are being used. 10 | 11 | Any directory containing a `.tf` file with a `backend {}` configuration contained within a `terraform {}` configuration will be added to the metadata file as a `config_path`. 12 | 13 | ```json 14 | "repo_name": "mordor2", 15 | "config_paths": [ 16 | { 17 | "path": "mordor2/deployments/dev", 18 | "workspace_info": { 19 | "uses_workspaces": false, 20 | "workspace_names": [ 21 | "default" 22 | ] 23 | } 24 | }, 25 | { 26 | "path": "mordor2/deployments/prod", 27 | "workspace_info": { 28 | "uses_workspaces": false, 29 | "workspace_names": [ 30 | "default" 31 | ] 32 | } 33 | }, 34 | ``` 35 | 36 | Any `config_path` that displays a terraform workspace in addition to the default one will be considered to be using terraform CE workspaces and `uses_workspaces` will be `true`. `workspace_names` will be populated with all of the terraform CE workspaces in use for the `config_path`. 37 | 38 | ```json 39 | { 40 | "repo_name": "rivendell", 41 | "config_paths": [ 42 | { 43 | "path": "rivendell", 44 | "workspace_info": { 45 | "uses_workspaces": true, 46 | "workspace_names": [ 47 | "default", 48 | "newrivendell", 49 | "oldrivendell" 50 | ] 51 | } 52 | } 53 | ] 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /site/docs/commands/core_link-vcs.md: -------------------------------------------------------------------------------- 1 | # tfm core link-vcs 2 | 3 | ## Requirements 4 | 5 | - A `terraform_config_metadata.json` must exist in the tfm working directory. Run `tfm core init-repos` to generate one. 6 | - The VCS provider must be configured in TFE/TFC and you must provide the VCS providers Oauth ID as the `vcs_provider_id` in the config file. 7 | - Configure the following credentials in the tfm config file: 8 | 9 | ```hcl 10 | dst_tfc_hostname="app.terraform.io" 11 | dst_tfc_org="organization" 12 | dst_tfc_token="A token with permissions to create TFC/TFE workspaces" 13 | ``` 14 | 15 | ## Link VCS 16 | `tfm core link-vcs` will add the VCS connection to the workspaces that were created using `tfm core create-workspaces`. tfm will use the `terraform_config_metadata.json` file and look at each `config_path` and find a matching TFC/TFE workspace with a name that matches to the path. 17 | 18 | tfm will update the workspace settings using the `vcs_provider_id` defined in the config file and the `repo_name` that the `config_path` belongs to and map the repo to the workspace. 19 | 20 | ## Out of Band Workspace Creation 21 | 22 | You can create workspaces using the terraform tfe provider instead of tfm. As long as the workspace names match the constructed workspace name that tfm is looking for then the state will still be uploaded. See the documentation for the `tfm create-workspaces` command for more information regarding workspace name creation. 23 | 24 | 27 | -------------------------------------------------------------------------------- /site/docs/commands/core_migrate.md: -------------------------------------------------------------------------------- 1 | # tfm core migrate 2 | 3 | 4 | ## Requirements 5 | 6 | - The VCS provider must be configured in TFE/TFC and you must provide the VCS providers Oauth ID as the `vcs_provider_id` in the config file. 7 | - Configure the `clone_repos_path` in the config file. 8 | - Configure the `vcs_type` with a [supported vcs types](../migration/supported-vcs.md) in the config file. 9 | - Authentication credentials for the cloned terraform configuration backends must be configured in the environment. 10 | - Terraform CLI must be installed in the environment and in the path. 11 | - Configure the following credentials in the tfm config file: 12 | 13 | ```hcl 14 | dst_tfc_hostname="app.terraform.io" 15 | dst_tfc_org="organization" 16 | dst_tfc_token="A token with permissions to create TFC/TFE workspaces" 17 | ``` 18 | 19 | - Configure the VCS credentials in the config file required for your [supported vcs type](../migration/supported-vcs.md) 20 | 21 | `tfm core migrate` will sequentially run all of the commands required to migrate terraform open source / community edition configurations to TFE/TFC workspace management. 22 | 23 | tfm will run the following commands in the following order when the migrate command is used: 24 | 25 | `tfm core clone` 26 | `tfm core init-repos` 27 | `tfm core getstate` 28 | `tfm core create-worksapces` 29 | `tfm core upload-state` 30 | `tfm core link-vcs` 31 | 32 | ## Flags 33 | 34 | `--include remove-backend` will add the `tfm core remove-backend` command to be run last as part of the `tfm core migrate` command. This requires a VCS API token with write permissions to the VCS repositories. 35 | 36 | 39 | -------------------------------------------------------------------------------- /site/docs/commands/core_remove-backend.md: -------------------------------------------------------------------------------- 1 | # tfm core remove-backend 2 | 3 | ## Requirements 4 | 5 | - Using this feature requires the VCS token defined in the configuration file to have write permissions to the contents of the repository. 6 | - Add the following example to your confiuration file, modifying the github values based on your [supported VCS type](../migration/supported-vcs.md): 7 | 8 | ```hcl 9 | github_token = "api token" 10 | github_organization = "org" 11 | github_username = "username" 12 | commit_message = "Remove Terraform backend configuration" 13 | commit_author_name = "username" 14 | commit_author_email = "user@email.com" 15 | ``` 16 | 17 | `tfm core remove-backend` is used to assist in removing the `backend{}` configuration block from the `terraform{}` block in terraform configurations that have been migrated. 18 | 19 | tfm will use the `terraform_config_metadata.json` config file to iterate through all cloned repositories in the `clone_repos_path`. tmf will removed the backend from all `config_paths` for the repo. 20 | 21 | tfm will create a branch, commit the branch, and push it to the origin for code owners to create a PR. 22 | 23 | The following config file options are required to use this command: 24 | 25 | ```hcl 26 | commit_message = "commitm message" 27 | commit_author_name = "name" 28 | commit_author_email = "email" 29 | ``` 30 | 31 | ## Flags 32 | 33 | `--autoapprove` Automatically approve the operation without a confirmation prompt. 34 | `--comment` Will comment out the backend configuration instead of removing it. 35 | 36 | ## Cleanup 37 | 38 | `tfm core cleanup` can be used to remove all cloned repos from the `clone_repos_path` 39 | -------------------------------------------------------------------------------- /site/docs/commands/core_upload-state.md: -------------------------------------------------------------------------------- 1 | # tfm core upload-state 2 | 3 | 4 | ## Requirements 5 | 6 | - A `terraform_config_metadata.json` must exist in the tfm working directory. Run `tfm core init-repos` to generate one. 7 | 8 | ## Upload State 9 | 10 | `tfm core upload-state` is used to upload the state files that were downloaded using the `tfm core getstate` command to workspace created with the `tfm core create-worksapces` command. tfm will use the `terraform_config_metadata.json` config file to iterate through all of the `config_paths`. Any config path containing a `.terraform/pulled_terraform.tfstate` or `.terraform/pulled_workspaceName_terraform.tfstate` file will have the state file uploaded to a workspace that matches with the `config_path`. 11 | 12 | Running this command multiple times will result in the same state file being uploaded multiple times. 13 | 14 | ## Out of Band Workspace Creation 15 | 16 | You can create workspaces using the terraform tfe provider instead of tfm. As long as the workspace names match the constructed workspace name that tfm is looking for then the state will still be uploaded. See the documentation for the `tfm create-workspaces` command for more information regarding workspace name creation. 17 | 18 | 22 | -------------------------------------------------------------------------------- /site/docs/commands/delete.md: -------------------------------------------------------------------------------- 1 | # Delete 2 | 3 | delete sub commands will delete certain objects from an organization. 4 | 5 | !!! warning "" 6 | DANGER this will delete things! 7 | 8 | ``` 9 | # tfm delete -h 10 | 11 | delete objects in an org. DANGER this will delete things! 12 | 13 | Usage: 14 | tfm delete [command] 15 | 16 | Available Commands: 17 | workspace Workspace command 18 | 19 | Flags: 20 | -h, --help help for delete 21 | --side string Specify source or destination side to process 22 | 23 | Global Flags: 24 | --autoapprove Auto approve the tfm run. --autoapprove=true . false by default 25 | --config string Config file, can be used to store common flags, (default is ./.tfm.hcl). 26 | 27 | Use "tfm delete [command] --help" for more information about a command. 28 | ``` 29 | 30 | ## delete sub commands 31 | 32 | - [`tfm delete workspace`](delete_workspace.md) 33 | 34 | ## Possible Future list command enhancements 35 | 36 | - `tfm delete projects` 37 | - `tfm delete teams` 38 | - `tfm delete variable` 39 | 40 | Got an idea for a feature to `tfm`? Submit a [feature request](https://github.com/hashicorp-services/tfm/issues/new?assignees=&labels=&template=feature_request.md&title=)! 41 | -------------------------------------------------------------------------------- /site/docs/commands/delete_workspace.md: -------------------------------------------------------------------------------- 1 | # tfm delete workspace 2 | 3 | `tfm delete workspace` will delete Workspace in an org 4 | 5 | ## `--workspace-id` flag 6 | 7 | ![delete_workspace_id](../images/delete_workspace_id_dst.png) 8 | 9 | ## `--workspace-name` flag 10 | 11 | ![delete_workspace_name](../images/delete_workspace_name_dst.png) 12 | 13 | ## `--side` flag 14 | 15 | Providing the `--side=destination` or `--side=source`flag will delete the workspace of the destination TFE/TFC instance. 16 | 17 | ## `--autoapprove` flag 18 | 19 | ![delete_workspace_autoapprove](../images/delete_workspace_name_autoapprove.png) 20 | -------------------------------------------------------------------------------- /site/docs/commands/generate_config.md: -------------------------------------------------------------------------------- 1 | # Generate 2 | 3 | Generate a template config `.tfm.hcl` file that you can then go back and configure. 4 | 5 | The template file will be created in the directory in which you run the `tfm generate config` command. 6 | 7 | ``` 8 | # tfm generate config 9 | 10 | generate a .tfm.hcl file template 11 | 12 | Usage: 13 | tfm generate [command] 14 | 15 | Available Commands: 16 | config config command 17 | 18 | Flags: 19 | -h, --help help for generate 20 | 21 | Global Flags: 22 | --autoapprove Auto approve the tfm run. --autoapprove=true . false by default 23 | --config string Config file, can be used to store common flags, (default is ./.tfm.hcl). 24 | --json Print the output in JSON format 25 | 26 | Use "tfm generate [command] --help" for more information about a command. 27 | ``` 28 | 29 | Got an idea for a feature to `tfm`? Submit a [feature request](https://github.com/hashicorp-services/tfm/issues/new?assignees=&labels=&template=feature_request.md&title=)! -------------------------------------------------------------------------------- /site/docs/commands/list.md: -------------------------------------------------------------------------------- 1 | # List 2 | 3 | list sub commands will list out certain resources from the source organization or destination organization. 4 | 5 | ```sh 6 | # tfm list -h 7 | 8 | List objects in an org 9 | 10 | Usage: 11 | tfm list [command] 12 | 13 | Available Commands: 14 | organization List Organizations 15 | projects Projects command 16 | ssh ssh-keys command 17 | teams Teams command 18 | vcs List VCS Providers 19 | workspace-filter Filter workspaces 20 | workspaces Workspaces command 21 | 22 | Flags: 23 | -h, --help help for list 24 | --side string Specify source or destination side to process 25 | 26 | Global Flags: 27 | --config string Config file, can be used to store common flags, (default is ./.tfm.hcl). 28 | 29 | Use "tfm list [command] --help" for more information about a command. 30 | ``` 31 | 32 | ## list sub commands 33 | 34 | - [`tfm list organizations`](list_orgs.md) 35 | - [`tfm list ssh`](list_ssh.md) 36 | - [`tfm list teams`](list_teams.md) 37 | - [`tfm list vcs`](list_vcs.md) 38 | - [`tfm list projects`](list_projects.md) 39 | - [`tfm list workspaces`](list_workspaces.md) 40 | 41 | ## Possible Future list command enhancements 42 | 43 | - `tfm list agents` 44 | 45 | Got an idea for a feature to `tfm`? Submit a [feature request](https://github.com/hashicorp-services/tfm/issues/new?assignees=&labels=&template=feature_request.md&title=)! 46 | -------------------------------------------------------------------------------- /site/docs/commands/list_orgs.md: -------------------------------------------------------------------------------- 1 | # tfm list organization 2 | 3 | 4 | `tfm list organization` will list organization teams by default of the source TFE/TFC instance. 5 | `tmf list org` is also the shorthand command. 6 | 7 | ![list_organizations](../images/list_organization_src.png) 8 | 9 | 10 | ## `--side` flag 11 | Providing the `--side destination` flag will list organizations of the destination TFE/TFC instance. 12 | 13 | ![list_organizations](../images/list_organization_dst.png) 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /site/docs/commands/list_projects.md: -------------------------------------------------------------------------------- 1 | # tfm list projects 2 | 3 | 4 | `tfm list projects` will list projects by default of the source TFE/TFC instance. 5 | 6 | ![list_projects](../images/list_projects_src.png) 7 | 8 | 9 | ## `--side` flag 10 | Providing the `--side destination` flag will list projects of the destination TFE/TFC instance. 11 | 12 | ![list_projects](../images/list_projects_dst.png) 13 | 14 | ## `--json` flag 15 | Providing the `--json` flag will output the project names and IDs in JSON format to make configuring the tfx configuration file more managable. 16 | 17 | ![list_projects_json](../images/list_projects_json.png) 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | !!! warning "" 28 | Note: Projects is a relatively new feature. If the source or destination TFE API endpoint does not support projects, `tfm` will error out. 29 | -------------------------------------------------------------------------------- /site/docs/commands/list_ssh.md: -------------------------------------------------------------------------------- 1 | # tfm list ssh 2 | 3 | 4 | `tfm list ssh` will list ssh keys teams by default of the source TFE/TFC instance. 5 | 6 | ![list_ssh](../images/list_ssh_src.png) 7 | 8 | 9 | ## `--side` flag 10 | Providing the `--side destination` flag will list ssh keys of the destination TFE/TFC instance. 11 | 12 | ![list_ssh](../images/list_ssh_dst.png) 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /site/docs/commands/list_teams.md: -------------------------------------------------------------------------------- 1 | # tfm list teams 2 | 3 | 4 | `tfm list teams` will list teams keys teams by default of the source TFE/TFC instance. 5 | 6 | ![list_teams](../images/list_teams_src.png) 7 | 8 | 9 | ## `--side` flag 10 | Providing the `--side destination` flag will list teams keys of the destination TFE/TFC instance. 11 | 12 | ![list_teams](../images/list_teams_dst.png) 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /site/docs/commands/list_vcs.md: -------------------------------------------------------------------------------- 1 | # tfm list vcs 2 | 3 | 4 | `tfm list vcs` will list vcs keys teams by default of the source TFE/TFC instance. 5 | 6 | ![list_vcs](../images/list_vcs_src.png) 7 | 8 | 9 | ## `--side` flag 10 | Providing the `--side destination` flag will list vcs keys of the destination TFE/TFC instance. 11 | 12 | ![list_vcs](../images/list_vcs_dst.png) 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /site/docs/commands/list_workspaces.md: -------------------------------------------------------------------------------- 1 | # tfm list workspaces 2 | 3 | 4 | `tfm list workspaces` will list workspaces by default of the source TFE/TFC instance. 5 | 6 | ![list_workspaces](../images/list_workspaces_src.png) 7 | 8 | 9 | ## `--side` flag 10 | Providing the `--side destination` flag will list workspaces of the destination TFE/TFC instance. 11 | 12 | ![list_workspaces](../images/list_workspaces_dst1.png) 13 | 14 | 15 | ## `--json` flag 16 | Providing the `--json` flag will output information about the workspaces in JSON format to make configuring the tfx configuration file more managable and assist in automating tasks. 17 | 18 | ![list_workspaces](../images/list_workspaces_json.png) 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /site/docs/configuration_file/config_file.md: -------------------------------------------------------------------------------- 1 | # TFM Configuration File Settings 2 | 3 | | Parameter | Supported Values| Description | Required | 4 | | --------- | --------------- | ----------- | -------- | 5 | | src_tfe_hostname | A hostname such as app.terraform.io | The hostname of a TFE server that you are migrating from | `yes` for TFE to TFC or TFC to TFC migrations | 6 | | src_tfe_org | A TFC/TFE organization name | The TFE/TFC Organization that you are migrating from | `yes` for TFE to TFC or TFC to TFC migrations | 7 | | src_tfe_token | A TFC/TFE Token | A Token for the TFE/TFC Organization that you are migrating from | `yes` for TFE to TFC or TFC to TFC migrations | 8 | | dst_tfc_hostname | A hostname such as app.terraform.io | The hostname of a TFE server or the TFC hostname that you are migrating to | `yes` for all migrations | 9 | | dst_tfc_org | A TFC/TFE organization name | A TFC/TFE organization that you are migrating to | `yes` for all migrations | 10 | | dst_tfc_token | A TFC/TFE Token | | `yes` for all migrations | 11 | | repos_to_clone | A list of VCS repository names | Used with the`tfm core clone` command to clone a set of VCS repositories. If not provided, all VCS repos will be cloned | `no` | 12 | | vcs-map | A list of source=destination VCS oauth IDs | TFM will look at each workspace in the source for the source VCS oauth ID and assign the matching workspace in the destination with the destination VCS oauth ID | `yes` for `tfm copy workspaces --vcs` | 13 | | workspaces | A list of workspaces to migrate from TFE to TFC or TFC org to TFC org | Provide a list of source workspaces in the source TFC/TFE org to migrate. If not provided and no "workspaces-map" is detected, all workspaces will be migrated. | `no` | 14 | | projects | A list of projects to migrate across from TFE to TFC or TFC org to TFC org | Provide a list of source projects in the source TFC/TFE org to migrate. If not "projects-map" if detected, all projects will be migrated | `no` | 15 | | projects-map | A list of source=destination project names | TFM will look at each project in the source for the source project name and recreate the project in the destination with the new destination project name. Takes precedence over "projects" list. | `no` | 16 | | workspaces-map | A list of source=destination workspace names | TFM will look at each source workspace and recreate the workspace with the specified destination name | `no` | 17 | | commit_message | A commit message | Used when creating a branch for the `tfm core remove-backend` command | `yes` only for the `tfm core remove-backend` command | 18 | | commit_author_name | Author name to appear on commits | Used when creating a branch for the `tfm core remove-backend` command | `yes` only for the `tfm core remove-backend` command | 19 | | commit_author_email | Author email to appear on commits | Used when creating a branch for the `tfm core remove-backend` command | `yes` only for the `tfm core remove-backend` command | 20 | | github_token | A github token | Used for `tfm core` commands when `vcs_type = "github"` | `yes` only for `tfm core` migrations | 21 | | github_organization | A github organization | Used for `tfm core` commands when `vcs_type = "github" | `yes` only for `tfm core` migrations | 22 | | github_username | A github username | Used for `tfm core` commands when `vcs_type = "github" | `yes` only for `tfm core` migrations | 23 | | gitlab_token | A gitlab username | Used for `tfm core` commands when `vcs_type = "gitlab" | `yes` only for `tfm core` migrations | 24 | | gitlab_username | A gitlab username | Used for `tfm core` commands when `vcs_type = "gitlab"` | `yes` only for `tfm core` migrations | 25 | | gitlab_group | A gitlab group | Used for `tfm core` commands when `vcs_type = "gitlab"` | `yes` only for `tfm core` migrations | 26 | | clone_repos_path | | | `yes` only for `tfm core` migrations | 27 | | vcs_type | | | `yes` only for `tfm core` migrations | 28 | | vcs_provider_id | | | `yes` only for `tfm core link-vcs` command migrations | 29 | | agents_map | A list of source=destination agent pool IDs | TFM will look at each workspace in the source for the source agent pool ID and assign the matching workspace in the destination the destination agent pool ID. Conflicts with agent-assignment | `no` | 30 | | agent-assignment-id | An agent Pool ID | An agent pool ID to assign to all workspaces in the destination. Conflicts with agents-map | `no` | 31 | | varsets_map | A list of source=destination variable set names | TFM will look at each source variable set and recreate the variable set with the specified destination name | `no` | 32 | | ssh-map | A list of source=destination SSH IDs | TFM will look at each workspace in the source for the source SSH ID and assign the matching workspace in the destination with the destination SSH ID | `no` | 33 | | | | | | 34 | 35 | 36 | -------------------------------------------------------------------------------- /site/docs/feedback.md: -------------------------------------------------------------------------------- 1 | If you have issues with the embedded form, you can also [access it here](https://docs.google.com/forms/d/e/1FAIpQLScxwWWMVSAOO-gr7LTWqXjvuiLQV2P1GQH8WOMDpiGx987aPg/viewform?usp=sharing). 2 | 3 | 4 | -------------------------------------------------------------------------------- /site/docs/images/TFE-workspaces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/TFE-workspaces.png -------------------------------------------------------------------------------- /site/docs/images/TFM-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/TFM-black.png -------------------------------------------------------------------------------- /site/docs/images/TFM-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/TFM-white.png -------------------------------------------------------------------------------- /site/docs/images/copy_projects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/copy_projects.png -------------------------------------------------------------------------------- /site/docs/images/copy_teams.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/copy_teams.png -------------------------------------------------------------------------------- /site/docs/images/copy_varsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/copy_varsets.png -------------------------------------------------------------------------------- /site/docs/images/copy_ws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/copy_ws.png -------------------------------------------------------------------------------- /site/docs/images/copy_ws_exists.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/copy_ws_exists.png -------------------------------------------------------------------------------- /site/docs/images/copy_ws_remote_state_sharing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/copy_ws_remote_state_sharing.png -------------------------------------------------------------------------------- /site/docs/images/copy_ws_remote_state_sharing_org_consolidation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/copy_ws_remote_state_sharing_org_consolidation.png -------------------------------------------------------------------------------- /site/docs/images/copy_ws_run_triggers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/copy_ws_run_triggers.png -------------------------------------------------------------------------------- /site/docs/images/copy_ws_ssh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/copy_ws_ssh.png -------------------------------------------------------------------------------- /site/docs/images/copy_ws_state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/copy_ws_state.png -------------------------------------------------------------------------------- /site/docs/images/copy_ws_state_last_x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/copy_ws_state_last_x.png -------------------------------------------------------------------------------- /site/docs/images/copy_ws_teamaccess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/copy_ws_teamaccess.png -------------------------------------------------------------------------------- /site/docs/images/copy_ws_vars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/copy_ws_vars.png -------------------------------------------------------------------------------- /site/docs/images/copy_ws_vcs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/copy_ws_vcs.png -------------------------------------------------------------------------------- /site/docs/images/delete_workspace_id_dst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/delete_workspace_id_dst.png -------------------------------------------------------------------------------- /site/docs/images/delete_workspace_name_autoapprove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/delete_workspace_name_autoapprove.png -------------------------------------------------------------------------------- /site/docs/images/delete_workspace_name_dst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/delete_workspace_name_dst.png -------------------------------------------------------------------------------- /site/docs/images/list_organization_dst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/list_organization_dst.png -------------------------------------------------------------------------------- /site/docs/images/list_organization_src.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/list_organization_src.png -------------------------------------------------------------------------------- /site/docs/images/list_projects_dst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/list_projects_dst.png -------------------------------------------------------------------------------- /site/docs/images/list_projects_json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/list_projects_json.png -------------------------------------------------------------------------------- /site/docs/images/list_projects_src.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/list_projects_src.png -------------------------------------------------------------------------------- /site/docs/images/list_ssh_dst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/list_ssh_dst.png -------------------------------------------------------------------------------- /site/docs/images/list_ssh_src.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/list_ssh_src.png -------------------------------------------------------------------------------- /site/docs/images/list_teams_dst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/list_teams_dst.png -------------------------------------------------------------------------------- /site/docs/images/list_teams_src.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/list_teams_src.png -------------------------------------------------------------------------------- /site/docs/images/list_vcs_dst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/list_vcs_dst.png -------------------------------------------------------------------------------- /site/docs/images/list_vcs_src.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/list_vcs_src.png -------------------------------------------------------------------------------- /site/docs/images/list_workspaces_dst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/list_workspaces_dst.png -------------------------------------------------------------------------------- /site/docs/images/list_workspaces_dst1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/list_workspaces_dst1.png -------------------------------------------------------------------------------- /site/docs/images/list_workspaces_json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/list_workspaces_json.png -------------------------------------------------------------------------------- /site/docs/images/list_workspaces_src.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/list_workspaces_src.png -------------------------------------------------------------------------------- /site/docs/images/list_workspaces_src1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/list_workspaces_src1.png -------------------------------------------------------------------------------- /site/docs/images/migration-journey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/migration-journey.png -------------------------------------------------------------------------------- /site/docs/images/tfe_tfm_tfc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/tfe_tfm_tfc.png -------------------------------------------------------------------------------- /site/docs/images/tfm-history-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/tfm-history-1.png -------------------------------------------------------------------------------- /site/docs/images/tfm-history-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/tfm-history-2.png -------------------------------------------------------------------------------- /site/docs/images/tfm_copy_ws_confirm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/tfm_copy_ws_confirm.png -------------------------------------------------------------------------------- /site/docs/images/tfm_copy_ws_confirm_autoapprove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-services/tfm/9ceb3c4515f8114a6a168946e993b046d89ea17c/site/docs/images/tfm_copy_ws_confirm_autoapprove.png -------------------------------------------------------------------------------- /site/docs/migration/case-studies.md: -------------------------------------------------------------------------------- 1 | # Case Studies 2 | 3 | The following are a list of proven case studies that have utilized `tfm` for its intended use cases. 4 | 5 | ## Financial Customer - TFE to TFC migration 6 | 7 | -------------------------------------------------------------------------------- /site/docs/migration/journey-tfe-tfc.md: -------------------------------------------------------------------------------- 1 | ![Migration Journey](../images/migration-journey.png) 2 | 3 | The above is an estimated customer experience when migrating TFE to TFC (or another TFE). 4 | Offered and ran as a Fast Track offering, we run customer through a proven delivery methodolgy Implementation Services have been using to adopt other HashiCorp products. 5 | 6 | --- 7 | 8 | ## Technical Planning 9 | 10 | Technical Planning Sprint (TPS) is a series of workshops to discuss and work through the technical requirements of a migration. Customer engineers and any technical hands on personal are encouraged to join these sessions to clarify what will be executed in the upcoming implementation sprint/s. 11 | 12 | - Review necessary prerequisites and verify ability to execute the `tfm` tool within customer environment. 13 | - Ascertain if individuals with appropriate access levels will be involved in the migration process 14 | - Normal TFC configuration items (VCS, IdP, Terraform Cloud Agents) that will be covered 15 | - Agents - Differences for on-prem deployments TFE vs. TFC. 16 | 17 | ## tfm copy 18 | 19 | This phase introduces the `tfm` tool and demonstrates it capabilities. We plan to migrate the workspaces that were identified in technical planning. The following are the outcomes of this phase. 20 | 21 | - Confirm Migration pre-requisites 22 | - VCS, Teams, Agent Pools, Workspace readiness 23 | - Copy of Workspaces 24 | - States 25 | - Variables 26 | - Team Access * 27 | - Agent Pools * 28 | - Copy Teams * 29 | - Workspace locked in destination 30 | 31 | ## Cutover Planning 32 | 33 | Migrating/Copying over workspaces into the destination TFC or TFE is easy with `tfm`, however there are other external dependencies and factors that need to be planned. Some examples of this are: 34 | 35 | - When does each team cutover completely from using the older TFE workspaces and the new TFC workspaces. 36 | - Does VCS repository need to be cutover or locked ? 37 | - Verifying workspaces are Terraform planning cleanly (source and destination) 38 | - Ensure the old TFE workspace cannot be updated or utilized by existing engineers/developers 39 | - If there any updates to the source workspace state, re-run `tfm copy` to ensure the destination is up to date 40 | - Using `tfm list` to verify workspaces that have been cut over/migrated 41 | - Communication plan and lead time of cutover. 42 | 43 | ## Future Cutovers 44 | 45 | There are times some workspaces cannot be migrated directly with `tfm` due to external factors. These workspaces may require future work to make it compatible to migrate to TFC or it is a timing and scheduling requirement. 46 | -------------------------------------------------------------------------------- /site/docs/migration/pre-migration-tfe-tfc.md: -------------------------------------------------------------------------------- 1 | # HashiCorp Implementation Services 2 | 3 | Migrating from Terraform Enterprise to Terraform Cloud. 4 | 5 | Updated copy of questionaire found at [Google Drive](https://docs.google.com/spreadsheets/d/1yi2TRF0G3AN7XTJQxdMneJHX2vTV6-BO4YNQs0F65Bc/edit?usp=sharing). 6 | 7 | ## Pre-Migration Questionnaire 8 | 9 | ### Terraform Enterprise, TFE, Current Deployment (Source) 10 | 11 | - TFE Version? 12 | - Number of TFE Organizations? 13 | - Number of users/teams using TFE (10/100/1000)? 14 | - Number of workspaces (per organization)? 15 | - Number of Modules in the PMR (per organization)? 16 | - Number of Sentinel Policies/Sets (per organization)? 17 | - Number of TFE Teams (per organization)? 18 | - Which VCS? 19 | - Is the VCS routable from the internet and serving a publicly trusted certificate? 20 | - Which IdP? 21 | - Where are you deploying infrastructure from TFE today? 22 | - Do you have the ability to run Python scripts or a binary tool from a developer workstation? 23 | - Is there an established maintenance window for transitioning from TFE to TFC? 24 | 25 | ### Terraform Cloud, TFC, New Deployment (Destination) 26 | 27 | - How many TFC Organizations? 28 | - Do you want to migrate everything to TFC? 29 | - Do you require Cloud Agents? 30 | - Will the workspaces all be going into the same Org? 31 | 32 | ### Project Flow 33 | 34 | The high level Migration path has 6 key components: 35 | 36 | 1. Discovery 37 | 1. Planning 38 | 1. Configuration 39 | 1. Technical Validation 40 | 1. Migration 41 | 1. End-User Validation 42 | 43 | #### Discovery 44 | 45 | - Determine current Terraform Enterprise landscape 46 | - Version Control Providers (Which VCS, Count, Distribution of Use) 47 | - Identity Platform (Which IdP, Number of Teams) 48 | - Modules in the Private Module Registry (Count, Publishing Method, No. of Versions, Frequency of Change) 49 | - Policies and Policy Sets (Count, Publishing Method, Frequency of Change) 50 | - Workspaces (Count, Publishing Method, Frequency of Change) 51 | - Inventory Terraform Enterprise footprint (Aids in tracking completed work) 52 | - Establish Workspace criteria required to be eligible for migration 53 | - Determine if Cloud Agents are required (Typically due to on-premise deployments from Terraform) 54 | - Identify how Cloud Agents are deployed and configured 55 | - Determine if any utilities are needed within the agent (Local-Exec, Custom Providers, etc...) 56 | - Discuss new user onboarding in Terraform Cloud (Slight differences from Terraform Enterprise) 57 | - Discuss State Migration on Workspaces (Latest State only) 58 | - Evaluate current use of Workspace Variables marked as "sensitive" (these values are write-only from the API) 59 | 60 | #### Planning 61 | 62 | - Identify Admins of VCS and IdP that will be needed to configure both in Terraform Cloud 63 | - Determine order in which to migrate Modules, Policies, and most importantly Workspaces 64 | - Establish Workspace Migration Timeline and Priority 65 | - Establish Workspace Deprecation process in Terraform Enterprise (Common Options: Locking, Deleting, Archiving) 66 | - Establish Validation process as agreed upon with the Customer 67 | - Determine required API tokens needed for the migration (must have access to all needed Organizations) 68 | - Terraform Enterprise token with Owner permissions(Source) 69 | - Terraform Cloud token with Owner permissions (Destination) 70 | - Determine if Workspace Code changes are required (Module Sourcing, Provider Initialization) 71 | - Design Cloud Agent Pool structure and required infrastructure, including networking and authentication routes 72 | - Determine how to update destination Workspace Variables that are marked as "sensitive" in the source Workspace 73 | - Establish a control plane to run python from to perform migration tasks (Requires access to both Terraform Enterprise and Terraform Cloud) 74 | 75 | #### Configuration 76 | 77 | - Configure Terraform Cloud for Version Control 78 | - Configure Terraform Cloud for Single Sign-On 79 | - Create Teams in Terraform Cloud 80 | - (If Needed) Create Cloud Agent Pools, Deploy Agents, and register the Agents with their respective Agent Pool 81 | 82 | #### Technical Validation (Proof of Concept) 83 | 84 | - Leveraging a subset of Modules, Policies, and Workspaces, migrate enough to verify the migration process 85 | - Test validation steps on the items migrated 86 | 87 | #### Migrate 88 | 89 | - Perform Migration of Modules in the Private Module Registry, and Validate 90 | - Perform Migration of Sentinel Policies and Policy Sets, and Validate 91 | - Perform Migration of Workspaces in priority order, and Validate 92 | - Run Plan on Terraform Enterprise 93 | - Perform any code changes to the Terraform consumed by the Workspace 94 | - Migrate Workspace to Terraform Cloud 95 | - Run Plan on Terraform Cloud (verify it matches the Plan from Terraform Enterprise) 96 | - Validate 97 | - Deprecation of Terraform Enterprise Workspace 98 | 99 | #### End-User Validation 100 | 101 | - Verify end users can access and leverage Workspaces in Terraform Cloud as they would have in Terraform Enterprise 102 | -------------------------------------------------------------------------------- /site/docs/migration/pre-requisites-ce-tfc.md: -------------------------------------------------------------------------------- 1 | # Pre-Requisites For Migrating From Terraform Community Edition to TFC/TFE 2 | 3 | Note: The Terraform Community Edition migration as part of `tfm` has been deprecated in favor of [tf-migrate](https://developer.hashicorp.com/terraform/cloud-docs/migrate/tf-migrate). The CE migration feature has not been removed from `tfm` however it will not be receiving further developments. 4 | 5 | ## The following pre-reqs should be completed in the destination TFC/TFE before using tfm 6 | 7 | - [Supported](../migration/supported-vcs.md) VCS provisioned in TFC/TFE 8 | 9 | ## The Following pre-reqs are Required to use the tfm Features for Cloning VCS Repositories 10 | 11 | - A VCS token with permissions to read each repository of interestin the GitHub Organization. 12 | - A Github organization or GitLab Project depending on the [supported vcs](../migration/supported-vcs.md) in use. 13 | - A VCS username. 14 | 15 | ## The Following pre-reqs are Required to use the tfm Features for Removing Backend Configurations From Cloned Repositories 16 | 17 | - A VCS token permissions to write contents to repositories. 18 | 19 | ## The Following pre-reqs are Required to use the tfm Features for Retrieving State Files 20 | 21 | - The execution environment must provide credentials to the backend 22 | - Terraform CLI must be installed in the execution environment 23 | - The `tfm core init-repos` command must be run to create a metadata file 24 | 25 | ## Constraints 26 | 27 | The following are environment/configuration constraints where a migration using tfm cannot occur: 28 | 29 | - At the time of this writing tfm only supports the cloning of GitHub and GitLab repositories. 30 | - At this time there is no way to handle CLI driven workspace migrations. 31 | - At this time there is no way to handle variable migration. 32 | -------------------------------------------------------------------------------- /site/docs/migration/pre-requisites-tfe-tfc.md: -------------------------------------------------------------------------------- 1 | # Pre-Requisites 2 | 3 | ## The following pre-reqs should be completed in the destination TFC/TFE before using tfm 4 | 5 | - A TFC/TFE Token with owner permissions is required 6 | - Existing Workspaces should have a recent clean TF Plan/Apply 7 | - VCS provisioned 8 | - VCS Map provided as configuration file 9 | - Teams created 10 | - Team map provided as configuration file 11 | - Agent Pools created 12 | - Agent map provided as configuration file 13 | - Variable Sets created 14 | - Variable Set map provided as configuration file 15 | - Variables with secrets known OR can be regenerated 16 | 17 | ## Constraints 18 | 19 | The following are environment/configuration constraints where a migration of workspaces cannot occur: 20 | 21 | - TFE Instances utilising a [Custom (Alternative Terraform Build Worker image)](https://developer.hashicorp.com/terraform/enterprise/install/interactive/installer#custom-image) as TFC does not support this feature. 22 | - TFE environments utilising [Network Mirror Provider protocol](https://developer.hashicorp.com/terraform/internals/provider-network-mirror-protocol) 23 | - A strategy for this is to change the workspace configuration in TFC to utilize Cloud Agents which requires further strategy and planning. 24 | - Workspaces [pre 0.12](https://developer.hashicorp.com/terraform/cloud-docs/agents/requirements#supported-terraform-versions) cannot use Cloud Agents in TFC. 25 | - They would need to be upgraded by workspace owners before migrating to TFC. 26 | - Customers with ONLY private Version Control Systems (VCS), TFC doees have a [list of supported VCS](https://developer.hashicorp.com/terraform/cloud-docs/vcs) solutions, however if private, some features of TFC may not work as intented. 27 | - Workspaces that utilize `local-exec` or `remote-exec` [provisioner](https://developer.hashicorp.com/terraform/enterprise/install/interactive/installer#custom-image). 28 | -------------------------------------------------------------------------------- /site/docs/migration/supported-vcs.md: -------------------------------------------------------------------------------- 1 | # Supported VCS Types for TFM CE to TFC Migrations 2 | 3 | Note: The Terraform Community Edition migration as part of `tfm` has been deprecated in favor of [tf-migrate](https://developer.hashicorp.com/terraform/cloud-docs/migrate/tf-migrate). The CE migration feature has not been removed from `tfm` however it will not be receiving further developments. 4 | 5 | ## Supported VCS Types 6 | 7 | The following VCS types are supported values for the `vcs_type` configuration in the tfm configuration file at this time. 8 | 9 | - github 10 | - gitlab 11 | 12 | ## Required Github Configuration File Settings 13 | 14 | ```hcl 15 | github_token = "api token" 16 | github_organization = "org" 17 | github_username = "username" 18 | ``` 19 | 20 | ## Required Gitlab Configuration File Settings 21 | 22 | ```hcl 23 | gitlab_token = "api token" 24 | gitlab_group = "group102109" 25 | gitlab_username = "username" 26 | ``` -------------------------------------------------------------------------------- /site/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: TFM 2 | site_url: https://go.hashi.co/tfm 3 | site_author: HashiCorp Implementation Services 4 | site_description: "A standalone CLI for Terraform Cloud and Terraform Enterprise migrations." 5 | repo_url: "https://github.com/hashicorp-services/tfm" 6 | edit_uri: edit/main/site/docs 7 | copyright: "TFM is licensed under the Mozilla Public License 2.0" 8 | 9 | theme: 10 | name: material 11 | favicon: img/favicon.ico 12 | icon: 13 | logo: material/console 14 | # custom_dir: 'cinder' 15 | # colorscheme: github 16 | # highlightjs: true 17 | # hljs_languages: 18 | font: 19 | text: Roboto 20 | # - hcl 21 | palette: 22 | # Palette toggle for light mode 23 | - media: "(prefers-color-scheme: light)" 24 | # primary: blue grey 25 | scheme: default 26 | toggle: 27 | icon: material/toggle-switch-off-outline 28 | name: Switch to dark mode 29 | 30 | # Palette toggle for dark mode 31 | - media: "(prefers-color-scheme: dark)" 32 | scheme: slate 33 | toggle: 34 | icon: material/toggle-switch 35 | name: Switch to system preference 36 | 37 | markdown_extensions: 38 | - admonition 39 | - pymdownx.highlight: 40 | anchor_linenums: true 41 | line_spans: __span 42 | pygments_lang_class: true 43 | - pymdownx.inlinehilite 44 | - pymdownx.snippets 45 | - pymdownx.superfences 46 | 47 | 48 | nav: 49 | - Home: index.md 50 | - TFM Configuration File: 51 | - Settings: configuration_file/config_file.md 52 | - Migration Planning: 53 | - Migration Journey TFE to TFC: migration/journey-tfe-tfc.md 54 | - Pre-requisites TFE to TFC: migration/pre-requisites-tfe-tfc.md 55 | - Pre-requisites Community Edition to TFC: migration/pre-requisites-ce-tfc.md 56 | - Example Scenario TFE to TFC: migration/example-scenario-tfe-tfc.md 57 | - Example Scenario Community Edition to TFC: migration/example-scenario-ce-tfc.md 58 | - Pre-Migration Questionaire TFE to TFC: migration/pre-migration-tfe-tfc.md 59 | - Pre-Migration Questionaire Community Edition to TFC: migration/pre-migration-ce-tfc.md 60 | - Case Studies: migration/case-studies.md 61 | - Supported VCS Types for CE to TFC Migration: migration/supported-vcs.md 62 | - Commands: 63 | - Core: 64 | - General: commands/core.md 65 | - Clone: commands/core_clone.md 66 | - Init-repos: commands/core_init-repos.md 67 | - Getstate: commands/core_getstate.md 68 | - Create-worksapces: commands/core_create-workspaces.md 69 | - Upload-state: commands/core_upload-state.md 70 | - Link-vcs: commands/core_link-vcs.md 71 | - Remove-backend: commands/core_remove-backend.md 72 | - Cleanup: commands/core_cleanup.md 73 | - Migrate: commands/core_migrate.md 74 | - Copy: 75 | - General: commands/copy.md 76 | - Projects: 77 | - General: commands/copy_projects.md 78 | - Workspaces: 79 | - General: commands/copy_workspaces.md 80 | - States: commands/copy_workspace_state.md 81 | - Team Access: commands/copy_workspace_teamaccess.md 82 | - Variables: commands/copy_workspace_variables.md 83 | - Agents: commands/copy_workspace_agents.md 84 | - VCS: commands/copy_workspace_vcs.md 85 | - Remote State Sharing: commands/copy_workspace_remote_state_sharing.md 86 | - Run Triggers: commands/copy_workspace_run_triggers.md 87 | - Teams: commands/copy_teams.md 88 | - Variable Sets: commands/copy_varsets.md 89 | - List: 90 | - General: commands/list.md 91 | - Organization: commands/list_orgs.md 92 | - SSH: commands/list_ssh.md 93 | - Teams: commands/list_teams.md 94 | - VCS: commands/list_vcs.md 95 | - Projects: commands/list_projects.md 96 | - Workspaces: commands/list_workspaces.md 97 | - Delete: 98 | - General: commands/delete.md 99 | - Workspace: commands/delete_workspace.md 100 | - Generate: 101 | - General: commands/generate_config.md 102 | - Development: 103 | - MVP Details: code/mvp.md 104 | - Project Details: code/project-details.md 105 | - Architectual Decision Records: code/adr.md 106 | - Future Work / Roadmap: code/future.md 107 | - FAQs: faqs.md 108 | - About: 109 | - Why TFM?: about/purpose.md 110 | - Release Notes: about/release_notes.md 111 | - Contributing: about/contributing.md 112 | - Feedback: feedback.md 113 | 114 | 115 | extra: 116 | analytics: 117 | provider: google 118 | property: G-H9KF63939N 119 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # TFM Testing 2 | Effective TFM testing includes: 3 | - Migrating objects from a source organization to a destination organization in which resources already exist in the destination organization. 4 | - Migrating objects from a source organization to a destination organization in which resources DO NOT already exist in the destination organization. 5 | 6 | ### /terraform 7 | The /terraform directory contains terraform code used to create resources in the source and destination TFC orgs 8 | 9 | ### /configs 10 | The /configs directroy contains the tfm config files used by GitHub actions to perform TFM tests 11 | 12 | ### /cleanup 13 | The /cleanup directory contains scripts for cleaning up resources 14 | 15 | ### /state 16 | The /state directory contains the scripts for creating state files -------------------------------------------------------------------------------- /test/cleanup/e2e-nuke.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # This script is used to wipe out changes that tfm unit testing will do 5 | # Eventually this will be variablized and smarter but for now it is using hardcoded names and values 6 | 7 | 8 | echo "Removing workspaces" 9 | 10 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/organizations/$DST_TFC_ORG/workspaces/tfm-ci-test-vcs-0" 11 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/organizations/$DST_TFC_ORG/workspaces/tfm-ci-test-vcs-1" 12 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/organizations/$DST_TFC_ORG/workspaces/tfm-ci-test-vcs-2" 13 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/organizations/$DST_TFC_ORG/workspaces/tfm-ci-test-vcs-3" 14 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/organizations/$DST_TFC_ORG/workspaces/tfm-ci-test-vcs-4" 15 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/organizations/$DST_TFC_ORG/workspaces/tfm-ci-test-vcs-bare-bones" 16 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/organizations/$DST_TFC_ORG/workspaces/tfm-ci-test-cli-nostate" 17 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/organizations/$DST_TFC_ORG/workspaces/tfm-ci-test-vcs-agent" 18 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/organizations/$DST_TFC_ORG/workspaces/ci-workspace-test" 19 | 20 | echo "Removing Teams" 21 | 22 | ADMINTEAMID=$(curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request GET "https://app.terraform.io/api/v2/organizations/$DST_TFC_ORG/teams" | jq '.data[] | select(.attributes.name == "tfm-ci-testing-admins") | .id' | tr -d '"') 23 | APPOWNERTEAMID=$(curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request GET "https://app.terraform.io/api/v2/organizations/$DST_TFC_ORG/teams" | jq '.data[] | select(.attributes.name == "tfm-ci-testing-appowner") | .id' | tr -d '"') 24 | DEVELOPERTEAMID=$(curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request GET "https://app.terraform.io/api/v2/organizations/$DST_TFC_ORG/teams" | jq '.data[] | select(.attributes.name == "tfm-ci-testing-developer") | .id' | tr -d '"') 25 | 26 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/teams/$ADMINTEAMID" 27 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/teams/$APPOWNERTEAMID" 28 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/teams/$DEVELOPERTEAMID" 29 | 30 | echo "Removing Varsets" 31 | 32 | AWSVARSETID=$(curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request GET "https://app.terraform.io/api/v2/organizations/$DST_TFC_ORG/varsets" | jq '.data[] | select(.attributes.name == "tfm-ci-testing-varset-aws") | .id' | tr -d '"') 33 | AZUREVARSETID=$(curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request GET "https://app.terraform.io/api/v2/organizations/$DST_TFC_ORG/varsets" | jq '.data[] | select(.attributes.name == "tfm-ci-testing-varset-azure") | .id' | tr -d '"') 34 | 35 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/varsets/$AWSVARSETID" 36 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/varsets/$AZUREVARSETID" 37 | 38 | echo "Removing projects" 39 | 40 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/projects/tfm-ci-test-0" 41 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/projects/tfm-ci-test-1" 42 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/projects/tfm-ci-test-3" 43 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/projects/tfm-ci-test-20" 44 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/projects/tfm-ci-test-30" 45 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/projects/tfm-ci-test-40" 46 | 47 | echo "Target Nuked!" 48 | 49 | 50 | -------------------------------------------------------------------------------- /test/cleanup/nuke.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # This script is used to wipe out changes that tfm unit testing will do 5 | # Eventually this will be variablized and smarter but for now it is using hardcoded names and values 6 | 7 | if $RUNNUKE = "true" 8 | then 9 | 10 | echo "Removing workspaces" 11 | 12 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/organizations/$DST_TFC_ORG/workspaces/tfc-mig-vcs-0" 13 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/organizations/$DST_TFC_ORG/workspaces/tfc-mig-vcs-1" 14 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/organizations/$DST_TFC_ORG/workspaces/tfc-mig-vcs-2" 15 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/organizations/$DST_TFC_ORG/workspaces/tfc-mig-vcs-30" 16 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/organizations/$DST_TFC_ORG/workspaces/tfc-mig-vcs-40" 17 | 18 | echo "Removing Team" 19 | 20 | TEAMID=$(curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request GET "https://app.terraform.io/api/v2/organizations/$DST_TFC_ORG/teams" | jq '.data[] | select(.attributes.name == "tfc-team") | .id' | tr -d '"') 21 | 22 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/teams/$TEAMID" 23 | 24 | echo "Removing Varset" 25 | 26 | VARSETID=$(curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request GET "https://app.terraform.io/api/v2/organizations/$DST_TFC_ORG/varsets" | jq '.data[] | select(.attributes.name == "source-varset") | .id' | tr -d '"') 27 | 28 | curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request DELETE "https://app.terraform.io/api/v2/varsets/$VARSETID" 29 | 30 | echo "Target Nuked!" 31 | else 32 | echo "Not running Nuke" 33 | fi 34 | 35 | 36 | -------------------------------------------------------------------------------- /test/configs/.e2e-all-projects-test.hcl: -------------------------------------------------------------------------------- 1 | # Blank -------------------------------------------------------------------------------- /test/configs/.e2e-all-workspaces-test.hcl: -------------------------------------------------------------------------------- 1 | agents-map=[ 2 | "apool-7mev8r4Dck7D1iba=apool-Uv3enMDH6eCtNCsc", 3 | ] 4 | 5 | vcs-map=[ 6 | "ot-ctRWZMffM36canL2=ot-ctRWZMffM36canL2", 7 | ] 8 | 9 | ssh-map=[ 10 | "sshkey-rnFYuMfwFvbjzxrB=sshkey-28pxkndxTwVVi9n6", 11 | ] 12 | -------------------------------------------------------------------------------- /test/configs/.e2e-ce-to-tfc-test.hcl: -------------------------------------------------------------------------------- 1 | repos_to_clone = [ 2 | "tfm-oss-migration-isengard", 3 | "tfm-oss-migration-mordor2", 4 | "tfm-oss-migration-rivendell" 5 | ] 6 | 7 | commit_message = "TFM CI Remove Terraform backend configuration" 8 | commit_author_name = "tfm" 9 | commit_author_email = "tfm@hashicorp.com" 10 | clone_repos_path = "./test" 11 | 12 | # github hashicorp services org OAuth ID 13 | vcs_provider_id = "ot-qmSw2nPbWaw7Y1Ji" -------------------------------------------------------------------------------- /test/configs/.e2e-project-list-test.hcl: -------------------------------------------------------------------------------- 1 | "projects"=[ 2 | "tfm-ci-test-0", 3 | "tfm-ci-test-1", 4 | "tfm-ci-test-3" 5 | ] -------------------------------------------------------------------------------- /test/configs/.e2e-project-map-test.hcl: -------------------------------------------------------------------------------- 1 | "projects-map"=[ 2 | "tfm-ci-test-0=tfm-ci-test-20", 3 | "tfm-ci-test-1=tfm-ci-test-30", 4 | "tfm-ci-test-3=tfm-ci-test-40" 5 | ] -------------------------------------------------------------------------------- /test/configs/.e2e-workspace-map-test.hcl: -------------------------------------------------------------------------------- 1 | agents-map=[ 2 | "apool-7mev8r4Dck7D1iba=apool-Uv3enMDH6eCtNCsc", 3 | ] 4 | 5 | vcs-map=[ 6 | "ot-ctRWZMffM36canL2=ot-ctRWZMffM36canL2", 7 | ] 8 | 9 | ssh-map=[ 10 | "ssshkey-rnFYuMfwFvbjzxrB=sshkey-28pxkndxTwVVi9n6", 11 | ] 12 | 13 | workspaces-map=[ 14 | "tfm-ci-test-vcs-0=tfm-ci-test-vcs-0", 15 | "tfm-ci-test-vcs-1=tfm-ci-test-vcs-1", 16 | "tfm-ci-test-vcs-agent=tfm-ci-test-vcs-agent", 17 | "tfm-ci-test-vcs-4=new-tfm-ci-test-vcs-4", 18 | "tfm-ci-test-vcs-3=new-tfm-ci-test-vcs-3". 19 | "run-trigger-child-auto=new-run-trigger-child-auto", 20 | "run-trigger-child=new-run-trigger-child", 21 | "run-trigger-source=new-run-trigger-source" 22 | ] -------------------------------------------------------------------------------- /test/configs/.e2e-workspaces-list-destination-agent-test.hcl: -------------------------------------------------------------------------------- 1 | agent-assignment-id="apool-7mev8r4Dck7D1iba" 2 | 3 | vcs-map=[ 4 | "ot-ctRWZMffM36canL2=ot-ctRWZMffM36canL2", 5 | ] 6 | 7 | ssh-map=[ 8 | "sshkey-rnFYuMfwFvbjzxrB=sshkey-28pxkndxTwVVi9n6", 9 | ] 10 | -------------------------------------------------------------------------------- /test/configs/.e2e-workspaces-list-test.hcl: -------------------------------------------------------------------------------- 1 | agents-map=[ 2 | "apool-7mev8r4Dck7D1iba=apool-Uv3enMDH6eCtNCsc", 3 | ] 4 | 5 | vcs-map=[ 6 | "ot-ctRWZMffM36canL2=ot-ctRWZMffM36canL2", 7 | ] 8 | 9 | ssh-map=[ 10 | "ssshkey-rnFYuMfwFvbjzxrB=sshkey-28pxkndxTwVVi9n6", 11 | ] 12 | 13 | "workspaces" = [ 14 | "tfm-ci-test-vcs-0", 15 | "tfm-ci-test-vcs-1", 16 | "tfm-ci-test-cli-nostate", 17 | "tfm-ci-test-vcs-agent", 18 | "run-trigger-child-auto", 19 | "run-trigger-child", 20 | "run-trigger-source" 21 | ] -------------------------------------------------------------------------------- /test/configs/.state-test-tfm.hcl: -------------------------------------------------------------------------------- 1 | agents-map=[ 2 | "apool-7mev8r4Dck7D1iba=apool-Uv3enMDH6eCtNCsc", 3 | ] 4 | 5 | vcs-map=[ 6 | "ot-ctRWZMffM36canL2=ot-ctRWZMffM36canL2", 7 | ] 8 | 9 | ssh-map=[ 10 | "ssshkey-rnFYuMfwFvbjzxrB=sshkey-28pxkndxTwVVi9n6", 11 | ] 12 | 13 | workspaces-map=[ 14 | "tfc-mig-state-test=tfc-mig-state-test" 15 | ] 16 | -------------------------------------------------------------------------------- /test/configs/.unit-test-tfm.hcl: -------------------------------------------------------------------------------- 1 | agents-map=[ 2 | "apool-7mev8r4Dck7D1iba=apool-Uv3enMDH6eCtNCsc", 3 | ] 4 | 5 | vcs-map=[ 6 | "ot-ctRWZMffM36canL2=ot-ctRWZMffM36canL2", 7 | ] 8 | 9 | ssh-map=[ 10 | "ssshkey-rnFYuMfwFvbjzxrB=sshkey-28pxkndxTwVVi9n6", 11 | ] 12 | 13 | workspaces-map=[ 14 | "tfc-mig-vcs-0=tfc-mig-vcs-0", 15 | "tfc-mig-vcs-1=tfc-mig-vcs-1", 16 | "tfc-mig-vcs-2=tfc-mig-vcs-2", 17 | "tfc-mig-vcs-3=tfc-mig-vcs-30", 18 | "tfc-mig-vcs-4=tfc-mig-vcs-40", 19 | ] 20 | -------------------------------------------------------------------------------- /test/configs/build-configs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # This script is used to build the ssh-map, vcs-map, and agents-map within the config files. 5 | # Because these are created with new IDs each time e2e testing is run, we need a way to create these each time the test is run. 6 | SOURCE_SSH_KEY_ID=$(curl --header "Authorization: Bearer $SRC_TFE_TOKEN" --request GET "https://app.terraform.io/api/v2/organizations/$SRC_TFE_ORG/ssh-keys" | jq '.data[] | select(.attributes.name == "tfm-ci-testing-src") | .id' | tr -d '"') 7 | DESTINATION_SSH_KEY_ID=$(curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request GET "https://app.terraform.io/api/v2/organizations/$DST_TFC_ORG/ssh-keys" | jq '.data[] | select(.attributes.name == "tfm-ci-testing-dest") | .id' | tr -d '"') 8 | 9 | SOURCE_AGENTPOOL_ID=$(curl --header "Authorization: Bearer $SRC_TFE_TOKEN" --request GET "https://app.terraform.io/api/v2/organizations/$SRC_TFE_ORG/agent-pools" | jq '.data[] | select(.attributes.name == "tfm-ci-testing-src") | .id' | tr -d '"') 10 | DESTINATION_AGENTPOOL_ID=$(curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request GET "https://app.terraform.io/api/v2/organizations/$DST_TFC_ORG/agent-pools" | jq '.data[] | select(.attributes.name == "tfm-ci-testing-dest") | .id' | tr -d '"') 11 | 12 | SOURCE_OAUTH_CLIENT_ID=$(curl --header "Authorization: Bearer $SRC_TFE_TOKEN" --request GET "https://app.terraform.io/api/v2/organizations/$SRC_TFE_ORG/oauth-clients" | jq '.data[] | select(.attributes.name == "github-hashicorp-services-ci") | .id' | tr -d '"') 13 | DESTINATION_OAUTH_CLIENT_ID=$(curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request GET "https://app.terraform.io/api/v2/organizations/$DST_TFC_ORG/oauth-clients" | jq '.data[] | select(.attributes.name == "github-hashicorp-services-ci") | .id' | tr -d '"') 14 | 15 | SOURCE_VCS_ID=$(curl --header "Authorization: Bearer $SRC_TFE_TOKEN" --request GET "https://app.terraform.io/api/v2/oauth-clients/$SOURCE_OAUTH_CLIENT_ID/oauth-tokens" | jq '.data[] | .id' | tr -d '"') 16 | DESTINATION_VCS_ID=$(curl --header "Authorization: Bearer $DST_TFC_TOKEN" --request GET "https://app.terraform.io/api/v2/oauth-clients/$DESTINATION_OAUTH_CLIENT_ID/oauth-tokens" | jq '.data[] | .id' | tr -d '"') 17 | 18 | cat > ./test/configs/.e2e-all-workspaces-test.hcl < ./test/configs/.e2e-workspace-map-test.hcl < ./test/configs/.e2e-workspaces-list-test.hcl < ./test/configs/.e2e-workspaces-list-destination-agent-test.hcl < 8 | # example: ./create_states.sh tfc-mig-state-test 10 9 | # 10 | # Currently the wait time between calling an API run is 8 seconds which is hard coded. 11 | # 12 | 13 | 14 | 15 | WORKSPACE_NAME=$1 16 | ORG_NAME='tfm-testing-source' 17 | 18 | 19 | echo "Look Up the Workspace ID" 20 | WORKSPACE_ID=($(curl \ 21 | --header "Authorization: Bearer $TF_TOKEN" \ 22 | --header "Content-Type: application/vnd.api+json" \ 23 | https://app.terraform.io/api/v2/organizations/$ORG_NAME/workspaces/$WORKSPACE_NAME \ 24 | | jq -r '.data.id')) 25 | 26 | 27 | echo "Creating Payload" 28 | 29 | cat << EOF > create_state_payload.json 30 | { 31 | "data": { 32 | "attributes": { 33 | "message": "Create new State" 34 | }, 35 | "type":"runs", 36 | "relationships": { 37 | "workspace": { 38 | "data": { 39 | "type": "workspaces", 40 | "id": "$WORKSPACE_ID" 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | EOF 48 | 49 | 50 | counter=1 51 | 52 | 53 | while [ $counter -le $2 ] 54 | do 55 | echo "\n Creating Run $counter for Workspace: $WORKSPACE_NAME :: ID: $WORKSPACE_ID" 56 | curl \ 57 | --header "Authorization: Bearer $TF_TOKEN" \ 58 | --header "Content-Type: application/vnd.api+json" \ 59 | --request POST \ 60 | --data @create_state_payload.json \ 61 | https://app.terraform.io/api/v2/runs 62 | ((counter++)) 63 | sleep 8 64 | done; 65 | -------------------------------------------------------------------------------- /test/terraform/README.md: -------------------------------------------------------------------------------- 1 | # Pre-reqs for tfm testing 2 | 3 | For TFM to complete unit testing, Terraform infrastructure needs to be created in source and destination TFC orgs and source workspaces with runs need to be created. 4 | 5 | This Terraform code will build out the following resources 6 | 7 | - Workspaces 8 | - Agent Pools 9 | - SSH Keys 10 | - Teams 11 | - VCS Integration with GitHub 12 | - Variable Sets 13 | - Workspace Variables 14 | 15 | Once completed with unit testing, a terraform destroy will run removing all these resources, leaving the orgs ready to be used again. 16 | 17 | A workspace named `unit-test-baseline` in the `hc-implementation-services` organization maintains the state file for these Terraform resources. 18 | 19 | #### Workspace Variables 20 | ``` 21 | destination_tfe_token = tfm-testing-destination token 22 | source_tfe_token = tfm-testing-source token 23 | tfe_token = hc-implementation-services token 24 | gh_token = the GitHub OAuth token for the 25 | ``` 26 | 27 | The `tfm-testing-source` and `tfm-testing-destination` organizations are upgraded TFC organizations used for unit testing. If these organizations are deleted, recreate them and request them to be upgraded in the `#team-se-trial-requests` HashiCorp slack channel. 28 | 29 | >Note: Org tokens cannot be used to create SSH keys. 30 | 31 | ### Generating State Files 32 | After resources are created run `/tests/state/create_state.sh` to generate state files within the workspaces. -------------------------------------------------------------------------------- /test/terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | output "source_agents_pool_id" { 2 | value = tfe_agent_pool.source.id 3 | } 4 | 5 | output "destination_agents_pool_id" { 6 | value = tfe_agent_pool.destination.id 7 | } 8 | 9 | output "source_ssh_id" { 10 | value = tfe_ssh_key.source.id 11 | } 12 | 13 | output "destination_ssh_id" { 14 | value = tfe_ssh_key.destination.id 15 | } 16 | 17 | output "source_team_id" { 18 | value = tfe_team.source.id 19 | } 20 | 21 | output "source_team_name" { 22 | value = tfe_team.source.name 23 | } 24 | 25 | output "source_gh_oauth_token_id" { 26 | value = tfe_oauth_client.source.oauth_token_id 27 | } 28 | 29 | output "destination_gh_oauth_token_id" { 30 | value = tfe_oauth_client.destination.oauth_token_id 31 | } 32 | 33 | output "source_workspace" { 34 | value = module.workspacer_source 35 | } 36 | -------------------------------------------------------------------------------- /test/terraform/outputs.txt: -------------------------------------------------------------------------------- 1 | destination_agents_pool_id = "apool-ionpTPFGPTjytp58" 2 | destination_gh_oauth_token_id = "ot-kU1vb9khCZrk1HZY" 3 | destination_ssh_id = "sshkey-MFyZn4EzcqPmdnrY" 4 | source_agents_pool_id = "apool-E4e9fijGCqnMixgw" 5 | source_gh_oauth_token_id = "ot-UKM5cGmCkcsptXfq" 6 | source_ssh_id = "sshkey-gKJBRGBd8DvcGspu" 7 | source_team_id = "team-LbEpGNbGdtaPppL9" 8 | source_team_name = "tfc-team" 9 | source_workspace = [ 10 | { 11 | "workspace_id" = "ws-DHyCaCMBGavNrmwi" 12 | "workspace_name" = "tfc-mig-vcs-0" 13 | }, 14 | { 15 | "workspace_id" = "ws-EWe42jZPxQUkmyfv" 16 | "workspace_name" = "tfc-mig-vcs-1" 17 | }, 18 | { 19 | "workspace_id" = "ws-nH1NAzoXziwXKVnp" 20 | "workspace_name" = "tfc-mig-vcs-2" 21 | }, 22 | { 23 | "workspace_id" = "ws-Lnfe4oPq1xsXNxAg" 24 | "workspace_name" = "tfc-mig-vcs-3" 25 | }, 26 | { 27 | "workspace_id" = "ws-b6RHQa2rM82TRXCk" 28 | "workspace_name" = "tfc-mig-vcs-4" 29 | }, 30 | ] -------------------------------------------------------------------------------- /test/terraform/sample-resources/main.tf: -------------------------------------------------------------------------------- 1 | variable "pet_count" { 2 | type = number 3 | description = "Count of random_pet." 4 | default = 10 5 | } 6 | 7 | variable "length" { 8 | type = number 9 | description = "Length of random_pet." 10 | default = 3 11 | } 12 | 13 | resource "random_pet" "main" { 14 | count = var.pet_count 15 | 16 | length = var.length 17 | separator = "-" 18 | 19 | keepers = { 20 | always = timestamp() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "tfe_hostname" { 2 | description = "The TFE hostname" 3 | type = string 4 | default = "app.terraform.io" 5 | } 6 | 7 | variable "organization" { 8 | description = "The TFE Org" 9 | type = string 10 | default = "hc-implementation-services" 11 | } 12 | 13 | variable "gh_token" { 14 | description = "The Oauth Token for GitHub" 15 | type = string 16 | } 17 | 18 | variable "source_tfe_token" { 19 | description = "The TFE token used for the source" 20 | type = string 21 | } 22 | 23 | variable "destination_tfe_token" { 24 | description = "The TFE token used for the destination" 25 | type = string 26 | } 27 | 28 | variable "source_tfe_organization" { 29 | description = "The Source TFE Organization" 30 | type = string 31 | default = "tfm-testing-source" 32 | } 33 | 34 | variable "destination_tfe_organization" { 35 | description = "The Destination TFE Organization" 36 | type = string 37 | default = "tfm-testing-destination" 38 | } 39 | 40 | variable "workspace_count" { 41 | description = "How many workspaces to create" 42 | type = number 43 | default = 5 44 | } 45 | -------------------------------------------------------------------------------- /tfclient/destination-only.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tfclient 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "log" 10 | "math" 11 | "net/http" 12 | "time" 13 | 14 | tfe "github.com/hashicorp/go-tfe" 15 | "github.com/spf13/viper" 16 | ) 17 | 18 | type DestinationContexts struct { 19 | DestinationClient *tfe.Client 20 | DestinationContext context.Context 21 | DestinationHostname string 22 | DestinationOrganizationName string 23 | DestinationToken string 24 | } 25 | 26 | // Create the destination client and if ther is an error, retry 27 | func createStandaloneDestClientWithRetry(destinationConfig *tfe.Config, maxRetries int, initialBackoff time.Duration) (*tfe.Client, error) { 28 | var destinationClient *tfe.Client 29 | var err error 30 | 31 | for retry := 0; retry <= maxRetries; retry++ { 32 | destinationClient, err = tfe.NewClient(destinationConfig) 33 | if err == nil { 34 | // Context creation successful, exit retry loop. 35 | return destinationClient, nil 36 | } 37 | 38 | // Handle the error (e.g., log it). 39 | fmt.Printf("Error creating client on attempt %d: %v\n", retry+1, err) 40 | 41 | if retry < maxRetries { 42 | // Calculate the backoff duration using an exponential strategy. 43 | backoff := time.Duration(math.Pow(2, float64(retry))) * initialBackoff 44 | 45 | // Sleep for the calculated backoff duration before retrying. 46 | fmt.Printf("Retrying after sleeping for %s...\n", backoff) 47 | time.Sleep(backoff) 48 | } 49 | } 50 | 51 | return nil, fmt.Errorf("Max retries reached. Last error: %v", err) 52 | } 53 | 54 | func GetDestinationClientContexts() DestinationContexts { 55 | 56 | maxRetries := 5 // Maximum number of retries. Used in instances where API rate limiting or network connectivity is less than ideal. 57 | initialBackoff := 2 * time.Second // Initial backoff duration. Used in instances where API rate limiting or network connectivity is less than ideal. 58 | 59 | destinationConfig := &tfe.Config{ 60 | Address: "https://" + viper.GetString("dst_tfc_hostname"), 61 | Token: viper.GetString("dst_tfc_token"), 62 | RetryServerErrors: true, 63 | RetryLogHook: func(attemptNum int, resp *http.Response) { 64 | }, 65 | } 66 | 67 | destinationClient, err := createDestClientWithRetry(destinationConfig, maxRetries, initialBackoff) 68 | if err != nil { 69 | println("There was an issue creating the destination client connection.") 70 | log.Fatal(err) 71 | } 72 | 73 | destinationCtx := context.Background() 74 | 75 | return DestinationContexts{ 76 | destinationClient, 77 | destinationCtx, 78 | viper.GetString("dst_tfc_hostname"), 79 | viper.GetString("dst_tfc_org"), 80 | viper.GetString("dst_tfc_token")} 81 | } 82 | -------------------------------------------------------------------------------- /tfclient/tfclient.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package tfclient 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "log" 10 | "math" 11 | "net/http" 12 | "time" 13 | 14 | tfe "github.com/hashicorp/go-tfe" 15 | "github.com/spf13/viper" 16 | ) 17 | 18 | type ClientContexts struct { 19 | SourceClient *tfe.Client 20 | SourceContext context.Context 21 | SourceHostname string 22 | SourceOrganizationName string 23 | SourceToken string 24 | DestinationClient *tfe.Client 25 | DestinationContext context.Context 26 | DestinationHostname string 27 | DestinationOrganizationName string 28 | DestinationToken string 29 | } 30 | 31 | // Create the source client and if ther is an error, retry 32 | func createSrcClientWithRetry(sourceConfig *tfe.Config, maxRetries int, initialBackoff time.Duration) (*tfe.Client, error) { 33 | var SourceClient *tfe.Client 34 | var err error 35 | 36 | for retry := 0; retry <= maxRetries; retry++ { 37 | SourceClient, err = tfe.NewClient(sourceConfig) 38 | if err == nil { 39 | // Context creation successful, exit retry loop. 40 | return SourceClient, nil 41 | } 42 | 43 | // Handle the error (e.g., log it). 44 | fmt.Printf("Error creating client on attempt %d: %v\n", retry+1, err) 45 | 46 | if retry < maxRetries { 47 | // Calculate the backoff duration using an exponential strategy. 48 | backoff := time.Duration(math.Pow(2, float64(retry))) * initialBackoff 49 | 50 | // Sleep for the calculated backoff duration before retrying. 51 | fmt.Printf("Retrying after sleeping for %s...\n", backoff) 52 | time.Sleep(backoff) 53 | } 54 | } 55 | 56 | return nil, fmt.Errorf("Max retries reached. Last error: %v", err) 57 | } 58 | 59 | // Create the destination client and if ther is an error, retry 60 | func createDestClientWithRetry(destinationConfig *tfe.Config, maxRetries int, initialBackoff time.Duration) (*tfe.Client, error) { 61 | var destinationClient *tfe.Client 62 | var err error 63 | 64 | for retry := 0; retry <= maxRetries; retry++ { 65 | destinationClient, err = tfe.NewClient(destinationConfig) 66 | if err == nil { 67 | // Context creation successful, exit retry loop. 68 | return destinationClient, nil 69 | } 70 | 71 | // Handle the error (e.g., log it). 72 | fmt.Printf("Error creating client on attempt %d: %v\n", retry+1, err) 73 | 74 | if retry < maxRetries { 75 | // Calculate the backoff duration using an exponential strategy. 76 | backoff := time.Duration(math.Pow(2, float64(retry))) * initialBackoff 77 | 78 | // Sleep for the calculated backoff duration before retrying. 79 | fmt.Printf("Retrying after sleeping for %s...\n", backoff) 80 | time.Sleep(backoff) 81 | } 82 | } 83 | 84 | return nil, fmt.Errorf("Max retries reached. Last error: %v", err) 85 | } 86 | 87 | func GetClientContexts() ClientContexts { 88 | 89 | maxRetries := 5 // Maximum number of retries. Used in instances where API rate limiting or network connectivity is less than ideal. 90 | initialBackoff := 2 * time.Second // Initial backoff duration. Used in instances where API rate limiting or network connectivity is less than ideal. 91 | 92 | sourceConfig := &tfe.Config{ 93 | Address: "https://" + viper.GetString("src_tfe_hostname"), 94 | Token: viper.GetString("src_tfe_token"), 95 | RetryServerErrors: true, 96 | RetryLogHook: func(attemptNum int, resp *http.Response) { 97 | }, 98 | } 99 | 100 | sourceClient, err := createSrcClientWithRetry(sourceConfig, maxRetries, initialBackoff) 101 | if err != nil { 102 | println("There was an issue creating the source client connection.") 103 | log.Fatal(err) 104 | } 105 | 106 | destinationConfig := &tfe.Config{ 107 | Address: "https://" + viper.GetString("dst_tfc_hostname"), 108 | Token: viper.GetString("dst_tfc_token"), 109 | RetryServerErrors: true, 110 | RetryLogHook: func(attemptNum int, resp *http.Response) { 111 | }, 112 | } 113 | 114 | destinationClient, err := createDestClientWithRetry(destinationConfig, maxRetries, initialBackoff) 115 | if err != nil { 116 | println("There was an issue creating the destination client connection.") 117 | log.Fatal(err) 118 | } 119 | 120 | // Create a context 121 | sourceCtx := context.Background() 122 | destinationCtx := context.Background() 123 | 124 | return ClientContexts{ 125 | sourceClient, 126 | sourceCtx, 127 | viper.GetString("src_tfe_hostname"), 128 | viper.GetString("src_tfe_org"), 129 | viper.GetString("src_tfe_token"), 130 | destinationClient, 131 | destinationCtx, 132 | viper.GetString("dst_tfc_hostname"), 133 | viper.GetString("dst_tfc_org"), 134 | viper.GetString("dst_tfc_token")} 135 | } 136 | 137 | func Foo() string { 138 | return "Called Foo(), Return with Bar" 139 | } 140 | 141 | // GetTfcConfig returns a TFE/TFC config with token if found 142 | // in the terraform local cred file 143 | func GetTfcConfig(hostname string) (tfe.Config, error) { 144 | config := tfe.Config{ 145 | Address: "https://" + viper.GetString("hostname"), 146 | Token: viper.GetString("token"), 147 | } 148 | 149 | return config, nil 150 | } 151 | -------------------------------------------------------------------------------- /vcsclients/github.go: -------------------------------------------------------------------------------- 1 | package vcsclients 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/google/go-github/github" 7 | "github.com/spf13/viper" 8 | "golang.org/x/oauth2" 9 | ) 10 | 11 | func NewGitHubClient(ctx context.Context) *github.Client { 12 | // Retrieve the GitHub token from viper 13 | token := viper.GetString("github_token") 14 | 15 | ts := oauth2.StaticTokenSource( 16 | &oauth2.Token{AccessToken: token}, 17 | ) 18 | tc := oauth2.NewClient(ctx, ts) 19 | client := github.NewClient(tc) 20 | return client 21 | } 22 | 23 | type ClientContext struct { 24 | GitHubClient *github.Client 25 | GithubContext context.Context 26 | GithubToken string 27 | GithubOrganization string 28 | GithubUsername string 29 | } 30 | 31 | func CreateContext() *ClientContext { 32 | ctx := context.Background() 33 | githubClient := NewGitHubClient(ctx) 34 | 35 | return &ClientContext{ 36 | GitHubClient: githubClient, 37 | GithubContext: ctx, 38 | GithubToken: viper.GetString("github_token"), 39 | GithubOrganization: viper.GetString("github_organization"), 40 | GithubUsername: viper.GetString("github_username"), 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /vcsclients/gitlab.go: -------------------------------------------------------------------------------- 1 | package vcsclients 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/viper" 7 | gitlab "github.com/xanzy/go-gitlab" 8 | ) 9 | 10 | // NewGitLabClient creates a new GitLab client 11 | func NewGitLabClient(ctx context.Context) *gitlab.Client { 12 | 13 | token := viper.GetString("gitlab_token") 14 | 15 | client, err := gitlab.NewClient(token) 16 | if err != nil { 17 | panic(err) 18 | } 19 | return client 20 | } 21 | 22 | type GitLabClientContext struct { 23 | GitLabClient *gitlab.Client 24 | GitLabContext context.Context 25 | GitLabToken string 26 | GitLabGroup string 27 | GitLabUsername string 28 | } 29 | 30 | func CreateContextGitlab() *GitLabClientContext { 31 | ctx := context.Background() 32 | gitlabClient := NewGitLabClient(ctx) 33 | 34 | return &GitLabClientContext{ 35 | GitLabClient: gitlabClient, 36 | GitLabContext: ctx, 37 | GitLabToken: viper.GetString("gitlab_token"), 38 | GitLabGroup: viper.GetString("gitlab_group"), 39 | GitLabUsername: viper.GetString("gitlab_username"), 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package version 22 | 23 | var ( 24 | Version = "0.14.0" 25 | Prerelease = "" 26 | Build = "" 27 | Date = "" 28 | BuiltBy = "" 29 | ) 30 | 31 | func String() string { 32 | v := Version 33 | if Prerelease != "" { 34 | v += "-" + Prerelease 35 | } 36 | if Build != "" { 37 | v += "\nBuild: " + Build 38 | } 39 | if Date != "" { 40 | v += "\nDate: " + Date 41 | } 42 | v += "\nBuilt By: " + BuiltBy 43 | 44 | return v 45 | } 46 | --------------------------------------------------------------------------------