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

Merge branch 'belanger/infra-improvements' into dev

Alexander Belanger 3 лет назад
Родитель
Сommit
9f945c4345
25 измененных файлов с 445 добавлено и 143 удалено
  1. 30 14
      api/server/handlers/cluster_integration/aws/get_cluster_info.go
  2. 31 0
      api/server/handlers/infra/forms.go
  3. 3 0
      api/types/cluster_integration.go
  4. 2 0
      api/types/infra.go
  5. 9 6
      api/types/stacks.go
  6. 39 2
      dashboard/src/components/repo-selector/RepoList.tsx
  7. 26 7
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/ExpandedStack.tsx
  8. 4 1
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/_RevisionList.tsx
  9. 12 10
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/_SourceConfig.tsx
  10. 11 5
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Select.tsx
  11. 9 2
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/SourceEditorDocker.tsx
  12. 25 9
      dashboard/src/main/home/cluster-dashboard/stacks/_StackList.tsx
  13. 33 0
      dashboard/src/main/home/cluster-dashboard/stacks/components/styles.ts
  14. 1 0
      dashboard/src/main/home/cluster-dashboard/stacks/types.ts
  15. 4 4
      dashboard/src/main/home/infrastructure/InfrastructureList.tsx
  16. 34 62
      dashboard/src/main/home/sidebar/Sidebar.tsx
  17. 1 0
      dashboard/src/shared/types.tsx
  18. 37 0
      internal/models/infra.go
  19. 1 0
      internal/models/stack.go
  20. 62 19
      internal/repository/gorm/environment.go
  21. 32 0
      internal/repository/gorm/infra.go
  22. 16 0
      internal/repository/gorm/stack.go
  23. 1 0
      internal/repository/stack.go
  24. 4 0
      internal/repository/test/stack.go
  25. 18 2
      internal/stacks/hooks.go

+ 30 - 14
api/server/handlers/cluster_integration/aws/get_cluster_info.go

