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

Merge pull request #806 from porter-dev/fix-launch-addon-blank-screen

Fix launch addon blank screen
sunguroku 4 лет назад
Родитель
Сommit
6d64e0935d

+ 6 - 2
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx

@@ -420,12 +420,14 @@ export default class ExpandedJobChart extends Component<PropsType, StateType> {
   };
   };
 
 
   renderTabContents = (currentTab: string, submitValues?: any) => {
   renderTabContents = (currentTab: string, submitValues?: any) => {
-    let saveButton = <SaveButton
+    let saveButton = (
+      <SaveButton
         text="Rerun Job"
         text="Rerun Job"
         onClick={() => this.handleSaveValues(submitValues, true)}
         onClick={() => this.handleSaveValues(submitValues, true)}
         status={this.state.saveValuesStatus}
         status={this.state.saveValuesStatus}
         makeFlush={true}
         makeFlush={true}
       />
       />
+    );
 
 
     switch (currentTab) {
     switch (currentTab) {
       case "jobs":
       case "jobs":
@@ -620,7 +622,9 @@ export default class ExpandedJobChart extends Component<PropsType, StateType> {
               isInModal={true}
               isInModal={true}
               renderTabContents={this.renderTabContents}
               renderTabContents={this.renderTabContents}
               tabOptionsOnly={true}
               tabOptionsOnly={true}
-              onSubmit={(formValues) => this.handleSaveValues(formValues, false)}
+              onSubmit={(formValues) =>
+                this.handleSaveValues(formValues, false)
+              }
               saveValuesStatus={this.state.saveValuesStatus}
               saveValuesStatus={this.state.saveValuesStatus}
               saveButtonText="Save Config"
               saveButtonText="Save Config"
             />
             />

+ 11 - 16
dashboard/src/main/home/launch/launch-flow/LaunchFlow.tsx

@@ -118,9 +118,8 @@ class LaunchFlow extends Component<PropsType, StateType> {
       .catch((err) => {
       .catch((err) => {
         let parsedErr =
         let parsedErr =
           err?.response?.data?.errors && err.response.data.errors[0];
           err?.response?.data?.errors && err.response.data.errors[0];
-        if (parsedErr) {
-          err = parsedErr;
-        }
+        err = parsedErr || err.message || JSON.stringify(err);
+
         this.setState({
         this.setState({
           saveValuesStatus: `Could not create GitHub Action: ${err}`,
           saveValuesStatus: `Could not create GitHub Action: ${err}`,
         });
         });
@@ -142,7 +141,7 @@ class LaunchFlow extends Component<PropsType, StateType> {
     }
     }
 
 
     api
     api
-      .deployTemplate(
+      .deployAddon(
         "<token>",
         "<token>",
         {
         {
           templateName: this.props.currentTemplate.name,
           templateName: this.props.currentTemplate.name,
@@ -182,12 +181,13 @@ class LaunchFlow extends Component<PropsType, StateType> {
       .catch((err) => {
       .catch((err) => {
         let parsedErr =
         let parsedErr =
           err?.response?.data?.errors && err.response.data.errors[0];
           err?.response?.data?.errors && err.response.data.errors[0];
-        if (parsedErr) {
-          err = parsedErr;
-        }
+
+        err = parsedErr || err.message || JSON.stringify(err);
+
         this.setState({
         this.setState({
-          saveValuesStatus: parsedErr,
+          saveValuesStatus: err,
         });
         });
+
         setCurrentError(err);
         setCurrentError(err);
         window.analytics.track("Failed to Deploy Add-on", {
         window.analytics.track("Failed to Deploy Add-on", {
           name: this.props.currentTemplate.name,
           name: this.props.currentTemplate.name,
@@ -260,7 +260,7 @@ class LaunchFlow extends Component<PropsType, StateType> {
 
 
     // pause jobs automatically
     // pause jobs automatically
     if (this.props.currentTemplate?.name == "job") {
     if (this.props.currentTemplate?.name == "job") {
-      _.set(values, "paused", true)
+      _.set(values, "paused", true);
     }
     }
 
 
     var url: string;
     var url: string;
@@ -285,9 +285,7 @@ class LaunchFlow extends Component<PropsType, StateType> {
             .catch((err) => {
             .catch((err) => {
               let parsedErr =
               let parsedErr =
                 err?.response?.data?.errors && err.response.data.errors[0];
                 err?.response?.data?.errors && err.response.data.errors[0];
-              if (parsedErr) {
-                err = parsedErr;
-              }
+              err = parsedErr || err.message || JSON.stringify(err);
               this.setState({
               this.setState({
                 saveValuesStatus: `Could not create subdomain: ${err}`,
                 saveValuesStatus: `Could not create subdomain: ${err}`,
               });
               });
@@ -341,10 +339,7 @@ class LaunchFlow extends Component<PropsType, StateType> {
       .catch((err: any) => {
       .catch((err: any) => {
         let parsedErr =
         let parsedErr =
           err?.response?.data?.errors && err.response.data.errors[0];
           err?.response?.data?.errors && err.response.data.errors[0];
-        console.log(parsedErr);
-        if (parsedErr) {
-          err = parsedErr;
-        }
+        err = parsedErr || err.message || JSON.stringify(err);
         this.setState({
         this.setState({
           saveValuesStatus: `Could not deploy template: ${err}`,
           saveValuesStatus: `Could not deploy template: ${err}`,
         });
         });

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

@@ -292,6 +292,27 @@ const deployTemplate = baseApi<
   return `/api/projects/${id}/deploy/${name}/${version}?cluster_id=${cluster_id}`;
   return `/api/projects/${id}/deploy/${name}/${version}?cluster_id=${cluster_id}`;
 });
 });
 
 
+const deployAddon = baseApi<
+  {
+    templateName: string;
+    formValues?: any;
+    storage: StorageType;
+    namespace: string;
+    name: string;
+  },
+  {
+    id: number;
+    cluster_id: number;
+    name: string;
+    version: string;
+    repo_url?: string;
+  }
+>("POST", (pathParams) => {
+  let { cluster_id, id, name, version, repo_url } = pathParams;
+
+  return `/api/projects/${id}/deploy/addon/${name}/${version}?cluster_id=${cluster_id}&repo_url=${repo_url}`;
+});
+
 const destroyCluster = baseApi<
 const destroyCluster = baseApi<
   {
   {
     eks_name: string;
     eks_name: string;
@@ -914,6 +935,7 @@ export default {
   deleteRegistryIntegration,
   deleteRegistryIntegration,
   createSubdomain,
   createSubdomain,
   deployTemplate,
   deployTemplate,
+  deployAddon,
   destroyEKS,
   destroyEKS,
   destroyGKE,
   destroyGKE,
   destroyDOKS,
   destroyDOKS,

+ 35 - 33
dashboard/src/shared/hooks/useWebsockets.ts

@@ -1,4 +1,4 @@
-import { useRef } from "react"
+import { useRef } from "react";
 
 
 interface NewWebsocketOptions {
 interface NewWebsocketOptions {
   onopen?: () => void;
   onopen?: () => void;
@@ -12,17 +12,17 @@ interface WebsocketConfig extends NewWebsocketOptions {
 }
 }
 
 
 type WebsocketConfigMap = {
 type WebsocketConfigMap = {
-  [id: string]: WebsocketConfig
-}
+  [id: string]: WebsocketConfig;
+};
 
 
 type WebsocketMap = {
 type WebsocketMap = {
-  [id: string]: WebSocket
-}
+  [id: string]: WebSocket;
+};
 
 
 export const useWebsockets = () => {
 export const useWebsockets = () => {
   const websocketMap = useRef<WebsocketMap>({});
   const websocketMap = useRef<WebsocketMap>({});
-  const websocketConfigMap = useRef<WebsocketConfigMap>({})
-  
+  const websocketConfigMap = useRef<WebsocketConfigMap>({});
+
   /**
   /**
    * Setup for a new websocket, after calling new websocket you can open the connection with openWebsocket
    * Setup for a new websocket, after calling new websocket you can open the connection with openWebsocket
    * @param id Id to access later the websocket config/connection
    * @param id Id to access later the websocket config/connection
@@ -30,50 +30,52 @@ export const useWebsockets = () => {
    * @param options Websocket listeners
    * @param options Websocket listeners
    * @returns An object with the config setted for that websocket. This config will be used to open the ws on openWebsocket
    * @returns An object with the config setted for that websocket. This config will be used to open the ws on openWebsocket
    */
    */
-  const newWebsocket = (id: string, apiEndpoint: string, options: NewWebsocketOptions): WebsocketConfig => {
-    
+  const newWebsocket = (
+    id: string,
+    apiEndpoint: string,
+    options: NewWebsocketOptions
+  ): WebsocketConfig => {
     if (!id) {
     if (!id) {
       console.log("Id cannot be empty");
       console.log("Id cannot be empty");
       return;
       return;
     }
     }
 
 
     if (!apiEndpoint) {
     if (!apiEndpoint) {
-      console.log("Api endpoint string cannot be empty")
+      console.log("Api endpoint string cannot be empty");
       return;
       return;
     }
     }
 
 
-
     let protocol = window.location.protocol == "https:" ? "wss" : "ws";
     let protocol = window.location.protocol == "https:" ? "wss" : "ws";
 
 
-    const url = `${protocol}://${window.location.host}${apiEndpoint}`
+    const url = `${protocol}://${window.location.host}${apiEndpoint}`;
+
+    const mockFunction = () => {};
 
 
-    const mockFunction = () => {}
-    
     const wsConfig: WebsocketConfig = {
     const wsConfig: WebsocketConfig = {
       url,
       url,
       onopen: options?.onopen || mockFunction,
       onopen: options?.onopen || mockFunction,
       onmessage: options?.onmessage || mockFunction,
       onmessage: options?.onmessage || mockFunction,
       onerror: options?.onerror || mockFunction,
       onerror: options?.onerror || mockFunction,
       onclose: options?.onclose || mockFunction,
       onclose: options?.onclose || mockFunction,
-    }
-    
+    };
+
     websocketConfigMap.current = {
     websocketConfigMap.current = {
       ...websocketConfigMap.current,
       ...websocketConfigMap.current,
       [id]: wsConfig,
       [id]: wsConfig,
-    }
+    };
     return wsConfig;
     return wsConfig;
-  }
+  };
 
 
   /**
   /**
    * Opens the websocket connection based on a config previously setted by
    * Opens the websocket connection based on a config previously setted by
-   * newWebsocket 
+   * newWebsocket
    */
    */
   const openWebsocket = (id: string) => {
   const openWebsocket = (id: string) => {
     const wsConfig = websocketConfigMap.current[id];
     const wsConfig = websocketConfigMap.current[id];
 
 
     // Prevent calling openWebsocket before newWebsocket
     // Prevent calling openWebsocket before newWebsocket
     if (!wsConfig) {
     if (!wsConfig) {
-      console.log("Couldn't find ws config")
+      console.log("Couldn't find ws config");
       return;
       return;
     }
     }
     // In case of having a previous websocket opened with the same ID, close the previous one
     // In case of having a previous websocket opened with the same ID, close the previous one
@@ -85,14 +87,14 @@ export const useWebsockets = () => {
     const { url, ...listeners } = wsConfig;
     const { url, ...listeners } = wsConfig;
 
 
     const ws = new WebSocket(wsConfig.url);
     const ws = new WebSocket(wsConfig.url);
-    
+
     Object.assign(ws, listeners);
     Object.assign(ws, listeners);
 
 
     websocketMap.current = {
     websocketMap.current = {
       ...websocketMap.current,
       ...websocketMap.current,
       [id]: ws,
       [id]: ws,
-    }
-  }
+    };
+  };
 
 
   /**
   /**
    * Close specific websocket
    * Close specific websocket
@@ -106,29 +108,29 @@ export const useWebsockets = () => {
     }
     }
 
 
     ws.close(code, reason);
     ws.close(code, reason);
-  }
+  };
 
 
-  /** 
+  /**
    * Closes all websockets opened by the useWebsocket hook
    * Closes all websockets opened by the useWebsocket hook
-   */ 
+   */
   const closeAllWebsockets = () => {
   const closeAllWebsockets = () => {
-    Object.keys(websocketMap.current).forEach(key => {
+    Object.keys(websocketMap.current).forEach((key) => {
       closeWebsocket(key);
       closeWebsocket(key);
-    })
-  }
+    });
+  };
 
 
   /**
   /**
    * Get websocket by id
    * Get websocket by id
    */
    */
   const getWebsocket = (id: string) => {
   const getWebsocket = (id: string) => {
     return websocketMap.current[id];
     return websocketMap.current[id];
-  }
+  };
 
 
   return {
   return {
     newWebsocket,
     newWebsocket,
     openWebsocket,
     openWebsocket,
     getWebsocket,
     getWebsocket,
     closeWebsocket,
     closeWebsocket,
-    closeAllWebsockets
-  }
-}
+    closeAllWebsockets,
+  };
+};

+ 109 - 1
server/api/deploy_handler.go

@@ -125,7 +125,13 @@ func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 
 
 	// create release with webhook token in db
 	// create release with webhook token in db
-	repository := rel.Config["image"].(map[string]interface{})["repository"]
+	image, ok := rel.Config["image"].(map[string]interface{})
+	if !ok {
+		app.handleErrorInternal(fmt.Errorf("Could not find field image in config"), w)
+		return
+	}
+
+	repository := image["repository"]
 	repoStr, ok := repository.(string)
 	repoStr, ok := repository.(string)
 
 
 	if !ok {
 	if !ok {
@@ -176,6 +182,108 @@ func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 }
 }
 
 
+// HandleDeployAddon triggers a addon deployment from a template
+func (app *App) HandleDeployAddon(w http.ResponseWriter, r *http.Request) {
+	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+	if err != nil || projID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	name := chi.URLParam(r, "name")
+	version := chi.URLParam(r, "version")
+
+	// if version passed as latest, pass empty string to loader to get latest
+	if version == "latest" {
+		version = ""
+	}
+
+	getChartForm := &forms.ChartForm{
+		Name:    name,
+		Version: version,
+		RepoURL: app.ServerConf.DefaultApplicationHelmRepoURL,
+	}
+
+	// if a repo_url is passed as query param, it will be populated
+	vals, err := url.ParseQuery(r.URL.RawQuery)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		return
+	}
+
+	getChartForm.PopulateRepoURLFromQueryParams(vals)
+
+	chart, err := loader.LoadChartPublic(getChartForm.RepoURL, getChartForm.Name, getChartForm.Version)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		return
+	}
+
+	form := &forms.InstallChartTemplateForm{
+		ReleaseForm: &forms.ReleaseForm{
+			Form: &helm.Form{
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
+			},
+		},
+		ChartTemplateForm: &forms.ChartTemplateForm{},
+	}
+
+	form.ReleaseForm.PopulateHelmOptionsFromQueryParams(
+		vals,
+		app.Repo.Cluster,
+	)
+
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		app.handleErrorFormDecoding(err, ErrUserDecode, w)
+		return
+	}
+
+	agent, err := app.getAgentFromReleaseForm(
+		w,
+		r,
+		form.ReleaseForm,
+	)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrUserDecode, w)
+		return
+	}
+
+	registries, err := app.Repo.Registry.ListRegistriesByProjectID(uint(projID))
+
+	if err != nil {
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	conf := &helm.InstallChartConfig{
+		Chart:      chart,
+		Name:       form.ChartTemplateForm.Name,
+		Namespace:  form.ReleaseForm.Form.Namespace,
+		Values:     form.ChartTemplateForm.FormValues,
+		Cluster:    form.ReleaseForm.Cluster,
+		Repo:       *app.Repo,
+		Registries: registries,
+	}
+
+	_, err = agent.InstallChart(conf, app.DOConf)
+
+	if err != nil {
+		app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
+			Code:   ErrReleaseDeploy,
+			Errors: []string{"error installing a new chart: " + err.Error()},
+		}, w)
+
+		return
+	}
+
+	w.WriteHeader(http.StatusOK)
+}
+
 // HandleUninstallTemplate triggers a chart deployment from a template
 // HandleUninstallTemplate triggers a chart deployment from a template
 func (app *App) HandleUninstallTemplate(w http.ResponseWriter, r *http.Request) {
 func (app *App) HandleUninstallTemplate(w http.ResponseWriter, r *http.Request) {
 	name := chi.URLParam(r, "name")
 	name := chi.URLParam(r, "name")

+ 14 - 0
server/router/router.go

@@ -1476,6 +1476,20 @@ func New(a *api.App) *chi.Mux {
 					mw.ReadAccess,
 					mw.ReadAccess,
 				),
 				),
 			)
 			)
+
+			r.Method(
+				"POST",
+				"/projects/{project_id}/deploy/addon/{name}/{version}",
+				auth.DoesUserHaveProjectAccess(
+					auth.DoesUserHaveClusterAccess(
+						requestlog.NewHandler(a.HandleDeployAddon, l),
+						mw.URLParam,
+						mw.QueryParam,
+					),
+					mw.URLParam,
+					mw.ReadAccess,
+				),
+			)
 		})
 		})
 
 
 		// Create group for long-running Helm operations
 		// Create group for long-running Helm operations