Просмотр исходного кода

Merge pull request #770 from porter-dev/0.4.0-job-deletion

[0.4.0] Add job deletion of specific jobs
abelanger5 5 лет назад
Родитель
Сommit
143b4ac15f

+ 0 - 0
cli/cmd/job.go


BIN
dashboard/src/assets/trash.png


+ 28 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx

@@ -141,6 +141,14 @@ export default class ExpandedJobChart extends Component<PropsType, StateType> {
     this.sortJobsAndSave(jobs);
   };
 
+  removeJob = (deletedJob: any) => {
+    let jobs = this.state.jobs.filter(job => {
+      return deletedJob.metadata?.name !== job.metadata?.name
+    });
+
+    this.sortJobsAndSave(jobs);
+  }
+
   setupJobWebsocket = (chart: ChartType) => {
     let chartVersion = `${chart.chart.metadata.name}-${chart.chart.metadata.version}`;
 
@@ -173,6 +181,20 @@ export default class ExpandedJobChart extends Component<PropsType, StateType> {
         ) {
           this.mergeNewJob(event.Object);
         }
+      } else if (event.event_type == "DELETE") {
+        // filter job belonging to chart
+        let chartLabel = event.Object?.metadata?.labels["helm.sh/chart"];
+        let releaseLabel =
+          event.Object?.metadata?.labels["meta.helm.sh/release-name"];
+
+        if (
+          chartLabel &&
+          releaseLabel &&
+          chartLabel == chartVersion &&
+          releaseLabel == chart.name
+        ) {
+          this.removeJob(event.Object)
+        }
       }
     };
 
