2
0
Soham Dessai 3 жил өмнө
parent
commit
85b1a4a515

+ 3 - 5
api/server/handlers/stacks/create_porter_app.go

@@ -31,10 +31,9 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
 	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
 
-	request := &types.CreatePorterAppRequest{}
+	request := &types.UpdatePorterAppRequest{}
 
 	ok := c.DecodeAndValidate(w, r, request)
-
 	if !ok {
 		return
 	}
@@ -53,11 +52,10 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		ImageRepoURI: request.ImageRepoURI,
 	}
 
-	_, err := c.Repo().PorterApp().CreatePorterApp(app)
-
+	porterApp, err := c.Repo().PorterApp().UpdatePorterApp(app)
 	if err != nil {
 		return
 	}
 
-	w.WriteHeader(http.StatusCreated)
+	c.WriteResult(w, r, porterApp.ToPorterAppType())
 }

+ 1 - 1
api/server/handlers/stacks/get_porter_app.go

@@ -32,7 +32,7 @@ func (c *GetPorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
 	name, _ := requestutils.GetURLParamString(r, types.URLParamReleaseName)
 
-	app, err := c.Repo().PorterApp().ReadPorterApp(cluster.ID, name)
+	app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, name)
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return

+ 18 - 1
api/server/handlers/stacks/list_porter_app.go

@@ -5,7 +5,10 @@ import (
 
 	"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/types"
+	"github.com/porter-dev/porter/internal/models"
 )
 
 type PorterAppListHandler struct {
@@ -22,6 +25,20 @@ func NewPorterAppListHandler(
 }
 
 func (p *PorterAppListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
 
-	p.WriteResult(w, r, nil)
+	porterApps, err := p.Repo().PorterApp().ListPorterAppByClusterID(cluster.ID)
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res := make(types.ListPorterAppResponse, 0)
+
+	for _, porterApp := range porterApps {
+		res = append(res, porterApp.ToPorterAppType())
+	}
+
+	p.WriteResult(w, r, res)
 }

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

@@ -0,0 +1,57 @@
+package stacks
+
+import (
+	"fmt"
+	"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 UpdatePorterAppHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewUpdatePorterAppHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *UpdatePorterAppHandler {
+	return &UpdatePorterAppHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+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)
+
+	porterApp, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, name)
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	request := &types.UpdatePorterAppRequest{}
+	ok := c.DecodeAndValidate(w, r, request)
+	if !ok {
+		return
+	}
+
+	updatedPorterApp, err := c.Repo().PorterApp().UpdatePorterApp(porterApp)
+	if err != nil {
+		return
+	}
+
+	c.WriteResult(w, r, updatedPorterApp.ToPorterAppType())
+}

+ 31 - 2
api/server/router/stack.go

@@ -95,7 +95,7 @@ func getStackRoutes(
 			Scopes: []types.PermissionScope{
 				types.UserScope,
 				types.ProjectScope,
-				types.RegistryScope,
+				types.ClusterScope,
 			},
 		},
 	)
@@ -111,7 +111,7 @@ func getStackRoutes(
 		Router:   r,
 	})
 
-	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/update_config -> stacks.NewCreateStackHandler
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/update_config -> stacks.NewCreatePorterAppHandler
 	createPorterAppEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbCreate,
@@ -140,6 +140,35 @@ func getStackRoutes(
 		Router:   r,
 	})
 
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/{name} -> stacks.NewCreatePorterAppHandler
+	updatePorterAppEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbUpdate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/{name}",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	updatePorterAppHandler := stacks.NewUpdatePorterAppHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: updatePorterAppEndpoint,
+		Handler:  updatePorterAppHandler,
+		Router:   r,
+	})
+
 	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks -> stacks.NewCreateStackHandler
 	createEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 27 - 0
api/types/porter_app.go

@@ -20,3 +20,30 @@ type PorterApp struct {
 	Buildpacks   string `json:"build_packs,omitempty"`
 	Dockerfile   string `json:"dockerfile,omitempty"`
 }
