Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[TECH] Créer une application Scalingo depuis Slack #144

Merged
merged 1 commit into from
Sep 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions common/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@ const {
const {
sampleView: releaseTagSelection,
} = require('../../run/services/slack/surfaces/modals/deploy-release/release-tag-selection');
const {
sampleView: createAppOnScalingoSelection,
} = require('../../run/services/slack/surfaces/modals/scalingo-apps/application-creation');
const {
sampleView: releaseDeploymentConfirmation,
} = require('../../run/services/slack/surfaces/modals/deploy-release/release-deployment-confirmation');
const {
sampleView: releasePublicationConfirmation,
} = require('../../build/services/slack/surfaces/modals/publish-release/release-publication-confirmation');
const {
sampleView: submitApplicationNameSelection,
} = require('../../run/services/slack/surfaces/modals/scalingo-apps/application-creation-confirmation');

module.exports = {
getApiInfo() {
Expand All @@ -25,8 +31,10 @@ module.exports = {
const views = [
{ name: 'release-type-selection', view: releaseTypeSelection() },
{ name: 'release-tag-selection', view: releaseTagSelection() },
{ name: 'create-app-on-scalingo', view: createAppOnScalingoSelection() },
{ name: 'release-deployment-confirmation', view: releaseDeploymentConfirmation() },
{ name: 'release-publication-confirmation', view: releasePublicationConfirmation() },
{ name: 'application-creation-confirmation', view: submitApplicationNameSelection() },
];
return views
.map(({ name, view }) => {
Expand Down
18 changes: 18 additions & 0 deletions common/models/ScalingoAppName.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//TODO use EnvVars
const config = require('../../config');
const alphanumericAndDashOnly = /^([a-zA-Z0-9]+-)+[a-zA-Z0-9]+$/;
const prefix = 'pix-';
class ScalingoAppName {
static isApplicationNameValid(applicationName) {
const suffix = config.scalingo.validAppSuffix;
const appNameMatchesRegex = applicationName.search(alphanumericAndDashOnly) >= 0;
const appNameHasCorrectLength = applicationName.length >= 6 && applicationName.length <= 46;
const appNameStartsWithPix = applicationName.startsWith(prefix);
const appNameEndsWithCorrectSuffix = suffix.includes(applicationName.split('-').slice(-1)[0]);
return appNameMatchesRegex && appNameHasCorrectLength && appNameStartsWithPix && appNameEndsWithCorrectSuffix;
yoandl marked this conversation as resolved.
Show resolved Hide resolved
}
}

module.exports = {
ScalingoAppName,
};
22 changes: 22 additions & 0 deletions common/services/scalingo-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,28 @@ class ScalingoClient {
throw err;
}
}
async inviteCollaborator(applicationId, collaboratorEmail) {
try {
const { invitation_link } = await this.client.Collaborators.invite(applicationId, collaboratorEmail);
return invitation_link;
} catch (e) {
console.error(JSON.stringify(e));
throw new Error(`Impossible to invite ${collaboratorEmail} on ${applicationId}`);
}
}

async createApplication(name) {
const app = {
name: name,
};
try {
const { id } = await this.client.Apps.create(app);
return id;
} catch (e) {
console.error(JSON.stringify(e));
throw new Error(`Impossible to create ${app.name}, ${e.name}`);
}
}
}

async function _isUrlReachable(url) {
Expand Down
4 changes: 2 additions & 2 deletions common/services/slack/surfaces/messages/post-message.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const axios = require('axios');
const config = require('../../../../../config');

module.exports = {
async postMessage(message, attachments) {
async postMessage(message, attachments, channel = '#tech-releases') {
const options = {
method: 'POST',
url: 'https://slack.com/api/chat.postMessage',
Expand All @@ -11,7 +11,7 @@ module.exports = {
authorization: `Bearer ${config.slack.botToken}`,
},
data: {
channel: '#tech-releases',
channel: channel,
text: message,
attachments: attachments,
},
Expand Down
21 changes: 21 additions & 0 deletions common/services/slack/surfaces/user-infos/get-user-infos.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const axios = require('axios');
const config = require('../../../../../config');

module.exports = {
async getUserEmail(userId) {
const options = {
method: 'GET',
url: `https://slack.com/api/users.info?user=${userId}`,
headers: {
'content-type': 'application/json',
authorization: `Bearer ${config.slack.botToken}`,
},
};
const response = await axios(options);
if (!response.data.ok) {
console.error(response.data);
throw new Error('Slack error received');
}
return response.data.user.profile.email;
},
};
4 changes: 3 additions & 1 deletion config.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ module.exports = (function() {
production: {
token: process.env.SCALINGO_TOKEN_PRODUCTION,
apiUrl: process.env.SCALINGO_API_URL_PRODUCTION,
}
},
validAppSuffix: _getJSON(process.env.SCALINGO_VALID_APP_SUFFIX) ||
["production","review","integration","recette","sandbox","dev","router","test"]
octo-topi marked this conversation as resolved.
Show resolved Hide resolved
},

openApi: {
Expand Down
9 changes: 9 additions & 0 deletions run/controllers/slack.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,23 @@ module.exports = {
if (payload.callback_id === 'deploy-release') {
shortcuts.openViewDeployReleaseTagSelection(payload);
}
if (payload.callback_id === 'scalingo-app-creation') {
shortcuts.openViewCreateAppOnScalingoSelection(payload);
}
return null;
case 'view_submission':
if (payload.view.callback_id === shortcuts.openViewDeployReleaseTagSelectionCallbackId) {
return viewSubmissions.submitReleaseTagSelection(payload);
}
if (payload.view.callback_id === shortcuts.openViewCreateAppOnScalingoSelectionCallbackId) {
return viewSubmissions.submitApplicationNameSelection(payload);
}
if (payload.view.callback_id === viewSubmissions.submitReleaseTagSelectionCallbackId) {
return viewSubmissions.submitReleaseDeploymentConfirmation(payload);
}
if (payload.view.callback_id === viewSubmissions.submitApplicationNameSelectionCallbackId) {
return viewSubmissions.submitCreateAppOnScalingoConfirmation(payload);
}
return null;
case 'view_closed':
case 'block_actions':
Expand Down
7 changes: 7 additions & 0 deletions run/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@ manifest.registerShortcut({
description: "Lance le déploiement d'une version sur l'environnement de production",
});

manifest.registerShortcut({
name: 'Créer une application sur Scalingo',
type: 'global',
callback_id: 'scalingo-app-creation',
description: "Lance la création d'une application sur Scalingo",
});

manifest.addInteractivity({
path: '/run/slack/interactive-endpoint',
handler: slackbotController.interactiveEndpoint,
Expand Down
19 changes: 18 additions & 1 deletion run/services/slack/shortcuts.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
const axios = require('axios');
const deployReleaseTagSelectionModal = require('./surfaces/modals/deploy-release/release-tag-selection');
const createAppOnScalingoModal = require('./surfaces/modals/scalingo-apps/application-creation');
const config = require('../../../config');
const openViewUrl = 'https://slack.com/api/views.open';

module.exports = {
openViewDeployReleaseTagSelectionCallbackId: deployReleaseTagSelectionModal.callbackId,

openViewCreateAppOnScalingoSelectionCallbackId: createAppOnScalingoModal.callbackId,

openViewDeployReleaseTagSelection(payload) {
const options = {
method: 'POST',
url: 'https://slack.com/api/views.open',
url: openViewUrl,
headers: {
'content-type': 'application/json',
authorization: `Bearer ${config.slack.botToken}`,
Expand All @@ -17,4 +21,17 @@ module.exports = {
};
return axios(options);
},

openViewCreateAppOnScalingoSelection(payload) {
const options = {
method: 'POST',
url: openViewUrl,
headers: {
'content-type': 'application/json',
authorization: `Bearer ${config.slack.botToken}`,
},
data: createAppOnScalingoModal(payload.trigger_id),
};
return axios(options);
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const { Modal, Blocks } = require('slack-block-builder');

const callbackId = 'application-creation-confirmation';

function modal(applicationName, applicationEnvironment, applicationEnvironmentName, userEmail) {
return Modal({
title: 'Confirmation',
callbackId,
privateMetaData: JSON.stringify({
applicationName: applicationName,
applicationEnvironment: applicationEnvironment,
userEmail: userEmail,
}),
submit: '🚀 Go !',
close: 'Annuler',
}).blocks([
Blocks.Section({
text: `Vous vous apprêtez à créer l'application *${applicationName}* dans la région : *${applicationEnvironmentName}* et à inviter cet adesse email en tant que collaborateur : *${userEmail}*`,
}),
]);
}

module.exports = (applicationName, applicationEnvironment, applicationEnvironmentName, userEmail) => {
return {
response_action: 'push',
view: modal(applicationName, applicationEnvironment, applicationEnvironmentName, userEmail).buildToObject(),
};
};

module.exports.sampleView = () => {
return modal('pix-application-name-recette', 'recette', 'Paris - SecNumCloud - Outscale', 'john.doe@pix.fr');
};

module.exports.callbackId = callbackId;
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const { Modal, Blocks, Elements, Bits } = require('slack-block-builder');
const regions = [
{ id: 'production', name: 'Paris - SecNumCloud - Outscale' },
{ id: 'recette', name: 'Paris - Outscale' },
];
const callbackId = 'application-name-selection';

function modal() {
return Modal({
title: 'Créer une application',
callbackId,
submit: 'Créer',
close: 'Annuler',
}).blocks([
Blocks.Input({
blockId: 'create-app-name',
label: "Nom de l'application",
}).element(
Elements.TextInput({
actionId: 'scalingo-app-name',
placeholder: 'application-name',
initialValue: 'pix-super-application-recette',
})
),
Blocks.Input({ blockId: 'application-env', label: 'Quelle région ?' }).element(
Elements.StaticSelect({ placeholder: 'Choisis la région' })
.actionId('item')
.options(regions.map((item) => Bits.Option({ text: item.name, value: item.id })))
),
]);
}

module.exports = (triggerId) => {
return {
trigger_id: triggerId,
view: modal(regions).buildToObject(),
};
};

module.exports.sampleView = () => {
return modal(regions);
};

module.exports.callbackId = callbackId;
36 changes: 36 additions & 0 deletions run/services/slack/view-submissions.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
const openModalReleaseDeploymentConfirmation = require('./surfaces/modals/deploy-release/release-deployment-confirmation');
const openModalApplicationCreationConfirmation = require('./surfaces/modals/scalingo-apps/application-creation-confirmation');
const { environments, deploy } = require('../../../common/services/releases');
const githubService = require('../../../common/services/github');
const slackPostMessageService = require('../../../common/services/slack/surfaces/messages/post-message');
const slackGetUserInfos = require('../../../common/services/slack/surfaces/user-infos/get-user-infos');
const ScalingoClient = require('../../../common/services/scalingo-client');
const { ScalingoAppName } = require('../../../common/models/ScalingoAppName');

module.exports = {
async submitReleaseTagSelection(payload) {
Expand All @@ -10,8 +14,27 @@ module.exports = {
return openModalReleaseDeploymentConfirmation(releaseTag, hasConfigFileChanged);
},

async submitApplicationNameSelection(payload) {
const applicationName = payload.view.state.values['create-app-name']['scalingo-app-name'].value;
const applicationEnvironment = payload.view.state.values['application-env']['item']['selected_option'].value;
const applicationEnvironmentName =
payload.view.state.values['application-env']['item']['selected_option']['text']['text'];
const userEmail = await slackGetUserInfos.getUserEmail(payload.user.id);
if (!ScalingoAppName.isApplicationNameValid(applicationName)) {
return `${applicationName} is incorrect`;
}
return openModalApplicationCreationConfirmation(
applicationName,
applicationEnvironment,
applicationEnvironmentName,
userEmail
);
},

submitReleaseTagSelectionCallbackId: openModalReleaseDeploymentConfirmation.callbackId,

submitApplicationNameSelectionCallbackId: openModalApplicationCreationConfirmation.callbackId,

submitReleaseDeploymentConfirmation(payload) {
const releaseTag = payload.view.private_metadata;
if (!githubService.isBuildStatusOK({ tagName: releaseTag.trim().toLowerCase() })) {
Expand All @@ -23,4 +46,17 @@ module.exports = {
response_action: 'clear',
};
},

async submitCreateAppOnScalingoConfirmation(payload) {
const { applicationName, applicationEnvironment, userEmail } = JSON.parse(payload.view.private_metadata);
const client = await ScalingoClient.getInstance(applicationEnvironment);
const appId = await client.createApplication(applicationName);
const invitationLink = await client.inviteCollaborator(appId, userEmail);
const message = `app ${applicationName} created <${invitationLink}|invitation link>`;
const channel = `@${payload.user.id}`;
slackPostMessageService.postMessage(message, null, channel);
return {
response_action: 'clear',
};
},
};
6 changes: 6 additions & 0 deletions test/acceptance/run/manifest_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ describe('Acceptance | Run | Manifest', function () {
callback_id: 'deploy-release',
description: "Lance le déploiement d'une version sur l'environnement de production",
},
{
name: 'Créer une application sur Scalingo',
type: 'global',
callback_id: 'scalingo-app-creation',
description: "Lance la création d'une application sur Scalingo",
},
],
slash_commands: [
{
Expand Down
Loading