@@ -409,7 +431,12 @@ export default class ExpandedJobChart extends Component<PropsType, StateType> {
         }
         return (
           <TabWrapper>
-            <JobList jobs={this.state.jobs} />
+            <JobList 
+              jobs={this.state.jobs} 
+              setJobs={(jobs: any) => {
+                this.setState({ jobs })
+              }}
+            />
             <SaveButton
               text="Rerun Job"
               onClick={() => this.handleSaveValues(submitValues)}

+ 65 - 3
dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobList.tsx

@@ -1,17 +1,28 @@
 import React, { Component } from "react";
 import styled from "styled-components";
 
+import api from "shared/api";
 import _ from "lodash";
 import { Context } from "shared/Context";
 import JobResource from "./JobResource";
+import ConfirmOverlay from "components/ConfirmOverlay";
 
 type PropsType = {
   jobs: any[];
+  setJobs: (job: any) => void;
 };
 
-type StateType = {};
+type StateType = {
+  deletionCandidate: any;
+  deletionJob: any;
+};
 
 export default class JobList extends Component<PropsType, StateType> {
+  state = {
+    deletionCandidate: null as any,
+    deletionJob: null as any,
+  }
+
   renderJobList = () => {
     if (this.props.jobs.length === 0) {
       return (
@@ -24,15 +35,66 @@ export default class JobList extends Component<PropsType, StateType> {
       return (
         <>
           {this.props.jobs.map((job: any, i: number) => {
-            return <JobResource key={job?.metadata?.name} job={job} />;
+            return (
+              <JobResource
+                key={job?.metadata?.name}
+                job={job} 
+                handleDelete={() => this.setState({ deletionCandidate: job })}
+                deleting={this.state.deletionJob?.metadata?.name == job.metadata?.name}
+              />
+            );
           })}
         </>
       );
     }
   };
 
+  deleteJob = () => {
+    let { currentCluster, currentProject, setCurrentError } = this.context;
+    let job = this.state.deletionCandidate
+
+    api
+      .deleteJob(
+        "<token>",
+        {
+          cluster_id: currentCluster.id,
+        },
+        {
+          id: currentProject.id,
+          name: job.metadata?.name,
+          namespace: job.metadata?.namespace,
+        }
+      )
+      .then((res) => {
+        this.setState({
+          deletionJob: this.state.deletionCandidate,
+          deletionCandidate: null,
+        })
+      })
+      .catch((err) => {
+        let parsedErr =
+          err?.response?.data?.errors && err.response.data.errors[0];
+        if (parsedErr) {
+          err = parsedErr;
+        }
+        setCurrentError(err);
+      });
+  }
+
   render() {
-    return <JobListWrapper>{this.renderJobList()}</JobListWrapper>;
+    return (
+      <>
+        <ConfirmOverlay
+          show={this.state.deletionCandidate}
+          message={`Are you sure you want to delete this job run?`}
+          onYes={this.deleteJob}
+          onNo={() => this.setState({ deletionCandidate: null })}
+        />
+        <JobListWrapper>
+          {this.renderJobList()}
+        </JobListWrapper>
+      </>
+    );
   }
 }
 

+ 47 - 25
dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobResource.tsx

@@ -7,10 +7,13 @@ import api from "shared/api";
 import Logs from "../status/Logs";
 import plus from "assets/plus.svg";
 import closeRounded from "assets/close-rounded.png";
+import trash from "assets/trash.png";
 import KeyValueArray from "components/values-form/KeyValueArray";
 
 type PropsType = {
   job: any;
+  handleDelete: () => void;
+  deleting: boolean;
 };
 
 type StateType = {
@@ -55,7 +58,14 @@ export default class JobResource extends Component<PropsType, StateType> {
         }
       )
       .then((res) => {})
-      .catch((err) => setCurrentError(JSON.stringify(err)));
+      .catch((err) => {
+        let parsedErr =
+          err?.response?.data?.errors && err.response.data.errors[0];
+        if (parsedErr) {
+          err = parsedErr;
+        }
+        setCurrentError(err);
+      });
   };
 
   getPods = (callback: () => void) => {
@@ -217,6 +227,10 @@ export default class JobResource extends Component<PropsType, StateType> {
   };
 
   renderStatus = () => {
+    if (this.props.deleting) {
+      return <Status color="#cc3d42">Deleting</Status>;
+    }
+
     if (this.props.job.status?.succeeded >= 1) {
       return <Status color="#38a88a">Succeeded</Status>;
     }
@@ -249,30 +263,38 @@ export default class JobResource extends Component<PropsType, StateType> {
     );
 
     return (
-      <StyledJob>
-        <MainRow onClick={this.expandJob}>
-          <Flex>
-            <Icon src={icon && icon} />
-            <Description>
-              <Label>
-                Started at {this.readableDate(this.props.job.status?.startTime)}
-              </Label>
-              <Subtitle>{this.getSubtitle()}</Subtitle>
-            </Description>
-          </Flex>
-          <EndWrapper>
-            <CommandString>{commandString}</CommandString>
-            {this.renderStatus()}
-            <MaterialIconTray disabled={false}>
-              {this.renderStopButton()}
-              <i className="material-icons" onClick={this.expandJob}>
-                {this.state.expanded ? "expand_less" : "expand_more"}
-              </i>
-            </MaterialIconTray>
-          </EndWrapper>
-        </MainRow>
-        {this.renderLogsSection()}
-      </StyledJob>
+      <>
+        <StyledJob>
+          <MainRow onClick={this.expandJob}>
+            <Flex>
+              <Icon src={icon && icon} />
+              <Description>
+                <Label>
+                  Started at {this.readableDate(this.props.job.status?.startTime)}
+                </Label>
+                <Subtitle>{this.getSubtitle()}</Subtitle>
+              </Description>
+            </Flex>
+            <EndWrapper>
+              <CommandString>{commandString}</CommandString>
+              {this.renderStatus()}
+              <MaterialIconTray disabled={false}>
+                {this.renderStopButton()}
+                <i className="material-icons" onClick={(e) => {
+                  e.stopPropagation();
+                  this.props.handleDelete();
+                }}>
+                  delete
+                </i>
+                <i className="material-icons" onClick={this.expandJob}>
+                  {this.state.expanded ? "expand_less" : "expand_more"}
+                </i>
+              </MaterialIconTray>
+            </EndWrapper>
+          </MainRow>
+          {this.renderLogsSection()}
+        </StyledJob>
+      </>
     );
   }
 }

+ 9 - 0
dashboard/src/shared/api.tsx

@@ -839,6 +839,14 @@ const deleteNamespace = baseApi<
   return `/api/projects/${id}/k8s/namespaces/delete`;
 });
 
