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

Merge branch 'stacks-v1' of github.com:porter-dev/porter into stacks-v1

Feroze Mohideen 3 лет назад
Родитель
Сommit
003e4a9169

+ 55 - 0
api/server/handlers/stacks/delete_porter_app.go

@@ -0,0 +1,55 @@
+package stacks
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type DeletePorterAppByNameHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewDeletePorterAppByNameHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *DeletePorterAppByNameHandler {
+	return &DeletePorterAppByNameHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *DeletePorterAppByNameHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+
+	name, reqErr := requestutils.GetURLParamString(r, types.URLParamReleaseName)
+	if reqErr != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(reqErr, http.StatusBadRequest))
+		return
+	}
+
+	porterApp, appErr := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, name)
+	if appErr != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(appErr))
+		return
+	}
+
+	delApp, delErr := c.Repo().PorterApp().DeletePorterApp(porterApp)
+	if delErr != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(delErr))
+		return
+	}
+
+	c.WriteResult(w, r, delApp)
+}

+ 0 - 4
api/server/handlers/stacks/update_porter_app.go

@@ -1,7 +1,6 @@
 package stacks
 
 import (
-	"fmt"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/authz"
@@ -30,14 +29,11 @@ func NewUpdatePorterAppHandler(
 }
 
 func (c *UpdatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	fmt.Println("so an update was attempted...")
 	ctx := r.Context()
 	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
 
 	name, _ := requestutils.GetURLParamString(r, types.URLParamReleaseName)
 
-	fmt.Println("name is", name)
-
 	porterApp, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, name)
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 30 - 1
api/server/router/stack.go

@@ -83,7 +83,7 @@ func getStackRoutes(
 		Router:   r,
 	})
 
-	// GET /api/projects/{project_id}/clusters/{cluster_id}/stacks -> stacks.NewPorterAppListHandler
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/stacks/{name} -> stacks.NewPorterAppListHandler
 	listPorterAppEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbList,
@@ -111,6 +111,35 @@ func getStackRoutes(
 		Router:   r,
 	})
 
+	// DELETE /api/projects/{project_id}/clusters/{cluster_id}/stacks -> release.NewDeletePorterAppByNameHandler
+	deletePorterAppByNameEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbDelete,
+			Method: types.HTTPVerbDelete,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/{name}",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	deletePorterAppByNameHandler := stacks.NewDeletePorterAppByNameHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: deletePorterAppByNameEndpoint,
+		Handler:  deletePorterAppByNameHandler,
+		Router:   r,
+	})
+
 	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/update_config -> stacks.NewCreatePorterAppHandler
 	createPorterAppEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 104 - 0
dashboard/src/components/porter/ConfirmOverlay.tsx