@@ -89,26 +89,42 @@ func (c *GetClusterInfoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 
 
 	ec2Svc := ec2.New(awsSession, awsConf)
 	ec2Svc := ec2.New(awsSession, awsConf)
 
 
-	subnetsInfo, err := ec2Svc.DescribeSubnets(&ec2.DescribeSubnetsInput{
-		SubnetIds: clusterInfo.Cluster.ResourcesVpcConfig.SubnetIds,
-	})
-
-	if err != nil || len(subnetsInfo.Subnets) == 0 {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
 	res := &types.GetAWSClusterInfoResponse{
 	res := &types.GetAWSClusterInfoResponse{
 		Name:       clusterName,
 		Name:       clusterName,
+		ARN:        *clusterInfo.Cluster.Arn,
+		Status:     *clusterInfo.Cluster.Status,
 		K8sVersion: *clusterInfo.Cluster.Version,
 		K8sVersion: *clusterInfo.Cluster.Version,
 		EKSVersion: *clusterInfo.Cluster.PlatformVersion,
 		EKSVersion: *clusterInfo.Cluster.PlatformVersion,
 	}
 	}
 
 
-	for _, subnet := range subnetsInfo.Subnets {
-		res.Subnets = append(res.Subnets, &types.AWSSubnet{
-			AvailabilityZone:        *subnet.AvailabilityZone,
-			AvailableIPAddressCount: *subnet.AvailableIpAddressCount,
-		})
+	err = ec2Svc.DescribeSubnetsPages(&ec2.DescribeSubnetsInput{
+		Filters: []*ec2.Filter{
+			{
+				Name: aws.String("vpc-id"),
+				Values: []*string{
+					clusterInfo.Cluster.ResourcesVpcConfig.VpcId,
+				},
+			},
+		},
+	}, func(page *ec2.DescribeSubnetsOutput, lastPage bool) bool {
+		if page == nil {
+			return false
+		}
+
+		for _, subnet := range page.Subnets {
+			res.Subnets = append(res.Subnets, &types.AWSSubnet{
+				SubnetID:                *subnet.SubnetId,
+				AvailabilityZone:        *subnet.AvailabilityZone,
+				AvailableIPAddressCount: *subnet.AvailableIpAddressCount,
+			})
+		}
+
+		return !lastPage
+	})
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
 	}
 	}
 
 
 	c.WriteResult(w, r, res)
 	c.WriteResult(w, r, res)

+ 31 - 0
api/server/handlers/infra/forms.go

@@ -500,6 +500,37 @@ tabs:
       placeholder: "ex: 10"
       placeholder: "ex: 10"
       settings:
       settings:
         default: 10
         default: 10
+- name: iam
+  label: IAM
+  sections:
+  - name: toggle_aws_auth
+    contents:
+    - type: heading
+      label: Configure IAM Access
+    - type: checkbox
+      variable: manage_aws_auth_configmap
+      label: Allow Porter to manage AWS authentication for the cluster.
+      settings:
+        default: true
+  - name: arns
+    show_if: manage_aws_auth_configmap
+    contents:
+    - type: heading
+      label: Users
+    - type: subtitle
+      label: "Add AWS users to the cluster. The left input should be a valid AWS user ARN, and the right side should be a group on the cluster. For example, arn:aws:iam::66666666666:user/user1: system:masters."
+    - type: key-value-array
+      variable: aws_auth_users
+      settings:
+        default: {}
+    - type: heading
+      label: Roles
+    - type: subtitle
+      label: "Add AWS roles to the cluster. The left input should be a valid AWS role ARN, and the right side should be a group on the cluster. For example, arn:aws:iam::66666666666:role/role1: system:masters."
+    - type: key-value-array
+      variable: aws_auth_roles
+      settings:
+        default: {}
 - name: advanced
 - name: advanced
   label: Advanced
   label: Advanced
   sections:
   sections:

+ 3 - 0
api/types/cluster_integration.go

@@ -1,13 +1,16 @@
 package types
 package types
 
 
 type AWSSubnet struct {
 type AWSSubnet struct {
+	SubnetID                string `json:"subnet_id"`
 	AvailabilityZone        string `json:"availability_zone"`
 	AvailabilityZone        string `json:"availability_zone"`
 	AvailableIPAddressCount int64  `json:"available_ip_address_count"`
 	AvailableIPAddressCount int64  `json:"available_ip_address_count"`
 }
 }
 
 
 type GetAWSClusterInfoResponse struct {
 type GetAWSClusterInfoResponse struct {
 	Name       string       `json:"name"`
 	Name       string       `json:"name"`
+	ARN        string       `json:"arn"`
 	K8sVersion string       `json:"kubernetes_server_version"`
 	K8sVersion string       `json:"kubernetes_server_version"`
 	EKSVersion string       `json:"eks_version"`
 	EKSVersion string       `json:"eks_version"`
+	Status     string       `json:"status"`
 	Subnets    []*AWSSubnet `json:"subnets"`
 	Subnets    []*AWSSubnet `json:"subnets"`
 }
 }

+ 2 - 0
api/types/infra.go

@@ -42,6 +42,8 @@ type Infra struct {
 	// The project that this integration belongs to
 	// The project that this integration belongs to
 	ProjectID uint `json:"project_id"`
 	ProjectID uint `json:"project_id"`
 
 
+	Name string `json:"name"`
+
 	APIVersion    string `json:"api_version,omitempty"`
 	APIVersion    string `json:"api_version,omitempty"`
 	SourceLink    string `json:"source_link,omitempty"`
 	SourceLink    string `json:"source_link,omitempty"`
 	SourceVersion string `json:"source_version,omitempty"`
 	SourceVersion string `json:"source_version,omitempty"`

+ 9 - 6
api/types/stacks.go

@@ -71,6 +71,9 @@ type Stack struct {
 	// The display name of the stack
 	// The display name of the stack
 	Name string `json:"name"`
 	Name string `json:"name"`
 
 
+	// The namespace that the stack was deployed to
+	Namespace string `json:"namespace"`
+
 	// A unique id for the stack
 	// A unique id for the stack
 	ID string `json:"id"`
 	ID string `json:"id"`
 
 
@@ -190,13 +193,13 @@ type StackSourceConfig struct {
 // swagger:model
 // swagger:model
 type CreateStackSourceConfigRequest struct {
 type CreateStackSourceConfigRequest struct {
 	// required: true
 	// required: true
-	Name string `json:"name"`
+	Name string `json:"name" form:"required"`
 
 
 	// required: true
 	// required: true
-	ImageRepoURI string `json:"image_repo_uri"`
+	ImageRepoURI string `json:"image_repo_uri" form:"required"`
 
 
 	// required: true
 	// required: true
-	ImageTag string `json:"image_tag"`
+	ImageTag string `json:"image_tag" form:"required"`
 
 
 	// If this field is empty, the resource is deployed directly from the image repo uri
 	// If this field is empty, the resource is deployed directly from the image repo uri
 	StackSourceConfigBuild *StackSourceConfigBuild `json:"build,omitempty"`
 	StackSourceConfigBuild *StackSourceConfigBuild `json:"build,omitempty"`
@@ -205,13 +208,13 @@ type CreateStackSourceConfigRequest struct {
 // swagger:model
 // swagger:model
 type UpdateStackSourceConfigRequest struct {
 type UpdateStackSourceConfigRequest struct {
 	// required: true
 	// required: true
-	Name string `json:"name"`
+	Name string `json:"name" form:"required"`
 
 
 	// required: true
 	// required: true
-	ImageRepoURI string `json:"image_repo_uri"`
+	ImageRepoURI string `json:"image_repo_uri" form:"required"`
 
 
 	// required: true
 	// required: true
-	ImageTag string `json:"image_tag"`
+	ImageTag string `json:"image_tag" form:"required"`
 }
 }
 
 
 type StackSourceConfigBuild struct {
 type StackSourceConfigBuild struct {

+ 39 - 2
dashboard/src/components/repo-selector/RepoList.tsx

@@ -19,6 +19,42 @@ type Props = {
   filteredRepos?: string[];
   filteredRepos?: string[];
 };
 };
 
 
+type Provider =
+  | {
+      provider: "github";
+      name: string;
+      installation_id: number;
+    }
+  | {
+      provider: "gitlab";
+      instance_url: string;
+      integration_id: number;
+    };
+
+// Sort provider by name if it's github or instance url if it's gitlab
+const sortProviders = (providers: Provider[]) => {
+  const githubProviders = providers.filter(
+    (provider) => provider.provider === "github"
+  );
+
+  const gitlabProviders = providers.filter(
+    (provider) => provider.provider === "gitlab"
+  );
+
+  const githubSortedProviders = githubProviders.sort((a, b) => {
+    if (a.provider === "github" && b.provider === "github") {
+      return a.name.localeCompare(b.name);
+    }
+  });
+
+  const gitlabSortedProviders = gitlabProviders.sort((a, b) => {
+    if (a.provider === "gitlab" && b.provider === "gitlab") {
+      return a.instance_url.localeCompare(b.instance_url);
+    }
+  });
+  return [...gitlabSortedProviders, ...githubSortedProviders];
+};
+
 const RepoList: React.FC<Props> = ({
 const RepoList: React.FC<Props> = ({
   actionConfig,
   actionConfig,
   setActionConfig,
   setActionConfig,
@@ -51,8 +87,9 @@ const RepoList: React.FC<Props> = ({
           return;
           return;
         }
         }
 
 
-        setProviders(data);
-        setCurrentProvider(data[0]);
+        const sortedProviders = sortProviders(data);
+        setProviders(sortedProviders);
+        setCurrentProvider(sortedProviders[0]);
       })
       })
       .catch((err) => {
       .catch((err) => {
         setHasProviders(false);
         setHasProviders(false);

+ 26 - 7
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/ExpandedStack.tsx

@@ -17,6 +17,7 @@ import {
   InfoWrapper,
   InfoWrapper,
   LastDeployed,
   LastDeployed,
   LineBreak,
   LineBreak,
+  NamespaceTag,
   SepDot,
   SepDot,
   Text,
   Text,
 } from "../components/styles";
 } from "../components/styles";
@@ -79,13 +80,19 @@ const ExpandedStack = () => {
 
 
   return (
   return (
     <div>
     <div>
-      <TitleSection
-        materialIconClass="material-icons-outlined"
-        icon={"lan"}
-        capitalize
-      >
-        {stack.name}
-      </TitleSection>
+      <StackTitleWrapper>
+        <TitleSection
+          materialIconClass="material-icons-outlined"
+          icon={"lan"}
+          capitalize
+        >
+          {stack.name}
+        </TitleSection>
+        <NamespaceTag.Wrapper>
+          Namespace
+          <NamespaceTag.Tag>{stack.namespace}</NamespaceTag.Tag>
+        </NamespaceTag.Wrapper>
+      </StackTitleWrapper>
       <RevisionList
       <RevisionList
         revisions={stack.revisions}
         revisions={stack.revisions}
         currentRevision={currentRevision}
         currentRevision={currentRevision}
@@ -222,3 +229,15 @@ const StackErrorMessageStyles = {
     font-weight: bold;
     font-weight: bold;
   `,
   `,
 };
 };
+
+const StackTitleWrapper = styled.div`
+  width: 100%;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+
+  // Hotfix to make sure the title section and the namespace tag are aligned
+  ${NamespaceTag.Wrapper} {
+    margin-bottom: 15px;
+  }
+`;

+ 4 - 1
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/_RevisionList.tsx

@@ -168,6 +168,8 @@ const _RevisionList = ({
 export default _RevisionList;
 export default _RevisionList;
 
 
 const StyledRevisionSection = styled.div`
 const StyledRevisionSection = styled.div`
+  display: flex;
+  flex-direction: column;
   position: relative;
   position: relative;
   width: 100%;
   width: 100%;
   max-height: ${(props: { showRevisions: boolean }) =>
   max-height: ${(props: { showRevisions: boolean }) =>
@@ -195,7 +197,7 @@ const RevisionHeader = styled.div`
   display: flex;
   display: flex;
   justify-content: space-between;
   justify-content: space-between;
   align-items: center;
   align-items: center;
-  height: 40px;
+  min-height: 40px;
   font-size: 13px;
   font-size: 13px;
   width: 100%;
   width: 100%;
   padding-left: 15px;
   padding-left: 15px;
@@ -228,6 +230,7 @@ const RevisionPreview = styled.div`
 
 
 const TableWrapper = styled.div`
 const TableWrapper = styled.div`
   padding-bottom: 20px;
   padding-bottom: 20px;
+  overflow-y: auto;
 `;
 `;
 
 
 const RevisionsTable = styled.table`
 const RevisionsTable = styled.table`

+ 12 - 10
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/_SourceConfig.tsx

@@ -99,16 +99,18 @@ const _SourceConfig = ({
           </SourceConfigStyles.ItemContainer>
           </SourceConfigStyles.ItemContainer>
         );
         );
       })}
       })}
-      <SourceConfigStyles.SaveButtonRow>
-        <SourceConfigStyles.SaveButton
-          onClick={handleSave}
-          text="Save"
-          clearPosition={true}
-          makeFlush={true}
-          status={buttonStatus}
-          statusPosition="left"
-        />
-      </SourceConfigStyles.SaveButtonRow>
+      {readOnly ? null : (
+        <SourceConfigStyles.SaveButtonRow>
+          <SourceConfigStyles.SaveButton
+            onClick={handleSave}
+            text="Save"
+            clearPosition={true}
+            makeFlush={true}
+            status={buttonStatus}
+            statusPosition="left"
+          />
+        </SourceConfigStyles.SaveButtonRow>
+      )}
     </SourceConfigStyles.Wrapper>
     </SourceConfigStyles.Wrapper>
   );
   );
 };
 };

+ 11 - 5
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Select.tsx

@@ -75,6 +75,16 @@ const Select = <T extends unknown>({
     );
     );
   }
   }
 
 
+  const isSelected = (option: T, value: T) => {
+    if (!value) {
+      return false;
+    }
+
+    if (isOptionEqualToValue) {
+      return isOptionEqualToValue(option, value);
+    }
+  };
+
   return (
   return (
     <div>
     <div>
       {getLabel()}
       {getLabel()}
@@ -107,11 +117,7 @@ const Select = <T extends unknown>({
                     key={i}
                     key={i}
                     onClick={() => !readOnly && handleOptionClick(option)}
                     onClick={() => !readOnly && handleOptionClick(option)}
                     lastItem={i === options.length - 1}
                     lastItem={i === options.length - 1}
-                    selected={
-                      isOptionEqualToValue
-                        ? isOptionEqualToValue(option, value)
-                        : option === value
-                    }
+                    selected={isSelected(option, value)}
                     height={dropdown?.option?.height}
                     height={dropdown?.option?.height}
                   >
                   >
                     {accessor(option)}
                     {accessor(option)}

+ 9 - 2
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/SourceEditorDocker.tsx

@@ -37,6 +37,13 @@ const SourceEditorDocker = ({
     return image.replace(registry.url + "/", "");
     return image.replace(registry.url + "/", "");
   }, [image, registry]);
   }, [image, registry]);
 
 
+  useEffect(() => {
+    if (sourceConfig.image_repo_uri) {
+      setImage(sourceConfig.image_repo_uri);
+      setTag(sourceConfig.image_tag);
+    }
+  }, [sourceConfig]);
+
   useEffect(() => {
   useEffect(() => {
     const newSourceConfig: SourceConfig = {
     const newSourceConfig: SourceConfig = {
       ...sourceConfig,
       ...sourceConfig,
@@ -126,7 +133,7 @@ const _DockerRepositorySelector = ({
         }
         }
         setIsLoading(false);
         setIsLoading(false);
       });
       });
-  }, []);
+  }, [currentImageUrl]);
 
 
   const handleChange = (newRegistry: DockerRegistry) => {
   const handleChange = (newRegistry: DockerRegistry) => {
     onChange(newRegistry);
     onChange(newRegistry);
@@ -141,7 +148,7 @@ const _DockerRepositorySelector = ({
         accessor={(val) => val.name}
         accessor={(val) => val.name}
         label="Docker Registry"
         label="Docker Registry"
         placeholder="Select a registry"
         placeholder="Select a registry"
-        isOptionEqualToValue={(a, b) => a.url === b.url}
+        isOptionEqualToValue={(a, b) => a?.url === b?.url}
         readOnly={readOnly}
         readOnly={readOnly}
         isLoading={isLoading}
         isLoading={isLoading}
         dropdown={{
         dropdown={{

+ 25 - 9
dashboard/src/main/home/cluster-dashboard/stacks/_StackList.tsx

@@ -13,6 +13,7 @@ import {
   Flex,
   Flex,
   InfoWrapper,
   InfoWrapper,
   LastDeployed,
   LastDeployed,
+  NamespaceTag,
   SepDot,
   SepDot,
   Text,
   Text,
 } from "./components/styles";
 } from "./components/styles";
@@ -80,6 +81,10 @@ const StackList = ({ namespace }: { namespace: string }) => {
           setIsLoading(false);
           setIsLoading(false);
         }
         }
       });
       });
+
+    return () => {
+      isSubscribed = false;
+    };
   }, [namespace]);
   }, [namespace]);
 
 
   if (isLoading) {
   if (isLoading) {
@@ -104,16 +109,22 @@ const StackList = ({ namespace }: { namespace: string }) => {
           <StackCard
           <StackCard
             as={DynamicLink}
             as={DynamicLink}
             key={stack?.id}
             key={stack?.id}
-            to={`/stacks/${namespace}/${stack?.id}`}
+            to={`/stacks/${stack?.namespace}/${stack?.id}`}
           >
           >
             <DataContainer>
             <DataContainer>
-              <StackName>
-                <StackIcon>
-                  <i className="material-icons-outlined">lan</i>
-                </StackIcon>
-                <span>{stack.name}</span>
-              </StackName>
-
+              <Top>
+                <StackName>
+                  <StackIcon>
+                    <i className="material-icons-outlined">lan</i>
+                  </StackIcon>
+                  <span>{stack.name}</span>
+                </StackName>
+                <SepDot>•</SepDot>
+                <NamespaceTag.Wrapper>
+                  Namespace
+                  <NamespaceTag.Tag>{stack.namespace}</NamespaceTag.Tag>
+                </NamespaceTag.Wrapper>
+              </Top>
               <InfoWrapper>
               <InfoWrapper>
                 <LastDeployed>
                 <LastDeployed>
                   <Status
                   <Status
@@ -200,7 +211,6 @@ const StackName = styled.div`
   display: flex;
   display: flex;
   font-size: 14px;
   font-size: 14px;
   align-items: center;
   align-items: center;
-  margin-bottom: 10px;
 `;
 `;
 
 
 const DataContainer = styled.div`
 const DataContainer = styled.div`
@@ -215,3 +225,9 @@ const StackCard = styled(Card)`
   font-size: 13px;
   font-size: 13px;
   font-weight: 500;
   font-weight: 500;
 `;
 `;
+
+const Top = styled.div`
+  display: flex;
+  align-items: center;
+  margin-bottom: 10px;
+`;

+ 33 - 0
dashboard/src/main/home/cluster-dashboard/stacks/components/styles.ts

@@ -96,3 +96,36 @@ export const Flex = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
 `;
 `;
+
+export const NamespaceTag = {
+  Wrapper: styled.div`
+    height: 20px;
+    font-size: 12px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #ffffff44;
+    border: 1px solid #ffffff44;
+    border-radius: 3px;
+    padding-left: 5px;
+  `,
+
+  Tag: styled.div`
+    height: 20px;
+    margin-left: 6px;
+    color: #aaaabb;
+    background: #ffffff22;
+    border-radius: 3px;
+    font-size: 12px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 0px 6px;
+    padding-left: 7px;
+    border-top-left-radius: 0px;
+    border-bottom-left-radius: 0px;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  `,
+};

+ 1 - 0
dashboard/src/main/home/cluster-dashboard/stacks/types.ts

@@ -31,6 +31,7 @@ export type Stack = {
   name: string;
   name: string;
   created_at: string;
   created_at: string;
   updated_at: string;
   updated_at: string;
+  namespace: string;
 
 
   revisions: StackRevision[];
   revisions: StackRevision[];
 
 

+ 4 - 4
dashboard/src/main/home/infrastructure/InfrastructureList.tsx

@@ -92,6 +92,10 @@ const InfrastructureList = () => {
           );
           );
         },
         },
       },
       },
+      {
+        Header: "Name",
+        accessor: "name",
+      },
       {
       {
         Header: "Status",
         Header: "Status",
         accessor: "status",
         accessor: "status",
@@ -131,10 +135,6 @@ const InfrastructureList = () => {
           return readableDate(row.original.updated_at);
           return readableDate(row.original.updated_at);
         },
         },
       },
       },
-      {
-        Header: "Source",
-        accessor: "source_link",
-      },
       {
       {
         Header: "Version",
         Header: "Version",
         accessor: "source_version",
         accessor: "source_version",

+ 34 - 62
dashboard/src/main/home/sidebar/Sidebar.tsx

@@ -103,6 +103,34 @@ class Sidebar extends Component<PropsType, StateType> {
     }
     }
   };
   };
 
 
