|
|
@@ -6,30 +6,23 @@ import backArrow from "assets/back_arrow.png";
|
|
|
import { merge, set } from "lodash";
|
|
|
import loading from "assets/loading.gif";
|
|
|
|
|
|
-import {
|
|
|
- ChartType,
|
|
|
- ChartTypeWithExtendedConfig,
|
|
|
- ClusterType,
|
|
|
-} from "shared/types";
|
|
|
+import { ChartType, ClusterType } from "shared/types";
|
|
|
import { Context } from "shared/Context";
|
|
|
-import api from "shared/api";
|
|
|
|
|
|
import TitleSection from "components/TitleSection";
|
|
|
import SettingsSection from "./SettingsSection";
|
|
|
import PorterFormWrapper from "components/porter-form/PorterFormWrapper";
|
|
|
import ValuesYaml from "./ValuesYaml";
|
|
|
import DeploymentType from "./DeploymentType";
|
|
|
-import { useRouting } from "../../../../shared/routing";
|
|
|
-import { useRouteMatch } from "react-router";
|
|
|
import RevisionSection from "./RevisionSection";
|
|
|
-import { onlyInLeft } from "shared/array_utils";
|
|
|
import Loading from "components/Loading";
|
|
|
-import { NewWebsocketOptions, useWebsockets } from "shared/hooks/useWebsockets";
|
|
|
import JobList from "./jobs/JobList";
|
|
|
import SaveButton from "components/SaveButton";
|
|
|
import useAuth from "shared/auth/useAuth";
|
|
|
import ExpandedJobRun from "./jobs/ExpandedJobRun";
|
|
|
-import { useEffectDebugger } from "shared/hooks/useEffectDebugger";
|
|
|
+import { useJobs } from "./jobs/useJobs";
|
|
|
+import { useChart } from "shared/hooks/useChart";
|
|
|
+import Modal from "main/home/modals/Modal";
|
|
|
|
|
|
const readableDate = (s: string) => {
|
|
|
let ts = new Date(s);
|
|
|
@@ -59,6 +52,7 @@ export const ExpandedJobChartFC: React.FC<{
|
|
|
upgradeChart,
|
|
|
loadChartWithSpecificRevision,
|
|
|
} = useChart(oldChart, closeChart);
|
|
|
+
|
|
|
const {
|
|
|
jobs,
|
|
|
hasPorterImageTemplate,
|
|
|
@@ -353,564 +347,6 @@ const ExpandedJobHeader: React.FC<{
|
|
|
</HeaderWrapper>
|
|
|
);
|
|
|
|
|
|
-const useChart = (oldChart: ChartType, closeChart: () => void) => {
|
|
|
- const { currentProject, currentCluster, setCurrentError } = useContext(
|
|
|
- Context
|
|
|
- );
|
|
|
- const [chart, setChart] = useState<ChartTypeWithExtendedConfig>(null);
|
|
|
- const { url: matchUrl } = useRouteMatch();
|
|
|
-
|
|
|
- const [status, setStatus] = useState<"ready" | "loading" | "deleting">(
|
|
|
- "loading"
|
|
|
- );
|
|
|
- const { pushFiltered, getQueryParam, pushQueryParams } = useRouting();
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
- const { namespace, name: chartName } = oldChart;
|
|
|
- setStatus("loading");
|
|
|
-
|
|
|
- const revision = getQueryParam("chart_revision");
|
|
|
-
|
|
|
- api
|
|
|
- .getChart<ChartTypeWithExtendedConfig>(
|
|
|
- "token",
|
|
|
- {},
|
|
|
- {
|
|
|
- id: currentProject?.id,
|
|
|
- cluster_id: currentCluster?.id,
|
|
|
- namespace,
|
|
|
- name: chartName,
|
|
|
- revision: Number(revision) ? Number(revision) : 0,
|
|
|
- }
|
|
|
- )
|
|
|
- .then((res) => {
|
|
|
- if (res?.data) {
|
|
|
- setChart(res.data);
|
|
|
- }
|
|
|
- })
|
|
|
- .finally(() => {
|
|
|
- setStatus("ready");
|
|
|
- });
|
|
|
- }, [oldChart, currentCluster, currentProject]);
|
|
|
-
|
|
|
- /**
|
|
|
- * Upgrade chart version
|
|
|
- */
|
|
|
- const upgradeChart = async () => {
|
|
|
- // convert current values to yaml
|
|
|
- let valuesYaml = yaml.dump({
|
|
|
- ...(chart.config as Object),
|
|
|
- });
|
|
|
-
|
|
|
- try {
|
|
|
- await api.upgradeChartValues(
|
|
|
- "<token>",
|
|
|
- {
|
|
|
- values: valuesYaml,
|
|
|
- version: chart.latest_version,
|
|
|
- },
|
|
|
- {
|
|
|
- id: currentProject.id,
|
|
|
- name: chart.name,
|
|
|
- namespace: chart.namespace,
|
|
|
- cluster_id: currentCluster.id,
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- window.analytics.track("Chart Upgraded", {
|
|
|
- chart: chart.name,
|
|
|
- values: valuesYaml,
|
|
|
- });
|
|
|
- } catch (err) {
|
|
|
- let parsedErr = err?.response?.data?.error;
|
|
|
-
|
|
|
- if (parsedErr) {
|
|
|
- err = parsedErr;
|
|
|
- }
|
|
|
- setCurrentError(parsedErr);
|
|
|
-
|
|
|
- window.analytics.track("Failed to Upgrade Chart", {
|
|
|
- chart: chart.name,
|
|
|
- values: valuesYaml,
|
|
|
- error: err,
|
|
|
- });
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- /**
|
|
|
- * Delete/Uninstall chart
|
|
|
- */
|
|
|
- const deleteChart = async () => {
|
|
|
- try {
|
|
|
- await api.uninstallTemplate(
|
|
|
- "<token>",
|
|
|
- {},
|
|
|
- {
|
|
|
- namespace: chart.namespace,
|
|
|
- name: chart.name,
|
|
|
- id: currentProject.id,
|
|
|
- cluster_id: currentCluster.id,
|
|
|
- }
|
|
|
- );
|
|
|
- setStatus("ready");
|
|
|
- closeChart();
|
|
|
- return;
|
|
|
- } catch (error) {
|
|
|
- console.log(error);
|
|
|
- throw new Error("Couldn't uninstall the chart");
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- /**
|
|
|
- * Update chart values
|
|
|
- */
|
|
|
- const updateChart = async (
|
|
|
- processValues:
|
|
|
- | ((chart: ChartType) => string)
|
|
|
- | ((chart: ChartType, oldChart?: ChartType) => string)
|
|
|
- ) => {
|
|
|
- const values = processValues(chart, oldChart);
|
|
|
-
|
|
|
- const oldSyncedEnvGroups = oldChart.config?.container?.env?.synced || [];
|
|
|
- const newSyncedEnvGroups = chart.config?.container?.env?.synced || [];
|
|
|
-
|
|
|
- const deletedEnvGroups = onlyInLeft<{
|
|
|
- keys: Array<any>;
|
|
|
- name: string;
|
|
|
- version: number;
|
|
|
- }>(
|
|
|
- oldSyncedEnvGroups,
|
|
|
- newSyncedEnvGroups,
|
|
|
- (oldVal, newVal) => oldVal.name === newVal.name
|
|
|
- );
|
|
|
-
|
|
|
- const addedEnvGroups = onlyInLeft<{
|
|
|
- keys: Array<any>;
|
|
|
- name: string;
|
|
|
- version: number;
|
|
|
- }>(
|
|
|
- newSyncedEnvGroups,
|
|
|
- oldSyncedEnvGroups,
|
|
|
- (oldVal, newVal) => oldVal.name === newVal.name
|
|
|
- );
|
|
|
-
|
|
|
- const addApplicationToEnvGroupPromises = addedEnvGroups.map(
|
|
|
- (envGroup: any) => {
|
|
|
- return api.addApplicationToEnvGroup(
|
|
|
- "<token>",
|
|
|
- {
|
|
|
- name: envGroup?.name,
|
|
|
- app_name: chart.name,
|
|
|
- },
|
|
|
- {
|
|
|
- project_id: currentProject.id,
|
|
|
- cluster_id: currentCluster.id,
|
|
|
- namespace: chart.namespace,
|
|
|
- }
|
|
|
- );
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- try {
|
|
|
- await Promise.all(addApplicationToEnvGroupPromises);
|
|
|
- } catch (error) {
|
|
|
- setCurrentError(
|
|
|
- "We coudln't sync the env group to the application, please try again."
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- const removeApplicationToEnvGroupPromises = deletedEnvGroups.map(
|
|
|
- (envGroup: any) => {
|
|
|
- return api.removeApplicationFromEnvGroup(
|
|
|
- "<token>",
|
|
|
- {
|
|
|
- name: envGroup?.name,
|
|
|
- app_name: chart.name,
|
|
|
- },
|
|
|
- {
|
|
|
- project_id: currentProject.id,
|
|
|
- cluster_id: currentCluster.id,
|
|
|
- namespace: chart.namespace,
|
|
|
- }
|
|
|
- );
|
|
|
- }
|
|
|
- );
|
|
|
- try {
|
|
|
- await Promise.all(removeApplicationToEnvGroupPromises);
|
|
|
- } catch (error) {
|
|
|
- setCurrentError(
|
|
|
- "We coudln't remove the synced env group from the application, please try again."
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- api
|
|
|
- .upgradeChartValues(
|
|
|
- "<token>",
|
|
|
- {
|
|
|
- values,
|
|
|
- },
|
|
|
- {
|
|
|
- id: currentProject.id,
|
|
|
- name: chart.name,
|
|
|
- namespace: chart.namespace,
|
|
|
- cluster_id: currentCluster.id,
|
|
|
- }
|
|
|
- )
|
|
|
- .then((res) => {
|
|
|
- refreshChart();
|
|
|
- })
|
|
|
- .catch((err) => {
|
|
|
- let parsedErr = err?.response?.data?.error;
|
|
|
-
|
|
|
- if (parsedErr) {
|
|
|
- err = parsedErr;
|
|
|
- }
|
|
|
- throw new Error(parsedErr);
|
|
|
- });
|
|
|
- };
|
|
|
-
|
|
|
- /**
|
|
|
- * Refresh the chart data
|
|
|
- */
|
|
|
- const refreshChart = async () => {
|
|
|
- try {
|
|
|
- const newChart = await api
|
|
|
- .getChart(
|
|
|
- "<token>",
|
|
|
- {},
|
|
|
- {
|
|
|
- name: chart.name,
|
|
|
- revision: 0,
|
|
|
- namespace: chart.namespace,
|
|
|
- cluster_id: currentCluster.id,
|
|
|
- id: currentProject.id,
|
|
|
- }
|
|
|
- )
|
|
|
- .then((res) => res.data);
|
|
|
-
|
|
|
- pushQueryParams({
|
|
|
- chart_version: newChart.version,
|
|
|
- });
|
|
|
-
|
|
|
- setChart(newChart);
|
|
|
- } catch (error) {}
|
|
|
- };
|
|
|
-
|
|
|
- const loadChartWithSpecificRevision = async (revision: number) => {
|
|
|
- try {
|
|
|
- const newChart = await api
|
|
|
- .getChart(
|
|
|
- "<token>",
|
|
|
- {},
|
|
|
- {
|
|
|
- name: chart.name,
|
|
|
- revision: revision,
|
|
|
- namespace: chart.namespace,
|
|
|
- cluster_id: currentCluster.id,
|
|
|
- id: currentProject.id,
|
|
|
- }
|
|
|
- )
|
|
|
- .then((res) => res.data);
|
|
|
-
|
|
|
- pushFiltered(matchUrl, ["project_id", "job"], {
|
|
|
- chart_revision: newChart.version,
|
|
|
- });
|
|
|
-
|
|
|
- setChart(newChart);
|
|
|
- } catch (error) {}
|
|
|
- };
|
|
|
-
|
|
|
- return {
|
|
|
- chart,
|
|
|
- status,
|
|
|
- upgradeChart,
|
|
|
- deleteChart,
|
|
|
- updateChart,
|
|
|
- refreshChart,
|
|
|
- loadChartWithSpecificRevision,
|
|
|
- };
|
|
|
-};
|
|
|
-
|
|
|
-const PORTER_IMAGE_TEMPLATES = [
|
|
|
- "porterdev/hello-porter-job",
|
|
|
- "porterdev/hello-porter-job:latest",
|
|
|
- "public.ecr.aws/o1j4x7p4/hello-porter-job",
|
|
|
- "public.ecr.aws/o1j4x7p4/hello-porter-job:latest",
|
|
|
-];
|
|
|
-
|
|
|
-const useJobs = (chart: ChartType) => {
|
|
|
- const { currentProject, currentCluster, setCurrentError } = useContext(
|
|
|
- Context
|
|
|
- );
|
|
|
- const [jobs, setJobs] = useState([]);
|
|
|
- const jobsRef = useRef([]);
|
|
|
- const [hasPorterImageTemplate, setHasPorterImageTemplate] = useState(true);
|
|
|
- const [selectedJob, setSelectedJob] = useState(null);
|
|
|
-
|
|
|
- const {
|
|
|
- newWebsocket,
|
|
|
- openWebsocket,
|
|
|
- closeAllWebsockets,
|
|
|
- closeWebsocket,
|
|
|
- } = useWebsockets();
|
|
|
-
|
|
|
- const sortJobsAndSave = (newJobs: any[]) => {
|
|
|
- // Set job run from URL if needed
|
|
|
- const urlParams = new URLSearchParams(location.search);
|
|
|
- const urlJob = urlParams.get("job");
|
|
|
-
|
|
|
- const getTime = (job: any) => {
|
|
|
- return new Date(job?.status?.startTime).getTime();
|
|
|
- };
|
|
|
-
|
|
|
- newJobs.sort((job1, job2) => {
|
|
|
- // if (job1.metadata.name === urlJob) {
|
|
|
- // this.setJobRun(job1);
|
|
|
- // } else if (job2.metadata.name === urlJob) {
|
|
|
- // this.setJobRun(job2);
|
|
|
- // }
|
|
|
-
|
|
|
- return getTime(job2) - getTime(job1);
|
|
|
- });
|
|
|
-
|
|
|
- let latestImageDetected =
|
|
|
- newJobs[0]?.spec?.template?.spec?.containers[0]?.image;
|
|
|
- if (!PORTER_IMAGE_TEMPLATES.includes(latestImageDetected)) {
|
|
|
- // this.setState({ jobs, newestImage, imageIsPlaceholder: false });
|
|
|
- setHasPorterImageTemplate(false);
|
|
|
- }
|
|
|
- jobsRef.current = newJobs;
|
|
|
- setJobs(newJobs);
|
|
|
- };
|
|
|
-
|
|
|
- const mergeNewJob = (newJob: any) => {
|
|
|
- let newJobs = [...jobsRef.current];
|
|
|
- const existingJobIndex = newJobs.findIndex((currentJob) => {
|
|
|
- return (
|
|
|
- currentJob.metadata?.name === newJob.metadata?.name &&
|
|
|
- currentJob.metadata?.namespace === newJob.metadata?.namespace
|
|
|
- );
|
|
|
- });
|
|
|
-
|
|
|
- if (existingJobIndex > -1) {
|
|
|
- newJobs.splice(existingJobIndex, 1, newJob);
|
|
|
- } else {
|
|
|
- newJobs.push(newJob);
|
|
|
- }
|
|
|
- sortJobsAndSave(newJobs);
|
|
|
- };
|
|
|
-
|
|
|
- const removeJob = (deletedJob: any) => {
|
|
|
- let newJobs = jobsRef.current.filter((job: any) => {
|
|
|
- return deletedJob.metadata?.name !== job.metadata?.name;
|
|
|
- });
|
|
|
-
|
|
|
- sortJobsAndSave([...newJobs]);
|
|
|
- };
|
|
|
-
|
|
|
- const setupCronJobWebsocket = () => {
|
|
|
- const releaseName = chart.name;
|
|
|
- const releaseNamespace = chart.namespace;
|
|
|
- if (!releaseName || !releaseNamespace) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const websocketId = `cronjob-websocket-${releaseName}`;
|
|
|
-
|
|
|
- const endpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/cronjob/status`;
|
|
|
-
|
|
|
- const config: NewWebsocketOptions = {
|
|
|
- onopen: console.log,
|
|
|
- onmessage: (evt: MessageEvent) => {
|
|
|
- const event = JSON.parse(evt.data);
|
|
|
- const object = event.Object;
|
|
|
- object.metadata.kind = event.Kind;
|
|
|
-
|
|
|
- setHasPorterImageTemplate((prevValue) => {
|
|
|
- // if imageIsPlaceholder is true update the newestImage and imageIsPlaceholder fields
|
|
|
-
|
|
|
- if (event.event_type !== "ADD" && event.event_type !== "UPDATE") {
|
|
|
- return prevValue;
|
|
|
- }
|
|
|
-
|
|
|
- if (!hasPorterImageTemplate) {
|
|
|
- return prevValue;
|
|
|
- }
|
|
|
-
|
|
|
- // filter job belonging to chart
|
|
|
- const relNameAnnotation =
|
|
|
- event.Object?.metadata?.annotations["meta.helm.sh/release-name"];
|
|
|
- const relNamespaceAnnotation =
|
|
|
- event.Object?.metadata?.annotations[
|
|
|
- "meta.helm.sh/release-namespace"
|
|
|
- ];
|
|
|
-
|
|
|
- if (
|
|
|
- releaseName !== relNameAnnotation ||
|
|
|
- releaseNamespace !== relNamespaceAnnotation
|
|
|
- ) {
|
|
|
- return prevValue;
|
|
|
- }
|
|
|
-
|
|
|
- const newestImage =
|
|
|
- event.Object?.spec?.jobTemplate?.spec?.template?.spec?.containers[0]
|
|
|
- ?.image;
|
|
|
-
|
|
|
- if (!PORTER_IMAGE_TEMPLATES.includes(newestImage)) {
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- return true;
|
|
|
- });
|
|
|
- },
|
|
|
- onclose: console.log,
|
|
|
- onerror: (err: ErrorEvent) => {
|
|
|
- console.log(err);
|
|
|
- closeWebsocket(websocketId);
|
|
|
- },
|
|
|
- };
|
|
|
-
|
|
|
- newWebsocket(websocketId, endpoint, config);
|
|
|
- openWebsocket(websocketId);
|
|
|
- };
|
|
|
-
|
|
|
- const setupJobWebsocket = () => {
|
|
|
- const chartVersion = `${chart?.chart?.metadata?.name}-${chart?.chart?.metadata?.version}`;
|
|
|
-
|
|
|
- const websocketId = `job-websocket-${chart.name}`;
|
|
|
-
|
|
|
- const endpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/job/status`;
|
|
|
-
|
|
|
- const config: NewWebsocketOptions = {
|
|
|
- onopen: console.log,
|
|
|
- onmessage: (evt: MessageEvent) => {
|
|
|
- const event = JSON.parse(evt.data);
|
|
|
-
|
|
|
- const chartLabel = event.Object?.metadata?.labels["helm.sh/chart"];
|
|
|
- const releaseLabel =
|
|
|
- event.Object?.metadata?.labels["meta.helm.sh/release-name"];
|
|
|
-
|
|
|
- if (chartLabel !== chartVersion || releaseLabel !== chart.name) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // if event type is add or update, merge with existing jobs
|
|
|
- if (event.event_type === "ADD" || event.event_type === "UPDATE") {
|
|
|
- mergeNewJob(event.Object);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (event.event_type === "DELETE") {
|
|
|
- // filter job belonging to chart
|
|
|
- removeJob(event.Object);
|
|
|
- }
|
|
|
- },
|
|
|
- onclose: console.log,
|
|
|
- onerror: (err: ErrorEvent) => {
|
|
|
- console.log(err);
|
|
|
- closeWebsocket(websocketId);
|
|
|
- },
|
|
|
- };
|
|
|
- newWebsocket(websocketId, endpoint, config);
|
|
|
- openWebsocket(websocketId);
|
|
|
- };
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
- let isSubscribed = true;
|
|
|
-
|
|
|
- if (!chart) {
|
|
|
- return () => {
|
|
|
- isSubscribed = false;
|
|
|
- closeAllWebsockets();
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- const newestImage = chart?.config?.image?.repository;
|
|
|
-
|
|
|
- setHasPorterImageTemplate(PORTER_IMAGE_TEMPLATES.includes(newestImage));
|
|
|
-
|
|
|
- api
|
|
|
- .getJobs(
|
|
|
- "<token>",
|
|
|
- {},
|
|
|
- {
|
|
|
- id: currentProject?.id,
|
|
|
- cluster_id: currentCluster?.id,
|
|
|
- namespace: chart.namespace,
|
|
|
- release_name: chart.name,
|
|
|
- }
|
|
|
- )
|
|
|
- .then((res) => {
|
|
|
- if (isSubscribed) {
|
|
|
- sortJobsAndSave(res.data);
|
|
|
- setupJobWebsocket();
|
|
|
- setupCronJobWebsocket();
|
|
|
- }
|
|
|
- });
|
|
|
- return () => {
|
|
|
- isSubscribed = false;
|
|
|
- closeAllWebsockets();
|
|
|
- };
|
|
|
- }, [chart]);
|
|
|
-
|
|
|
- const runJob = () => {
|
|
|
- const config = chart.config;
|
|
|
- const values = {};
|
|
|
-
|
|
|
- for (let key in config) {
|
|
|
- set(values, key, config[key]);
|
|
|
- }
|
|
|
-
|
|
|
- set(values, "paused", false);
|
|
|
-
|
|
|
- const yamlValues = yaml.dump(
|
|
|
- {
|
|
|
- ...values,
|
|
|
- },
|
|
|
- { forceQuotes: true }
|
|
|
- );
|
|
|
-
|
|
|
- api
|
|
|
- .upgradeChartValues(
|
|
|
- "<token>",
|
|
|
- {
|
|
|
- values: yamlValues,
|
|
|
- },
|
|
|
- {
|
|
|
- id: currentProject.id,
|
|
|
- name: chart.name,
|
|
|
- namespace: chart.namespace,
|
|
|
- cluster_id: currentCluster.id,
|
|
|
- }
|
|
|
- )
|
|
|
- .then((res) => {
|
|
|
- // this.setState({ saveValuesStatus: "successful" });
|
|
|
- // this.refreshChart(0);
|
|
|
- })
|
|
|
- .catch((err) => {
|
|
|
- let parsedErr = err?.response?.data?.error;
|
|
|
-
|
|
|
- if (parsedErr) {
|
|
|
- err = parsedErr;
|
|
|
- }
|
|
|
-
|
|
|
- // this.setState({
|
|
|
- // saveValuesStatus: parsedErr,
|
|
|
- // });
|
|
|
-
|
|
|
- setCurrentError(parsedErr);
|
|
|
- });
|
|
|
- };
|
|
|
-
|
|
|
- return {
|
|
|
- jobs,
|
|
|
- hasPorterImageTemplate,
|
|
|
- runJob,
|
|
|
- selectedJob,
|
|
|
- setSelectedJob,
|
|
|
- };
|
|
|
-};
|
|
|
-
|
|
|
const LineBreak = styled.div`
|
|
|
width: calc(100% - 0px);
|
|
|
height: 2px;
|