@@ -0,0 +1,104 @@
+import React from "react";
+import { createPortal } from "react-dom";
+import styled from "styled-components";
+
+type Props = {
+  message: string;
+  onYes: React.MouseEventHandler;
+  onNo: React.MouseEventHandler;
+};
+
+const TemplateComponent: React.FC<Props> = ({
+  message,
+  onYes,
+  onNo,
+}) => {
+  return (
+    <>
+      {
+        createPortal(
+          <StyledConfirmOverlay>
+            {message}
+            <ButtonRow>
+              <ConfirmButton onClick={onYes}>Yes</ConfirmButton>
+              <ConfirmButton onClick={onNo}>No</ConfirmButton>
+            </ButtonRow>
+          </StyledConfirmOverlay>,
+          document.body
+        )
+      }
+    </>
+  );
+};
+
+export default TemplateComponent;
+
+const StyledConfirmOverlay = styled.div`
+  position: absolute;
+  top: 0px;
+  opacity: 100%;
+  left: 0px;
+  width: 100%;
+  height: 100%;
+  z-index: 999;
+  display: flex;
+  padding-bottom: 30px;
+  align-items: center;
+  justify-content: center;
+  font-family: "Work Sans", sans-serif;
+  font-size: 18px;
+  color: white;
+  flex-direction: column;
+  background: rgb(0, 0, 0, 0.73);
+  backdrop-filter: blur(5px);
+  animation: lindEnter 0.2s;
+  animation-fill-mode: forwards;
+
+  @keyframes lindEnter {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+`;
+
+const ButtonRow = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 140px;
+  margin-top: 30px;
+`;
+
+const ConfirmButton = styled.div`
+  outline: none;
+  height: 40px;
+  border: 1px solid white;
+  border-radius: 5px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 60px;
+  cursor: pointer;
+  opacity: 0;
+  font-family: "Work Sans", sans-serif;
+  font-size: 15px;
+  animation: linEnter 0.3s 0.1s;
+  animation-fill-mode: forwards;
+  @keyframes linEnter {
+    from {
+      transform: translateY(20px);
+      opacity: 0;
+    }
+    to {
+      transform: translateY(0px);
+      opacity: 1;
+    }
+  }
+  :hover {
+    background: white;
+    color: #232323;
+  }
+`;

+ 21 - 22
dashboard/src/components/porter/Select.tsx

@@ -26,35 +26,34 @@ const Select: React.FC<Props> = ({
 }) => {
   return (
     <Block width={width}>
-      {
-        label && (
-          <Label>{label}</Label>
-        )
-      }
+      {label && <Label>{label}</Label>}
       <SelectWrapper>
         <i className="material-icons">arrow_drop_down</i>
         <StyledSelect
-          onChange={e => {
+          onChange={(e) => {
             setValue(e.target.value);
           }}
           width={width}
           height={height}
-          hasError={(error && true) || (error === "")}
+          hasError={(error && true) || error === ""}
           disabled={disabled ? disabled : false}
+          value={value}
         >
           {options.map((option, i) => {
-            return <option value={option.value} key={i}>{option.label}</option>;
+            return (
+              <option value={option.value} key={i}>
+                {option.label}
+              </option>
+            );
           })}
         </StyledSelect>
       </SelectWrapper>
-      {
-        error && (
-          <Error>
-            <i className="material-icons">error</i>
-            {error}
-          </Error>
-        )
-      }
+      {error && (
+        <Error>
+          <i className="material-icons">error</i>
+          {error}
+        </Error>
+      )}
       {children}
     </Block>
   );
@@ -67,7 +66,7 @@ const Block = styled.div<{
 }>`
   display: block;
   position: relative;
-  width: ${props => props.width || "200px"};
+  width: ${(props) => props.width || "200px"};
 `;
 
 const Label = styled.div`