+  /**
+   * Helper function that will keep the query params before redirect the user to a new page
+   *
+   * @param location
+   * @param path Path to redirect to
+   * @returns React router `to` object
+   */
+  withQueryParams = (location: any, path: string) => {
+    let { currentCluster, currentProject } = this.context;
+    let params = this.props.match.params as any;
+    let pathNamespace = params.namespace;
+    let search = `?cluster=${currentCluster.name}&project_id=${currentProject.id}`;
+
+    if (!pathNamespace) {
+      pathNamespace = getQueryParam(this.props, "namespace");
+    }
+
+    if (pathNamespace) {
+      search = search.concat(`&namespace=${pathNamespace}`);
+    }
+
+    return {
+      ...location,
+      pathname: path,
+      search,
+    };
+  };
+
   renderClusterContent = () => {
   renderClusterContent = () => {
     let { currentCluster, currentProject } = this.context;
     let { currentCluster, currentProject } = this.context;
 
 
@@ -110,74 +138,16 @@ class Sidebar extends Component<PropsType, StateType> {
       return (
       return (
         <>
         <>
           <NavButton
           <NavButton
-            to={(location) => {
-              let params = this.props.match.params as any;
-              let pathNamespace = params.namespace;
-              let search = `?cluster=${currentCluster.name}&project_id=${currentProject.id}`;
-
-              if (!pathNamespace) {
-                pathNamespace = getQueryParam(this.props, "namespace");
-              }
-
-              if (pathNamespace) {
-                search = search.concat(`&namespace=${pathNamespace}`);
-              }
-
-              return {
-                ...location,
-                pathname: "/applications",
-                search,
-              };
-            }}
+            to={(location) => this.withQueryParams(location, "/applications")}
           >
           >
             <Img src={monoweb} />
             <Img src={monoweb} />
             Applications
             Applications
           </NavButton>
           </NavButton>
-          <NavButton
-            to={() => {
-              let params = this.props.match.params as any;
-              let pathNamespace = params.namespace;
-              let search = `?cluster=${currentCluster.name}&project_id=${currentProject.id}`;
-
-              if (!pathNamespace) {
-                pathNamespace = getQueryParam(this.props, "namespace");
-              }
-
-              if (pathNamespace) {
-                search = search.concat(`&namespace=${pathNamespace}`);
-              }
-
-              return {
-                ...location,
-                pathname: "/jobs",
-                search,
-              };
-            }}
-          >
+          <NavButton to={() => this.withQueryParams(location, "/jobs")}>
             <Img src={monojob} />
             <Img src={monojob} />
             Jobs
             Jobs
           </NavButton>
           </NavButton>
-          <NavButton
-            to={() => {
-              let params = this.props.match.params as any;
-              let pathNamespace = params.namespace;
-              let search = `?cluster=${currentCluster.name}&project_id=${currentProject.id}`;
-
-              if (!pathNamespace) {
-                pathNamespace = getQueryParam(this.props, "namespace");
-              }
-
-              if (pathNamespace) {
-                search = search.concat(`&namespace=${pathNamespace}`);
-              }
-
-              return {
-                ...location,
-                pathname: "/env-groups",
-                search,
-              };
-            }}
-          >
+          <NavButton to={() => this.withQueryParams(location, "/env-groups")}>
             <Img src={sliders} />
             <Img src={sliders} />
             Env Groups
             Env Groups
           </NavButton>
           </NavButton>
@@ -224,7 +194,9 @@ class Sidebar extends Component<PropsType, StateType> {
             </NavButton>
             </NavButton>
           )}
           )}
           {currentProject?.stacks_enabled ? (
           {currentProject?.stacks_enabled ? (
-            <NavButton to="/stacks">
+            <NavButton
+              to={(location) => this.withQueryParams(location, "/stacks")}
+            >
               <Icon className="material-icons-outlined">lan</Icon>
               <Icon className="material-icons-outlined">lan</Icon>
               Stacks
               Stacks
             </NavButton>
             </NavButton>

+ 1 - 0
dashboard/src/shared/types.tsx

@@ -426,6 +426,7 @@ export type OperationType =
 
 
 export type Infrastructure = {
 export type Infrastructure = {
   id: number;
   id: number;
+  name?: string;
   api_version: string;
   api_version: string;
   created_at: string;
   created_at: string;
   updated_at: string;
   updated_at: string;

+ 37 - 0
internal/models/infra.go

@@ -125,10 +125,47 @@ func GetOperationID() (string, error) {
 	return encryption.GenerateRandomBytes(10)
 	return encryption.GenerateRandomBytes(10)
 }
 }
 
 
+type getInfraName struct {
+	Name        string `json:"name"`
+	ClusterName string `json:"cluster_name"`
+	DOCRName    string `json:"docr_name"`
+	ECRName     string `json:"ecr_name"`
+	ACRName     string `json:"acr_name"`
+}
+
 // ToInfraType generates an external Infra to be shared over REST
 // ToInfraType generates an external Infra to be shared over REST
 func (i *Infra) ToInfraType() *types.Infra {
 func (i *Infra) ToInfraType() *types.Infra {
+	// perform best attempt to get infra name
+	var name string
+	infraName := &getInfraName{}
+
+	if err := json.Unmarshal(i.LastApplied, infraName); err == nil {
+		if infraName.DOCRName != "" {
+			name = infraName.DOCRName
+		}
+
+		if infraName.ECRName != "" {
+			name = infraName.ECRName
+		}
+
+		if infraName.ACRName != "" {
+			name = infraName.ACRName
+		}
+
+		if infraName.ClusterName != "" {
+			name = infraName.ClusterName
+		}
+
+		if infraName.Name != "" {
+			name = infraName.Name
+		}
+	} else if err != nil {
+		fmt.Println("ERRWAS", err)
+	}
+
 	return &types.Infra{
 	return &types.Infra{
 		ID:               i.ID,
 		ID:               i.ID,
+		Name:             name,
 		CreatedAt:        i.CreatedAt,
 		CreatedAt:        i.CreatedAt,
 		UpdatedAt:        i.UpdatedAt,
 		UpdatedAt:        i.UpdatedAt,
 		ProjectID:        i.ProjectID,
 		ProjectID:        i.ProjectID,

+ 1 - 0
internal/models/stack.go

@@ -39,6 +39,7 @@ func (s *Stack) ToStackType() *types.Stack {
 		CreatedAt:      s.CreatedAt,
 		CreatedAt:      s.CreatedAt,
 		UpdatedAt:      s.UpdatedAt,
 		UpdatedAt:      s.UpdatedAt,
 		Name:           s.Name,
 		Name:           s.Name,
+		Namespace:      s.Namespace,
 		ID:             s.UID,
 		ID:             s.UID,
 		LatestRevision: latestRevision,
 		LatestRevision: latestRevision,
 		Revisions:      revisions,
 		Revisions:      revisions,

+ 62 - 19
internal/repository/gorm/environment.go

@@ -28,13 +28,24 @@ func (repo *EnvironmentRepository) CreateEnvironment(env *models.Environment) (*
 
 
 func (repo *EnvironmentRepository) ReadEnvironment(projectID, clusterID, gitInstallationID uint, gitRepoOwner, gitRepoName string) (*models.Environment, error) {
 func (repo *EnvironmentRepository) ReadEnvironment(projectID, clusterID, gitInstallationID uint, gitRepoOwner, gitRepoName string) (*models.Environment, error) {
 	env := &models.Environment{}
 	env := &models.Environment{}
-	if err := repo.db.Order("id desc").Where(
-		"project_id = ? AND cluster_id = ? AND git_installation_id = ? AND git_repo_owner = LOWER(?) AND git_repo_name = LOWER(?)",
-		projectID, clusterID, gitInstallationID,
-		strings.ToLower(gitRepoOwner), strings.ToLower(gitRepoName),
-	).First(&env).Error; err != nil {
-		return nil, err
+
+	switch repo.db.Dialector.Name() {
+	case "sqlite":
+		if err := repo.db.Order("id desc").Where(
+			"project_id = ? AND cluster_id = ? AND git_installation_id = ? AND git_repo_owner LIKE ? AND git_repo_name LIKE ?",
+			projectID, clusterID, gitInstallationID, gitRepoOwner, gitRepoName,
+		).First(&env).Error; err != nil {
+			return nil, err
+		}
+	case "postgres":
+		if err := repo.db.Order("id desc").Where(
+			"project_id = ? AND cluster_id = ? AND git_installation_id = ? AND git_repo_owner iLIKE ? AND git_repo_name iLIKE ?",
+			projectID, clusterID, gitInstallationID, gitRepoOwner, gitRepoName,
+		).First(&env).Error; err != nil {
+			return nil, err
+		}
 	}
 	}
+
 	return env, nil
 	return env, nil
 }
 }
 
 
@@ -56,11 +67,22 @@ func (repo *EnvironmentRepository) ReadEnvironmentByOwnerRepoName(
 	gitRepoOwner, gitRepoName string,
 	gitRepoOwner, gitRepoName string,
 ) (*models.Environment, error) {
 ) (*models.Environment, error) {
 	env := &models.Environment{}
 	env := &models.Environment{}
-	if err := repo.db.Order("id desc").Where("project_id = ? AND cluster_id = ? AND git_repo_owner = LOWER(?) AND git_repo_name = LOWER(?)",
-		projectID, clusterID, strings.ToLower(gitRepoOwner), strings.ToLower(gitRepoName),
-	).First(&env).Error; err != nil {
-		return nil, err
+
+	switch repo.db.Dialector.Name() {
+	case "sqlite":
+		if err := repo.db.Order("id desc").Where("project_id = ? AND cluster_id = ? AND git_repo_owner LIKE ? AND git_repo_name LIKE ?",
+			projectID, clusterID, gitRepoOwner, gitRepoName,
+		).First(&env).Error; err != nil {
+			return nil, err
+		}
+	case "postgres":
+		if err := repo.db.Order("id desc").Where("project_id = ? AND cluster_id = ? AND git_repo_owner iLIKE ? AND git_repo_name iLIKE ?",
+			projectID, clusterID, gitRepoOwner, gitRepoName,
+		).First(&env).Error; err != nil {
+			return nil, err
+		}
 	}
 	}
+
 	return env, nil
 	return env, nil
 }
 }
 
 
@@ -68,11 +90,22 @@ func (repo *EnvironmentRepository) ReadEnvironmentByWebhookIDOwnerRepoName(
 	webhookID, gitRepoOwner, gitRepoName string,
 	webhookID, gitRepoOwner, gitRepoName string,
 ) (*models.Environment, error) {
 ) (*models.Environment, error) {
 	env := &models.Environment{}
 	env := &models.Environment{}
-	if err := repo.db.Order("id desc").Where("webhook_id = ? AND git_repo_owner = LOWER(?) AND git_repo_name = LOWER(?)",
-		webhookID, strings.ToLower(gitRepoOwner), strings.ToLower(gitRepoName),
-	).First(&env).Error; err != nil {
-		return nil, err
+
+	switch repo.db.Dialector.Name() {
+	case "sqlite":
+		if err := repo.db.Order("id desc").Where("webhook_id = ? AND git_repo_owner LIKE ? AND git_repo_name LIKE ?",
+			webhookID, gitRepoOwner, gitRepoName,
+		).First(&env).Error; err != nil {
+			return nil, err
+		}
+	case "postgres":
+		if err := repo.db.Order("id desc").Where("webhook_id = ? AND git_repo_owner iLIKE ? AND git_repo_name iLIKE ?",
+			webhookID, gitRepoOwner, gitRepoName,
+		).First(&env).Error; err != nil {
+			return nil, err
+		}
 	}
 	}
+
 	return env, nil
 	return env, nil
 }
 }
 
 
@@ -148,11 +181,21 @@ func (repo *EnvironmentRepository) ReadDeploymentByGitDetails(
 ) (*models.Deployment, error) {
 ) (*models.Deployment, error) {
 	depl := &models.Deployment{}
 	depl := &models.Deployment{}
 
 
-	if err := repo.db.Order("id asc").
-		Where("environment_id = ? AND repo_owner = LOWER(?) AND repo_name = LOWER(?) AND pull_request_id = ?",
-			environmentID, strings.ToLower(gitRepoOwner), strings.ToLower(gitRepoName), prNumber).
-		First(&depl).Error; err != nil {
-		return nil, err
+	switch repo.db.Dialector.Name() {
+	case "sqlite":
+		if err := repo.db.Order("id asc").
+			Where("environment_id = ? AND repo_owner LIKE ? AND repo_name LIKE ? AND pull_request_id = ?",
+				environmentID, gitRepoOwner, gitRepoName, prNumber).
+			First(&depl).Error; err != nil {
+			return nil, err
+		}
+	case "postgres":
+		if err := repo.db.Order("id asc").
+			Where("environment_id = ? AND repo_owner iLIKE ? AND repo_name iLIKE ? AND pull_request_id = ?",
+				environmentID, gitRepoOwner, gitRepoName, prNumber).
+			First(&depl).Error; err != nil {
+			return nil, err
+		}
 	}
 	}
 
 
 	return depl, nil
 	return depl, nil

+ 32 - 0
internal/repository/gorm/infra.go

@@ -91,8 +91,40 @@ func (repo *InfraRepository) ListInfrasByProjectID(
 		return nil, err
 		return nil, err
 	}
 	}
 
 
+	infraIDs := make([]uint, 0)
+
 	for _, infra := range infras {
 	for _, infra := range infras {
 		repo.DecryptInfraData(infra, repo.key)
 		repo.DecryptInfraData(infra, repo.key)
+		infraIDs = append(infraIDs, infra.ID)
+	}
+
+	// get the latest operation for each infra and use it to set LastApplied
+	operations := make([]*models.Operation, 0)
+
+	if err := repo.db.Where("operations.infra_id IN (?)", infraIDs).Where(`
+	operations.id IN (
+	  SELECT o2.id FROM (SELECT MAX(operations.id) id FROM operations WHERE operations.infra_id IN (?) GROUP BY operations.infra_id) o2
+	)
+  `, infraIDs).Find(&operations).Error; err != nil {
+		return nil, err
+	}
+
+	// insert operations into a map
+	infraIDToOperationMap := make(map[uint]models.Operation)
+
+	for _, op := range operations {
+		err := repo.DecryptOperationData(op, repo.key)
+
+		if err == nil {
+			infraIDToOperationMap[op.InfraID] = *op
+		}
+	}
+
+	// look up each revision for each stack
+	for _, infra := range infras {
+		if _, exists := infraIDToOperationMap[infra.ID]; exists {
+			infra.LastApplied = infraIDToOperationMap[infra.ID].LastApplied
+		}
 	}
 	}
 
 
 	return infras, nil
 	return infras, nil

+ 16 - 0
internal/repository/gorm/stack.go

@@ -74,6 +74,22 @@ func (repo *StackRepository) ListStacks(projectID, clusterID uint, namespace str
 	return stacks, nil
 	return stacks, nil
 }
 }
 
 
+func (repo *StackRepository) ReadStackByID(projectID, stackID uint) (*models.Stack, error) {
+	stack := &models.Stack{}
+
+	if err := repo.db.
+		Preload("Revisions", func(db *gorm.DB) *gorm.DB {
+			return db.Order("stack_revisions.revision_number DESC").Limit(100)
+		}).
+		Preload("Revisions.Resources").
+		Preload("Revisions.SourceConfigs").
+		Where("stacks.project_id = ? AND stacks.id = ?", projectID, stackID).First(&stack).Error; err != nil {
+		return nil, err
+	}
+
+	return stack, nil
+}
+
 // ReadStack gets a stack specified by its string id
 // ReadStack gets a stack specified by its string id
 func (repo *StackRepository) ReadStackByStringID(projectID uint, stackID string) (*models.Stack, error) {
 func (repo *StackRepository) ReadStackByStringID(projectID uint, stackID string) (*models.Stack, error) {
 	stack := &models.Stack{}
 	stack := &models.Stack{}

+ 1 - 0
internal/repository/stack.go

@@ -5,6 +5,7 @@ import "github.com/porter-dev/porter/internal/models"
 // StackRepository represents the set of queries on the Stack model
 // StackRepository represents the set of queries on the Stack model
 type StackRepository interface {
 type StackRepository interface {
 	CreateStack(stack *models.Stack) (*models.Stack, error)
 	CreateStack(stack *models.Stack) (*models.Stack, error)
+	ReadStackByID(projectID, stackID uint) (*models.Stack, error)
 	ReadStackByStringID(projectID uint, stackID string) (*models.Stack, error)
 	ReadStackByStringID(projectID uint, stackID string) (*models.Stack, error)
 	ListStacks(projectID uint, clusterID uint, namespace string) ([]*models.Stack, error)
 	ListStacks(projectID uint, clusterID uint, namespace string) ([]*models.Stack, error)
 	DeleteStack(stack *models.Stack) (*models.Stack, error)
 	DeleteStack(stack *models.Stack) (*models.Stack, error)

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

@@ -21,6 +21,10 @@ func (repo *StackRepository) ListStacks(projectID, clusterID uint, namespace str
 	panic("unimplemented")
 	panic("unimplemented")
 }
 }
 
 
+func (repo *StackRepository) ReadStackByID(projectID, stackID uint) (*models.Stack, error) {
+	panic("unimplemented")
+}
+
 // ReadStack gets a stack specified by its string id
 // ReadStack gets a stack specified by its string id
 func (repo *StackRepository) ReadStackByStringID(projectID uint, stackID string) (*models.Stack, error) {
 func (repo *StackRepository) ReadStackByStringID(projectID uint, stackID string) (*models.Stack, error) {
 	panic("unimplemented")
 	panic("unimplemented")

+ 18 - 2
internal/stacks/hooks.go

@@ -1,6 +1,8 @@
 package stacks
 package stacks
 
 
 import (
 import (
+	"fmt"
+
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"gorm.io/gorm"
 	"gorm.io/gorm"
 	"helm.sh/helm/v3/pkg/release"
 	"helm.sh/helm/v3/pkg/release"
@@ -24,13 +26,27 @@ func UpdateHelmRevision(config *config.Config, projID, clusterID uint, rel *rele
 		return err
 		return err
 	}
 	}
 
 
-	// read the revision number and create a new revision of the stack
-	stackRevision, err := config.Repo.Stack().ReadStackRevision(stackResource.StackRevisionID)
+	// read the revision number corresponding and create a new revision of the stack
+	oldStackRevision, err := config.Repo.Stack().ReadStackRevision(stackResource.StackRevisionID)
 
 
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
+	// get the latest revision for that stack
+	stack, err := config.Repo.Stack().ReadStackByID(projID, oldStackRevision.StackID)
+
+	if err != nil {
+		return err
+	}
+
+	if len(stack.Revisions) == 0 {
+		return fmt.Errorf("length of stack revision list was 0")
+	}
+
+	currStackRevision := stack.Revisions[0]
+	stackRevision := &currStackRevision
+
 	clonedSourceConfigs, err := CloneSourceConfigs(stackRevision.SourceConfigs)
 	clonedSourceConfigs, err := CloneSourceConfigs(stackRevision.SourceConfigs)
 
 
 	if err != nil {
 	if err != nil {