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

Merge branch 'master' into feat/check-updates-for-non-porter-charts

Stefan McShane 3 лет назад
Родитель
Сommit
0e5ae994eb
28 измененных файлов с 1646 добавлено и 1232 удалено
  1. 28 38
      api/server/handlers/cluster_integration/aws/get_cluster_info.go
  2. 2 0
      api/server/handlers/environment/list_deployments_by_cluster.go
  3. 11 7
      api/server/handlers/namespace/get_ingress.go
  4. 7 2
      api/server/handlers/project_integration/create_aws.go
  5. 2 1
      api/server/handlers/project_integration/list_aws.go
  6. 2 1
      api/server/handlers/project_integration/overwrite_aws.go
  7. 6 12
      api/server/handlers/registry/get_token.go
  8. 8 6
      api/types/environment.go
  9. 5 9
      cli/cmd/connect/ecr.go
  10. 1 1
      dashboard/src/components/form-components/Helper.tsx
  11. 116 91
      dashboard/src/main/home/cluster-dashboard/chart/Chart.tsx
  12. 16 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  13. 15 8
      dashboard/src/main/home/cluster-dashboard/preview-environments/components/BranchFilterSelector.tsx
  14. 352 0
      dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreateBranchEnvironment.tsx
  15. 22 368
      dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreateEnvironment.tsx
  16. 535 0
      dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreatePREnvironment.tsx
  17. 2 0
      dashboard/src/main/home/cluster-dashboard/preview-environments/types.ts
  18. 1 6
      dashboard/src/main/home/navbar/Help.tsx
  19. 95 96
      go.mod
  20. 102 285
      go.sum
  21. 5 2
      internal/helm/postrenderer.go
  22. 28 3
      internal/kubernetes/agent.go
  23. 3 1
      internal/kubernetes/config.go
  24. 0 159
      internal/models/integrations/aws.go
  25. 177 0
      internal/models/integrations/aws_v2.go
  26. 67 92
      internal/registry/registry.go
  27. 32 34
      provisioner/integrations/storage/s3/s3.go
  28. 6 9
      provisioner/server/handlers/state/create_resource.go

+ 28 - 38
api/server/handlers/cluster_integration/aws/get_cluster_info.go

@@ -6,9 +6,10 @@ import (
 	"net/http"
 	"strings"
 
-	"github.com/aws/aws-sdk-go/aws"
-	"github.com/aws/aws-sdk-go/service/ec2"
-	"github.com/aws/aws-sdk-go/service/eks"
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/service/ec2"
+	ec2Types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
+	"github.com/aws/aws-sdk-go-v2/service/eks"
 	"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"
@@ -33,6 +34,8 @@ func NewGetClusterInfoHandler(
 }
 
 func (c *GetClusterInfoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+
 	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
@@ -55,31 +58,19 @@ func (c *GetClusterInfoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 		return
 	}
 
-	awsSession, err := awsInt.GetSession()
+	// clusterName := cluster.Name
 
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("error fetching new session for AWS with "+
-			"project ID: %d and integration ID: %d. Error: %w", proj.ID, cluster.AWSIntegrationID, err), http.StatusConflict))
-		return
+	if strings.HasPrefix(cluster.Name, "arn:aws:eks:") {
+		parts := strings.Split(cluster.Name, "/")
+		cluster.Name = parts[len(parts)-1]
 	}
 
-	clusterName := cluster.Name
+	awsConf := awsInt.Config()
 
-	if strings.HasPrefix(clusterName, "arn:aws:eks:") {
-		parts := strings.Split(clusterName, "/")
-		clusterName = parts[len(parts)-1]
-	}
-
-	awsConf := aws.NewConfig()
-
-	if awsInt.AWSRegion != "" {
-		awsConf = awsConf.WithRegion(awsInt.AWSRegion)
-	}
+	eksSvc := eks.NewFromConfig(awsConf)
 
