Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(frontend): trigger build pipeline (#202)
* feat(frontend): trigger build pipeline * fix(frontend): update pending before request * Update targets/frontend/src/components/confirmButton/index.js Co-authored-by: Julien Bouquillon <julien.bouquillon@sg.social.gouv.fr> * fix: revalidate after error * fix: protect pipelines / pipeline trigger with jwt * fix(frontend): use fetcher fn for swr * wip * fix: fix gitlab buttons * fix: review * fix: pass private token in headers request * remove log Co-authored-by: Julien Bouquillon <julien.bouquillon@sg.social.gouv.fr>
- Loading branch information
Showing
11 changed files
with
324 additions
and
2 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
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,78 @@ | ||
/** @jsx jsx */ | ||
import PropTypes from "prop-types"; | ||
import { useEffect, useState } from "react"; | ||
import { | ||
MdDoNotDisturbAlt, | ||
MdLoop, | ||
MdSyncProblem, | ||
MdTimelapse, | ||
} from "react-icons/md"; | ||
import { getToken } from "src/lib/auth/token"; | ||
import { request } from "src/lib/request"; | ||
import useSWR from "swr"; | ||
import { jsx } from "theme-ui"; | ||
|
||
import { ConfirmButton } from "../confirmButton"; | ||
|
||
function fetchPipelines(url) { | ||
const { jwt_token } = getToken(); | ||
return request(url, { headers: { token: jwt_token } }); | ||
} | ||
|
||
export function GitlabButton({ env, children }) { | ||
const [status, setStatus] = useState("disabled"); | ||
const token = getToken(); | ||
const { error, data, isValidating, mutate } = useSWR( | ||
`/api/pipelines`, | ||
fetchPipelines | ||
); | ||
|
||
console.log("swr", { data, error, isValidating }); | ||
|
||
async function clickHandler() { | ||
if (isDisabled) { | ||
return; | ||
} | ||
setStatus("pending"); | ||
await request("/api/trigger_pipeline", { | ||
body: { | ||
env, | ||
}, | ||
headers: { | ||
token: token?.jwt_token, | ||
}, | ||
}).catch(() => { | ||
setStatus("disabled"); | ||
}); | ||
mutate(); | ||
} | ||
|
||
useEffect(() => { | ||
if (!error && data) { | ||
if (data[env] === false) { | ||
console.log(env, "ready to update", data); | ||
setStatus("ready"); | ||
} | ||
if (data[env] === true) { | ||
setStatus("pending"); | ||
} | ||
} | ||
}, [env, data, error]); | ||
|
||
const isDisabled = | ||
status === "disabled" || status === "pending" || status === "error"; | ||
return ( | ||
<ConfirmButton disabled={isDisabled} onClick={clickHandler}> | ||
{status === "pending" && <MdTimelapse />} | ||
{status === "ready" && <MdLoop />} | ||
{status === "disabled" && <MdDoNotDisturbAlt />} | ||
{status === "error" && <MdSyncProblem />} | ||
{children} | ||
</ConfirmButton> | ||
); | ||
} | ||
|
||
GitlabButton.propTypes = { | ||
children: PropTypes.node, | ||
env: PropTypes.string, | ||
}; |
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,92 @@ | ||
/** @jsx jsx */ | ||
|
||
import PropTypes from "prop-types"; | ||
import React, { useState } from "react"; | ||
import { MdClose } from "react-icons/md"; | ||
import { Button as BaseButton, jsx } from "theme-ui"; | ||
|
||
const buttonPropTypes = { | ||
size: PropTypes.oneOf(["small", "normal"]), | ||
variant: PropTypes.oneOf(["accent", "secondary", "primary", "link"]), | ||
}; | ||
|
||
const defaultButtonStyles = { | ||
alignItems: "center", | ||
appearance: "none", | ||
borderRadius: "small", | ||
borderStyle: "solid", | ||
borderWidth: 2, | ||
cursor: "pointer", | ||
display: "inline-flex", | ||
fontSize: "inherit", | ||
fontWeight: "bold", | ||
lineHeight: "inherit", | ||
m: 0, | ||
minWidth: 0, | ||
textAlign: "center", | ||
textDecoration: "none", | ||
}; | ||
const normalSize = { | ||
px: "xsmall", | ||
py: "xsmall", | ||
}; | ||
const smallSize = { | ||
px: "xxsmall", | ||
py: "xxsmall", | ||
}; | ||
|
||
export const ConfirmButton = React.forwardRef( | ||
( | ||
{ variant = "primary", size = "normal", children, onClick, ...props }, | ||
ref | ||
) => { | ||
const [needConfirm, setNeedConfirm] = useState(false); | ||
|
||
const onClickCustom = (event) => { | ||
if (!needConfirm) { | ||
setNeedConfirm(true); | ||
} else { | ||
setNeedConfirm(false); | ||
onClick(event); | ||
} | ||
}; | ||
const cancel = (event) => { | ||
event.stopPropagation(); | ||
setNeedConfirm(false); | ||
}; | ||
return ( | ||
<BaseButton | ||
{...props} | ||
ref={ref} | ||
sx={{ | ||
...defaultButtonStyles, | ||
...(size === "small" ? smallSize : normalSize), | ||
"&:hover:not([disabled])": { | ||
bg: (theme) => theme.buttons[variant].bgHover, | ||
borderColor: (theme) => theme.buttons[variant].bgHover, | ||
}, | ||
"&[disabled]": { | ||
bg: "muted", | ||
borderColor: "muted", | ||
}, | ||
bg: (theme) => theme.buttons[variant].bg, | ||
borderColor: (theme) => theme.buttons[variant].bg, | ||
borderRadius: "small", | ||
color: (theme) => theme.buttons[variant].color, | ||
}} | ||
onClick={onClickCustom} | ||
> | ||
{needConfirm ? ( | ||
<> | ||
Vraiment ? <MdClose onClick={cancel} /> | ||
</> | ||
) : ( | ||
children | ||
)} | ||
</BaseButton> | ||
); | ||
} | ||
); | ||
ConfirmButton.propTypes = { | ||
...buttonPropTypes, | ||
}; |
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,45 @@ | ||
import subHours from "date-fns/subHours"; | ||
|
||
import { request } from "./request"; | ||
|
||
const url = process.env.GITLAB_URL; | ||
const projectId = process.env.GITLAB_PROJECT_ID; | ||
const accessToken = process.env.GITLAB_ACCESS_TOKEN; | ||
const token = process.env.GITLAB_TRIGGER_TOKEN; | ||
|
||
export function getPipelines({ ref = "master", since }) { | ||
if (!since) { | ||
since = subHours(new Date(), 2); | ||
} | ||
|
||
return request( | ||
`${url}/projects/${projectId}/pipelines?updated_after=${since.toISOString()}&ref=${ref}&order_by=updated_at`, | ||
{ | ||
headers: { private_token: accessToken }, | ||
} | ||
); | ||
} | ||
|
||
export function getPipelineInfos(id) { | ||
return request(`${url}/projects/${projectId}/pipelines/${id}`, { | ||
headers: { private_token: accessToken }, | ||
}); | ||
} | ||
|
||
export function getPipelineVariables(id) { | ||
return request(`${url}/projects/${projectId}/pipelines/${id}/variables`, { | ||
headers: { private_token: accessToken }, | ||
}); | ||
} | ||
|
||
export function triggerDeploy(env) { | ||
return request(`${url}/projects/${projectId}/trigger/pipeline`, { | ||
body: { | ||
ref: "master", | ||
token, | ||
variables: { | ||
UPDATE_ES_INDEX: env.toUpperCase(), | ||
}, | ||
}, | ||
}); | ||
} |
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,37 @@ | ||
import Boom from "@hapi/boom"; | ||
import { verify } from "jsonwebtoken"; | ||
import { createErrorFor } from "src/lib/apiError"; | ||
import { getPipelines, getPipelineVariables } from "src/lib/gitlab.api"; | ||
|
||
const { HASURA_GRAPHQL_JWT_SECRET } = process.env; | ||
const jwtSecret = JSON.parse(HASURA_GRAPHQL_JWT_SECRET); | ||
|
||
export default async function pipelines(req, res) { | ||
const apiError = createErrorFor(res); | ||
const { token } = req.headers; | ||
|
||
if (!token || !verify(token, jwtSecret.key, { algorithms: jwtSecret.type })) { | ||
return apiError(Boom.badRequest("wrong token")); | ||
} | ||
|
||
const pipelines = await getPipelines({ ref: "master" }); | ||
|
||
const activePipelines = pipelines.filter( | ||
({ status }) => status === "pending" || status === "running" | ||
); | ||
|
||
const pipelinesDetails = await Promise.all( | ||
activePipelines.map(({ id }) => getPipelineVariables(id)) | ||
); | ||
const runningDeployementPipeline = pipelinesDetails.flat().reduce( | ||
(state, { key, value }) => { | ||
if (key === "UPDATE_ES_INDEX") { | ||
state[value.toLowerCase()] = true; | ||
} | ||
return state; | ||
}, | ||
{ preprod: false, prod: false } | ||
); | ||
res.json(runningDeployementPipeline); | ||
res.end(); | ||
} |
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,41 @@ | ||
import Boom from "@hapi/boom"; | ||
import Joi from "@hapi/joi"; | ||
import { verify } from "jsonwebtoken"; | ||
import { createErrorFor } from "src/lib/apiError"; | ||
import { triggerDeploy } from "src/lib/gitlab.api"; | ||
|
||
const { HASURA_GRAPHQL_JWT_SECRET } = process.env; | ||
const jwtSecret = JSON.parse(HASURA_GRAPHQL_JWT_SECRET); | ||
|
||
export default async function (req, res) { | ||
const apiError = createErrorFor(res); | ||
|
||
if (req.method === "GET") { | ||
res.setHeader("Allow", ["POST"]); | ||
return apiError(Boom.methodNotAllowed("GET method not allowed")); | ||
} | ||
|
||
const { token } = req.headers; | ||
if (!verify(token, jwtSecret.key, { algorithms: jwtSecret.type })) { | ||
return apiError(Boom.badRequest("wrong token")); | ||
} | ||
|
||
const schema = Joi.object({ | ||
env: Joi.string().required(), | ||
}); | ||
|
||
const { error, value } = schema.validate(req.body); | ||
|
||
if (error) { | ||
console.error(error); | ||
return apiError(Boom.badRequest(error.details[0].message)); | ||
} | ||
if (["PROD", "PREPROD"].includes(value.env)) { | ||
return apiError(Boom.badRequest(`unknow env ${value.env}`)); | ||
} | ||
|
||
await triggerDeploy(value.env); | ||
res.status(200).json({ message: "ok" }); | ||
|
||
res.end(); | ||
} |
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