-
Notifications
You must be signed in to change notification settings - Fork 68
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4271 from cloud-gov/feat-admin-site-repo-migrator…
…-4237 feat(admin): Add site repo migrator script
- Loading branch information
Showing
7 changed files
with
261 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# Platform Operations | ||
|
||
These are tips and functionalities available to run operations against the platform | ||
|
||
## Scripts | ||
|
||
The [`./scripts/`](./scripts/) directory is used for scripts to be run on the platform in any environment. These can be scripts run at adhoc by platform operators or scripts that are run in CI. | ||
|
||
### Migrating a site's repository | ||
|
||
Run this script if the site's Github repository needs to be moved to a new Github owner and/or repository name. | ||
|
||
Requirements: | ||
|
||
- `siteId`: The unique integer id of the site | ||
- `email`: The user's UAA Identity email with the proper Github credentials to add webhooks to the new site repository | ||
- `owner`: The new Github owner or organization name | ||
- `repository`: The new Github repository name | ||
|
||
Example: | ||
|
||
```bash | ||
$ cf run-task pages-<env> "yarn migrate-site-repo 1 [email protected] agency-org site-repo" --name site-1-migrate | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
const { Site, UAAIdentity, User } = require('../models'); | ||
const GitHub = require('./GitHub'); | ||
const CloudFoundryAPIClient = require('../utils/cfApiClient'); | ||
const { generateS3ServiceName, generateSubdomain } = require('../utils'); | ||
|
||
const apiClient = new CloudFoundryAPIClient(); | ||
|
||
async function getUserGHCredentials(email) { | ||
const uaaUser = await UAAIdentity.findOne({ | ||
where: { email }, | ||
include: [User], | ||
raw: true, | ||
nest: true, | ||
}); | ||
|
||
if (!uaaUser) throw new Error('No UAA Identity exists with that email.'); | ||
|
||
const { githubAccessToken, githubUserId } = uaaUser.User; | ||
|
||
return { githubAccessToken, githubUserId }; | ||
} | ||
|
||
async function setRepoWebhook(site, uaaEmail) { | ||
const { githubAccessToken } = await getUserGHCredentials(uaaEmail); | ||
const webhook = await GitHub.setWebhook(site, githubAccessToken); | ||
|
||
if (webhook) { | ||
site.set('webhookId', webhook.data.id); | ||
} | ||
|
||
return site.save(); | ||
} | ||
|
||
async function updateSiteServices(oldServceName, newServiceName) { | ||
const s3ServiceInstance = await apiClient.fetchServiceInstance(oldServceName); | ||
const s3KeyInstance = await apiClient.fetchCredentialBindingsInstance( | ||
`${oldServceName}-key` | ||
); | ||
|
||
// Rename s3 service instance | ||
await apiClient.authRequest( | ||
'PATCH', | ||
`/v3/service_instances/${s3ServiceInstance.guid}/`, | ||
{ name: newServiceName } | ||
); | ||
|
||
// Create new service key based on new service name | ||
await apiClient.createServiceKey(newServiceName, s3ServiceInstance.guid); | ||
|
||
// Delete old service key | ||
await apiClient.authRequest( | ||
'DELETE', | ||
`/v3/service_credential_bindings/${s3KeyInstance.guid}` | ||
); | ||
} | ||
|
||
async function siteRepoMigrator(siteId, uaaEmail, { repository, owner }) { | ||
const subdomain = generateSubdomain(owner, repository); | ||
const s3ServiceName = generateS3ServiceName(owner, repository); | ||
const site = await Site.findByPk(siteId); | ||
const oldS3ServiceName = site.s3ServiceName; | ||
|
||
site.set({ | ||
owner, | ||
repository, | ||
s3ServiceName, | ||
subdomain, | ||
}); | ||
|
||
await site.save(); | ||
await updateSiteServices(oldS3ServiceName, s3ServiceName); | ||
await setRepoWebhook(site, uaaEmail); | ||
} | ||
|
||
module.exports = { | ||
getUserGHCredentials, | ||
setRepoWebhook, | ||
siteRepoMigrator, | ||
updateSiteServices, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/* eslint-disable no-console */ | ||
const { siteRepoMigrator } = require('../api/services/SiteRepoMigrator'); | ||
|
||
async function main() { | ||
try { | ||
const args = Array.prototype.slice.call(process.argv, 2); | ||
|
||
if (args.length !== 4) { | ||
throw ` | ||
Please make sure you provide 4 arguments. (siteId, uaaEmail, owner, repository).\n | ||
You provided the following: ${args} | ||
`; | ||
} | ||
|
||
const [siteId, email, owner, repository] = args; | ||
|
||
await siteRepoMigrator(siteId, email, { owner, repository }); | ||
|
||
console.log( | ||
`Site Id: ${siteId} migrated to new repo ${owner}/${repository}` | ||
); | ||
|
||
process.exit(0); | ||
} catch (error) { | ||
console.error(error); | ||
process.exit(1); | ||
} | ||
} | ||
|
||
main(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
const crypto = require('node:crypto'); | ||
const { expect } = require('chai'); | ||
const { | ||
getUserGHCredentials, | ||
setRepoWebhook, | ||
siteRepoMigrator, | ||
} = require('../../../../api/services/SiteRepoMigrator'); | ||
const { Site, User, UAAIdentity } = require('../../../../api/models'); | ||
const githubAPINocks = require('../../support/githubAPINocks'); | ||
const factory = require('../../support/factory'); | ||
|
||
async function cleanDb() { | ||
return await Promise.all([ | ||
User.truncate({ force: true, cascade: true }), | ||
Site.truncate({ force: true, cascade: true }), | ||
UAAIdentity.truncate({ force: true, cascade: true }), | ||
]); | ||
} | ||
|
||
describe('SiteRepoMigrator', () => { | ||
beforeEach(async () => await cleanDb); | ||
|
||
afterEach(async () => await cleanDb); | ||
|
||
describe('getUserGHCredentials', () => { | ||
it('Should get the Github credentials of a user based on their UAA email', async () => { | ||
const githubAccessToken = crypto.randomUUID(); | ||
const githubUserId = crypto.randomUUID(); | ||
const user = await factory.user({ githubAccessToken, githubUserId }); | ||
const uaaId = await factory.uaaIdentity({ | ||
userId: user.id, | ||
}); | ||
|
||
const creds = await getUserGHCredentials(uaaId.email); | ||
|
||
expect(creds).to.have.keys(['githubAccessToken', 'githubUserId']); | ||
expect(creds.githubAccessToken).to.equal(githubAccessToken); | ||
expect(creds.githubUserId).to.equal(githubUserId); | ||
}); | ||
|
||
it('Should throw an error if UAA Identity does not exist with the email', async () => { | ||
const nonExistingEmail = '[email protected]'; | ||
const user = await factory.user(); | ||
await factory.uaaIdentity({ | ||
userId: user.id, | ||
}); | ||
|
||
try { | ||
await getUserGHCredentials(nonExistingEmail); | ||
} catch (error) { | ||
expect(error).to.throw; | ||
expect(error.message).to.equal( | ||
'No UAA Identity exists with that email.' | ||
); | ||
} | ||
}); | ||
}); | ||
|
||
describe('setRepoWebhook', () => { | ||
it('should set a webhook on the repository and return the site instance', async () => { | ||
const oldWebhookId = 90210; | ||
const webhookId = 8675309; | ||
const githubAccessToken = crypto.randomUUID(); | ||
const githubUserId = crypto.randomUUID(); | ||
const site = await factory.site({ webhookId: oldWebhookId }); | ||
const user = await factory.user({ githubAccessToken, githubUserId }); | ||
const uaaId = await factory.uaaIdentity({ | ||
userId: user.id, | ||
}); | ||
|
||
githubAPINocks.webhook( | ||
{ | ||
accessToken: user.githubAccessToken, | ||
owner: site.owner, | ||
repo: site.repository, | ||
response: 201, | ||
}, | ||
{ id: webhookId } | ||
); | ||
|
||
const updatedSite = await setRepoWebhook(site, uaaId.email); | ||
expect(updatedSite.webhookId).to.equal(webhookId); | ||
}); | ||
|
||
it('should resolve if the webhook already exists on the site repository', async () => { | ||
const oldWebhookId = 90210; | ||
const githubAccessToken = crypto.randomUUID(); | ||
const githubUserId = crypto.randomUUID(); | ||
const site = await factory.site({ webhookId: oldWebhookId }); | ||
const user = await factory.user({ githubAccessToken, githubUserId }); | ||
const uaaId = await factory.uaaIdentity({ | ||
userId: user.id, | ||
}); | ||
|
||
githubAPINocks.webhook({ | ||
accessToken: user.githubAccessToken, | ||
owner: site.owner, | ||
repo: site.repository, | ||
response: [ | ||
400, | ||
{ | ||
errors: [{ message: 'Hook already exists on this repository' }], | ||
}, | ||
], | ||
}); | ||
|
||
const updatedSite = await setRepoWebhook(site, uaaId.email); | ||
expect(updatedSite.webhookId).to.equal(oldWebhookId); | ||
}); | ||
}); | ||
}); |