+const deleteJob = baseApi<
+  { cluster_id: number },
+  { name: string; namespace: string; id: number }
+>("DELETE", (pathParams) => {
+  let { id, name, namespace } = pathParams;
+  return `/api/projects/${id}/k8s/jobs/${namespace}/${name}`;
+});
+
 const stopJob = baseApi<
   {},
   { name: string; namespace: string; id: number; cluster_id: number }
@@ -933,5 +941,6 @@ export default {
   updateUser,
   updateConfigMap,
   upgradeChartValues,
+  deleteJob,
   stopJob,
 };

+ 9 - 0
internal/kubernetes/agent.go

@@ -284,6 +284,15 @@ func (a *Agent) ListJobsByLabel(namespace string, labels ...Label) ([]batchv1.Jo
 	return resp.Items, nil
 }
 
+// DeleteJob deletes the job in the given name and namespace.
+func (a *Agent) DeleteJob(name, namespace string) error {
+	return a.Clientset.BatchV1().Jobs(namespace).Delete(
+		context.TODO(),
+		name,
+		metav1.DeleteOptions{},
+	)
+}
+
 // GetJobPods lists all pods belonging to a job in a namespace
 func (a *Agent) GetJobPods(namespace, jobName string) ([]v1.Pod, error) {
 	resp, err := a.Clientset.CoreV1().Pods(namespace).List(

+ 52 - 1
server/api/k8s_handler.go

@@ -858,6 +858,54 @@ func (app *App) HandleListJobsByChart(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
+// HandleDeleteConfigMap deletes the pod given the name and namespace.
+func (app *App) HandleDeleteJob(w http.ResponseWriter, r *http.Request) {
+	// get path parameters
+	namespace := chi.URLParam(r, "namespace")
+	name := chi.URLParam(r, "name")
+
+	vals, err := url.ParseQuery(r.URL.RawQuery)
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		return
+	}
+
+	// get the filter options
+	form := &forms.K8sForm{
+		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
+			Repo:              app.Repo,
+			DigitalOceanOAuth: app.DOConf,
+		},
+	}
+
+	form.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
+
+	// validate the form
+	if err := app.validator.Struct(form); err != nil {
+		app.handleErrorFormValidation(err, ErrK8sValidate, w)
+		return
+	}
+
+	// create a new agent
+	var agent *kubernetes.Agent
+
+	if app.ServerConf.IsTesting {
+		agent = app.TestAgents.K8sAgent
+	} else {
+		agent, err = kubernetes.GetAgentOutOfClusterConfig(form.OutOfClusterConfig)
+	}
+
+	err = agent.DeleteJob(name, namespace)
+
+	if err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	w.WriteHeader(http.StatusOK)
+	return
+}
+
 // HandleStopJob stops a running job
 func (app *App) HandleStopJob(w http.ResponseWriter, r *http.Request) {
 	// get path parameters
@@ -899,7 +947,10 @@ func (app *App) HandleStopJob(w http.ResponseWriter, r *http.Request) {
 	err = agent.StopJobWithJobSidecar(namespace, name)
 
 	if err != nil {
-		app.handleErrorInternal(err, w)
+		app.sendExternalError(err, 500, HTTPError{
+			Code:   500,
+			Errors: []string{err.Error()},
+		}, w)
 		return
 	}
 

+ 14 - 0
server/router/router.go

@@ -1351,6 +1351,20 @@ func New(a *api.App) *chi.Mux {
 				),
 			)
 
+			r.Method(
+				"DELETE",
+				"/projects/{project_id}/k8s/jobs/{namespace}/{name}",
+				auth.DoesUserHaveProjectAccess(
+					auth.DoesUserHaveClusterAccess(
+						requestlog.NewHandler(a.HandleDeleteJob, l),
+						mw.URLParam,
+						mw.QueryParam,
+					),
+					mw.URLParam,
+					mw.WriteAccess,
+				),
+			)
+
 			r.Method(
 				"POST",
 				"/projects/{project_id}/k8s/jobs/{namespace}/{name}/stop",