@@ -109,9 +108,9 @@ const StyledSelect = styled.select<{
   height: string;
   hasError: boolean;
 }>`
-  height: ${props => props.height || "35px"};
+  height: ${(props) => props.height || "35px"};
   padding: 5px 10px;
-  width: ${props => props.width || "200px"};
+  width: ${(props) => props.width || "200px"};
   color: #ffffff;
   font-size: 13px;
   outline: none;
@@ -121,8 +120,8 @@ const StyledSelect = styled.select<{
   appearance: none;
   overflow: hidden;
   z-index: 1;
-  border: 1px solid ${props => props.hasError ? "#ff3b62" : "#494b4f"};
+  border: 1px solid ${(props) => (props.hasError ? "#ff3b62" : "#494b4f")};
   :hover {
-    border: 1px solid ${props => props.hasError ? "#ff3b62" : "#7a7b80"};
+    border: 1px solid ${(props) => (props.hasError ? "#ff3b62" : "#7a7b80")};
   }
-`;
+`;

+ 0 - 1
dashboard/src/components/repo-selector/BuildpackSelection.tsx

@@ -60,7 +60,6 @@ export const BuildpackSelection: React.FC<{
 
   useEffect(() => {
     let buildConfig: BuildConfig = {} as BuildConfig;
-
     buildConfig.builder = selectedStack;
     buildConfig.buildpacks = selectedBuildpacks?.map((buildpack) => {
       return buildpack.buildpack;

+ 76 - 28
dashboard/src/components/repo-selector/BuildpackStack.tsx

@@ -52,13 +52,23 @@ export const BuildpackStack: React.FC<{
   branch: string;
   hide: boolean;
   onChange: (config: BuildConfig) => void;
-}> = ({ actionConfig, folderPath, branch, hide, onChange }) => {
+  currentBuildConfig?: BuildConfig;
+}> = ({
+  actionConfig,
+  folderPath,
+  branch,
+  hide,
+  onChange,
+  currentBuildConfig,
+}) => {
   const { currentProject } = useContext(Context);
 
   const [builders, setBuilders] = useState<DetectedBuildpack[]>(null);
 
   const [stacks, setStacks] = useState<string[]>(null);
-  const [selectedStack, setSelectedStack] = useState<string>(null);
+  const [selectedStack, setSelectedStack] = useState<string>(
+    currentBuildConfig?.builder || null
+  );
   const [isModalOpen, setIsModalOpen] = useState(false);
 
   const [selectedBuildpacks, setSelectedBuildpacks] = useState<Buildpack[]>([]);
@@ -66,22 +76,24 @@ export const BuildpackStack: React.FC<{
     []
   );
   const renderModalContent = () => {
+    console.log(selectedBuildpacks);
     return (
       <>
         <Text size={16}>Buildpack Configuration</Text>
         <Spacer y={1} />
         <Scrollable>
-          <Text color="helper">
-            Configure your buildpacks here.
-          </Text>
+          <Text color="helper">Configure your buildpacks here.</Text>
           <Spacer y={1} />
           {!!selectedBuildpacks?.length &&
             renderBuildpacksList(selectedBuildpacks, "remove")}
           <Spacer y={1} />
-          <Text color="helper">Available buildpacks:</Text>
+
           <Spacer y={1} />
           {!!availableBuildpacks?.length && (
-            <>{renderBuildpacksList(availableBuildpacks, "add")}</>
+            <>
+              <Text color="helper">Available buildpacks:</Text>
+              <>{renderBuildpacksList(availableBuildpacks, "add")}</>
+            </>
           )}
           <Spacer y={1} />
           <Text color="helper">
@@ -92,9 +104,7 @@ export const BuildpackStack: React.FC<{
           <AddCustomBuildpackForm onAdd={handleAddCustomBuildpack} />
         </Scrollable>
         <Spacer y={1} />
-        <Button onClick={() => setIsModalOpen(false)}>
-          Save
-        </Button>
+        <Button onClick={() => setIsModalOpen(false)}>Save</Button>
       </>
     );
   };
@@ -102,10 +112,10 @@ export const BuildpackStack: React.FC<{
     let buildConfig: BuildConfig = {} as BuildConfig;
 
     buildConfig.builder = selectedStack;
-    console.log(buildConfig);
     buildConfig.buildpacks = selectedBuildpacks?.map((buildpack) => {
       return buildpack.buildpack;
     });
+
     if (typeof onChange === "function") {
       onChange(buildConfig);
     }
@@ -155,16 +165,56 @@ export const BuildpackStack: React.FC<{
           (builder) => builder.name.toLowerCase() === DEFAULT_BUILDER_NAME
         );
 
-        const detectedBuildpacks = defaultBuilder.detected;
-        const availableBuildpacks = defaultBuilder.others;
-        const defaultStack = builders
-          .flatMap((builder) => builder.builders)
-          .find((stack) => {
-            return (
-              stack === DEFAULT_HEROKU_STACK || stack === DEFAULT_PAKETO_STACK
+        var detectedBuildpacks = defaultBuilder.detected;
+        var availableBuildpacks = defaultBuilder.others;
+        var defaultStack = "";
+        if (currentBuildConfig) {
+          if (!detectedBuildpacks) {
+            detectedBuildpacks = [];
+          }
+
+          defaultStack = currentBuildConfig.builder;
+          for (const buildpackName of currentBuildConfig.buildpacks) {
+            const matchingBuildpackIndex = availableBuildpacks.findIndex(
+              (buildpack) => buildpack.buildpack === buildpackName
             );
-          });
 
+            if (matchingBuildpackIndex >= 0) {
+              const matchingBuildpack = availableBuildpacks.splice(
+                matchingBuildpackIndex,
+                1
+              )[0];
+              const existingBuildpackIndex = detectedBuildpacks.findIndex(
+                (buildpack) => buildpack.buildpack === buildpackName
+              );
+              if (existingBuildpackIndex < 0) {
+                detectedBuildpacks.push(matchingBuildpack);
+              }
+            } else {
+              const newBuildpack: Buildpack = {
+                name: buildpackName,
+                buildpack: buildpackName,
+                config: null,
+              };
+              const existingBuildpackIndex = detectedBuildpacks.findIndex(
+                (buildpack) => buildpack.buildpack === buildpackName
+              );
+              if (existingBuildpackIndex < 0) {
+                detectedBuildpacks.push(newBuildpack);
+              }
+            }
+          }
+        } else {
+          detectedBuildpacks = defaultBuilder.detected;
+          availableBuildpacks = defaultBuilder.others;
+          defaultStack = builders
+            .flatMap((builder) => builder.builders)
+            .find((stack) => {
+              return (
+                stack === DEFAULT_HEROKU_STACK || stack === DEFAULT_PAKETO_STACK
+              );
+            });
+        }
         setBuilders(builders);
         setSelectedStack(defaultStack);
 
@@ -174,6 +224,7 @@ export const BuildpackStack: React.FC<{
           setSelectedBuildpacks([]);
         } else {
           setSelectedBuildpacks(detectedBuildpacks);
+          console.log(selectedBuildpacks);
         }
         if (!Array.isArray(availableBuildpacks)) {
           setAvailableBuildpacks([]);
@@ -346,7 +397,9 @@ export const BuildpackStack: React.FC<{
           value={selectedStack}
           width="300px"
           options={stackOptions}
-          setValue={(option) => setSelectedStack(option)}
+          setValue={(option) => {
+            setSelectedStack(option);
+          }}
           label="Select your builder and stack"
         />
         {!!selectedBuildpacks?.length && (
@@ -359,14 +412,9 @@ export const BuildpackStack: React.FC<{
           <>{renderBuildpacksList(selectedBuildpacks, "remove")}</>
         )}
         <Spacer y={1} />
-        <Button onClick={() => setIsModalOpen(true)}>
-          Add build pack
-        </Button>
-
+        <Button onClick={() => setIsModalOpen(true)}>Add build pack</Button>
         {isModalOpen && (
-          <Modal
-            closeModal={() => setIsModalOpen(false)}
-          >
+          <Modal closeModal={() => setIsModalOpen(false)}>
             {renderModalContent()}
           </Modal>
         )}
@@ -463,7 +511,7 @@ const StyledCard = styled.div<{ marginBottom?: string }>`
   justify-content: space-between;
   border: 1px solid #494b4f;
   background: ${({ theme }) => theme.fg};
