Skip to content

Commit

Permalink
feat(frontend): trigger build pipeline (#202)
Browse files Browse the repository at this point in the history
* 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
lionelB and Julien Bouquillon committed Dec 11, 2020
1 parent 7e804ca commit dde03ed
Show file tree
Hide file tree
Showing 11 changed files with 324 additions and 2 deletions.
5 changes: 5 additions & 0 deletions targets/frontend/.env
Expand Up @@ -17,3 +17,8 @@ NEXT_PUBLIC_ACTIVATION_TOKEN_EXPIRES=10080
FRONTEND_URL=http://localhost:3000
HASURA_GRAPHQL_ENDPOINT=http://localhost:8080/v1/graphql
PORT=3000

GITLAB_URL = https://gitlab.factory.social.gouv.fr/api/v4
GITLAB_PROJECT_ID = 51
GITLAB_ACCESS_TOKEN = your-token
GITLAB_TRIGGER_TOKEN = your-token
2 changes: 2 additions & 0 deletions targets/frontend/package.json
Expand Up @@ -26,6 +26,7 @@
"cookie": "^0.4.1",
"d3": "^6.3.1",
"d3-hierarchy": "^2.0.0",
"date-fns": "^2.16.1",
"diff": "^5.0.0",
"graphql": "^15.4.0",
"http-proxy-middleware": "^1.0.6",
Expand Down Expand Up @@ -54,6 +55,7 @@
"sentry-testkit": "^3.2.1",
"strip-markdown": "^4.0.0",
"styled-components": "^5.2.1",
"swr": "^0.3.9",
"theme-ui": "^0.3.4",
"unified": "^9.2.0",
"unist-util-parents": "^1.0.3",
Expand Down
78 changes: 78 additions & 0 deletions targets/frontend/src/components/button/GitlabButton.js
@@ -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,
};
1 change: 1 addition & 0 deletions targets/frontend/src/components/button/index.js
Expand Up @@ -81,6 +81,7 @@ const SolidButton = React.forwardRef(function _SolidButton(
"&[disabled]": {
bg: "muted",
borderColor: "muted",
cursor: "default",
},
bg: (theme) => theme.buttons[variant].bg,
borderColor: (theme) => theme.buttons[variant].bg,
Expand Down
92 changes: 92 additions & 0 deletions targets/frontend/src/components/confirmButton/index.js
@@ -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,
};
1 change: 0 additions & 1 deletion targets/frontend/src/lib/auth/exchanges.js
Expand Up @@ -55,7 +55,6 @@ export function customAuthExchange(ctx) {
// if your refresh logic is a separate RESTful endpoint, use fetch or similar
setToken(null);
const result = await auth(ctx);
console.log({ result });
if (result?.jwt_token) {
// return the new tokens
return { token: result.jwt_token };
Expand Down
45 changes: 45 additions & 0 deletions targets/frontend/src/lib/gitlab.api.js
@@ -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(),
},
},
});
}
37 changes: 37 additions & 0 deletions targets/frontend/src/pages/api/pipelines.js
@@ -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();
}
41 changes: 41 additions & 0 deletions targets/frontend/src/pages/api/trigger_pipeline.js
@@ -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();
}
7 changes: 6 additions & 1 deletion targets/frontend/src/pages/index.js
@@ -1,6 +1,7 @@
/** @jsx jsx */

import { GitlabButton } from "src/components/button/GitlabButton";
import { Layout } from "src/components/layout/auth.layout";
import { Inline } from "src/components/layout/Inline";
import { withCustomUrqlClient } from "src/hoc/CustomUrqlClient";
import { withUserProvider } from "src/hoc/UserProvider";
import { jsx, Text } from "theme-ui";
Expand All @@ -9,6 +10,10 @@ export function IndexPage() {
return (
<Layout title="Home">
<Text>Administration des contenus et gestion des alertes</Text>
<Inline>
{/* <GitlabButton env="prod">Mettre à jour la prod</GitlabButton> */}
<GitlabButton env="preprod">Mettre à jour la preprod</GitlabButton>
</Inline>
</Layout>
);
}
Expand Down
17 changes: 17 additions & 0 deletions yarn.lock
Expand Up @@ -5682,6 +5682,11 @@ date-fns@^1.23.0:
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==

date-fns@^2.16.1:
version "2.16.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.16.1.tgz#05775792c3f3331da812af253e1a935851d3834b"
integrity sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ==

dateformat@^3.0.0:
version "3.0.3"
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
Expand Down Expand Up @@ -5850,6 +5855,11 @@ deprecation@^2.0.0, deprecation@^2.3.1:
resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919"
integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==

dequal@2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d"
integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==

des.js@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
Expand Down Expand Up @@ -13506,6 +13516,13 @@ supports-hyperlinks@^2.0.0:
has-flag "^4.0.0"
supports-color "^7.0.0"

swr@^0.3.9:
version "0.3.9"
resolved "https://registry.yarnpkg.com/swr/-/swr-0.3.9.tgz#a179a795244c7b68684af6a632f1ad579e6a69e0"
integrity sha512-lyN4SjBzpoW4+v3ebT7JUtpzf9XyzrFwXIFv+E8ZblvMa5enSNaUBs4EPkL8gGA/GDMLngEmB53o5LaNboAPfg==
dependencies:
dequal "2.0.2"

symbol-tree@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
Expand Down

0 comments on commit dde03ed

Please sign in to comment.