+
+// swagger:model
+type CreatePorterAppRequest struct {
+	Name         string `json:"name" form:"required"`
+	ClusterID    uint   `json:"cluster_id"`
+	ProjectID    uint   `json:"project_id"`
+	RepoName     string `json:"repo_name"`
+	GitBranch    string `json:"git_branch"`
+	BuildContext string `json:"build_context"`
+	Builder      string `json:"builder"`
+	Buildpacks   string `json:"buildpacks"`
+	Dockerfile   string `json:"dockerfile"`
+	ImageRepoURI string `json:"image_repo_uri"`
+}
+
+type UpdatePorterAppRequest struct {
+	Name         string `json:"name"`
+	RepoName     string `json:"repo_name"`
+	GitBranch    string `json:"git_branch"`
+	BuildContext string `json:"build_context"`
+	Builder      string `json:"builder"`
+	Buildpacks   string `json:"buildpacks"`
+	Dockerfile   string `json:"dockerfile"`
+	ImageRepoURI string `json:"image_repo_uri"`
+}
+
+type ListPorterAppResponse []*PorterApp

+ 1 - 0
api/types/request.go

@@ -43,6 +43,7 @@ const (
 	URLParamInviteID              URLParam = "invite_id"
 	URLParamNamespace             URLParam = "namespace"
 	URLParamReleaseName           URLParam = "name"
+	URLParamPorterAppID           URLParam = "porter_app_id"
 	URLParamStackID               URLParam = "stack_id"
 	URLParamReleaseVersion        URLParam = "version"
 	URLParamWildcard              URLParam = "*"

+ 0 - 14
api/types/stacks.go

@@ -22,20 +22,6 @@ type CreateStackRequest struct {
 	EnvGroups []*CreateStackEnvGroupRequest `json:"env_groups,omitempty" form:"required,dive,required"`
 }
 
-// swagger:model
-type CreatePorterAppRequest struct {
-	Name         string `json:"name" form:"required"`
-	ClusterID    uint   `json:"cluster_id"`
-	ProjectID    uint   `json:"project_id"`
-	RepoName     string `json:"repo_name"`
-	GitBranch    string `json:"git_branch"`
-	BuildContext string `json:"build_context"`
-	Builder      string `json:"builder"`
-	Buildpacks   string `json:"buildpacks"`
-	Dockerfile   string `json:"dockerfile"`
-	ImageRepoURI string `json:"image_repo_uri"`
-}
-
 // swagger:model
 type PutStackSourceConfigRequest struct {
 	SourceConfigs []*CreateStackSourceConfigRequest `json:"source_configs,omitempty" form:"required,dive,required"`

+ 16 - 3
dashboard/src/components/porter/ExpandableSection.tsx

@@ -12,6 +12,8 @@ type Props = {
   expandText?: string;
   collapseText?: string;
   maxHeight?: string;
+  spaced?: boolean;
+  copy?: string;
 };
 
 const ExpandableSection: React.FC<Props> = ({
@@ -24,6 +26,8 @@ const ExpandableSection: React.FC<Props> = ({
   expandText,
   collapseText,
   maxHeight,
+  spaced,
+  copy,
 }) => {
   const [isExpanded, setIsExpanded] = useState(isInitiallyExpanded ?? false);
 
@@ -34,11 +38,17 @@ const ExpandableSection: React.FC<Props> = ({
       noWrapper={noWrapper}
     >
       {noWrapper ? (
-        <Container row>
+        <Container row spaced={spaced}>
           {Header}
-          <ExpandButton onClick={() => setIsExpanded(!isExpanded)}>
+          {copy ? (<ExpandButton onClick={() => setIsExpanded(!isExpanded)}>
             {isExpanded ? collapseText : expandText}
-          </ExpandButton>
+          </ExpandButton>) : (<div>          <ExpandButton onClick={() => setIsExpanded(!isExpanded)}>
+            {isExpanded ? collapseText : expandText}
+          </ExpandButton>          <ExpandButton onClick={() => setIsExpanded(!isExpanded)}>
+              {isExpanded ? collapseText : expandText}
+            </ExpandButton></div>)}
+
+
         </Container>
       ) : (
         <HeaderRow
@@ -66,6 +76,9 @@ const ExpandButton = styled.div`
   color: #aaaabb;
   cursor: pointer;
   font-size: 13px;
+  :hover {
+    color: #ffffff;
+  }
 `;
 
 const HeaderRow = styled.div<{

+ 2 - 1
dashboard/src/components/porter/Toggle.tsx

@@ -16,8 +16,9 @@ const Toggle: React.FC<Props> = ({
 }) => {
   return (
     <StyledToggle>
-      {items.map((item, index) => (
+      {items.map((item, i) => (
         <Item
+          key={i}
           active={item.value === active}
           onClick={() => {
             setActive(item.value);

+ 1 - 1
dashboard/src/components/porter/VerticalSteps.tsx

@@ -16,7 +16,7 @@ const VerticalSteps: React.FC<Props> = ({
     <StyledVerticalSteps>
       {steps.map((step, i) => {
         return (
-          <StepWrapper isLast={i === steps.length - 1}>
+          <StepWrapper isLast={i === steps.length - 1} key={i}>
             {
               (i !== steps.length - 1) && (
                 <Line isActive={i + 1 <= currentStep} />

+ 6 - 3
dashboard/src/main/home/add-on-dashboard/AddOnDashboard.tsx

@@ -116,7 +116,10 @@ const AppDashboard: React.FC<Props> = ({
   };
 
   useEffect(() => {
-    getAddOns();
+    // currentCluster sometimes returns as -1 and passes null check
+    if (currentProject?.id >= 0 && currentCluster?.id >= 0) {
+      getAddOns();
+    }
   }, [currentCluster, currentProject]);
 
   const getExpandedChartLinkURL = useCallback((x: any) => {
@@ -181,7 +184,7 @@ const AppDashboard: React.FC<Props> = ({
         <GridList>
           {(filteredAddOns ?? []).map((app: any, i: number) => {
             return (
-              <Block to={getExpandedChartLinkURL(app)}>
+              <Block to={getExpandedChartLinkURL(app)} key={i}>
                 <Text size={14}>
                   <Icon 
                     src={
@@ -204,7 +207,7 @@ const AppDashboard: React.FC<Props> = ({
         <List>
           {(filteredAddOns ?? []).map((app: any, i: number) => {
             return (
-              <Row to={getExpandedChartLinkURL(app)}>
+              <Row to={getExpandedChartLinkURL(app)} key={i}>
                 <Text size={14}>
                   <MidIcon
                     src={

+ 18 - 9
dashboard/src/main/home/app-dashboard/AppDashboard.tsx

@@ -21,6 +21,7 @@ import Text from "components/porter/Text";
 import SearchBar from "components/porter/SearchBar";
 import Toggle from "components/porter/Toggle";
 import Link from "components/porter/Link";
+import Loading from "components/Loading";
 
 type Props = {
 };
@@ -47,6 +48,7 @@ const AppDashboard: React.FC<Props> = ({
 }) => {
   const { currentProject, currentCluster } = useContext(Context);
   const [apps, setApps] = useState([]);
+  const [error, setError] = useState(null);
   const [searchValue, setSearchValue] = useState("");
   const [view, setView] = useState("grid");
   const [isLoading, setIsLoading] = useState(true);
@@ -65,25 +67,32 @@ const AppDashboard: React.FC<Props> = ({
   }, [apps, searchValue]);
 
   const getApps = async () => {
-    
+    setIsLoading(true);
+
     // TODO: Currently using namespaces as placeholder (replace with apps)
     try {
-      const res = await api.getNamespaces(
+      const res = await api.getPorterApps(
         "<token>",
         {},
         {
-          id: currentProject.id,
+          project_id: currentProject.id,
           cluster_id: currentCluster.id,
         }
       )
       setApps(res.data);
+      setIsLoading(false);
+    }
+    catch (err) {
+      setError(err);
+      setIsLoading(false);
     }
-    catch (err) {}
   };
 
   useEffect(() => {
-    getApps();
-  }, []);
+    if (currentProject?.id > 0 && currentCluster?.id > 0) {
+      getApps();
+    }
+  }, [currentCluster, currentProject]);
 
   return (
     <StyledAppDashboard>
@@ -117,12 +126,12 @@ const AppDashboard: React.FC<Props> = ({
         </Link>
       </Container>
       <Spacer y={1} />
-      {view === "grid" ? (
+      {isLoading ? <Loading offset="-150px" /> : view === "grid" ? (
         <GridList>
          {(filteredApps ?? []).map((app: any, i: number) => {
            if (!namespaceBlacklist.includes(app.name)) {
              return (
-               <Block>
+               <Block key={i}>
                  <Text size={14}>
                    <Icon src={icons[i % icons.length]} />
                    {app.name}
@@ -146,7 +155,7 @@ const AppDashboard: React.FC<Props> = ({
           {(filteredApps ?? []).map((app: any, i: number) => {
             if (!namespaceBlacklist.includes(app.name)) {
               return (
-                <Row>
+                <Row key={i}>
                   <Text size={14}>
                     <MidIcon src={icons[i % icons.length]} />
                     {app.name}

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

@@ -29,6 +29,13 @@ import BuildSettingsTabStack from "./BuildSettingsTabStack";
 type Props = RouteComponentProps & {};
 
 const ExpandedApp: React.FC<Props> = ({ ...props }) => {
+  const {
+    currentCluster,
+    currentProject,
+    setCurrentError,
+    setCurrentOverlay,
+  } = useContext(Context);
+
   const [isLoading, setIsLoading] = useState(true);
   const [appData, setAppData] = useState(null);
   const [error, setError] = useState(null);
@@ -48,12 +55,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
   const [isAgentInstalled, setIsAgentInstalled] = useState<boolean>(false);
   const [showRevisions, setShowRevisions] = useState<boolean>(false);
   const [newestImage, setNewestImage] = useState<string>(null);
-  const {
-    currentCluster,
-    currentProject,
-    setCurrentError,
-    setCurrentOverlay,
-  } = useContext(Context);
+
   const getPorterApp = async () => {
     setIsLoading(true);
     const { appName } = props.match.params as any;
@@ -67,7 +69,6 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
           name: appName,
         }
       );
-      console.log(resPorterApp);
       const resChartData = await api.getChart(
         "<token>",
         {},
@@ -83,8 +84,6 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
         app: resPorterApp?.data,
         chart: resChartData?.data,
       });
-      console.log(resChartData?.data);
-      console.log(resPorterApp?.data);
       setIsLoading(false);
     } catch (err) {
       setError(err);
@@ -231,6 +230,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
     },
     [appData?.chart]
   );
+
   useEffect(() => {
     const { appName } = props.match.params as any;
     if (currentCluster && appName && currentProject) {

+ 3 - 2
dashboard/src/main/home/app-dashboard/new-app-flow/GithubActionModal.tsx

@@ -94,10 +94,11 @@ const GithubActionModal: React.FC<GithubActionModalProps> = ({
         Header={
           <ModalHeader>.github/workflows/porter.yml</ModalHeader>
         }
-        isInitiallyExpanded={true}
+        isInitiallyExpanded
+        spaced
         ExpandedSection={
           <YamlEditor
-            value={getGithubAction(projectId, stackName)}
+            value={getGithubAction(projectId, clusterId, stackName)}
             readOnly={true}
             height="300px"
           />

+ 28 - 4
dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx

@@ -21,7 +21,6 @@ import Placeholder from "components/Placeholder";
 import Button from "components/porter/Button";
 import { generateSlug } from "random-word-slugs";
 import { RouteComponentProps, withRouter } from "react-router";
-import Error from "components/porter/Error";
 import SourceSelector, { SourceType } from "./SourceSelector";
 import SourceSettings from "./SourceSettings";
 import Services from "./Services";
@@ -103,8 +102,8 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
   const [porterYaml, setPorterYaml] = useState("");
   const [showGHAModal, setShowGHAModal] = useState<boolean>(false);
   const [porterJson, setPorterJson] = useState<
-    z.infer<typeof PorterYamlSchema>
-  >(null);
+    z.infer<typeof PorterYamlSchema> | undefined
+  >(undefined);
   const [detected, setDetected] = useState<Detected | undefined>(undefined);
 
   const validatePorterYaml = (yamlString: string) => {
@@ -191,8 +190,19 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
       !isAppNameValid(formState.applicationName)
     );
   };
+
   const deployPorterApp = async () => {
     try {
+      if (
+        currentProject == null ||
+        currentCluster == null ||
+        currentProject.id == null ||
+        currentCluster.id == null
+      ) {
+        throw new Error("Project or cluster not found");
+      }
+
+      // validate form data
       const finalPorterYaml = createFinalPorterYaml();
       const yamlString = yaml.dump(finalPorterYaml);
       const base64Encoded = btoa(yamlString);
@@ -205,7 +215,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
           }
         : {};
 
-      // only deploy + write to DB if we can create a final porter yaml
+      // write to the db + deploy
       await Promise.all([
         api.createPorterApp(
           "<token>",
@@ -238,6 +248,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
         ),
       ]);
     } catch (err) {
+      // TODO: better error handling
       console.log(err);
     }
   };
@@ -262,6 +273,19 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
     const apps: z.infer<typeof AppsSchema> = {};
     for (const service of serviceList) {
       let config = Service.serialize(service);
+      // TODO: get rid of this block when we handle ingress on the backend
+      if (Service.isWeb(service)) {
+        const ingress = Service.handleWebIngress(
+          service,
+          formState.applicationName,
+          currentCluster?.id,
+          currentProject?.id
+        );
+        config = {
+          ...config,
+          ...ingress,
+        };
+      }
       if (
         porterJson != null &&
         porterJson.apps[service.name] != null &&

+ 1 - 0
dashboard/src/main/home/app-dashboard/new-app-flow/Services.tsx

@@ -43,6 +43,7 @@ const Services: React.FC<ServicesProps> = ({ services, setServices }) => {
             {services.map((service, index) => {
               return (
                 <ServiceContainer
+                  key={service.name}
                   service={service}
                   editService={(newService: Service) =>
                     setServices(

+ 51 - 10
dashboard/src/main/home/app-dashboard/new-app-flow/serviceTypes.ts

@@ -1,3 +1,5 @@
+import api from "shared/api";
+
 export type Service = WorkerService | WebService | JobService;
 export type ServiceType = 'web' | 'worker' | 'job';
 
@@ -80,7 +82,7 @@ const WebService = {
         targetCPUUtilizationPercentage: '50',
         targetRAMUtilizationPercentage: '50',
         port: '80',
-        generateUrlForExternalTraffic: true,
+        generateUrlForExternalTraffic: false,
         customDomain: '',
     }),
     serialize: (service: WebService) => {
@@ -93,13 +95,6 @@ const WebService = {
                 targetMemoryUtilizationPercentage: service.targetRAMUtilizationPercentage,
             }
         } : {};
-        const ingress = service.generateUrlForExternalTraffic ? {
-            ingress: {
-                enabled: true,
-                custom_domain: service.customDomain ? true : false,
-                hosts: service.customDomain ? [service.customDomain] : [],
-            }
-        } : {};
         return {
             replicaCount: service.replicas,
             resources: {
@@ -115,7 +110,6 @@ const WebService = {
                 port: service.port,
             },
             ...autoscaling,
-            ...ingress,
         }
     }
 }
@@ -173,5 +167,52 @@ export const Service = {
             case 'job':
                 return JobService.serialize(service);
         }
+    },
+    isWeb: (service: Service): service is WebService => service.type === 'web',
+    isWorker: (service: Service): service is WorkerService => service.type === 'worker',
+    isJob: (service: Service): service is JobService => service.type === 'job',
+    handleWebIngress: (service: WebService, stackName: string, projectId?: number, clusterId?: number) => {
+        if (projectId == null || clusterId == null) {
+            throw new Error('Project ID and Cluster ID must be provided to handle web ingress');
+        }
+        if (!service.generateUrlForExternalTraffic) {
+            return {}
+        }
+        const ingress: Ingress = {
+            enabled: true,
+            hosts: [],
+            custom_domain: false,
+            porter_hosts: [],
+        };
+        if (service.customDomain) {
+            ingress.hosts.push(service.customDomain);
+            ingress.custom_domain = true;
+        } else {
+            // const res = await api
+            //     .createSubdomain(
+            //         "<token>",
+            //         {},
+            //         {
+            //             id: projectId,
+            //             cluster_id: clusterId,
+            //             release_name: stackName,
+            //             namespace: `porter-stack-${stackName}`,
+            //         }
+            //     )
+            // if (res == null || res.data == null || res.data.external_url == null) {
+            //     throw new Error('Failed to create subdomain for web service');
+            // }
+            // ingress.porter_hosts.push(res.data.external_url)
+            throw new Error('Generating external URLs without custom subdomains not yet supported!');
+        }
+
+        return ingress;
     }
-} 
+}
+
+type Ingress = {
+    enabled: boolean;
+    hosts: string[];
+    custom_domain: boolean;
+    porter_hosts: string[];
+}

+ 17 - 17
dashboard/src/main/home/app-dashboard/new-app-flow/utils.tsx

@@ -1,22 +1,22 @@
 export const overrideObjectValues = (obj1: any, obj2: any) => {
-    // Iterate over the keys in obj2
-    for (const key in obj2) {
-        // Check if the key exists in obj1 and if its value is an object
-        if (key in obj1 && typeof obj1[key] === 'object' && typeof obj2[key] === 'object') {
-            // Recursively call the function to handle the nested object
-            obj1[key] = overrideObjectValues(obj1[key], obj2[key]);
-        } else {
-            // Otherwise, just assign the value from obj2 to obj1
-            obj1[key] = obj2[key];
-        }
+  // Iterate over the keys in obj2
+  for (const key in obj2) {
+    // Check if the key exists in obj1 and if its value is an object
+    if (key in obj1 && typeof obj1[key] === 'object' && typeof obj2[key] === 'object') {
+      // Recursively call the function to handle the nested object
+      obj1[key] = overrideObjectValues(obj1[key], obj2[key]);
+    } else {
+      // Otherwise, just assign the value from obj2 to obj1
+      obj1[key] = obj2[key];
     }
+  }
 
-    // Return the merged object
-    return obj1;
+  // Return the merged object
+  return obj1;
 };
 
-export const getGithubAction = (projectID?: number, stackName?: string) => {
-    return `on:
+export const getGithubAction = (projectID?: number, clusterId?: number, stackName?: string) => {
+  return `on:
   push:
     branches:
     - master
@@ -36,10 +36,10 @@ jobs:
       with:
         command: apply -f porter.yaml
       env:
-        PORTER_CLUSTER: "1"
-        PORTER_HOST: https://296e-160-72-72-58.ngrok-free.app
+        PORTER_CLUSTER: "${clusterId}"
+        PORTER_HOST: https://dashboard.getporter.dev
         PORTER_PROJECT: "${projectID}"
         PORTER_STACK_NAME: "${stackName}"
         PORTER_TAG: \${{ steps.vars.outputs.sha_short }}
-        PORTER_TOKEN: \${{ secrets.PORTER_STACK_1_1 }}`;
+        PORTER_TOKEN: \${{ secrets.PORTER_STACK_${projectID}_${clusterId} }}`;
 }

+ 3 - 0
dashboard/src/main/home/sidebar/Clusters.tsx

@@ -39,6 +39,9 @@ class Clusters extends Component<PropsType, StateType> {
   };
 
   updateClusters = () => {
+    if (!this.context.currentProject) {
+      return
+    }
     let {
       user,
       currentProject,

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

@@ -164,6 +164,17 @@ const createEmailVerification = baseApi<{}, {}>("POST", (pathParams) => {
   return `/api/email/verify/initiate`;
 });
 
+const getPorterApps = baseApi<
+{},
+{
+  project_id: number;
+  cluster_id: number;
+}
+>("GET", (pathParams) => {
+let { project_id, cluster_id } = pathParams;
+return `/api/projects/${project_id}/clusters/${cluster_id}/stacks`;
+});
+
 const getPorterApp = baseApi<
   {},
   {
@@ -196,6 +207,27 @@ const createPorterApp = baseApi<
   return `/api/projects/${project_id}/clusters/${cluster_id}/stacks/update_config`;
 });
 
+const updatePorterApp = baseApi<
+  {
+    name: string;
+    repo_name: string;
+    git_branch: string;
+    build_context: string;
+    builder: string;
+    buildpacks: string;
+    dockerfile: string;
+    image_repo_uri: string;
+  },
+  {
+    project_id: number;
+    cluster_id: number;
+    name: string;
+  }
+>("POST", (pathParams) => {
+  let { project_id, cluster_id, name } = pathParams;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/stacks/${name}`;
+});
+
 const updatePorterStack = baseApi<
   {
     stack_name: string;
@@ -2530,8 +2562,10 @@ export default {
   createPasswordResetVerify,
   createPasswordResetFinalize,
   createProject,
+  getPorterApps,
   getPorterApp,
   createPorterApp,
+  updatePorterApp,
   updatePorterStack,
   createConfigMap,
   deleteCluster,

+ 1 - 1
internal/repository/gorm/porter_app.go

@@ -34,7 +34,7 @@ func (repo *PorterAppRepository) ListPorterAppByClusterID(clusterID uint) ([]*mo
 	return apps, nil
 }
 
-func (repo *PorterAppRepository) ReadPorterApp(clusterID uint, name string) (*models.PorterApp, error) {
+func (repo *PorterAppRepository) ReadPorterAppByName(clusterID uint, name string) (*models.PorterApp, error) {
 	app := &models.PorterApp{}
 
 	if err := repo.db.Where("cluster_id = ? AND name = ?", clusterID, name).First(&app).Error; err != nil {

+ 2 - 2
internal/repository/porter_app.go

@@ -6,8 +6,8 @@ import (
 
 // PorterAppRepository represents the set of queries on the PorterApp model
 type PorterAppRepository interface {
+	ReadPorterAppByName(clusterID uint, name string) (*models.PorterApp, error)
 	CreatePorterApp(app *models.PorterApp) (*models.PorterApp, error)
-	// ListPorterAppByClusterID(clusterID uint) ([]*models.PorterApp, error)
-	ReadPorterApp(clusterID uint, name string) (*models.PorterApp, error)
+	ListPorterAppByClusterID(clusterID uint) ([]*models.PorterApp, error)
 	UpdatePorterApp(app *models.PorterApp) (*models.PorterApp, error)
 }

+ 6 - 2
internal/repository/test/porter_app.go

@@ -18,14 +18,18 @@ func NewPorterAppRepository(canQuery bool, failingMethods ...string) repository.
 
 }
 
-func (repo *PorterAppRepository) CreatePorterApp(app *models.PorterApp) (*models.PorterApp, error) {
+func (repo *PorterAppRepository) ReadPorterAppByName(clusterID uint, name string) (*models.PorterApp, error) {
 	return nil, errors.New("cannot write database")
 }
 
-func (repo *PorterAppRepository) ReadPorterApp(clusterID uint, name string) (*models.PorterApp, error) {
+func (repo *PorterAppRepository) CreatePorterApp(app *models.PorterApp) (*models.PorterApp, error) {
 	return nil, errors.New("cannot write database")
 }
 
 func (repo *PorterAppRepository) UpdatePorterApp(app *models.PorterApp) (*models.PorterApp, error) {
 	return nil, errors.New("cannot write database")
 }
+
+func (repo *PorterAppRepository) ListPorterAppByClusterID(clusterID uint) ([]*models.PorterApp, error) {
+	return nil, errors.New("cannot write database")
+}