-	eksSvc := eks.New(awsSession, awsConf)
-
-	clusterInfo, err := eksSvc.DescribeCluster(&eks.DescribeClusterInput{
-		Name: &clusterName,
+	clusterInfo, err := eksSvc.DescribeCluster(ctx, &eks.DescribeClusterInput{
+		Name: &cluster.Name,
 	})
 
 	if err != nil || clusterInfo.Cluster == nil {
@@ -87,40 +78,39 @@ func (c *GetClusterInfoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 		return
 	}
 
-	ec2Svc := ec2.New(awsSession, awsConf)
+	ec2Svc := ec2.NewFromConfig(awsConf)
 
 	res := &types.GetAWSClusterInfoResponse{
-		Name:       clusterName,
+		Name:       cluster.Name,
 		ARN:        *clusterInfo.Cluster.Arn,
-		Status:     *clusterInfo.Cluster.Status,
+		Status:     string(clusterInfo.Cluster.Status),
 		K8sVersion: *clusterInfo.Cluster.Version,
 		EKSVersion: *clusterInfo.Cluster.PlatformVersion,
 	}
 
-	err = ec2Svc.DescribeSubnetsPages(&ec2.DescribeSubnetsInput{
-		Filters: []*ec2.Filter{
+	subnetPaginate := ec2.NewDescribeSubnetsPaginator(ec2Svc, &ec2.DescribeSubnetsInput{
+		Filters: []ec2Types.Filter{
 			{
 				Name: aws.String("vpc-id"),
-				Values: []*string{
-					clusterInfo.Cluster.ResourcesVpcConfig.VpcId,
+				Values: []string{
+					*clusterInfo.Cluster.ResourcesVpcConfig.VpcId,
 				},
 			},
 		},
-	}, func(page *ec2.DescribeSubnetsOutput, lastPage bool) bool {
-		if page == nil {
-			return false
+	})
+	for subnetPaginate.HasMorePages() {
+		page, err := subnetPaginate.NextPage(ctx)
+		if err != nil {
+			continue
 		}
-
 		for _, subnet := range page.Subnets {
 			res.Subnets = append(res.Subnets, &types.AWSSubnet{
 				SubnetID:                *subnet.SubnetId,
 				AvailabilityZone:        *subnet.AvailabilityZone,
-				AvailableIPAddressCount: *subnet.AvailableIpAddressCount,
+				AvailableIPAddressCount: int64(*subnet.AvailableIpAddressCount),
 			})
 		}
-
-		return !lastPage
-	})
+	}
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 2 - 0
api/server/handlers/environment/list_deployments_by_cluster.go

@@ -293,6 +293,8 @@ func fetchOpenPullRequests(
 				RepoName:   env.GitRepoName,
 				BranchFrom: pr.GetHead().GetRef(),
 				BranchInto: pr.GetBase().GetRef(),
+				CreatedAt:  pr.GetCreatedAt(),
+				UpdatedAt:  pr.GetUpdatedAt(),
 			})
 		}
 	}

+ 11 - 7
api/server/handlers/namespace/get_ingress.go

@@ -33,10 +33,11 @@ func NewGetIngressHandler(
 
 func (c *GetIngressHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
-	agent, err := c.GetAgent(r, cluster, "")
 	name, _ := requestutils.GetURLParamString(r, types.URLParamIngressName)
 	namespace, _ := requestutils.GetURLParamString(r, types.URLParamNamespace)
 
+	agent, err := c.GetAgent(r, cluster, "")
+
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
@@ -58,17 +59,20 @@ func (c *GetIngressHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 	ingress3, err := agent.GetNetworkingV1Ingress(namespace, name)
 
-	if targetErr := kubernetes.IsNotFoundError; errors.Is(err, targetErr) {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			fmt.Errorf("ingress %s/%s was not found", namespace, name),
-			http.StatusNotFound,
-		))
+	if err == nil && ingress3 != nil {
+		c.WriteResult(w, r, ingress3)
+		return
+	}
+
+	ingress4, err := agent.GetIstioIngress(namespace, name)
 
+	if errors.Is(err, kubernetes.IsNotFoundError) {
+		c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("ingress %s/%s was not found", namespace, name)))
 		return
 	} else if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	c.WriteResult(w, r, ingress3)
+	c.WriteResult(w, r, ingress4)
 }

+ 7 - 2
api/server/handlers/project_integration/create_aws.go

@@ -1,6 +1,7 @@
 package project_integration
 
 import (
+	"context"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -45,14 +46,18 @@ func (p *CreateAWSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	aint := aws.ToAWSIntegrationType()
+
 	res := types.CreateAWSResponse{
-		AWSIntegration: aws.ToAWSIntegrationType(),
+		AWSIntegration: &aint,
 	}
 
 	p.WriteResult(w, r, res)
 }
 
 func CreateAWSIntegration(request *types.CreateAWSRequest, projectID, userID uint) *ints.AWSIntegration {
+	ctx := context.Background()
+
 	resp := &ints.AWSIntegration{
 		UserID:             userID,
 		ProjectID:          projectID,
@@ -64,7 +69,7 @@ func CreateAWSIntegration(request *types.CreateAWSRequest, projectID, userID uin
 	}
 
 	// attempt to populate the ARN
-	resp.PopulateAWSArn()
+	resp.PopulateAWSArn(ctx)
 
 	return resp
 }

+ 2 - 1
api/server/handlers/project_integration/list_aws.go

@@ -37,7 +37,8 @@ func (p *ListAWSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	var res types.ListAWSResponse = make([]*types.AWSIntegration, 0)
 
 	for _, awsInt := range awsInts {
-		res = append(res, awsInt.ToAWSIntegrationType())
+		aint := awsInt.ToAWSIntegrationType()
+		res = append(res, &aint)
 	}
 
 	p.WriteResult(w, r, res)

+ 2 - 1
api/server/handlers/project_integration/overwrite_aws.go

@@ -73,9 +73,10 @@ func (p *OverwriteAWSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	}
 
 	// app.Logger.Info().Msgf("AWS integration overwritten: %d", awsIntegration.ID)
+	aint := awsIntegration.ToAWSIntegrationType()
 
 	res := types.OverwriteAWSResponse{
-		AWSIntegration: awsIntegration.ToAWSIntegrationType(),
+		AWSIntegration: &aint,
 	}
 
 	p.WriteResult(w, r, res)

+ 6 - 12
api/server/handlers/registry/get_token.go

@@ -6,7 +6,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/aws/aws-sdk-go/service/ecr"
+	"github.com/aws/aws-sdk-go-v2/service/ecr"
 	"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"
@@ -16,7 +16,7 @@ import (
 	"github.com/porter-dev/porter/internal/oauth"
 	"github.com/porter-dev/porter/internal/registry"
 
-	"github.com/aws/aws-sdk-go/aws/arn"
+	"github.com/aws/aws-sdk-go-v2/aws/arn"
 )
 
 type RegistryGetECRTokenHandler struct {
@@ -34,6 +34,7 @@ func NewRegistryGetECRTokenHandler(
 }
 
 func (c *RegistryGetECRTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
 	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 
 	request := &types.GetRegistryECRTokenRequest{}
@@ -64,7 +65,7 @@ func (c *RegistryGetECRTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 
 			// if the aws integration doesn't have an ARN populated, populate it
 			if awsInt.AWSArn == "" {
-				err = awsInt.PopulateAWSArn()
+				err = awsInt.PopulateAWSArn(ctx)
 
 				if err != nil {
 					continue
@@ -80,16 +81,9 @@ func (c *RegistryGetECRTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 			// if the account id is passed as part of the request, verify the account id matches the account id in the ARN
 			if awsInt.AWSRegion == request.Region && (request.AccountID == "" || request.AccountID == parsedARN.AccountID) {
 				// get the aws integration and session
-				sess, err := awsInt.GetSession()
+				ecrSvc := ecr.NewFromConfig(awsInt.Config())
 
-				if err != nil {
-					c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-					return
-				}
-
-				ecrSvc := ecr.New(sess)
-
-				output, err := ecrSvc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})
+				output, err := ecrSvc.GetAuthorizationToken(ctx, &ecr.GetAuthorizationTokenInput{})
 
 				if err != nil {
 					c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 8 - 6
api/types/environment.go

@@ -129,12 +129,14 @@ type GetDeploymentRequest struct {
 }
 
 type PullRequest struct {
-	Title      string `json:"pr_title"`
-	Number     uint   `json:"pr_number"`
-	RepoOwner  string `json:"repo_owner"`
-	RepoName   string `json:"repo_name"`
-	BranchFrom string `json:"branch_from"`
-	BranchInto string `json:"branch_into"`
+	Title      string    `json:"pr_title"`
+	Number     uint      `json:"pr_number"`
+	RepoOwner  string    `json:"repo_owner"`
+	RepoName   string    `json:"repo_name"`
+	BranchFrom string    `json:"branch_from"`
+	BranchInto string    `json:"branch_into"`
+	CreatedAt  time.Time `json:"created_at"`
+	UpdatedAt  time.Time `json:"updated_at"`
 }
 
 type ToggleNewCommentRequest struct {

+ 5 - 9
cli/cmd/connect/ecr.go

@@ -9,7 +9,7 @@ import (
 
 	"github.com/porter-dev/porter/api/types"
 
-	"github.com/aws/aws-sdk-go/service/ecr"
+	"github.com/aws/aws-sdk-go-v2/service/ecr"
 	"github.com/fatih/color"
 	api "github.com/porter-dev/porter/api/client"
 	"github.com/porter-dev/porter/cli/cmd/utils"
@@ -154,22 +154,18 @@ func linkRegistry(client *api.Client, projectID uint, intID uint) (uint, error)
 }
 
 func waitForAuthorizationToken(region string, creds *aws.PorterAWSCredentials) error {
+	ctx := context.Background()
+
 	awsInt := &integrations.AWSIntegration{
 		AWSRegion:          region,
 		AWSAccessKeyID:     []byte(creds.AWSAccessKeyID),
 		AWSSecretAccessKey: []byte(creds.AWSSecretAccessKey),
 	}
 
-	sess, err := awsInt.GetSession()
-
-	if err != nil {
-		return err
-	}
-
-	ecrSvc := ecr.New(sess)
+	ecrSvc := ecr.NewFromConfig(awsInt.Config())
 
 	for i := 0; i < 30; i++ {
-		_, err := ecrSvc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})
+		_, err := ecrSvc.GetAuthorizationToken(ctx, &ecr.GetAuthorizationTokenInput{})
 
 		if err == nil {
 			return nil

+ 1 - 1
dashboard/src/components/form-components/Helper.tsx

@@ -5,7 +5,7 @@ export const Helper = styled.div<{ color?: string }>`
   color: ${({ color }) => (color ? color : "#aaaabb")};
   line-height: 1.6em;
   font-size: 13px;
-  margin-bottom: 15px;
+  margin-bottom: 35px;
   margin-top: 20px;
 `;
 

+ 116 - 91
dashboard/src/main/home/cluster-dashboard/chart/Chart.tsx

@@ -1,4 +1,10 @@
-import React, { useContext, useEffect, useMemo, useState } from "react";
+import React, {
+  useCallback,
+  useContext,
+  useEffect,
+  useMemo,
+  useState,
+} from "react";
 import styled from "styled-components";
 import { useHistory, useLocation, useRouteMatch } from "react-router";
 
@@ -21,6 +27,8 @@ import {
   MuiThemeProvider,
   withStyles,
 } from "@material-ui/core/styles";
+import { Link } from "react-router-dom";
+import _ from "lodash";
 
 type Props = {
   chart: ChartType;
@@ -117,97 +125,114 @@ const Chart: React.FunctionComponent<Props> = ({
     timeStyle: "long",
   });
 
+  const getExpandedChartLinkURL = useCallback(() => {
+    const params = new Proxy(new URLSearchParams(window.location.search), {
+      get: (searchParams, prop: string) => searchParams.get(prop),
+    });
+
+    const cluster = context.currentCluster?.name;
+
+    const route = `${isJob ? "/jobs" : "/applications"}/${cluster}/${
+      chart.namespace
+    }/${chart.name}`;
+
+    const newParams = {
+      // @ts-ignore
+      project_id: params.project_id,
+      closeChartRedirectUrl,
+    };
+
+    const newURLSearchParams = new URLSearchParams(
+      _.omitBy(newParams, _.isNil)
+    );
+
+    return `${route}?${newURLSearchParams.toString()}`;
+  }, [chart, context.currentCluster, isJob, closeChartRedirectUrl]);
+
   return (
-    <StyledChart
-      onClick={() => {
-        const cluster = context.currentCluster?.name;
-        let route = `${isJob ? "/jobs" : "/applications"}/${cluster}/${
-          chart.namespace
-        }/${chart.name}`;
-        pushFiltered({ location, history }, route, ["project_id"], {
-          closeChartRedirectUrl,
-        });
-      }}
-    >
-      <Title>
-        <IconWrapper>{renderIcon()}</IconWrapper>
-        {chart.canonical_name === "" ? chart.name : chart.canonical_name}
-        {chart?.config?.description && (
-          <>
-            <Dot style={{ marginLeft: "9px", color: "#ffffff88" }}>•</Dot>
-            <MuiThemeProvider theme={theme}>
-              <Tooltip
-                TransitionComponent={Zoom}
-                placement={"bottom-start"}
-                title={
-                  <div
-                    style={{
-                      fontFamily: "Work Sans, sans-serif",
-                      fontSize: "12px",
-                      fontWeight: "normal",
-                      padding: "5px 6px",
-                      color: "#ffffffdd",
-                      lineHeight: "16px",
-                    }}
-                  >
-                    {chart.config.description as string}
-                  </div>
-                }
-              >
-                <Description>{chart.config.description}</Description>
-              </Tooltip>
-            </MuiThemeProvider>
-          </>
-        )}
-      </Title>
-
-      <BottomWrapper>
-        <InfoWrapper>
-          <StatusIndicator
-            controllers={filteredControllers}
-            status={chart.info.status}
-            margin_left={"17px"}
-          />
-          <LastDeployed>
-            {jobStatus?.status ? (
-              <>
-                <Dot>•</Dot>
-                <JobStatus status={jobStatus.status}>
-                  {jobStatus.status === JobStatusType.Running
-                    ? "Started running"
-                    : `Last run ${jobStatus.status}`}{" "}
-                  at {readableDate(jobStatus.start_time)}
-                </JobStatus>
-              </>
-            ) : (
-              <>
-                <Dot>•</Dot>
-                <JobStatus>
-                  Last deployed {readableDate(chart.info.last_deployed)}
-                </JobStatus>
-              </>
-            )}
-            {chart.config?.schedule?.enabled ? (
-              <>
-                <Dot style={{ marginLeft: "10px" }}>•</Dot>
-                <JobStatus>
-                  Next run {rtf.format(interval?.next().toDate() || new Date())}
-                </JobStatus>
-              </>
-            ) : null}
-          </LastDeployed>
-        </InfoWrapper>
-
-        <TagWrapper>
-          Namespace
-          <NamespaceTag>{chart.namespace}</NamespaceTag>
-        </TagWrapper>
-      </BottomWrapper>
-
-      <TopRightContainer>
-        <span>v{chart.version}</span>
-      </TopRightContainer>
-    </StyledChart>
+    <Link to={getExpandedChartLinkURL}>
+      <StyledChart>
+        <Title>
+          <IconWrapper>{renderIcon()}</IconWrapper>
+          {chart.canonical_name === "" ? chart.name : chart.canonical_name}
+          {chart?.config?.description && (
+            <>
+              <Dot style={{ marginLeft: "9px", color: "#ffffff88" }}>•</Dot>
+              <MuiThemeProvider theme={theme}>
+                <Tooltip
+                  TransitionComponent={Zoom}
+                  placement={"bottom-start"}
+                  title={
+                    <div
+                      style={{
+                        fontFamily: "Work Sans, sans-serif",
+                        fontSize: "12px",
+                        fontWeight: "normal",
+                        padding: "5px 6px",
+                        color: "#ffffffdd",
+                        lineHeight: "16px",
+                      }}
+                    >
+                      {chart.config.description as string}
+                    </div>
+                  }
+                >
+                  <Description>{chart.config.description}</Description>
+                </Tooltip>
+              </MuiThemeProvider>
+            </>
+          )}
+        </Title>
+
+        <BottomWrapper>
+          <InfoWrapper>
+            <StatusIndicator
+              controllers={filteredControllers}
+              status={chart.info.status}
+              margin_left={"17px"}
+            />
+            <LastDeployed>
+              {jobStatus?.status ? (
+                <>
+                  <Dot>•</Dot>
+                  <JobStatus status={jobStatus.status}>
+                    {jobStatus.status === JobStatusType.Running
+                      ? "Started running"
+                      : `Last run ${jobStatus.status}`}{" "}
+                    at {readableDate(jobStatus.start_time)}
+                  </JobStatus>
+                </>
+              ) : (
+                <>
+                  <Dot>•</Dot>
+                  <JobStatus>
+                    Last deployed {readableDate(chart.info.last_deployed)}
+                  </JobStatus>
+                </>
+              )}
+              {chart.config?.schedule?.enabled ? (
+                <>
+                  <Dot style={{ marginLeft: "10px" }}>•</Dot>
+                  <JobStatus>
+                    Next run{" "}
+                    {rtf.format(interval?.next().toDate() || new Date())}
+                  </JobStatus>
+                </>
+              ) : null}
+            </LastDeployed>
+          </InfoWrapper>
+
+          <TagWrapper>
+            Namespace
+            <NamespaceTag>{chart.namespace}</NamespaceTag>
+          </TagWrapper>
+        </BottomWrapper>
+
+        <TopRightContainer>
+          <span>v{chart.version}</span>
+        </TopRightContainer>
+      </StyledChart>
+    </Link>
   );
 };
 

+ 16 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -819,7 +819,13 @@ const ExpandedChart: React.FC<Props> = (props) => {
   useEffect((): any => {
     let isSubscribed = true;
 
-    const ingressComponent = components?.find((c) => c.Kind === "Ingress");
+    const ingressComponent = components?.find(
+      (c) =>
+        c.Kind === "Ingress" ||
+        (c.Kind === "Gateway" &&
+          c.RawYAML?.apiVersion &&
+          c.RawYAML?.apiVersion?.startsWith("networking.istio.io"))
+    );
 
     const ingressName = ingressComponent?.Name;
 
@@ -851,6 +857,15 @@ const ExpandedChart: React.FC<Props> = (props) => {
           );
           return;
         }
+
+        if (
+          res.data?.spec?.servers &&
+          res.data?.spec?.servers[0]?.hosts &&
+          res.data?.spec?.servers[0]?.hosts[0]
+        ) {
+          setUrl(`http://${res.data?.spec?.servers[0]?.hosts[0]}`);
+          return;
+        }
       })
       .catch(console.log);
     return () => (isSubscribed = false);

+ 15 - 8
dashboard/src/main/home/cluster-dashboard/preview-environments/components/BranchFilterSelector.tsx

@@ -7,11 +7,13 @@ const BranchFilterSelector = ({
   options,
   onChange,
   showLoading,
+  multiSelect = true,
 }: {
   value: string[];
   options: string[];
   onChange: (value: string[]) => void;
   showLoading?: boolean;
+  multiSelect?: boolean;
 }) => {
   const filteredBranches = useMemo(() => {
     if (!options.length) {
@@ -26,6 +28,11 @@ const BranchFilterSelector = ({
   }, [options, value]);
 
   const handleAddBranch = (branch: string) => {
+    if (!multiSelect) {
+      onChange([branch]);
+      return;
+    }
+
     const newSelectedBranches = [...value, branch];
 
     onChange(newSelectedBranches);
@@ -55,14 +62,14 @@ const BranchFilterSelector = ({
       {/* List selected branches  */}
 
       <BranchRowList>
-      {value.map((branch) => (
-        <BranchRow key={branch}>
-          <div>{branch}</div>
-          <RemoveBranchButton onClick={() => handleDeleteBranch(branch)}>
-            x
-          </RemoveBranchButton>
-        </BranchRow>
-      ))}
+        {value.map((branch) => (
+          <BranchRow key={branch}>
+            <div>{branch}</div>
+            <RemoveBranchButton onClick={() => handleDeleteBranch(branch)}>
+              x
+            </RemoveBranchButton>
+          </BranchRow>
+        ))}
       </BranchRowList>
     </>
   );

+ 352 - 0
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreateBranchEnvironment.tsx

@@ -0,0 +1,352 @@
+import React, { useContext, useState } from "react";
+import styled from "styled-components";
+import { Context } from "shared/Context";
+import { Environment } from "../types";
+import Helper from "components/form-components/Helper";
+import api from "shared/api";
+import { useQuery } from "@tanstack/react-query";
+import { validatePorterYAML } from "../utils";
+import Banner from "components/Banner";
+import { useRouting } from "shared/routing";
+import PorterYAMLErrorsModal from "../components/PorterYAMLErrorsModal";
+import Placeholder from "components/Placeholder";
+import BranchFilterSelector from "../components/BranchFilterSelector";
+import _ from "lodash";
+
+interface Props {
+  environmentID: string;
+}
+
+const CreateBranchEnvironment = ({ environmentID }: Props) => {
+  const router = useRouting();
+  const [showErrorsModal, setShowErrorsModal] = useState<boolean>(false);
+  const { currentProject, currentCluster, setCurrentError } = useContext(
+    Context
+  );
+
+  const { data: environment } = useQuery<Environment>(
+    ["environment", currentProject.id, currentCluster.id, environmentID],
+    async () => {
+      const { data: environment } = await api.getEnvironment<Environment>(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+          cluster_id: currentCluster.id,
+          environment_id: parseInt(environmentID),
+        }
+      );
+
+      return environment;
+    }
+  );
+
+  // Get all branches for the current environment
+  const { isLoading: branchesLoading, data: branches } = useQuery<string[]>(
+    ["branches", currentProject.id, currentCluster.id, environment],
+    async () => {
+      try {
+        const res = await api.getBranches<string[]>(
+          "<token>",
+          {},
+          {
+            project_id: currentProject.id,
+            kind: "github",
+            name: environment.git_repo_name,
+            owner: environment.git_repo_owner,
+            git_repo_id: environment.git_installation_id,
+          }
+        );
+        return res.data ?? [];
+      } catch (err) {
+        setCurrentError(
+          "Couldn't load branches for this repository, please try again later."
+        );
+      }
+    },
+    {
+      enabled: !!environment,
+    }
+  );
+
+  const [selectedBranch, setSelectedBranch] = useState<string>();
+  const [loading, setLoading] = useState(false);
+  const [porterYAMLErrors, setPorterYAMLErrors] = useState<string[]>([]);
+
+  const handleRowItemClick = async (branch: string) => {
+    setSelectedBranch(branch);
+    setLoading(true);
+
+    const res = await validatePorterYAML({
+      projectID: currentProject.id,
+      clusterID: currentCluster.id,
+      environmentID: Number(environmentID),
+      branch,
+    });
+
+    setPorterYAMLErrors(res.data.errors ?? []);
+
+    setLoading(false);
+  };
+
+  const handleCreatePreviewDeployment = async () => {
+    try {
+      //   await api.createPreviewEnvironmentDeployment(
+      //     "<token>",
+      //     {
+      //       pr_title: "",
+      //       pr_number: 0,
+      //       repo_owner: environment.git_repo_name,
+      //       repo_name: environment.git_repo_owner,
+      //       branch_from: selectedBranch,
+      //       branch_into: selectedBranch,
+      //     },
+      //     {
+      //       cluster_id: currentCluster?.id,
+      //       project_id: currentProject?.id,
+      //     }
+      //   );
+
+      throw Error("Not implemented yet. (CreateBranchEnvironment.tsx:");
+
+      router.push(
+        `/preview-environments/deployments/${environmentID}/${environment.git_repo_name}/${environment.git_repo_owner}?status_filter=all`
+      );
+    } catch (err) {
+      setCurrentError(err);
+    }
+  };
+
+  if (!branches?.length) {
+    return (
+      <>
+        <Br height="30px" />
+        <Placeholder height="370px">You do not have any branches.</Placeholder>
+      </>
+    );
+  }
+
+  return (
+    <>
+      <Helper>
+        Select a branch to preview. Branches must contain a{" "}
+        <Code>porter.yaml</Code> file.
+      </Helper>
+      <Br height="10px" />
+      <BranchFilterSelector
+        onChange={(branches) => setSelectedBranch(branches[0])}
+        options={branches}
+        value={_.compact([selectedBranch])}
+        showLoading={branchesLoading}
+        multiSelect={false}
+      />
+      {showErrorsModal && selectedBranch ? (
+        <PorterYAMLErrorsModal
+          errors={porterYAMLErrors}
+          onClose={() => setShowErrorsModal(false)}
+          repo={environment.git_repo_name + "/" + environment.git_repo_owner}
+          branch={selectedBranch}
+        />
+      ) : null}
+      {selectedBranch && porterYAMLErrors.length ? (
+        <ValidationErrorBannerWrapper>
+          <Banner type="warning">
+            We found some errors in the porter.yaml file in the&nbsp;
+            {selectedBranch}&nbsp;branch. &nbsp;
+            <LearnMoreButton onClick={() => setShowErrorsModal(true)}>
+              Learn more
+            </LearnMoreButton>
+          </Banner>
+        </ValidationErrorBannerWrapper>
+      ) : null}
+      <CreatePreviewDeploymentWrapper>
+        <SubmitButton
+          onClick={handleCreatePreviewDeployment}
+          disabled={loading || !selectedBranch || porterYAMLErrors.length > 0}
+        >
+          Create preview deployment
+        </SubmitButton>
+        {selectedBranch && porterYAMLErrors.length ? (
+          <RevalidatePorterYAMLSpanWrapper>
+            Please fix your porter.yaml file to continue.{" "}
+            <RevalidateSpan
+              onClick={(e) => {
+                e.preventDefault();
+                e.stopPropagation();
+
+                if (!selectedBranch) {
+                  return;
+                }
+
+                handleRowItemClick(selectedBranch);
+              }}
+            >
+              Refresh
+            </RevalidateSpan>
+          </RevalidatePorterYAMLSpanWrapper>
+        ) : null}
+      </CreatePreviewDeploymentWrapper>
+    </>
+  );
+};
+
+export default CreateBranchEnvironment;
+
+const PullRequestRow = styled.div<{ isLast?: boolean; isSelected?: boolean }>`
+  width: 100%;
+  padding: 15px;
+  cursor: pointer;
+  background: ${(props) => (props.isSelected ? "#ffffff11" : "#26292e")};
+  border-bottom: ${(props) => (props.isLast ? "" : "1px solid #494b4f")};
+  :hover {
+    background: #ffffff11;
+  }
+`;
+
+const Code = styled.span`
+  font-family: monospace; ;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+const DeploymentImageContainer = styled.div`
+  height: 20px;
+  font-size: 13px;
+  position: relative;
+  display: flex;
+  align-items: center;
+  font-weight: 400;
+  justify-content: center;
+  color: #ffffff66;
+  padding-left: 10px;
+`;
+
+const LastDeployed = styled.div`
+  font-size: 13px;
+  margin-top: -1px;
+  margin-left: 10px;
+  display: flex;
+  align-items: center;
+  color: #aaaabb66;
+`;
+
+const MergeInfoWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  margin-right: 8px;
+  position: relative;
+  margin-left: 10px;
+`;
+
+const MergeInfo = styled.div`
+  font-size: 13px;
+  align-items: center;
+  color: #aaaabb66;
+  white-space: nowrap;
+  display: flex;
+  align-items: center;
+  text-overflow: ellipsis;
+  overflow: hidden;
+  max-width: 300px;
+
+  > i {
+    font-size: 16px;
+    margin: 0 2px;
+  }
+`;
+
+const PRIcon = styled.img`
+  font-size: 20px;
+  height: 16px;
+  margin-right: 10px;
+  color: #aaaabb;
+  opacity: 50%;
+`;
+
+const PRName = styled.div`
+  font-family: "Work Sans", sans-serif;
+  font-weight: 500;
+  color: #ffffff;
+  display: flex;
+  font-size: 14px;
+  align-items: center;
+  margin-bottom: 10px;
+`;
+
+const SubmitButton = styled.div`
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  font-size: 13px;
+  cursor: pointer;
+  font-family: "Work Sans", sans-serif;
+  border-radius: 5px;
+  font-weight: 500;
+  color: white;
+  height: 30px;
+  padding: 0 8px;
+  width: 200px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  cursor: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
+
+  background: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "#aaaabbee" : "#616FEEcc"};
+  :hover {
+    background: ${(props: { disabled?: boolean }) =>
+      props.disabled ? "" : "#505edddd"};
+  }
+
+  > i {
+    color: white;
+    width: 18px;
+    height: 18px;
+    font-weight: 600;
+    font-size: 12px;
+    border-radius: 20px;
+    display: flex;
+    align-items: center;
+    margin-right: 5px;
+    justify-content: center;
+  }
+`;
+
+const Br = styled.div<{ height: string }>`
+  width: 100%;
+  height: ${(props) => props.height || "2px"};
+`;
+
+const ValidationErrorBannerWrapper = styled.div`
+  margin-block: 20px;
+`;
+
+const LearnMoreButton = styled.div`
+  text-decoration: underline;
+  fontweight: bold;
+  cursor: pointer;
+`;
+
+const CreatePreviewDeploymentWrapper = styled.div`
+  margin-top: 30px;
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 10px;
+`;
+
+const RevalidatePorterYAMLSpanWrapper = styled.div`
+  font-size: 13px;
+  color: #aaaabb;
+`;
+
+const RevalidateSpan = styled.span`
+  color: #aaaabb;
+  text-decoration: underline;
+  cursor: pointer;
+`;

+ 22 - 368
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreateEnvironment.tsx

@@ -1,31 +1,21 @@
 import DynamicLink from "components/DynamicLink";
-import Loading from "components/Loading";
-import React, { useContext, useEffect, useState } from "react";
+import React, { useState } from "react";
 import styled from "styled-components";
-import { Context } from "shared/Context";
 import { useParams } from "react-router";
-import { PullRequest } from "../types";
 import DashboardHeader from "../../DashboardHeader";
 import PullRequestIcon from "assets/pull_request_icon.svg";
-import Helper from "components/form-components/Helper";
-import pr_icon from "assets/pull_request_icon.svg";
-import api from "shared/api";
-import { EllipsisTextWrapper, RepoLink } from "../components/styled";
-import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { getPRDeploymentList, validatePorterYAML } from "../utils";
-import Banner from "components/Banner";
-import Modal from "main/home/modals/Modal";
-import { useRouting } from "shared/routing";
-import PorterYAMLErrorsModal from "../components/PorterYAMLErrorsModal";
-import { PlaceHolder } from "brace";
-import Placeholder from "components/Placeholder";
+import CreatePREnvironment from "./CreatePREnvironment";
+import TabSelector from "components/TabSelector";
+import CreateBranchEnvironment from "./CreateBranchEnvironment";
+
+const TAB_OPTIONS = [
+  { label: "Pull Requests", value: "pull_requests" },
+  { label: "Branches", value: "branches" },
+];
 
 const CreateEnvironment: React.FC = () => {
-  const router = useRouting();
-  const queryClient = useQueryClient();
-  const [showErrorsModal, setShowErrorsModal] = useState<boolean>(false);
-  const { currentProject, currentCluster, setCurrentError } = useContext(
-    Context
+  const [currentTab, setCurrentTab] = useState<typeof TAB_OPTIONS[0]>(
+    TAB_OPTIONS[0]
   );
   const { environment_id, repo_name, repo_owner } = useParams<{
     environment_id: string;
@@ -33,158 +23,8 @@ const CreateEnvironment: React.FC = () => {
     repo_owner: string;
   }>();
 
-  const { isLoading: getPullRequestsLoading, data: pullRequests } = useQuery<
-    PullRequest[]
-  >(
-    ["pullRequests", currentProject.id, currentCluster.id, environment_id],
-    async () => {
-      try {
-        const res = await getPRDeploymentList({
-          projectID: currentProject.id,
-          clusterID: currentCluster.id,
-          environmentID: Number(environment_id),
-        });
-
-        return res.data.pull_requests || [];
-      } catch (err) {
-        setCurrentError(err);
-      }
-    }
-  );
-
-  const [selectedPR, setSelectedPR] = useState<PullRequest>();
-  const [loading, setLoading] = useState(false);
-  const [porterYAMLErrors, setPorterYAMLErrors] = useState<string[]>([]);
-
   const selectedRepo = `${repo_owner}/${repo_name}`;
 
-  const handlePRRowItemClick = async (pullRequest: PullRequest) => {
-    setSelectedPR(pullRequest);
-    setLoading(true);
-
-    const res = await validatePorterYAML({
-      projectID: currentProject.id,
-      clusterID: currentCluster.id,
-      environmentID: Number(environment_id),
-      branch: pullRequest.branch_from,
-    });
-
-    setPorterYAMLErrors(res.data.errors ?? []);
-
-    setLoading(false);
-  };
-
-  const handleCreatePreviewDeployment = async () => {
-    try {
-      await api.createPreviewEnvironmentDeployment("<token>", selectedPR, {
-        cluster_id: currentCluster?.id,
-        project_id: currentProject?.id,
-      });
-
-      router.push(
-        `/preview-environments/deployments/${environment_id}/${selectedPR.repo_owner}/${selectedPR.repo_name}?status_filter=all`
-      );
-    } catch (err) {
-      setCurrentError(err);
-    }
-  };
-
-  const renderPullRequestList = () => {
-    return (
-      <>
-        <Helper>
-          Select an open pull request to preview. Pull requests must contain a{" "}
-          <Code>porter.yaml</Code> file.
-        </Helper>
-        <Br height="10px" />
-        <PullRequestList>
-          {(pullRequests ?? []).map((pullRequest: PullRequest, i: number) => {
-            return (
-              <PullRequestRow
-                onClick={() => {
-                  handlePRRowItemClick(pullRequest);
-                }}
-                isLast={i === pullRequests.length - 1}
-                isSelected={pullRequest === selectedPR}
-              >
-                <PRName>
-                  <PRIcon src={pr_icon} alt="pull request icon" />
-                  <EllipsisTextWrapper tooltipText={pullRequest.pr_title}>
-                    {pullRequest.pr_title}
-                  </EllipsisTextWrapper>
-                </PRName>
-
-                <Flex>
-                  <DeploymentImageContainer>
-                    {/* <InfoWrapper>
-                    <LastDeployed>
-                      #{pullRequest.pr_number} last updated xyz
-                    </LastDeployed>
-                  </InfoWrapper>
-                  <SepDot>•</SepDot> */}
-                    <MergeInfoWrapper>
-                      <MergeInfo>
-                        {pullRequest.branch_from}
-                        <i className="material-icons">arrow_forward</i>
-                        {pullRequest.branch_into}
-                      </MergeInfo>
-                    </MergeInfoWrapper>
-                  </DeploymentImageContainer>
-                </Flex>
-              </PullRequestRow>
-            );
-          })}
-        </PullRequestList>
-        {showErrorsModal && selectedPR ? (
-          <PorterYAMLErrorsModal
-            errors={porterYAMLErrors}
-            onClose={() => setShowErrorsModal(false)}
-            repo={selectedPR.repo_owner + "/" + selectedPR.repo_name}
-            branch={selectedPR.branch_from}
-          />
-        ) : null}
-        {selectedPR && porterYAMLErrors.length ? (
-          <ValidationErrorBannerWrapper>
-            <Banner type="warning">
-              We found some errors in the porter.yaml file in the&nbsp;
-              {selectedPR.branch_from}&nbsp;branch. &nbsp;
-              <LearnMoreButton onClick={() => setShowErrorsModal(true)}>
-                Learn more
-              </LearnMoreButton>
-            </Banner>
-          </ValidationErrorBannerWrapper>
-        ) : null}
-        <CreatePreviewDeploymentWrapper>
-          <SubmitButton
-            onClick={handleCreatePreviewDeployment}
-            disabled={loading || !selectedPR || porterYAMLErrors.length > 0}
-          >
-            Create preview deployment
-          </SubmitButton>
-          {selectedPR && porterYAMLErrors.length ? (
-            <RevalidatePorterYAMLSpanWrapper>
-              Please fix your porter.yaml file to continue.{" "}
-              <RevalidateSpan
-                onClick={(e) => {
-                  e.preventDefault();
-                  e.stopPropagation();
-
-                  if (!selectedPR) {
-                    return;
-                  }
-
-                  handlePRRowItemClick(selectedPR);
-                }}
-              >
-                Refresh
-              </RevalidateSpan>
-            </RevalidatePorterYAMLSpanWrapper>
-          ) : null}
-        </CreatePreviewDeploymentWrapper>
-      </>
-    );
-  };
-
   return (
     <>
       <BreadcrumbRow>
@@ -206,15 +46,18 @@ const CreateEnvironment: React.FC = () => {
         capitalize={false}
       />
       <DarkMatter />
-      {pullRequests?.length ? (
-        renderPullRequestList()
+      {/* <TabSelector
+        options={TAB_OPTIONS}
+        currentTab={currentTab.value}
+        setCurrentTab={(value: string) =>
+          setCurrentTab(TAB_OPTIONS.find((tab) => tab.value === value))
+        }
+      /> */}
+
+      {currentTab.value === "pull_requests" ? (
+        <CreatePREnvironment environmentID={environment_id} />
       ) : (
-        <>
-          <Br height="30px" />
-          <Placeholder height="370px">
-            You do not have any pull requests.
-          </Placeholder>
-        </>
+        <CreateBranchEnvironment environmentID={environment_id} />
       )}
     </>
   );
@@ -222,161 +65,11 @@ const CreateEnvironment: React.FC = () => {
 
 export default CreateEnvironment;
 
-const PullRequestList = styled.div`
-  border: 1px solid #494b4f;
-  border-radius: 5px;
-  overflow: hidden;
-`;
-
-const PullRequestRow = styled.div<{ isLast?: boolean; isSelected?: boolean }>`
-  width: 100%;
-  padding: 15px;
-  cursor: pointer;
-  background: ${(props) => (props.isSelected ? "#ffffff11" : "#26292e")};
-  border-bottom: ${(props) => (props.isLast ? "" : "1px solid #494b4f")};
-  :hover {
-    background: #ffffff11;
-  }
-`;
-
-const Code = styled.span`
-  font-family: monospace; ;
-`;
-
-const SepDot = styled.div`
-  color: #aaaabb66;
-`;
-
-const Flex = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
-const DeploymentImageContainer = styled.div`
-  height: 20px;
-  font-size: 13px;
-  position: relative;
-  display: flex;
-  align-items: center;
-  font-weight: 400;
-  justify-content: center;
-  color: #ffffff66;
-  padding-left: 10px;
-`;
-
-const InfoWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  margin-right: 8px;
-  margin-left: 7px;
-`;
-
-const LastDeployed = styled.div`
-  font-size: 13px;
-  margin-top: -1px;
-  margin-left: 10px;
-  display: flex;
-  align-items: center;
-  color: #aaaabb66;
-`;
-
-const MergeInfoWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  margin-right: 8px;
-  position: relative;
-  margin-left: 10px;
-`;
-
-const MergeInfo = styled.div`
-  font-size: 13px;
-  align-items: center;
-  color: #aaaabb66;
-  white-space: nowrap;
-  display: flex;
-  align-items: center;
-  text-overflow: ellipsis;
-  overflow: hidden;
-  max-width: 300px;
-
-  > i {
-    font-size: 16px;
-    margin: 0 2px;
-  }
-`;
-
-const PRIcon = styled.img`
-  font-size: 20px;
-  height: 16px;
-  margin-right: 10px;
-  color: #aaaabb;
-  opacity: 50%;
-`;
-
-const PRName = styled.div`
-  font-family: "Work Sans", sans-serif;
-  font-weight: 500;
-  color: #ffffff;
-  display: flex;
-  font-size: 14px;
-  align-items: center;
-  margin-bottom: 10px;
-`;
-
-const SubmitButton = styled.div`
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: center;
-  font-size: 13px;
-  cursor: pointer;
-  font-family: "Work Sans", sans-serif;
-  border-radius: 5px;
-  font-weight: 500;
-  color: white;
-  height: 30px;
-  padding: 0 8px;
-  width: 200px;
-  overflow: hidden;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  cursor: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "not-allowed" : "pointer"};
-
-  background: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "#aaaabbee" : "#616FEEcc"};
-  :hover {
-    background: ${(props: { disabled?: boolean }) =>
-      props.disabled ? "" : "#505edddd"};
-  }
-
-  > i {
-    color: white;
-    width: 18px;
-    height: 18px;
-    font-weight: 600;
-    font-size: 12px;
-    border-radius: 20px;
-    display: flex;
-    align-items: center;
-    margin-right: 5px;
-    justify-content: center;
-  }
-`;
-
 const DarkMatter = styled.div`
   width: 100%;
   margin-top: -15px;
 `;
 
-const Br = styled.div<{ height: string }>`
-  width: 100%;
-  height: ${(props) => props.height || "2px"};
-`;
-
 const Slash = styled.div`
   margin: 0 4px;
   color: #aaaabb88;
@@ -420,42 +113,3 @@ const Breadcrumb = styled(DynamicLink)`
     background: #ffffff11;
   }
 `;
-
-const ValidationErrorBannerWrapper = styled.div`
-  margin-block: 20px;
-`;
-
-const LearnMoreButton = styled.div`
-  text-decoration: underline;
-  fontweight: bold;
-  cursor: pointer;
-`;
-
-const Message = styled.div`
-  padding: 20px;
-  background: #26292e;
-  border-radius: 5px;
-  line-height: 1.5em;
-  border: 1px solid #aaaabb33;
-  font-size: 13px;
-  margin-top: 40px;
-`;
-
-const CreatePreviewDeploymentWrapper = styled.div`
-  margin-top: 30px;
-  display: flex;
-  align-items: center;
-  flex-wrap: wrap;
-  gap: 10px;
-`;
-
-const RevalidatePorterYAMLSpanWrapper = styled.div`
-  font-size: 13px;
-  color: #aaaabb;
-`;
-
-const RevalidateSpan = styled.span`
-  color: #aaaabb;
-  text-decoration: underline;
-  cursor: pointer;
-`;

+ 535 - 0
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreatePREnvironment.tsx

@@ -0,0 +1,535 @@
+import React, { useContext, useMemo, useState } from "react";
+import styled from "styled-components";
+import { Context } from "shared/Context";
+import { PullRequest } from "../types";
+import Helper from "components/form-components/Helper";
+import pr_icon from "assets/pull_request_icon.svg";
+import api from "shared/api";
+import { EllipsisTextWrapper } from "../components/styled";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { getPRDeploymentList, validatePorterYAML } from "../utils";
+import Banner from "components/Banner";
+import { useRouting } from "shared/routing";
+import PorterYAMLErrorsModal from "../components/PorterYAMLErrorsModal";
+import Placeholder from "components/Placeholder";
+import RadioFilter from "components/RadioFilter";
+
+import sort from "assets/sort.svg";
+import { search } from "shared/search";
+import _ from "lodash";
+import { readableDate } from "shared/string_utils";
+import dayjs from "dayjs";
+import Loading from "components/Loading";
+
+interface Props {
+  environmentID: string;
+}
+
+const CreatePREnvironment = ({ environmentID }: Props) => {
+  const queryClient = useQueryClient();
+  const router = useRouting();
+  const [searchValue, setSearchValue] = useState("");
+  const [sortOrder, setSortOrder] = useState("Newest");
+  const [showErrorsModal, setShowErrorsModal] = useState<boolean>(false);
+  const { currentProject, currentCluster, setCurrentError } = useContext(
+    Context
+  );
+
+  // Get all PRs for the current environment
+  const { isLoading: pullRequestsLoading, data: pullRequests } = useQuery<
+    PullRequest[]
+  >(
+    ["pullRequests", currentProject.id, currentCluster.id, environmentID],
+    async () => {
+      try {
+        const res = await getPRDeploymentList({
+          projectID: currentProject.id,
+          clusterID: currentCluster.id,
+          environmentID: Number(environmentID),
+        });
+
+        return res.data.pull_requests || [];
+      } catch (err) {
+        setCurrentError(err);
+      }
+    }
+  );
+
+  const [selectedPR, setSelectedPR] = useState<PullRequest>();
+  const [loading, setLoading] = useState(false);
+  const [porterYAMLErrors, setPorterYAMLErrors] = useState<string[]>([]);
+
+  const handleRefresh = () => {
+    queryClient.invalidateQueries({
+      queryKey: ["pullRequests"],
+    });
+  };
+
+  const handlePRRowItemClick = async (pullRequest: PullRequest) => {
+    setSelectedPR(pullRequest);
+    setLoading(true);
+
+    const res = await validatePorterYAML({
+      projectID: currentProject.id,
+      clusterID: currentCluster.id,
+      environmentID: Number(environmentID),
+      branch: pullRequest.branch_from,
+    });
+
+    setPorterYAMLErrors(res.data.errors ?? []);
+
+    setLoading(false);
+  };
+
+  const handleCreatePreviewDeployment = async () => {
+    try {
+      await api.createPreviewEnvironmentDeployment("<token>", selectedPR, {
+        cluster_id: currentCluster?.id,
+        project_id: currentProject?.id,
+      });
+
+      router.push(
+        `/preview-environments/deployments/${environmentID}/${selectedPR.repo_owner}/${selectedPR.repo_name}?status_filter=all`
+      );
+    } catch (err) {
+      setCurrentError(err);
+    }
+  };
+
+  const filteredPullRequests = useMemo(() => {
+    const filteredBySearch = search<PullRequest>(
+      pullRequests ?? [],
+      searchValue,
+      {
+        isCaseSensitive: false,
+        keys: ["pr_title", "branch_from", "branch_into"],
+      }
+    );
+
+    switch (sortOrder) {
+      case "Recently Updated":
+        return _.sortBy(filteredBySearch, "updated_at").reverse();
+      case "Newest":
+        return _.sortBy(filteredBySearch, "created_at").reverse();
+      case "Oldest":
+        return _.sortBy(filteredBySearch, "created_at");
+      case "Alphabetical":
+      default:
+        return _.sortBy(filteredBySearch, "gh_pr_name");
+    }
+  }, [pullRequests, searchValue, sortOrder]);
+
+  if (pullRequestsLoading) {
+    return (
+      <>
+        <Br height="30px" />
+        <Placeholder minHeight="50vh">
+          <Loading />
+        </Placeholder>
+      </>
+    );
+  }
+
+  if (!pullRequests.length) {
+    return (
+      <>
+        <Br height="30px" />
+        <Placeholder height="50vh">{`You do not have any pull requests.`}</Placeholder>
+      </>
+    );
+  }
+
+  return (
+    <>
+      <Helper>
+        Select an open pull request to preview. Pull requests must contain a{" "}
+        <Code>porter.yaml</Code> file.
+      </Helper>
+      <FlexRow>
+        <Flex>
+          <SearchRowWrapper>
+            <SearchBarWrapper>
+              <i className="material-icons">search</i>
+              <SearchInput
+                value={searchValue}
+                onChange={(e: any) => {
+                  setSelectedPR(undefined);
+                  setPorterYAMLErrors([])
+                  setSearchValue(e.target.value);
+                }}
+                placeholder="Search"
+              />
+            </SearchBarWrapper>
+          </SearchRowWrapper>
+        </Flex>
+        <Flex>
+          <RefreshButton color={"#7d7d81"} onClick={handleRefresh}>
+            <i className="material-icons">refresh</i>
+          </RefreshButton>
+          <RadioFilter
+            icon={sort}
+            selected={sortOrder}
+            setSelected={setSortOrder}
+            options={[
+              { label: "Recently Updated", value: "Recently Updated" },
+              { label: "Newest", value: "Newest" },
+              { label: "Oldest", value: "Oldest" },
+              { label: "Alphabetical", value: "Alphabetical" },
+            ]}
+            name="Sort"
+          />
+        </Flex>
+      </FlexRow>
+      <Br height="10px" />
+      {filteredPullRequests?.length ? (
+        <PullRequestList>
+          {(filteredPullRequests ?? []).map(
+            (pullRequest: PullRequest, i: number) => {
+              return (
+                <PullRequestRow
+                  onClick={() => {
+                    handlePRRowItemClick(pullRequest);
+                  }}
+                  isLast={i === filteredPullRequests.length - 1}
+                  isSelected={pullRequest === selectedPR}
+                >
+                  <PRName>
+                    <PRIcon src={pr_icon} alt="pull request icon" />
+                    <EllipsisTextWrapper tooltipText={pullRequest.pr_title}>
+                      {pullRequest.pr_title}
+                    </EllipsisTextWrapper>
+                  </PRName>
+
+                  <Flex>
+                    <DeploymentImageContainer>
+                      {/* <InfoWrapper>
+                  <LastDeployed>
+                    #{pullRequest.pr_number} last updated xyz
+                  </LastDeployed>
+                </InfoWrapper>
+                <SepDot>•</SepDot> */}
+                      <MergeInfoWrapper>
+                        <MergeInfo>
+                          {pullRequest.branch_from}
+                          <i className="material-icons">arrow_forward</i>
+                          {pullRequest.branch_into}
+                        </MergeInfo>
+                        <SepDot>•</SepDot>
+                        <LastDeployed>
+                          Last updated{" "}
+                          {dayjs(pullRequest.updated_at).format(
+                            "hh:mma on MM/DD/YYYY"
+                          )}
+                        </LastDeployed>
+                      </MergeInfoWrapper>
+                    </DeploymentImageContainer>
+                  </Flex>
+                </PullRequestRow>
+              );
+            }
+          )}
+        </PullRequestList>
+      ) : (
+        <>
+          <Br height="30px" />
+          <Placeholder height="50vh">{`No pull requests match your search query.`}</Placeholder>
+        </>
+      )}
+      {showErrorsModal && selectedPR ? (
+        <PorterYAMLErrorsModal
+          errors={porterYAMLErrors}
+          onClose={() => setShowErrorsModal(false)}
+          repo={selectedPR.repo_owner + "/" + selectedPR.repo_name}
+          branch={selectedPR.branch_from}
+        />
+      ) : null}
+      {selectedPR && porterYAMLErrors.length ? (
+        <ValidationErrorBannerWrapper>
+          <Banner type="warning">
+            We found some errors in the porter.yaml file in the&nbsp;
+            {selectedPR.branch_from}&nbsp;branch. &nbsp;
+            <LearnMoreButton onClick={() => setShowErrorsModal(true)}>
+              Learn more
+            </LearnMoreButton>
+          </Banner>
+        </ValidationErrorBannerWrapper>
+      ) : null}
+      <CreatePreviewDeploymentWrapper>
+        <SubmitButton
+          onClick={handleCreatePreviewDeployment}
+          disabled={loading || !selectedPR || porterYAMLErrors.length > 0}
+        >
+          Create preview deployment
+        </SubmitButton>
+        {selectedPR && porterYAMLErrors.length ? (
+          <RevalidatePorterYAMLSpanWrapper>
+            Please fix your porter.yaml file to continue.{" "}
+            <RevalidateSpan
+              onClick={(e) => {
+                e.preventDefault();
+                e.stopPropagation();
+
+                if (!selectedPR) {
+                  return;
+                }
+
+                handlePRRowItemClick(selectedPR);
+              }}
+            >
+              Refresh
+            </RevalidateSpan>
+          </RevalidatePorterYAMLSpanWrapper>
+        ) : null}
+      </CreatePreviewDeploymentWrapper>
+    </>
+  );
+};
+
+export default CreatePREnvironment;
+
+const PullRequestList = styled.div`
+  border: 1px solid #494b4f;
+  border-radius: 5px;
+  overflow: hidden;
+  margin-top: 33px;
+`;
+
+const PullRequestRow = styled.div<{ isLast?: boolean; isSelected?: boolean }>`
+  width: 100%;
+  padding: 15px;
+  cursor: pointer;
+  background: ${(props) => (props.isSelected ? "#ffffff11" : "#26292e")};
+  border-bottom: ${(props) => (props.isLast ? "" : "1px solid #494b4f")};
+  :hover {
+    background: #ffffff11;
+  }
+`;
+
+const InfoWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  margin-right: 8px;
+`;
+
+const Code = styled.span`
+  font-family: monospace; ;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+const DeploymentImageContainer = styled.div`
+  height: 20px;
+  font-size: 13px;
+  position: relative;
+  display: flex;
+  align-items: center;
+  font-weight: 400;
+  justify-content: center;
+  color: #ffffff66;
+  padding-left: 10px;
+`;
+
+const LastDeployed = styled.div`
+  font-size: 13px;
+  margin-top: -1px;
+  display: flex;
+  align-items: center;
+  color: #aaaabb66;
+`;
+
+const MergeInfoWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  margin-right: 8px;
+  position: relative;
+  margin-left: 10px;
+  gap: 8px;
+`;
+
+const MergeInfo = styled.div`
+  font-size: 13px;
+  align-items: center;
+  color: #aaaabb66;
+  white-space: nowrap;
+  display: flex;
+  align-items: center;
+  text-overflow: ellipsis;
+  overflow: hidden;
+  max-width: 300px;
+
+  > i {
+    font-size: 16px;
+    margin: 0 2px;
+  }
+`;
+
+const SepDot = styled.div`
+  color: #aaaabb66;
+`;
+
+const PRIcon = styled.img`
+  font-size: 20px;
+  height: 16px;
+  margin-right: 10px;
+  color: #aaaabb;
+  opacity: 50%;
+`;
+
+const PRName = styled.div`
+  font-family: "Work Sans", sans-serif;
+  font-weight: 500;
+  color: #ffffff;
+  display: flex;
+  font-size: 14px;
+  align-items: center;
+  margin-bottom: 10px;
+`;
+
+const SubmitButton = styled.div`
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  font-size: 13px;
+  cursor: pointer;
+  font-family: "Work Sans", sans-serif;
+  border-radius: 5px;
+  font-weight: 500;
+  color: white;
+  height: 30px;
+  padding: 0 8px;
+  width: 200px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  cursor: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
+
+  background: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "#aaaabbee" : "#616FEEcc"};
+  :hover {
+    background: ${(props: { disabled?: boolean }) =>
+      props.disabled ? "" : "#505edddd"};
+  }
+
+  > i {
+    color: white;
+    width: 18px;
+    height: 18px;
+    font-weight: 600;
+    font-size: 12px;
+    border-radius: 20px;
+    display: flex;
+    align-items: center;
+    margin-right: 5px;
+    justify-content: center;
+  }
+`;
+
+const Br = styled.div<{ height: string }>`
+  width: 100%;
+  height: ${(props) => props.height || "2px"};
+`;
+
+const ValidationErrorBannerWrapper = styled.div`
+  margin-block: 20px;
+`;
+
+const LearnMoreButton = styled.div`
+  text-decoration: underline;
+  fontweight: bold;
+  cursor: pointer;
+`;
+
+const CreatePreviewDeploymentWrapper = styled.div`
+  margin-top: 30px;
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 10px;
+`;
+
+const RevalidatePorterYAMLSpanWrapper = styled.div`
+  font-size: 13px;
+  color: #aaaabb;
+`;
+
+const RevalidateSpan = styled.span`
+  color: #aaaabb;
+  text-decoration: underline;
+  cursor: pointer;
+`;
+
+const SearchInput = styled.input`
+  outline: none;
+  border: none;
+  font-size: 13px;
+  background: none;
+  width: 100%;
+  color: white;
+  height: 100%;
+`;
+
+const SearchRow = styled.div`
+  display: flex;
+  align-items: center;
+  height: 30px;
+  margin-right: 10px;
+  background: #26292e;
+  border-radius: 5px;
+  border: 1px solid #aaaabb33;
+`;
+
+const SearchRowWrapper = styled(SearchRow)`
+  border-radius: 5px;
+  width: 250px;
+`;
+
+const SearchBarWrapper = styled.div`
+  display: flex;
+  flex: 1;
+
+  > i {
+    color: #aaaabb;
+    padding-top: 1px;
+    margin-left: 8px;
+    font-size: 16px;
+    margin-right: 8px;
+  }
+`;
+
+const FlexRow = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  flex-wrap: wrap;
+  gap: 10px;
+`;
+
+const RefreshButton = styled.button`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: ${(props: { color: string }) => props.color};
+  cursor: pointer;
+  border: none;
+  width: 30px;
+  height: 30px;
+  margin-right: 15px;
+  background: none;
+  border-radius: 50%;
+  margin-left: 10px;
+  > i {
+    font-size: 20px;
+  }
+  :hover {
+    background-color: rgb(97 98 102 / 44%);
+    color: white;
+  }
+`;

+ 2 - 0
dashboard/src/main/home/cluster-dashboard/preview-environments/types.ts

@@ -55,4 +55,6 @@ export type PullRequest = {
   repo_name: string;
   branch_from: string;
   branch_into: string;
+  created_at: string;
+  updated_at: string;
 };

+ 1 - 6
dashboard/src/main/home/navbar/Help.tsx

@@ -29,12 +29,7 @@ export default class Help extends Component<PropsType, StateType> {
           <Dropdown dropdownWidth="155px" dropdownMaxHeight="300px">
             <Option
               onClick={() => {
-                window
-                  .open(
-                    "https://porter-docs-demo-22fd462fef4dcd45.onporter.run",
-                    "_blank"
-                  )
-                  .focus();
+                window.open("https://docs.porter.run", "_blank").focus();
               }}
             >
               <i className="material-icons-outlined">book</i>

+ 95 - 96
go.mod

@@ -3,11 +3,11 @@ module github.com/porter-dev/porter
 go 1.18
 
 require (
-	cloud.google.com/go v0.102.0
+	cloud.google.com/go v0.104.0 // indirect
 	github.com/AlecAivazis/survey/v2 v2.2.9
 	github.com/Masterminds/semver/v3 v3.1.1
-	github.com/aws/aws-sdk-go v1.43.28
-	github.com/bradleyfalzon/ghinstallation/v2 v2.0.3
+	github.com/aws/aws-sdk-go v1.44.143
+	github.com/bradleyfalzon/ghinstallation/v2 v2.0.4
 	github.com/buildpacks/pack v0.27.0
 	github.com/cli/cli v1.11.0
 	github.com/dgrijalva/jwt-go v3.2.0+incompatible
@@ -25,7 +25,7 @@ require (
 	github.com/go-test/deep v1.0.7
 	github.com/golang-jwt/jwt/v4 v4.4.1 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
-	github.com/google/go-github/v39 v39.2.0 // indirect
+	github.com/google/go-github/v39 v39.2.0
 	github.com/google/go-github/v41 v41.0.0
 	github.com/gorilla/schema v1.2.0
 	github.com/gorilla/securecookie v1.1.1
@@ -33,8 +33,8 @@ require (
 	github.com/gorilla/websocket v1.4.2
 	github.com/itchyny/gojq v0.12.1
 	github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd
-	github.com/kris-nova/logger v0.0.0-20181127235838-fd0d87064b06
-	github.com/mitchellh/mapstructure v1.4.3
+	github.com/kris-nova/logger v0.2.1
+	github.com/mitchellh/mapstructure v1.5.0
 	github.com/moby/moby v20.10.6+incompatible
 	github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
 	github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198
@@ -42,69 +42,81 @@ require (
 	github.com/porter-dev/switchboard v0.0.0-20221019155755-67ff2bf04935
 	github.com/rs/zerolog v1.26.0
 	github.com/sendgrid/sendgrid-go v3.8.0+incompatible
-	github.com/spf13/cobra v1.5.0
+	github.com/spf13/cobra v1.6.1
 	github.com/spf13/pflag v1.0.5
-	github.com/spf13/viper v1.10.0
-	github.com/stretchr/testify v1.8.0
-	golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
-	golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e
-	golang.org/x/oauth2 v0.0.0-20220628200809-02e64fa58f26
-	google.golang.org/api v0.88.0
-	google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03
+	github.com/spf13/viper v1.13.0
+	github.com/stretchr/testify v1.8.1
+	golang.org/x/crypto v0.3.0
+	golang.org/x/net v0.2.0
+	golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1
+	google.golang.org/api v0.97.0
+	google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce
 	google.golang.org/grpc v1.49.0
 	google.golang.org/protobuf v1.28.1
 	gorm.io/driver/sqlite v1.1.3
 	gorm.io/gorm v1.22.3
-	helm.sh/helm/v3 v3.9.0
-	k8s.io/api v0.24.2
-	k8s.io/apimachinery v0.24.2
-	k8s.io/cli-runtime v0.24.2
-	k8s.io/client-go v0.24.2
+	helm.sh/helm/v3 v3.10.2
+	k8s.io/api v0.25.2
+	k8s.io/apimachinery v0.25.2
+	k8s.io/cli-runtime v0.25.2
+	k8s.io/client-go v0.25.2
 	k8s.io/helm v2.17.0+incompatible
-	k8s.io/kubectl v0.24.1
-	sigs.k8s.io/aws-iam-authenticator v0.5.8
+	k8s.io/kubectl v0.25.2
 	sigs.k8s.io/yaml v1.3.0
 )
 
 require (
+	cloud.google.com/go/artifactregistry v1.6.0
+	cloud.google.com/go/iam v0.5.0
+	github.com/Azure/azure-sdk-for-go/sdk/azcore v0.23.1
+	github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v0.5.0
+	github.com/aws/aws-sdk-go-v2 v1.17.2
+	github.com/aws/aws-sdk-go-v2/credentials v1.13.4
+	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.43
+	github.com/aws/aws-sdk-go-v2/service/ec2 v1.72.0
+	github.com/aws/aws-sdk-go-v2/service/ecr v1.17.5
+	github.com/aws/aws-sdk-go-v2/service/eks v1.25.0
+	github.com/aws/aws-sdk-go-v2/service/s3 v1.29.5
+	github.com/aws/aws-sdk-go-v2/service/sts v1.17.6
+	github.com/aws/smithy-go v1.13.5
 	github.com/briandowns/spinner v1.18.1
+	github.com/open-policy-agent/opa v0.44.0
+	github.com/santhosh-tekuri/jsonschema/v5 v5.0.1
+	github.com/xanzy/go-gitlab v0.68.0
+	go.uber.org/goleak v1.1.12
 	gopkg.in/segmentio/analytics-go.v3 v3.1.0
 	gopkg.in/yaml.v2 v2.4.0
 	gorm.io/driver/postgres v1.2.3
+	istio.io/client-go v1.16.0
 )
 
 require (
-	cloud.google.com/go/artifactregistry v1.3.0 // indirect
-	cloud.google.com/go/compute v1.7.0 // indirect
-	cloud.google.com/go/iam v0.3.0 // indirect
-	github.com/Azure/azure-sdk-for-go v65.0.0+incompatible // indirect
-	github.com/Azure/azure-sdk-for-go/sdk/azcore v0.23.1 // indirect
+	cloud.google.com/go/compute v1.10.0 // indirect
+	github.com/Azure/azure-sdk-for-go v66.0.0+incompatible // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1 // indirect
-	github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v0.5.0 // indirect
 	github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect
 	github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
 	github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 // indirect
 	github.com/OneOfOne/xxhash v1.2.8 // indirect
-	github.com/PuerkitoBio/goquery v1.5.1 // indirect
 	github.com/agnivade/levenshtein v1.1.1 // indirect
-	github.com/andybalholm/cascadia v1.1.0 // indirect
-	github.com/aws/aws-sdk-go-v2 v1.16.4 // indirect
-	github.com/aws/aws-sdk-go-v2/config v1.15.9 // indirect
-	github.com/aws/aws-sdk-go-v2/credentials v1.12.4 // indirect
-	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.5 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.11 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.5 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/ini v1.3.12 // indirect
-	github.com/aws/aws-sdk-go-v2/service/ecr v1.17.5 // indirect
+	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
+	github.com/aws/aws-sdk-go-v2/config v1.18.4 // indirect
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.20 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.26 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.20 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/ini v1.3.27 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.17 // indirect
 	github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.13.5 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.5 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sso v1.11.7 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sts v1.16.6 // indirect
-	github.com/aws/smithy-go v1.11.2 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.21 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.20 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.20 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.11.26 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.9 // indirect
 	github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220517224237-e6f29200ae04 // indirect
 	github.com/chrismellard/docker-credential-acr-env v0.0.0-20220327082430-c57b701bfc08 // indirect
-	github.com/cosmtrek/air v1.30.0 // indirect
 	github.com/dimchansky/utfbom v1.1.1 // indirect
+	github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect
 	github.com/emicklei/go-restful/v3 v3.8.0 // indirect
 	github.com/go-gorp/gorp/v3 v3.0.2 // indirect
 	github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
@@ -112,32 +124,32 @@ require (
 	github.com/googleapis/enterprise-certificate-proxy v0.1.0 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
+	github.com/kris-nova/novaarchive v0.0.0-20210219195539-c7c1cabb2577 // indirect
 	github.com/kylelemons/godebug v1.1.0 // indirect
-	github.com/mmcdole/gofeed v1.1.3 // indirect
-	github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
-	github.com/open-policy-agent/opa v0.44.0 // indirect
+	github.com/onsi/ginkgo/v2 v2.5.1 // indirect
+	github.com/onsi/gomega v1.24.1 // indirect
+	github.com/pelletier/go-toml/v2 v2.0.5 // indirect
 	github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 // indirect
 	github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
-	github.com/santhosh-tekuri/jsonschema v1.2.4 // indirect
-	github.com/santhosh-tekuri/jsonschema/v5 v5.0.1 // indirect
+	github.com/rogpeppe/go-internal v1.9.0 // indirect
 	github.com/tchap/go-patricia/v2 v2.3.1 // indirect
-	github.com/tfkhsr/jsonschema v0.0.0-20180218143334-273afdd5a88c // indirect
-	github.com/xanzy/go-gitlab v0.68.0 // indirect
 	github.com/yashtewari/glob-intersection v0.1.0 // indirect
-	go.uber.org/goleak v1.1.12 // indirect
+	go.etcd.io/etcd/api/v3 v3.5.4 // indirect
+	golang.org/x/tools v0.3.0 // indirect
+	istio.io/api v0.0.0-20221109202042-b9e5d446a83d // indirect
 )
 
 require (
 	github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.14.0
 	github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
 	github.com/Azure/go-autorest v14.2.0+incompatible // indirect
-	github.com/Azure/go-autorest/autorest v0.11.27 // indirect
+	github.com/Azure/go-autorest/autorest v0.11.28 // indirect
 	github.com/Azure/go-autorest/autorest/adal v0.9.20 // indirect
 	github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
 	github.com/Azure/go-autorest/logger v0.2.1 // indirect
 	github.com/Azure/go-autorest/tracing v0.6.0 // indirect
-	github.com/BurntSushi/toml v1.1.0 // indirect
+	github.com/BurntSushi/toml v1.2.1 // indirect
 	github.com/MakeNowJust/heredoc v1.0.0 // indirect
 	github.com/Masterminds/goutils v1.1.1 // indirect
 	github.com/Masterminds/semver v1.5.0 // indirect
@@ -145,22 +157,17 @@ require (
 	github.com/Masterminds/squirrel v1.5.3 // indirect
 	github.com/Microsoft/go-winio v0.5.2 // indirect
 	github.com/Microsoft/hcsshim v0.9.4 // indirect
-	github.com/PuerkitoBio/purell v1.1.1 // indirect
-	github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
 	github.com/apex/log v1.9.0 // indirect
 	github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/buildpacks/imgutil v0.0.0-20220527150729-7a271a852e31 // indirect
 	github.com/buildpacks/lifecycle v0.14.1 // indirect
-	github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
 	github.com/cespare/xxhash/v2 v2.1.2 // indirect
-	github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect
+	github.com/chai2010/gettext-go v1.0.2 // indirect
 	github.com/cli/safeexec v1.0.0 // indirect
-	github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
-	github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect
 	github.com/containerd/cgroups v1.0.3 // indirect
 	github.com/containerd/containerd v1.6.8 // indirect
-	github.com/containerd/stargz-snapshotter/estargz v0.11.4 // indirect
+	github.com/containerd/stargz-snapshotter/estargz v0.12.0 // indirect
 	github.com/cyphar/filepath-securejoin v0.2.3 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
@@ -168,12 +175,9 @@ require (
 	github.com/docker/go-units v0.4.0 // indirect
 	github.com/dustin/go-humanize v1.0.0 // indirect
 	github.com/emirpasic/gods v1.18.1 // indirect
-	github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 // indirect
-	github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
 	github.com/evanphx/json-patch v5.6.0+incompatible // indirect
 	github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
 	github.com/fsnotify/fsnotify v1.5.4 // indirect
-	github.com/fvbommel/sortorder v1.0.1 // indirect
 	github.com/gdamore/encoding v1.0.0 // indirect
 	github.com/gdamore/tcell/v2 v2.5.1 // indirect
 	github.com/ghodss/yaml v1.0.0 // indirect
@@ -185,18 +189,16 @@ require (
 	github.com/go-playground/locales v0.13.0 // indirect
 	github.com/go-playground/universal-translator v0.17.0 // indirect
 	github.com/gobwas/glob v0.2.3 // indirect
-	github.com/gofrs/flock v0.8.1 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/google/btree v1.1.2 // indirect
-	github.com/google/go-cmp v0.5.8 // indirect
-	github.com/google/go-containerregistry v0.9.0 // indirect
+	github.com/google/go-cmp v0.5.9 // indirect
+	github.com/google/go-containerregistry v0.11.0 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
-	github.com/google/uuid v1.3.0 // indirect
-	github.com/googleapis/gax-go/v2 v2.4.0 // indirect
-	github.com/googleapis/gnostic v0.5.5 // indirect
+	github.com/google/uuid v1.3.0
+	github.com/googleapis/gax-go/v2 v2.5.1 // indirect
 	github.com/gorilla/mux v1.8.0 // indirect
 	github.com/gosuri/uitable v0.0.4 // indirect
 	github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
@@ -204,7 +206,7 @@ require (
 	github.com/heroku/color v0.0.6 // indirect
 	github.com/huandu/xstrings v1.3.2 // indirect
 	github.com/imdario/mergo v0.3.13 // indirect
-	github.com/inconshreveable/mousetrap v1.0.0 // indirect
+	github.com/inconshreveable/mousetrap v1.0.1 // indirect
 	github.com/itchyny/astgen-go v0.0.0-20210113000433-0da0671862a3 // indirect
 	github.com/itchyny/timefmt-go v0.1.1 // indirect
 	github.com/jackc/chunkreader/v2 v2.0.1 // indirect
@@ -224,20 +226,19 @@ require (
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
 	github.com/kevinburke/ssh_config v1.2.0 // indirect
-	github.com/klauspost/compress v1.15.7 // indirect
-	github.com/kris-nova/lolgopher v0.0.0-20180921204813-313b3abb0d9b // indirect
+	github.com/klauspost/compress v1.15.8 // indirect
 	github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
 	github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
 	github.com/leodido/go-urn v1.2.0 // indirect
-	github.com/lib/pq v1.10.6 // indirect
+	github.com/lib/pq v1.10.7 // indirect
 	github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
-	github.com/magiconair/properties v1.8.5 // indirect
+	github.com/magiconair/properties v1.8.6 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
-	github.com/mattn/go-colorable v0.1.12 // indirect
-	github.com/mattn/go-isatty v0.0.14 // indirect
+	github.com/mattn/go-colorable v0.1.13 // indirect
+	github.com/mattn/go-isatty v0.0.16 // indirect
 	github.com/mattn/go-runewidth v0.0.13 // indirect
-	github.com/mattn/go-sqlite3 v1.14.6 // indirect
+	github.com/mattn/go-sqlite3 v1.14.15 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
 	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
@@ -254,7 +255,6 @@ require (
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
 	github.com/morikuni/aec v1.0.0 // indirect
-	github.com/onsi/ginkgo v1.16.4 // indirect
 	github.com/opencontainers/go-digest v1.0.0 // indirect
 	github.com/opencontainers/runc v1.1.2 // indirect
 	github.com/opencontainers/selinux v1.10.1 // indirect
@@ -275,11 +275,11 @@ require (
 	github.com/sergi/go-diff v1.2.0 // indirect
 	github.com/shopspring/decimal v1.3.1 // indirect
 	github.com/sirupsen/logrus v1.9.0 // indirect
-	github.com/spf13/afero v1.6.0 // indirect
+	github.com/spf13/afero v1.9.3 // indirect
 	github.com/spf13/cast v1.5.0 // indirect
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
 	github.com/src-d/gcfg v1.4.0 // indirect
-	github.com/subosito/gotenv v1.2.0 // indirect
+	github.com/subosito/gotenv v1.4.1 // indirect
 	github.com/vbatts/tar-split v0.11.2 // indirect
 	github.com/xanzy/ssh-agent v0.3.1 // indirect
 	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
@@ -289,29 +289,28 @@ require (
 	github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
 	go.opencensus.io v0.23.0 // indirect
 	go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd // indirect
-	golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
-	golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect
-	golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
-	golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect
-	golang.org/x/text v0.3.7 // indirect
+	golang.org/x/mod v0.7.0 // indirect
+	golang.org/x/sync v0.1.0 // indirect
+	golang.org/x/sys v0.2.0 // indirect
+	golang.org/x/term v0.2.0 // indirect
+	golang.org/x/text v0.4.0 // indirect
 	golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	gopkg.in/gorp.v1 v1.7.2 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
-	gopkg.in/ini.v1 v1.66.2 // indirect
+	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/src-d/go-billy.v4 v4.3.2 // indirect
 	gopkg.in/src-d/go-git.v4 v4.13.1 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	k8s.io/apiextensions-apiserver v0.24.2 // indirect
-	k8s.io/apiserver v0.24.2 // indirect
-	k8s.io/component-base v0.24.2 // indirect
-	k8s.io/klog/v2 v2.70.0 // indirect
-	k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8 // indirect
-	k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
+	k8s.io/apiextensions-apiserver v0.25.2 // indirect
+	k8s.io/apiserver v0.25.2 // indirect
+	k8s.io/component-base v0.25.2 // indirect
+	k8s.io/klog/v2 v2.80.1 // indirect
+	k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect
+	k8s.io/utils v0.0.0-20220922104903-7796b5f52b7e // indirect
 	oras.land/oras-go v1.2.0 // indirect
-	sigs.k8s.io/json v0.0.0-20220525155127-227cbc7cc124 // indirect
-	sigs.k8s.io/kustomize/api v0.11.5 // indirect
-	sigs.k8s.io/kustomize/kyaml v0.13.7 // indirect
-	sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
+	sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
+	sigs.k8s.io/kustomize/api v0.12.1 // indirect
+	sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect
+	sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
 )

Разница между файлами не показана из-за своего большого размера
+ 102 - 285
go.sum


+ 5 - 2
internal/helm/postrenderer.go

@@ -2,6 +2,7 @@ package helm
 
 import (
 	"bytes"
+	"context"
 	"fmt"
 	"io"
 	"net/url"
@@ -9,7 +10,7 @@ import (
 	"sort"
 	"strings"
 
-	"github.com/aws/aws-sdk-go/aws/arn"
+	"github.com/aws/aws-sdk-go-v2/aws/arn"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
@@ -463,6 +464,8 @@ func (d *DockerSecretsPostRenderer) getImageList(podSpec resource) []string {
 var ecrPattern = regexp.MustCompile(`(^[a-zA-Z0-9][a-zA-Z0-9-_]*)\.dkr\.ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.amazonaws\.com(\.cn)?`)
 
 func (d *DockerSecretsPostRenderer) isRegistryNative(regName string) bool {
+	ctx := context.Background()
+
 	isNative := false
 
 	if strings.Contains(regName, "gcr") && d.Cluster.AuthMechanism == models.GCP {
@@ -503,7 +506,7 @@ func (d *DockerSecretsPostRenderer) isRegistryNative(regName string) bool {
 			return false
 		}
 
-		err = awsInt.PopulateAWSArn()
+		err = awsInt.PopulateAWSArn(ctx)
 
 		if err != nil {
 			return false

+ 28 - 3
internal/kubernetes/agent.go

@@ -23,8 +23,6 @@ import (
 	"github.com/porter-dev/porter/internal/repository"
 	"golang.org/x/oauth2"
 
-	errors2 "errors"
-
 	"github.com/porter-dev/porter/internal/helm/grapher"
 	appsv1 "k8s.io/api/apps/v1"
 	batchv1 "k8s.io/api/batch/v1"
@@ -49,6 +47,9 @@ import (
 	"k8s.io/kubectl/pkg/scheme"
 
 	rspb "helm.sh/helm/v3/pkg/release"
+
+	istiov1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
+	versionedclient "istio.io/client-go/pkg/clientset/versioned"
 )
 
 // Agent is a Kubernetes agent for performing operations that interact with the
@@ -949,6 +950,30 @@ func (a *Agent) GetNetworkingV1Beta1Ingress(namespace string, name string) (*net
 	return resp, nil
 }
 
+func (a *Agent) GetIstioIngress(namespace, name string) (*istiov1beta1.Gateway, error) {
+	restConf, err := a.RESTClientGetter.ToRESTConfig()
+
+	if err != nil {
+		return nil, err
+	}
+
+	clientset, err := versionedclient.NewForConfig(restConf)
+
+	if err != nil {
+		return nil, err
+	}
+
+	gateway, err := clientset.NetworkingV1beta1().Gateways(namespace).Get(
+		context.Background(), name, metav1.GetOptions{},
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return gateway, nil
+}
+
 var IsNotFoundError = fmt.Errorf("not found")
 
 type BadRequestError struct {
@@ -1370,7 +1395,7 @@ func (a *Agent) RunWebsocketTask(task func() error) error {
 			return nil
 		}
 
-		if !errors2.Is(err, &AuthError{}) {
+		if !goerrors.Is(err, &AuthError{}) {
 			return err
 		}
 

+ 3 - 1
internal/kubernetes/config.go

@@ -1,6 +1,7 @@
 package kubernetes
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"path/filepath"
@@ -228,6 +229,7 @@ func (conf *OutOfClusterConfig) GetClientConfigFromCluster() (clientcmd.ClientCo
 
 func (conf *OutOfClusterConfig) CreateRawConfigFromCluster() (*api.Config, error) {
 	cluster := conf.Cluster
+	ctx := context.Background()
 
 	apiConfig := &api.Config{}
 
@@ -352,7 +354,7 @@ func (conf *OutOfClusterConfig) CreateRawConfigFromCluster() (*api.Config, error
 			shouldOverride = true
 		}
 
-		tok, err := awsAuth.GetBearerToken(conf.getTokenCache, conf.setTokenCache, awsClusterID, shouldOverride)
+		tok, err := awsAuth.GetBearerToken(ctx, conf.getTokenCache, conf.setTokenCache, awsClusterID, shouldOverride)
 
 		if err != nil {
 			return nil, err

+ 0 - 159
internal/models/integrations/aws.go

@@ -1,159 +0,0 @@
-package integrations
-
-import (
-	"gorm.io/gorm"
-
-	"github.com/aws/aws-sdk-go/aws"
-	"github.com/aws/aws-sdk-go/service/sts"
-
-	"github.com/aws/aws-sdk-go/aws/credentials"
-	"github.com/aws/aws-sdk-go/aws/session"
-	"github.com/porter-dev/porter/api/types"
-	"sigs.k8s.io/aws-iam-authenticator/pkg/token"
-)
-
-// AWSIntegration is an auth mechanism that uses a AWS IAM user to
-// authenticate
-type AWSIntegration struct {
-	gorm.Model
-
-	// The id of the user that linked this auth mechanism
-	UserID uint `json:"user_id"`
-
-	// The project that this integration belongs to
-	ProjectID uint `json:"project_id"`
-
-	// The AWS arn this is integration is linked to
-	AWSArn string `json:"aws_arn"`
-
-	// The optional AWS region (required by some session configurations)
-	AWSRegion string `json:"aws_region"`
-
-	// The assumed role ARN to use for sessions
-	AWSAssumeRoleArn string
-
-	// ------------------------------------------------------------------
-	// All fields encrypted before storage.
-	// ------------------------------------------------------------------
-
-	// The AWS cluster ID
-	// See https://github.com/kubernetes-sigs/aws-iam-authenticator#what-is-a-cluster-id
-	AWSClusterID []byte `json:"aws_cluster_id"`
-
-	// The AWS access key for this IAM user
-	AWSAccessKeyID []byte `json:"aws_access_key_id"`
-
-	// The AWS secret key for this IAM user
-	AWSSecretAccessKey []byte `json:"aws_secret_access_key"`
-
-	// An optional session token, if the user is assuming a role
-	AWSSessionToken []byte `json:"aws_session_token"`
-}
-
-func (a *AWSIntegration) ToAWSIntegrationType() *types.AWSIntegration {
-	return &types.AWSIntegration{
-		CreatedAt: a.CreatedAt,
-		ID:        a.ID,
-		UserID:    a.UserID,
-		ProjectID: a.ProjectID,
-		AWSArn:    a.AWSArn,
-	}
-}
-
-// GetSession retrieves an AWS session to use based on the access key and secret
-// access key
-func (a *AWSIntegration) GetSession() (*session.Session, error) {
-	awsConf := &aws.Config{
-		Credentials: credentials.NewStaticCredentials(
-			string(a.AWSAccessKeyID),
-			string(a.AWSSecretAccessKey),
-			string(a.AWSSessionToken),
-		),
-	}
-
-	if a.AWSRegion != "" {
-		awsConf.Region = &a.AWSRegion
-	}
-
-	return session.NewSessionWithOptions(session.Options{
-		SharedConfigState: session.SharedConfigEnable,
-		Config:            *awsConf,
-	})
-}
-
-// PopulateAWSArn uses the access key/secret to get the caller identity, and
-// attaches it to the AWS integration
-func (a *AWSIntegration) PopulateAWSArn() error {
-	sess, err := a.GetSession()
-
-	if err != nil {
-		return err
-	}
-
-	svc := sts.New(sess)
-
-	result, err := svc.GetCallerIdentity(&sts.GetCallerIdentityInput{})
-
-	if err != nil {
-		return err
-	}
-
-	a.AWSArn = *result.Arn
-
-	return nil
-}
-
-// GetBearerToken retrieves a bearer token for an AWS account
-func (a *AWSIntegration) GetBearerToken(
-	getTokenCache GetTokenCacheFunc,
-	setTokenCache SetTokenCacheFunc,
-	clusterID string,
-	shouldClusterIdOverride bool,
-) (string, error) {
-	cache, err := getTokenCache()
-
-	// check the token cache for a non-expired token
-	if cache != nil {
-		if tok := cache.Token; err == nil && !cache.IsExpired() && len(tok) > 0 {
-			return string(tok), nil
-		}
-	}
-
-	generator, err := token.NewGenerator(false, false)
-
-	if err != nil {
-		return "", err
-	}
-
-	sess, err := a.GetSession()
-
-	if err != nil {
-		return "", err
-	}
-
-	var validClusterId string
-
-	if shouldClusterIdOverride {
-		validClusterId = clusterID
-	} else {
-		validClusterId = string(a.AWSClusterID)
-
-		if validClusterId == "" {
-			validClusterId = clusterID
-		}
-	}
-
-	tok, err := generator.GetWithOptions(&token.GetTokenOptions{
-		AssumeRoleARN: a.AWSAssumeRoleArn,
-		Session:       sess,
-		ClusterID:     validClusterId,
-	})
-
-	if err != nil {
-		return "", err
-	}
-
-	setTokenCache(tok.Token, tok.Expiration)
-
-	return tok.Token, nil
-}

+ 177 - 0
internal/models/integrations/aws_v2.go

@@ -0,0 +1,177 @@
+package integrations
+
+import (
+	"context"
+	"encoding/base64"
+	"fmt"
+	"time"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/credentials"
+	"github.com/aws/aws-sdk-go-v2/service/sts"
+	smithyhttp "github.com/aws/smithy-go/transport/http"
+	"github.com/porter-dev/porter/api/types"
+	"gorm.io/gorm"
+)
+
+// AWSIntegration is an auth mechanism that uses a AWS IAM user to
+// authenticate
+type AWSIntegration struct {
+	gorm.Model
+
+	// The id of the user that linked this auth mechanism
+	UserID uint `json:"user_id"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+
+	// The AWS arn this is integration is linked to
+	AWSArn string `json:"aws_arn"`
+
+	// The optional AWS region (required by some session configurations)
+	AWSRegion string `json:"aws_region"`
+
+	// The assumed role ARN to use for sessions
+	AWSAssumeRoleArn string
+
+	// ------------------------------------------------------------------
+	// All fields encrypted before storage.
+	// ------------------------------------------------------------------
+
+	// The AWS cluster ID
+	// See https://github.com/kubernetes-sigs/aws-iam-authenticator#what-is-a-cluster-id
+	AWSClusterID []byte `json:"aws_cluster_id"`
+
+	// The AWS access key for this IAM user
+	AWSAccessKeyID []byte `json:"aws_access_key_id"`
+
+	// The AWS secret key for this IAM user
+	AWSSecretAccessKey []byte `json:"aws_secret_access_key"`
+
+	// An optional session token, if the user is assuming a role
+	AWSSessionToken []byte `json:"aws_session_token"`
+}
+
+func (a *AWSIntegration) ToAWSIntegrationType() types.AWSIntegration {
+	return types.AWSIntegration{
+		CreatedAt: a.CreatedAt,
+		ID:        a.ID,
+		UserID:    a.UserID,
+		ProjectID: a.ProjectID,
+		AWSArn:    a.AWSArn,
+	}
+}
+
+// Config returns a populated AWS Config for use with aws-go-sdk-v2 services
+func (a *AWSIntegration) Config() aws.Config {
+	awsConf := aws.Config{
+		Credentials: credentials.NewStaticCredentialsProvider(
+			*aws.String(string(a.AWSAccessKeyID)),
+			*aws.String(string(a.AWSSecretAccessKey)),
+			*aws.String(string(a.AWSSessionToken)),
+		),
+	}
+
+	if a.AWSRegion != "" {
+		awsConf.Region = a.AWSRegion
+	}
+
+	return awsConf
+}
+
+// PopulateAWSArn uses the access key/secret to get the caller identity, and
+// attaches it to the AWS integration
+func (a *AWSIntegration) PopulateAWSArn(ctx context.Context) error {
+	svc := sts.NewFromConfig(a.Config())
+
+	result, err := svc.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{})
+
+	if err != nil {
+		return err
+	}
+
+	a.AWSArn = *result.Arn
+
+	return nil
+}
+
+// GetBearerToken retrieves a bearer token for an AWS account
+func (a *AWSIntegration) GetBearerToken(
+	ctx context.Context,
+	getTokenCache GetTokenCacheFunc,
+	setTokenCache SetTokenCacheFunc,
+	clusterID string,
+	shouldClusterIdOverride bool,
+) (string, error) {
+	cache, err := getTokenCache()
+
+	// check the token cache for a non-expired token
+	if cache != nil {
+		if tok := cache.Token; err == nil && !cache.IsExpired() && len(tok) > 0 {
+			return string(tok), nil
+		}
+	}
+
+	var validClusterId string
+
+	if shouldClusterIdOverride {
+		validClusterId = clusterID
+	} else {
+		validClusterId = string(a.AWSClusterID)
+
+		if validClusterId == "" {
+			validClusterId = clusterID
+		}
+	}
+
+	token, err := a.GetWithSTS(ctx, clusterID)
+	if err != nil {
+		return "", err
+	}
+
+	setTokenCache(token.Token, token.Expiration)
+
+	return token.Token, nil
+}
+
+// Token is generated and used by Kubernetes client-go to authenticate with a Kubernetes cluster.
+// Original source: https://github.com/weaveworks/eksctl/blob/5f2a59056a4852470c66502205d2db0aa7c84c5e/pkg/eks/auth/generator.go#LL46C24-L46C24
+type Token struct {
+	Token      string
+	Expiration time.Time
+}
+
+const (
+	clusterIDHeader        = "x-k8s-aws-id"
+	presignedURLExpiration = 10 * time.Minute
+	v1Prefix               = "k8s-aws-v1."
+)
+
+// GetWithSTS returns a token valid for clusterID using the given STS client.
+// This implementation follows the steps outlined here:
+// https://github.com/kubernetes-sigs/aws-iam-authenticator#api-authorization-from-outside-a-cluster
+// We either add this implementation or have to maintain two versions of STS since aws-iam-authenticator is
+// not switching over to aws-go-sdk-v2.
+func (a AWSIntegration) GetWithSTS(ctx context.Context, clusterID string) (Token, error) {
+	presignClient := sts.NewPresignClient(sts.NewFromConfig(a.Config()))
+	// generate a sts:GetCallerIdentity request and add our custom cluster ID header
+	presignedURLRequest, err := presignClient.PresignGetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}, func(presignOptions *sts.PresignOptions) {
+		presignOptions.ClientOptions = append(presignOptions.ClientOptions, a.appendPresignHeaderValuesFunc(clusterID))
+	})
+	if err != nil {
+		return Token{}, fmt.Errorf("failed to presign caller identity: %w", err)
+	}
+
+	tokenExpiration := time.Now().Local().Add(presignedURLExpiration)
+	// Add the token with k8s-aws-v1. prefix.
+	return Token{v1Prefix + base64.RawURLEncoding.EncodeToString([]byte(presignedURLRequest.URL)), tokenExpiration}, nil
+}
+
+func (a AWSIntegration) appendPresignHeaderValuesFunc(clusterID string) func(stsOptions *sts.Options) {
+	return func(stsOptions *sts.Options) {
+		// Add clusterId Header
+		stsOptions.APIOptions = append(stsOptions.APIOptions, smithyhttp.SetHeaderValue(clusterIDHeader, clusterID))
+		// Add X-Amz-Expires query param
+		stsOptions.APIOptions = append(stsOptions.APIOptions, smithyhttp.SetHeaderValue("X-Amz-Expires", "60"))
+	}
+}

+ 67 - 92
internal/registry/registry.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"encoding/base64"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"net/http"
 	"net/url"
@@ -12,10 +13,18 @@ import (
 	"time"
 
 	artifactregistry "cloud.google.com/go/artifactregistry/apiv1beta2"
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
 	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
-	"github.com/aws/aws-sdk-go/aws/awserr"
-	"github.com/aws/aws-sdk-go/service/ecr"
+	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry"
+	"github.com/aws/aws-sdk-go-v2/service/ecr"
+	ecrTypes "github.com/aws/aws-sdk-go-v2/service/ecr/types"
+	"github.com/digitalocean/godo"
+	"github.com/docker/cli/cli/config/configfile"
+	"github.com/docker/cli/cli/config/types"
+	"github.com/docker/distribution/reference"
+	ptypes "github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
 	"github.com/porter-dev/porter/internal/oauth"
 	"github.com/porter-dev/porter/internal/repository"
 	"golang.org/x/oauth2"
@@ -23,40 +32,22 @@ import (
 	"google.golang.org/api/iterator"
 	"google.golang.org/api/option"
 	artifactregistrypb "google.golang.org/genproto/googleapis/devtools/artifactregistry/v1beta2"
-
-	ints "github.com/porter-dev/porter/internal/models/integrations"
-
-	ptypes "github.com/porter-dev/porter/api/types"
-
-	"github.com/digitalocean/godo"
-	"github.com/docker/cli/cli/config/configfile"
-	"github.com/docker/cli/cli/config/types"
-	"github.com/docker/distribution/reference"
-
-	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry"
-
-	"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
 )
 
 // Registry wraps the gorm Registry model
 type Registry models.Registry
 
 func GetECRRegistryURL(awsIntRepo repository.AWSIntegrationRepository, projectID, awsIntID uint) (string, error) {
-	awsInt, err := awsIntRepo.ReadAWSIntegration(projectID, awsIntID)
-
-	if err != nil {
-		return "", err
-	}
-
-	sess, err := awsInt.GetSession()
+	ctx := context.Background()
 
+	awsInt, err := awsIntRepo.ReadAWSIntegration(projectID, awsIntID)
 	if err != nil {
 		return "", err
 	}
 
-	ecrSvc := ecr.New(sess)
+	svc := ecr.NewFromConfig(awsInt.Config())
 
-	output, err := ecrSvc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})
+	output, err := svc.GetAuthorizationToken(ctx, &ecr.GetAuthorizationTokenInput{})
 
 	if err != nil {
 		return "", err
@@ -380,6 +371,8 @@ func (r *Registry) listGARRepositories(
 }
 
 func (r *Registry) listECRRepositories(repo repository.Repository) ([]*ptypes.RegistryRepository, error) {
+	ctx := context.Background()
+
 	aws, err := repo.AWSIntegration().ReadAWSIntegration(
 		r.ProjectID,
 		r.AWSIntegrationID,
@@ -389,15 +382,9 @@ func (r *Registry) listECRRepositories(repo repository.Repository) ([]*ptypes.Re
 		return nil, err
 	}
 
-	sess, err := aws.GetSession()
-
-	if err != nil {
-		return nil, err
-	}
-
-	svc := ecr.New(sess)
+	svc := ecr.NewFromConfig(aws.Config())
 
-	resp, err := svc.DescribeRepositories(&ecr.DescribeRepositoriesInput{})
+	resp, err := svc.DescribeRepositories(ctx, &ecr.DescribeRepositoriesInput{})
 
 	if err != nil {
 		return nil, err
@@ -777,6 +764,8 @@ func (r *Registry) createECRRepository(
 	repo repository.Repository,
 	name string,
 ) error {
+	ctx := context.Background()
+
 	aws, err := repo.AWSIntegration().ReadAWSIntegration(
 		r.ProjectID,
 		r.AWSIntegrationID,
@@ -786,27 +775,23 @@ func (r *Registry) createECRRepository(
 		return err
 	}
 
-	sess, err := aws.GetSession()
-
-	if err != nil {
-		return err
-	}
-
-	svc := ecr.New(sess)
+	svc := ecr.NewFromConfig(aws.Config())
 
 	// determine if repository already exists
-	_, err = svc.DescribeRepositories(&ecr.DescribeRepositoriesInput{
-		RepositoryNames: []*string{&name},
+	_, err = svc.DescribeRepositories(ctx, &ecr.DescribeRepositoriesInput{
+		RepositoryNames: []string{name},
 	})
-
-	// if the repository was not found, create it
-	if aerr, ok := err.(awserr.Error); ok && aerr.Code() == ecr.ErrCodeRepositoryNotFoundException {
-		_, err = svc.CreateRepository(&ecr.CreateRepositoryInput{
-			RepositoryName: &name,
-		})
-
-		return err
-	} else if err != nil {
+	if err != nil {
+		// if the repository was not found, create it
+		var nsk *ecrTypes.RegistryPolicyNotFoundException
+		if errors.As(err, &nsk) {
+			_, err = svc.CreateRepository(ctx, &ecr.CreateRepositoryInput{
+				RepositoryName: &name,
+			})
+			if err != nil {
+				return err
+			}
+		}
 		return err
 	}
 
@@ -909,26 +894,22 @@ func (r *Registry) GetECRPaginatedImages(
 	maxResults int64,
 	nextToken *string,
 ) ([]*ptypes.Image, *string, error) {
+	ctx := context.Background()
+
 	aws, err := repo.AWSIntegration().ReadAWSIntegration(
 		r.ProjectID,
 		r.AWSIntegrationID,
 	)
-
 	if err != nil {
 		return nil, nil, err
 	}
 
-	sess, err := aws.GetSession()
+	svc := ecr.NewFromConfig(aws.Config())
 
-	if err != nil {
-		return nil, nil, err
-	}
-
-	svc := ecr.New(sess)
-
-	resp, err := svc.ListImages(&ecr.ListImagesInput{
+	mr := int32(maxResults)
+	resp, err := svc.ListImages(ctx, &ecr.ListImagesInput{
 		RepositoryName: &repoName,
-		MaxResults:     &maxResults,
+		MaxResults:     &mr,
 		NextToken:      nextToken,
 	})
 
@@ -941,11 +922,11 @@ func (r *Registry) GetECRPaginatedImages(
 	}
 
 	imageIDLen := len(resp.ImageIds)
-	imageDetails := make([]*ecr.ImageDetail, 0)
+	imageDetails := make([]ecrTypes.ImageDetail, 0)
 	imageIDMap := make(map[string]bool)
 
 	for _, id := range resp.ImageIds {
-		if id != nil && id.ImageTag != nil {
+		if id.ImageDigest != nil && id.ImageTag != nil {
 			imageIDMap[*id.ImageTag] = true
 		}
 	}
@@ -965,7 +946,7 @@ func (r *Registry) GetECRPaginatedImages(
 		go func(start, end int) {
 			defer wg.Done()
 
-			describeResp, err := svc.DescribeImages(&ecr.DescribeImagesInput{
+			describeResp, err := svc.DescribeImages(ctx, &ecr.DescribeImagesInput{
 				RepositoryName: &repoName,
 				ImageIds:       resp.ImageIds[start:end],
 			})
@@ -989,14 +970,14 @@ func (r *Registry) GetECRPaginatedImages(
 		for _, tag := range img.ImageTags {
 			newImage := &ptypes.Image{
 				Digest:         *img.ImageDigest,
-				Tag:            *tag,
+				Tag:            tag,
 				RepositoryName: repoName,
 				PushedAt:       img.ImagePushedAt,
 			}
 
-			if _, ok := imageIDMap[*tag]; ok {
-				if _, ok := imageInfoMap[*tag]; !ok {
-					imageInfoMap[*tag] = newImage
+			if _, ok := imageIDMap[tag]; ok {
+				if _, ok := imageInfoMap[tag]; !ok {
+					imageInfoMap[tag] = newImage
 				}
 			}
 
@@ -1018,6 +999,8 @@ func (r *Registry) GetECRPaginatedImages(
 }
 
 func (r *Registry) listECRImages(repoName string, repo repository.Repository) ([]*ptypes.Image, error) {
+	ctx := context.Background()
+
 	aws, err := repo.AWSIntegration().ReadAWSIntegration(
 		r.ProjectID,
 		r.AWSIntegrationID,
@@ -1027,21 +1010,16 @@ func (r *Registry) listECRImages(repoName string, repo repository.Repository) ([
 		return nil, err
 	}
 
-	sess, err := aws.GetSession()
-
-	if err != nil {
-		return nil, err
-	}
-
-	svc := ecr.New(sess)
+	svc := ecr.NewFromConfig(aws.Config())
 
 	maxResults := int64(1000)
 
-	var imageIDs []*ecr.ImageIdentifier
+	var imageIDs []ecrTypes.ImageIdentifier
 
-	resp, err := svc.ListImages(&ecr.ListImagesInput{
+	mr := int32(maxResults)
+	resp, err := svc.ListImages(ctx, &ecr.ListImagesInput{
 		RepositoryName: &repoName,
-		MaxResults:     &maxResults,
+		MaxResults:     &mr,
 	})
 
 	if err != nil {
@@ -1057,9 +1035,9 @@ func (r *Registry) listECRImages(repoName string, repo repository.Repository) ([
 	nextToken := resp.NextToken
 
 	for nextToken != nil {
-		resp, err := svc.ListImages(&ecr.ListImagesInput{
+		resp, err := svc.ListImages(ctx, &ecr.ListImagesInput{
 			RepositoryName: &repoName,
-			MaxResults:     &maxResults,
+			MaxResults:     &mr,
 			NextToken:      nextToken,
 		})
 
@@ -1072,7 +1050,7 @@ func (r *Registry) listECRImages(repoName string, repo repository.Repository) ([
 	}
 
 	imageIDLen := len(imageIDs)
-	imageDetails := make([]*ecr.ImageDetail, 0)
+	imageDetails := make([]ecrTypes.ImageDetail, 0)
 
 	var wg sync.WaitGroup
 	var mu sync.Mutex
@@ -1089,7 +1067,7 @@ func (r *Registry) listECRImages(repoName string, repo repository.Repository) ([
 		go func(start, end int) {
 			defer wg.Done()
 
-			describeResp, err := svc.DescribeImages(&ecr.DescribeImagesInput{
+			describeResp, err := svc.DescribeImages(ctx, &ecr.DescribeImagesInput{
 				RepositoryName: &repoName,
 				ImageIds:       imageIDs[start:end],
 			})
@@ -1113,13 +1091,13 @@ func (r *Registry) listECRImages(repoName string, repo repository.Repository) ([
 		for _, tag := range img.ImageTags {
 			newImage := &ptypes.Image{
 				Digest:         *img.ImageDigest,
-				Tag:            *tag,
+				Tag:            tag,
 				RepositoryName: repoName,
 				PushedAt:       img.ImagePushedAt,
 			}
 
-			if _, ok := imageInfoMap[*tag]; !ok {
-				imageInfoMap[*tag] = newImage
+			if _, ok := imageInfoMap[tag]; !ok {
+				imageInfoMap[tag] = newImage
 			}
 		}
 	}
@@ -1588,6 +1566,9 @@ func (r *Registry) GetDockerConfigJSON(
 func (r *Registry) getECRDockerConfigFile(
 	repo repository.Repository,
 ) (*configfile.ConfigFile, error) {
+
+	ctx := context.Background()
+
 	aws, err := repo.AWSIntegration().ReadAWSIntegration(
 		r.ProjectID,
 		r.AWSIntegrationID,
@@ -1597,15 +1578,9 @@ func (r *Registry) getECRDockerConfigFile(
 		return nil, err
 	}
 
-	sess, err := aws.GetSession()
-
-	if err != nil {
-		return nil, err
-	}
-
-	ecrSvc := ecr.New(sess)
+	svc := ecr.NewFromConfig(aws.Config())
 
-	output, err := ecrSvc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})
+	output, err := svc.GetAuthorizationToken(ctx, &ecr.GetAuthorizationTokenInput{})
 
 	if err != nil {
 		return nil, err

+ 32 - 34
provisioner/integrations/storage/s3/s3.go

@@ -2,21 +2,23 @@ package s3
 
 import (
 	"bytes"
+	"context"
+	"errors"
 	"fmt"
 	"io"
 
-	"github.com/aws/aws-sdk-go/aws"
-	"github.com/aws/aws-sdk-go/aws/awserr"
-	"github.com/aws/aws-sdk-go/aws/credentials"
-	"github.com/aws/aws-sdk-go/aws/session"
-	"github.com/aws/aws-sdk-go/service/s3"
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/credentials"
+	"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
+	"github.com/aws/aws-sdk-go-v2/service/s3"
+	"github.com/aws/aws-sdk-go-v2/service/s3/types"
 	"github.com/porter-dev/porter/internal/encryption"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/provisioner/integrations/storage"
 )
 
 type S3StorageClient struct {
-	client        *s3.S3
+	client        *s3.Client
 	bucket        string
 	encryptionKey *[32]byte
 }
@@ -30,35 +32,30 @@ type S3Options struct {
 }
 
 func NewS3StorageClient(opts *S3Options) (*S3StorageClient, error) {
-	var sess *session.Session
-	var err error
 
-	awsConf := &aws.Config{
-		Credentials: credentials.NewStaticCredentials(
+	awsConf := aws.Config{
+		Credentials: credentials.NewStaticCredentialsProvider(
 			opts.AWSAccessKeyID,
 			opts.AWSSecretKey,
 			"",
 		),
-		Region: &opts.AWSRegion,
+		Region: opts.AWSRegion,
 	}
 
-	sess, err = session.NewSessionWithOptions(session.Options{
-		SharedConfigState: session.SharedConfigEnable,
-		Config:            *awsConf,
-	})
-
-	if err != nil {
-		return nil, fmt.Errorf("cannot create AWS session: %v", err)
-	}
+	// TODO: delete this comment by 2023-01-30 if no issues are noticed when creating a new S3 client
+	// aws-sdk-go used a SharedConfigState: enabled which is no longer available in v2, without specifying the file name
+	client := s3.NewFromConfig(awsConf)
 
 	return &S3StorageClient{
 		bucket:        opts.AWSBucketName,
 		encryptionKey: opts.EncryptionKey,
-		client:        s3.New(sess),
+		client:        client,
 	}, nil
 }
 
 func (s *S3StorageClient) WriteFile(infra *models.Infra, name string, fileBytes []byte, shouldEncrypt bool) error {
+	ctx := context.Background()
+
 	body := fileBytes
 	var err error
 	if shouldEncrypt {
@@ -69,8 +66,8 @@ func (s *S3StorageClient) WriteFile(infra *models.Infra, name string, fileBytes
 		}
 	}
 
-	_, err = s.client.PutObject(&s3.PutObjectInput{
-		Body:   aws.ReadSeekCloser(bytes.NewReader(body)),
+	_, err = s.client.PutObject(ctx, &s3.PutObjectInput{
+		Body:   manager.ReadSeekCloser(bytes.NewReader(body)),
 		Bucket: &s.bucket,
 		Key:    aws.String(getKeyFromInfra(infra, name)),
 	})
@@ -79,6 +76,8 @@ func (s *S3StorageClient) WriteFile(infra *models.Infra, name string, fileBytes
 }
 
 func (s *S3StorageClient) WriteFileWithKey(fileBytes []byte, shouldEncrypt bool, key string) error {
+	ctx := context.Background()
+
 	body := fileBytes
 	var err error
 	if shouldEncrypt {
@@ -89,8 +88,8 @@ func (s *S3StorageClient) WriteFileWithKey(fileBytes []byte, shouldEncrypt bool,
 		}
 	}
 
-	_, err = s.client.PutObject(&s3.PutObjectInput{
-		Body:   aws.ReadSeekCloser(bytes.NewReader(body)),
+	_, err = s.client.PutObject(ctx, &s3.PutObjectInput{
+		Body:   manager.ReadSeekCloser(bytes.NewReader(body)),
 		Bucket: &s.bucket,
 		Key:    aws.String(key),
 	})
@@ -99,21 +98,18 @@ func (s *S3StorageClient) WriteFileWithKey(fileBytes []byte, shouldEncrypt bool,
 }
 
 func (s *S3StorageClient) ReadFile(infra *models.Infra, name string, shouldDecrypt bool) ([]byte, error) {
-	output, err := s.client.GetObject(&s3.GetObjectInput{
+	ctx := context.Background()
+
+	output, err := s.client.GetObject(ctx, &s3.GetObjectInput{
 		Bucket: &s.bucket,
 		Key:    aws.String(getKeyFromInfra(infra, name)),
 	})
 
 	if err != nil {
-		if aerr, ok := err.(awserr.Error); ok {
-			switch aerr.Code() {
-			case s3.ErrCodeNoSuchKey:
-				return nil, storage.FileDoesNotExist
-			default:
-				return nil, err
-			}
+		var nsk *types.NoSuchKey
+		if errors.As(err, &nsk) {
+			return nil, storage.FileDoesNotExist
 		}
-
 		return nil, err
 	}
 
@@ -139,7 +135,9 @@ func (s *S3StorageClient) ReadFile(infra *models.Infra, name string, shouldDecry
 }
 
 func (s *S3StorageClient) DeleteFile(infra *models.Infra, name string) error {
-	_, err := s.client.DeleteObject(&s3.DeleteObjectInput{
+	ctx := context.Background()
+
+	_, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
 		Bucket: &s.bucket,
 		Key:    aws.String(getKeyFromInfra(infra, name)),
 	})

+ 6 - 9
provisioner/server/handlers/state/create_resource.go

@@ -1,6 +1,7 @@
 package state
 
 import (
+	"context"
 	"encoding/base64"
 	"encoding/json"
 	"errors"
@@ -9,7 +10,7 @@ import (
 	"regexp"
 	"strings"
 
-	"github.com/aws/aws-sdk-go/service/ecr"
+	"github.com/aws/aws-sdk-go-v2/service/ecr"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/types"
@@ -123,6 +124,8 @@ func (c *CreateResourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 }
 
 func createECRRegistry(config *config.Config, infra *models.Infra, operation *models.Operation, output map[string]interface{}) (*models.Registry, error) {
+	ctx := context.Background()
+
 	reg := &models.Registry{
 		ProjectID:        infra.ProjectID,
 		AWSIntegrationID: infra.AWSIntegrationID,
@@ -137,15 +140,9 @@ func createECRRegistry(config *config.Config, infra *models.Infra, operation *mo
 		return nil, err
 	}
 
-	sess, err := awsInt.GetSession()
-
-	if err != nil {
-		return nil, err
-	}
-
-	ecrSvc := ecr.New(sess)
+	svc := ecr.NewFromConfig(awsInt.Config())
 
-	authOutput, err := ecrSvc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})
+	authOutput, err := svc.GetAuthorizationToken(ctx, &ecr.GetAuthorizationTokenInput{})
 
 	if err != nil {
 		return nil, err

Некоторые файлы не были показаны из-за большого количества измененных файлов