-  margin-bottom: ${props => props.marginBottom || "30px"};
+  margin-bottom: ${(props) => props.marginBottom || "30px"};
   border-radius: 8px;
   padding: 14px;
   overflow: hidden;

+ 1 - 0
dashboard/src/main/home/app-dashboard/expanded-app/BuildSettingsTabStack.tsx

@@ -144,6 +144,7 @@ const BuildSettingsTabStack: React.FC<Props> = ({ appData, setAppData }) => {
                 setDockerfilePath("");
               }}
               hide={!showSettings}
+              currentBuildConfig={buildConfig}
             />
           )}
         </StyledSourceBox>

+ 79 - 32
dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx

@@ -26,6 +26,7 @@ import BuildSettingsTabStack from "./BuildSettingsTabStack";
 import Button from "components/porter/Button";
 import Services from "../new-app-flow/Services";
 import { Service } from "../new-app-flow/serviceTypes";
+import ConfirmOverlay from "components/porter/ConfirmOverlay";
 
 type Props = RouteComponentProps & {};
 
@@ -38,17 +39,13 @@ const icons = [
 ];
 
 const ExpandedApp: React.FC<Props> = ({ ...props }) => {
-  const {
-    currentCluster,
-    currentProject,
-    setCurrentError,
-    setCurrentOverlay,
-  } = useContext(Context);
+  const { currentCluster, currentProject, setCurrentError } = useContext(
+    Context
+  );
 
   const [isLoading, setIsLoading] = useState(true);
   const [appData, setAppData] = useState(null);
   const [error, setError] = useState(null);
-  const [isAuthorized] = useAuth();
   const [forceRefreshRevisions, setForceRefreshRevisions] = useState<boolean>(
     false
   );
@@ -60,10 +57,9 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
   const [loading, setLoading] = useState<boolean>(false);
   const [components, setComponents] = useState<ResourceType[]>([]);
 
-  const [isExpanded, setIsExpanded] = useState(false);
-  const [isAgentInstalled, setIsAgentInstalled] = useState<boolean>(false);
   const [showRevisions, setShowRevisions] = useState<boolean>(false);
   const [newestImage, setNewestImage] = useState<string>(null);
+  const [showDeleteOverlay, setShowDeleteOverlay] = useState<boolean>(false);
 
   const getPorterApp = async () => {
     setIsLoading(true);
@@ -93,6 +89,37 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
         app: resPorterApp?.data,
         chart: resChartData?.data,
       });
