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

feat(frontend): trigger build pipeline #202

Merged
merged 11 commits into from
Dec 11, 2020
Merged
5 changes: 5 additions & 0 deletions targets/frontend/.env
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
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
UnbearableBear marked this conversation as resolved.
Show resolved Hide resolved
);

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
Original file line number Diff line number Diff line change
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
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,
};
1 change: 1 addition & 0 deletions targets/frontend/src/hoc/UserProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function withUserProvider(WrappedComponent) {
};

static async getInitialProps(ctx) {
console.log("GIP");
const token = await auth(ctx);

const componentProps =
Expand Down
45 changes: 45 additions & 0 deletions targets/frontend/src/lib/gitlab.api.js
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(),
},
},
});
}
37 changes: 37 additions & 0 deletions targets/frontend/src/pages/api/pipelines.js
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();
}
41 changes: 41 additions & 0 deletions targets/frontend/src/pages/api/trigger_pipeline.js
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();
}
7 changes: 6 additions & 1 deletion targets/frontend/src/pages/index.js
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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==

[email protected]:
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