Skip to content

Commit ea0fcf0

Browse files
authored
update list and vcs command (#270)
1 parent 1b50069 commit ea0fcf0

File tree

6 files changed

+115
-35
lines changed

6 files changed

+115
-35
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.11.1](https://github.com/hashicorp-services/tfm/compare/v0.11.0...v0.11.1) (2025-01-24)
9+
10+
### Features
11+
12+
- Update the `list vcs` command and the `copy workspaces --vcs` flag to support GitHub App connections instead of only support OAuth VCS connections. [[#268](https://github.com/hashicorp-services/tfm/issues/268)]
13+
814
## [0.11.0](https://github.com/hashicorp-services/tfm/compare/v0.10.0...v0.11.0) (2025-01-10)
915

1016
### Features

cmd/copy/workspaces-vcs.go

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func configureVCSsettings(c tfclient.ClientContexts, org string, vcsOptions tfe.
3333
// Main function for --vcs flag
3434
func createVCSConfiguration(c tfclient.ClientContexts, vcsConfig map[string]string) error {
3535

36-
// for each `source-ot-ID=dest-ot-ID` string in the map, define the source oauth-ID and the target oauth-ID
36+
// for each `source-ot-ID=dest-ot-ID` string in the map, define the source vcs ID and the target vcs ID
3737
for key, element := range vcsConfig {
3838
srcvcs := key
3939
destvcs := element
@@ -50,9 +50,9 @@ func createVCSConfiguration(c tfclient.ClientContexts, vcsConfig map[string]stri
5050
fmt.Println("Invalid input for workspaces-map")
5151
}
5252

53-
// For each source workspace with a VCS connection, compare the source oauth ID to the
54-
// user provided oauth ID. If they match, update the destination workspace with
55-
// the user provided oauth ID that exists in the destination.
53+
// For each source workspace with a VCS connection, compare the source ID to the
54+
// user provided ID. If they match, update the destination workspace with
55+
// the user provided ID that exists in the destination.
5656
for _, ws := range srcWorkspaces {
5757
destWorkSpaceName := ws.Name
5858

@@ -66,24 +66,50 @@ func createVCSConfiguration(c tfclient.ClientContexts, vcsConfig map[string]stri
6666
o.AddMessageUserProvided("No VCS ID Assigned to source Workspace: ", ws.Name)
6767
} else {
6868

69-
// If the source Workspace assigned VCS does not match the one provided by the user on the left side of the `vcs-map`, do nothing and inform the user
70-
if ws.VCSRepo.OAuthTokenID != srcvcs {
71-
o.AddFormattedMessageUserProvided2("Workspace %v configured VCS ID does not match provided source ID %v. Skipping.", ws.Name, srcvcs)
72-
73-
// If the source Workspace assigned VCS matches the one provided by the user on the left side of the `vcs-map`, update the destination Workspace
74-
// with the VCS provided by the user on the right side of the `vcs-map`
75-
} else {
76-
o.AddFormattedMessageUserProvided2("Updating destination Workspace %v VCS Settings and OauthID %v", destWorkSpaceName, destvcs)
77-
78-
vcsConfig := tfe.VCSRepoOptions{
79-
Branch: &ws.VCSRepo.Branch,
80-
Identifier: &ws.VCSRepo.Identifier,
81-
IngressSubmodules: &ws.VCSRepo.IngressSubmodules,
82-
OAuthTokenID: &destvcs,
83-
TagsRegex: &ws.VCSRepo.TagsRegex,
69+
if ws.VCSRepo.OAuthTokenID != "" {
70+
71+
// If the source Workspace assigned VCS does not match the one provided by the user on the left side of the `vcs-map`, do nothing and inform the user
72+
if ws.VCSRepo.OAuthTokenID != srcvcs {
73+
o.AddFormattedMessageUserProvided2("Workspace %v configured VCS ID does not match provided source ID %v. Skipping.", ws.Name, srcvcs)
74+
75+
// If the source Workspace assigned VCS matches the one provided by the user on the left side of the `vcs-map`, update the destination Workspace
76+
// with the VCS provided by the user on the right side of the `vcs-map`
77+
} else {
78+
o.AddFormattedMessageUserProvided2("Updating destination Workspace %v VCS Settings %v", destWorkSpaceName, destvcs)
79+
80+
vcsConfig := tfe.VCSRepoOptions{
81+
Branch: &ws.VCSRepo.Branch,
82+
Identifier: &ws.VCSRepo.Identifier,
83+
IngressSubmodules: &ws.VCSRepo.IngressSubmodules,
84+
OAuthTokenID: &destvcs,
85+
TagsRegex: &ws.VCSRepo.TagsRegex,
86+
}
87+
88+
configureVCSsettings(c, c.DestinationOrganizationName, vcsConfig, destWorkSpaceName)
8489
}
90+
}
8591

86-
configureVCSsettings(c, c.DestinationOrganizationName, vcsConfig, destWorkSpaceName)
92+
if ws.VCSRepo.GHAInstallationID != "" {
93+
94+
// If the source Workspace assigned VCS does not match the one provided by the user on the left side of the `vcs-map`, do nothing and inform the user
95+
if ws.VCSRepo.GHAInstallationID != srcvcs {
96+
o.AddFormattedMessageUserProvided2("Workspace %v configured VCS ID does not match provided source ID %v. Skipping.", ws.Name, srcvcs)
97+
98+
// If the source Workspace assigned VCS matches the one provided by the user on the left side of the `vcs-map`, update the destination Workspace
99+
// with the VCS provided by the user on the right side of the `vcs-map`
100+
} else {
101+
o.AddFormattedMessageUserProvided2("Updating destination Workspace %v VCS Settings %v", destWorkSpaceName, destvcs)
102+
103+
vcsConfig := tfe.VCSRepoOptions{
104+
Branch: &ws.VCSRepo.Branch,
105+
Identifier: &ws.VCSRepo.Identifier,
106+
IngressSubmodules: &ws.VCSRepo.IngressSubmodules,
107+
GHAInstallationID: &destvcs,
108+
TagsRegex: &ws.VCSRepo.TagsRegex,
109+
}
110+
111+
configureVCSsettings(c, c.DestinationOrganizationName, vcsConfig, destWorkSpaceName)
112+
}
87113
}
88114
}
89115
}

cmd/copy/workspaces.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ func init() {
138138
}
139139
workspacesCopyCmd.Flags().BoolVarP(&teamaccess, "teamaccess", "", false, "Copy workspace Team Access")
140140
workspacesCopyCmd.Flags().BoolVarP(&agents, "agents", "", false, "Mapping of source Agent Pool IDs to destination Agent Pool IDs in config file")
141-
workspacesCopyCmd.Flags().BoolVarP(&vcs, "vcs", "", false, "Mapping of source vcs Oauth ID to destination vcs Oath in config file")
141+
workspacesCopyCmd.Flags().BoolVarP(&vcs, "vcs", "", false, "Mapping of source vcs Oauth ID or GitHub App ID to destination vcs Oauth or GitHub App ID in config file")
142142
workspacesCopyCmd.Flags().BoolVarP(&ssh, "ssh", "", false, "Mapping of source ssh id to destination ssh id in config file")
143143
workspacesCopyCmd.Flags().BoolVarP(&lock, "lock", "", false, "Lock all source workspaces")
144144
workspacesCopyCmd.Flags().BoolVarP(&unlock, "unlock", "", false, "Unlock all source workspaces")

cmd/generate/config.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,10 @@ dst_tfc_org=""
4848
dst_tfc_token="Must have owner permissions"
4949
#dst_tfc_project_id=""
5050
51-
# 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.
51+
# A list of source=destination VCS IDs. TFM will look at each workspace in the source for the source VCS ID and assign the matching workspace in the destination with the destination VCS ID.
5252
#vcs-map=[
5353
# "ot-wF6KZMna4desiPRc=ot-JSQTcnWxqVL5zQ1w",
54+
# "ghain-sc8a3b12S212gy45=ghain-B3asgvX3oF541aDo"
5455
#]
5556
5657

cmd/list/vcs.go

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,18 @@ func init() {
3838
}
3939

4040
// helper functions
41-
func vcsListAllForOrganization(c tfclient.ClientContexts, orgName string) ([]*tfe.OAuthClient, error) {
41+
func vcsListAllForOrganization(c tfclient.ClientContexts, orgName string) ([]*tfe.OAuthClient, []*tfe.GHAInstallation, error) {
4242
var allItems []*tfe.OAuthClient
43+
var allGHAItems []*tfe.GHAInstallation
44+
45+
optsGHA := tfe.GHAInstallationListOptions{
46+
ListOptions: tfe.ListOptions{PageNumber: 1, PageSize: 100},
47+
}
48+
4349
opts := tfe.OAuthClientListOptions{
4450
ListOptions: tfe.ListOptions{PageNumber: 1, PageSize: 100},
4551
}
52+
4653
for {
4754
var items *tfe.OAuthClientList
4855
var err error
@@ -55,7 +62,7 @@ func vcsListAllForOrganization(c tfclient.ClientContexts, orgName string) ([]*tf
5562
items, err = c.DestinationClient.OAuthClients.List(c.DestinationContext, orgName, &opts)
5663
}
5764
if err != nil {
58-
return nil, err
65+
return nil, nil, err
5966
}
6067

6168
allItems = append(allItems, items.Items...)
@@ -66,7 +73,31 @@ func vcsListAllForOrganization(c tfclient.ClientContexts, orgName string) ([]*tf
6673
opts.PageNumber = items.NextPage
6774
}
6875

69-
return allItems, nil
76+
for {
77+
var ghaItems *tfe.GHAInstallationList
78+
var err error
79+
80+
if (ListCmd.Flags().Lookup("side").Value.String() == "source") || (!ListCmd.Flags().Lookup("side").Changed) {
81+
ghaItems, err = c.SourceClient.GHAInstallations.List(c.SourceContext, &optsGHA)
82+
}
83+
84+
if ListCmd.Flags().Lookup("side").Value.String() == "destination" {
85+
ghaItems, err = c.DestinationClient.GHAInstallations.List(c.DestinationContext, &optsGHA)
86+
}
87+
if err != nil {
88+
return nil, nil, err
89+
}
90+
91+
allGHAItems = append(allGHAItems, ghaItems.Items...)
92+
93+
if ghaItems.CurrentPage >= ghaItems.TotalPages {
94+
break
95+
}
96+
opts.PageNumber = ghaItems.NextPage
97+
98+
}
99+
100+
return allItems, allGHAItems, nil
70101
}
71102

72103
func organizationListAll(c tfclient.ClientContexts) ([]*tfe.Organization, error) {
@@ -123,27 +154,35 @@ func vcsListAll(c tfclient.ClientContexts) error {
123154
}
124155

125156
var allVcsList []*tfe.OAuthClient
157+
var allGhaList []*tfe.GHAInstallation
126158

127159
for _, v := range allOrgs {
128-
vcsList, err := vcsListAllForOrganization(c, v.Name)
160+
vcsList, ghaList, err := vcsListAllForOrganization(c, v.Name)
129161
if err != nil {
130162
helper.LogError(err, "failed to list vcs for organization")
131163
}
132164

133165
allVcsList = append(allVcsList, vcsList...)
166+
allGhaList = append(allGhaList, ghaList...)
134167
}
135168

136-
o.AddFormattedMessageCalculated("Found %d vcs", len(allVcsList))
169+
o.AddFormattedMessageCalculated("Found %d OAuth vcs connections", len(allVcsList))
170+
o.AddFormattedMessageCalculated("Found %d GHA vcs connections", len(allVcsList))
137171

138-
o.AddTableHeaders("Organization", "Name", "Id", "Service Provider", "Service Provider Name", "Created At", "URL")
172+
o.AddTableHeaders("Organization", "Name", "Id", "Service Provider", "Service Provider Name", "Created At", "URL")
139173
for _, i := range allVcsList {
140174

141175
vcsName := ""
142176
if i.Name != nil {
143177
vcsName = *i.Name
144178
}
145179

146-
o.AddTableRows(i.Organization.Name, vcsName, i.OAuthTokens[0].ID, i.ServiceProvider, i.ServiceProviderName, i.CreatedAt, i.HTTPURL)
180+
o.AddTableRows(i.Organization.Name, vcsName, i.OAuthTokens[0].ID ,i.ServiceProvider, i.ServiceProviderName, i.CreatedAt, i.HTTPURL)
181+
}
182+
183+
184+
for _, i := range allGhaList {
185+
o.AddTableRows("", *i.Name, *i.ID, "github", "GitHub App", "", "")
147186
}
148187

149188
return nil
@@ -153,23 +192,26 @@ func vcsList(c tfclient.ClientContexts) error {
153192
o.AddMessageUserProvided("List vcs for configured Organizations", "")
154193

155194
var orgVcsList []*tfe.OAuthClient
195+
var orgGhaList []*tfe.GHAInstallation
156196
var err error
157197

158198
if (ListCmd.Flags().Lookup("side").Value.String() == "source") || (!ListCmd.Flags().Lookup("side").Changed) {
159-
orgVcsList, err = vcsListAllForOrganization(c, c.SourceOrganizationName)
199+
orgVcsList, orgGhaList, err = vcsListAllForOrganization(c, c.SourceOrganizationName)
160200
}
161201

162202
if ListCmd.Flags().Lookup("side").Value.String() == "destination" {
163-
orgVcsList, err = vcsListAllForOrganization(c, c.DestinationOrganizationName)
203+
orgVcsList, orgGhaList, err = vcsListAllForOrganization(c, c.DestinationOrganizationName)
164204
}
165205

166206
if err != nil {
167207
helper.LogError(err, "failed to list vcs for organization")
168208
}
169209

170-
o.AddFormattedMessageCalculated("Found %d vcs", len(orgVcsList))
210+
211+
o.AddFormattedMessageCalculated("Found %d OAuth vcs connections", len(orgVcsList))
212+
o.AddFormattedMessageCalculated("Found %d GHA vcs connections", len(orgGhaList))
171213

172-
o.AddTableHeaders("Organization", "Name", "Id", "Service Provider", "Service Provider Name", "Created At", "URL")
214+
o.AddTableHeaders("Organization", "Name", "Id", "Service Provider", "Service Provider Name", "Created At", "URL")
173215
for _, i := range orgVcsList {
174216

175217
vcsName := ""
@@ -180,5 +222,9 @@ func vcsList(c tfclient.ClientContexts) error {
180222
o.AddTableRows(i.Organization.Name, vcsName, i.OAuthTokens[0].ID, i.ServiceProvider, i.ServiceProviderName, i.CreatedAt, i.HTTPURL)
181223
}
182224

225+
for _, i := range orgGhaList {
226+
o.AddTableRows("", *i.Name, *i.ID, "github", "GitHub App", "", "")
227+
}
228+
183229
return nil
184230
}

tfe-migration.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,14 @@ varsets-map = [
9292

9393
## Assign VCS
9494

95-
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.
95+
As part of the HCL config file (`/home/user/.tfm.hcl`), a list of vcs mappings of either `source-vcs-oauth-id=destination-vcs-oauth-id` or `source-vcs-github-app-id=destination-vcs-github-app-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 ID and assign the matching named workspace in the destination with the mapped destination VCS ID. `tfm` only supports like for like vcs migration, so if the source is a GitHub App VCS connection the destination must use a GitHub App VCS connection.
9696

9797
```hcl
98-
# 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.
98+
# A list of source=destination VCS IDs. TFM will look at each workspace in the source for the source VCS ID and assign the matching workspace in the destination with the destination VCS ID.
9999
vcs-map=[
100100
"ot-5uwu2Kq8mEyLFPzP=ot-coPDFTEr66YZ9X9n",
101101
"ot-gkj2An452kn2flfw=ot-8ALKBaqnvj232GB4",
102+
"ghain-sc8a3b12S212gy45=ghain-B3asgvX3oF541aDo"
102103
]
103104
```
104105

0 commit comments

Comments
 (0)