+      console.log(appData);
+      setIsLoading(false);
+    } catch (err) {
+      setError(err);
+      setIsLoading(false);
+    }
+  };
+
+  const deletePorterApp = async () => {
+    setIsLoading(true);
+    const { appName } = props.match.params as any;
+    try {
+      const res = await api.deletePorterApp(
+        "<token>",
+        {},
+        {
+          cluster_id: currentCluster.id,
+          project_id: currentProject.id,
+          name: appName,
+        }
+      );
+      const nsRes = await api.deleteNamespace(
+        "<token>",
+        {},
+        {
+          cluster_id: currentCluster.id,
+          id: currentProject.id,
+          namespace: `porter-stack-${appName}`,
+        }
+      );
+      console.log(res);
       setIsLoading(false);
     } catch (err) {
       setError(err);
@@ -121,9 +148,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
           break;
       }
     }
-    return (
-      <Icon src={src} />
-    );
+    return <Icon src={src} />;
   };
 
   const updateComponents = async (currentChart: ChartType) => {
@@ -287,18 +312,19 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
       case "settings":
         return (
           <>
-            <Text size={16}>
-              Delete app "{appData.app.name}"
-            </Text>
+            <Text size={16}>Delete "{appData.app.name}"</Text>
             <Spacer y={1} />
             <Text color="helper">
               Delete this application and all of its resources.
             </Text>
             <Spacer y={1} />
-            <Button onClick={() => {
-              // set delete overlay
-            }} color="#b91133">
-              Delete {appData.app.name}
+            <Button
+              onClick={() => {
+                setShowDeleteOverlay(true);
+              }}
+              color="#b91133"
+            >
+              Delete
             </Button>
           </>
         );
@@ -390,14 +416,24 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
           <DarkMatter antiHeight="-18px" />
           <Spacer y={1} />
           <TabSelector
-            options={[
-              { label: "Events", value: "events" },
-              { label: "Logs", value: "logs" },
-              { label: "Metrics", value: "metrics" },
-              { label: "Overview", value: "overview" },
-              { label: "Build settings", value: "build-settings" },
-              { label: "Settings", value: "settings" },
-            ]}
+            options={
+              appData.app.build_packs
+                ? [
+                  { label: "Events", value: "events" },
+                  { label: "Logs", value: "logs" },
+                  { label: "Metrics", value: "metrics" },
+                  { label: "Overview", value: "overview" },
+                  { label: "Build settings", value: "build-settings" },
+                  { label: "Settings", value: "settings" },
+                ]
+                : [
+                  { label: "Events", value: "events" },
+                  { label: "Logs", value: "logs" },
+                  { label: "Metrics", value: "metrics" },
+                  { label: "Overview", value: "overview" },
+                  { label: "Settings", value: "settings" },
+                ]
+            }
             currentTab={tab}
             setCurrentTab={setTab}
           />
@@ -405,6 +441,17 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
           {renderTabContents()}
         </StyledExpandedApp>
       )}
+      {showDeleteOverlay && (
+        <ConfirmOverlay
+          message={`Are you sure you want to delete "${appData.app.name}"?`}
+          onYes={() => {
+            deletePorterApp();
+          }}
+          onNo={() => {
+            setShowDeleteOverlay(false);
+          }}
+        />
+      )}
     </>
   );
 };
@@ -413,7 +460,7 @@ export default withRouter(ExpandedApp);
 
 const DarkMatter = styled.div<{ antiHeight?: string }>`
   width: 100%;
-  margin-top: ${props => props.antiHeight || "-20px"};
+  margin-top: ${(props) => props.antiHeight || "-20px"};
 `;
 
 const TagWrapper = styled.div`
@@ -448,13 +495,13 @@ const BranchTag = styled.div`
 `;
 
 const BranchSection = styled.div`
-  background: ${props => props.theme.fg};
+  background: ${(props) => props.theme.fg};
   border: 1px solid #494b4f;
 `;
 
-const SmallIcon = styled.img<{ opacity?: string, height?: string }>`
-  height: ${props => props.height || "15px"};
-  opacity: ${props => props.opacity || 1};
+const SmallIcon = styled.img<{ opacity?: string; height?: string }>`
+  height: ${(props) => props.height || "15px"};
+  opacity: ${(props) => props.opacity || 1};
   margin-right: 10px;
 `;
 

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

@@ -228,6 +228,18 @@ const updatePorterApp = baseApi<
   return `/api/projects/${project_id}/clusters/${cluster_id}/stacks/${name}`;
 });
 
+const deletePorterApp = baseApi<
+  {},
+  {
+    project_id: number;
+    cluster_id: number;
+    name: string;
+  }
+>("DELETE", (pathParams) => {
+  let { project_id, cluster_id, name } = pathParams;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/stacks/${name}`;
+});
+
 const updatePorterStack = baseApi<
   {
     stack_name: string;
@@ -2566,6 +2578,7 @@ export default {
   getPorterApp,
   createPorterApp,
   updatePorterApp,
+  deletePorterApp,
   updatePorterStack,
   createConfigMap,
   deleteCluster,

+ 8 - 0
internal/repository/gorm/porter_app.go

@@ -51,3 +51,11 @@ func (repo *PorterAppRepository) UpdatePorterApp(app *models.PorterApp) (*models
 
 	return app, nil
 }
+
+func (repo *PorterAppRepository) DeletePorterApp(app *models.PorterApp) (*models.PorterApp, error) {
+	if err := repo.db.Delete(&app).Error; err != nil {
+		return nil, err
+	}
+
+	return app, nil
+}

+ 1 - 0
internal/repository/porter_app.go

@@ -10,4 +10,5 @@ type PorterAppRepository interface {
 	CreatePorterApp(app *models.PorterApp) (*models.PorterApp, error)
 	ListPorterAppByClusterID(clusterID uint) ([]*models.PorterApp, error)
 	UpdatePorterApp(app *models.PorterApp) (*models.PorterApp, error)
+	DeletePorterApp(app *models.PorterApp) (*models.PorterApp, error)
 }

+ 4 - 0
internal/repository/test/porter_app.go

@@ -33,3 +33,7 @@ func (repo *PorterAppRepository) UpdatePorterApp(app *models.PorterApp) (*models
 func (repo *PorterAppRepository) ListPorterAppByClusterID(clusterID uint) ([]*models.PorterApp, error) {
 	return nil, errors.New("cannot write database")
 }
+
+func (repo *PorterAppRepository) DeletePorterApp(app *models.PorterApp) (*models.PorterApp, error) {
+	return nil, errors.New("cannot write database")
+}