Browse Source

fix merge conflicts by removing old incidents tab

Alexander Belanger 3 năm trước cách đây
mục cha
commit
b28d555dfc
73 tập tin đã thay đổi với 499 bổ sung171 xóa
  1. 4 4
      api/client/api.go
  2. 1 1
      api/server/handlers/cluster/install_agent.go
  3. 1 1
      api/server/handlers/cluster/upgrade_agent.go
  4. 1 38
      api/server/handlers/gitinstallation/webhook.go
  5. 1 1
      api/server/handlers/namespace/create_env_group.go
  6. 1 0
      api/server/handlers/project_integration/create_aws.go
  7. 1 1
      api/server/handlers/release/create.go
  8. 1 1
      api/server/handlers/release/create_addon.go
  9. 13 1
      api/server/handlers/release/update_image_batch.go
  10. 2 1
      api/server/handlers/release/upgrade.go
  11. 1 1
      api/server/handlers/release/upgrade_webhook.go
  12. 3 2
      api/server/handlers/stack/helpers.go
  13. 1 1
      api/server/handlers/v1/env_group/create.go
  14. 1 1
      api/server/handlers/v1/release/upgrade.go
  15. 4 0
      api/server/shared/config/env/envconfs.go
  16. 1 0
      api/types/project_integration.go
  17. 11 11
      cli/cmd/config.go
  18. 3 3
      cli/cmd/config/config.go
  19. 2 1
      cli/cmd/connect/ecr.go
  20. 7 7
      cli/cmd/connect/kubeconfig.go
  21. 3 2
      cli/cmd/errors.go
  22. 1 1
      cli/cmd/list.go
  23. 28 7
      cli/cmd/run.go
  24. 1 1
      cli/cmd/utils/close.go
  25. BIN
      dashboard/src/assets/azure.png
  26. 12 3
      dashboard/src/components/Banner.tsx
  27. 0 1
      dashboard/src/components/Button.tsx
  28. 0 2
      dashboard/src/components/MultiSaveButton.tsx
  29. 0 2
      dashboard/src/components/ProvisionerStatus.tsx
  30. 0 2
      dashboard/src/components/SaveButton.tsx
  31. 0 1
      dashboard/src/components/repo-selector/ContentsList.tsx
  32. 0 1
      dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx
  33. 1 2
      dashboard/src/main/home/cluster-dashboard/NamespaceSelector.tsx
  34. 1 1
      dashboard/src/main/home/cluster-dashboard/SortSelector.tsx
  35. 0 2
      dashboard/src/main/home/cluster-dashboard/dashboard/ClusterSettings.tsx
  36. 0 1
      dashboard/src/main/home/cluster-dashboard/dashboard/NamespaceList.tsx
  37. 0 1
      dashboard/src/main/home/cluster-dashboard/dashboard/events/EventsTab.tsx
  38. 0 2
      dashboard/src/main/home/cluster-dashboard/databases/DatabasesList.tsx
  39. 0 1
      dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx
  40. 0 2
      dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx
  41. 0 2
      dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx
  42. 0 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventsTab.tsx
  43. 1 1
      dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepo.tsx
  44. 1 2
      dashboard/src/main/home/cluster-dashboard/preview-environments/components/ButtonEnablePREnvironments.tsx
  45. 0 1
      dashboard/src/main/home/cluster-dashboard/preview-environments/components/RecreateWorkflowFilesModal.tsx
  46. 0 1
      dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentCard.tsx
  47. 0 1
      dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentsList.tsx
  48. 1 2
      dashboard/src/main/home/cluster-dashboard/stacks/Dashboard.tsx
  49. 0 2
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Settings.tsx
  50. 0 1
      dashboard/src/main/home/cluster-dashboard/stacks/components/styles.ts
  51. 14 24
      dashboard/src/main/home/dashboard/Dashboard.tsx
  52. 14 0
      dashboard/src/main/home/infrastructure/components/credentials/AWSCredentialForm.tsx
  53. 0 1
      dashboard/src/main/home/integrations/IntegrationCategories.tsx
  54. 0 1
      dashboard/src/main/home/integrations/IntegrationList.tsx
  55. 0 1
      dashboard/src/main/home/launch/expanded-template/TemplateInfo.tsx
  56. 0 1
      dashboard/src/main/home/modals/RedirectToOnboardingModal.tsx
  57. 0 1
      dashboard/src/main/home/modals/UsageWarningModal.tsx
  58. 0 1
      dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_DORegistryForm.tsx
  59. 0 1
      dashboard/src/main/home/onboarding/steps/ConnectSource.tsx
  60. 0 2
      dashboard/src/main/home/project-settings/APITokensSection.tsx
  61. 0 2
      dashboard/src/main/home/project-settings/InviteList.tsx
  62. 0 1
      dashboard/src/main/home/project-settings/ProjectSettings.tsx
  63. 296 0
      dashboard/src/main/home/provisioner/AzureFormSection.tsx
  64. 36 1
      dashboard/src/main/home/provisioner/ProvisionerSettings.tsx
  65. 1 0
      dashboard/src/shared/api.tsx
  66. 2 0
      ee/api/server/handlers/credentials/get_credentials.go
  67. 8 2
      internal/helm/agent.go
  68. 2 1
      internal/helm/postrenderer.go
  69. 6 2
      internal/models/integrations/aws.go
  70. 3 0
      internal/repository/credentials/credentials.go
  71. 2 2
      internal/templater/helm/values/writer.go
  72. 3 2
      provisioner/client/client.go
  73. 2 0
      provisioner/server/handlers/credentials/get_credentials_ee.go

+ 4 - 4
api/client/api.go

@@ -150,9 +150,9 @@ func (c *Client) postRequest(relPath string, data interface{}, response interfac
 
 		if i != int(retryCount)-1 {
 			if httpErr != nil {
-				fmt.Printf("Error: %s (status code %d), retrying request...\n", httpErr.Error, httpErr.Code)
+				fmt.Fprintf(os.Stderr, "Error: %s (status code %d), retrying request...\n", httpErr.Error, httpErr.Code)
 			} else {
-				fmt.Printf("Error: %v, retrying request...\n", err)
+				fmt.Fprintf(os.Stderr, "Error: %v, retrying request...\n", err)
 			}
 		}
 	}
@@ -205,9 +205,9 @@ func (c *Client) patchRequest(relPath string, data interface{}, response interfa
 
 		if i != int(retryCount)-1 {
 			if httpErr != nil {
-				fmt.Printf("Error: %s (status code %d), retrying request...\n", httpErr.Error, httpErr.Code)
+				fmt.Fprintf(os.Stderr, "Error: %s (status code %d), retrying request...\n", httpErr.Error, httpErr.Code)
 			} else {
-				fmt.Printf("Error: %v, retrying request...\n", err)
+				fmt.Fprintf(os.Stderr, "Error: %v, retrying request...\n", err)
 			}
 		}
 	}

+ 1 - 1
api/server/handlers/cluster/install_agent.go

@@ -144,7 +144,7 @@ func (c *InstallAgentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		Values:    porterAgentValues,
 	}
 
-	_, err = helmAgent.InstallChart(conf, c.Config().DOConf)
+	_, err = helmAgent.InstallChart(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(

+ 1 - 1
api/server/handlers/cluster/upgrade_agent.go

@@ -66,7 +66,7 @@ func (c *UpgradeAgentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		Cluster:    cluster,
 		Repo:       c.Repo(),
 		Registries: []*models.Registry{},
-	}, c.Config().DOConf)
+	}, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(

+ 1 - 38
api/server/handlers/gitinstallation/webhook.go

@@ -1,12 +1,7 @@
 package gitinstallation
 
 import (
-	"crypto/hmac"
-	"crypto/sha256"
-	"encoding/hex"
-	"io/ioutil"
 	"net/http"
-	"strings"
 
 	"github.com/google/go-github/v41/github"
 	"github.com/porter-dev/porter/api/server/authz"
@@ -35,21 +30,13 @@ func NewGithubAppWebhookHandler(
 }
 
 func (c *GithubAppWebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	payload, err := ioutil.ReadAll(r.Body)
+	payload, err := github.ValidatePayload(r, []byte(c.Config().GithubAppConf.WebhookSecret))
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	// verify webhook secret
-	signature := r.Header.Get("X-Hub-Signature-256")
-
-	if !verifySignature([]byte(c.Config().GithubAppConf.WebhookSecret), signature, payload) {
-		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
-		return
-	}
-
 	event, err := github.ParseWebHook(github.WebHookType(r), payload)
 
 	if err != nil {
@@ -89,27 +76,3 @@ func (c *GithubAppWebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 		}
 	}
 }
-
-// verifySignature verifies a signature based on hmac protocal
-// https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks
-func verifySignature(secret []byte, signature string, body []byte) bool {
-	if len(signature) != 71 || !strings.HasPrefix(signature, "sha256=") {
-		return false
-	}
-
-	actual := make([]byte, 32)
-	_, err := hex.Decode(actual, []byte(signature[7:]))
-
-	if err != nil {
-		return false
-	}
-
-	computed := hmac.New(sha256.New, secret)
-	_, err = computed.Write(body)
-
-	if err != nil {
-		return false
-	}
-
-	return hmac.Equal(computed.Sum(nil), actual)
-}

+ 1 - 1
api/server/handlers/namespace/create_env_group.go

@@ -191,7 +191,7 @@ func rolloutApplications(
 				Values:     newConfig,
 			}
 
-			_, err = helmAgent.UpgradeReleaseByValues(conf, config.DOConf)
+			_, err = helmAgent.UpgradeReleaseByValues(conf, config.DOConf, config.ServerConf.DisablePullSecretsInjection)
 
 			if err != nil {
 				mu.Lock()

+ 1 - 0
api/server/handlers/project_integration/create_aws.go

@@ -57,6 +57,7 @@ func CreateAWSIntegration(request *types.CreateAWSRequest, projectID, userID uin
 		UserID:             userID,
 		ProjectID:          projectID,
 		AWSRegion:          request.AWSRegion,
+		AWSAssumeRoleArn:   request.AWSAssumeRoleArn,
 		AWSClusterID:       []byte(request.AWSClusterID),
 		AWSAccessKeyID:     []byte(request.AWSAccessKeyID),
 		AWSSecretAccessKey: []byte(request.AWSSecretAccessKey),

+ 1 - 1
api/server/handlers/release/create.go

@@ -104,7 +104,7 @@ func (c *CreateReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		Registries: registries,
 	}
 
-	helmRelease, err := helmAgent.InstallChart(conf, c.Config().DOConf)
+	helmRelease, err := helmAgent.InstallChart(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(

+ 1 - 1
api/server/handlers/release/create_addon.go

@@ -94,7 +94,7 @@ func (c *CreateAddonHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		Registries: registries,
 	}
 
-	helmRelease, err := helmAgent.InstallChart(conf, c.Config().DOConf)
+	helmRelease, err := helmAgent.InstallChart(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(

+ 13 - 1
api/server/handlers/release/update_image_batch.go

@@ -81,6 +81,12 @@ func (c *UpdateImageBatchHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 			rel, err := helmAgent.GetRelease(releases[index].Name, 0, false)
 
 			if err != nil {
+				// if this is a release not found error, just return - the release has likely been deleted from the underlying
+				// cluster but has not been deleted from the Porter database yet
+				if strings.Contains(err.Error(), "release: not found") {
+					return
+				}
+
 				mu.Lock()
 				errors = append(errors, fmt.Sprintf("Error for %s, index %d: %s", releases[index].Name, index, err.Error()))
 				mu.Unlock()
@@ -102,9 +108,15 @@ func (c *UpdateImageBatchHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 					Values:     rel.Config,
 				}
 
-				_, err = helmAgent.UpgradeReleaseByValues(conf, c.Config().DOConf)
+				_, err = helmAgent.UpgradeReleaseByValues(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
 
 				if err != nil {
+					// if this is a release not found error, just return - the release has likely been deleted from the underlying
+					// cluster in the time since we've read the release, but has not been deleted from the Porter database yet
+					if strings.Contains(err.Error(), "release: not found") {
+						return
+					}
+
 					mu.Lock()
 					errors = append(errors, fmt.Sprintf("Error for %s, index %d: %s", releases[index].Name, index, err.Error()))
 					mu.Unlock()

+ 2 - 1
api/server/handlers/release/upgrade.go

@@ -160,7 +160,8 @@ func (c *UpgradeReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 		}
 	}
 
-	newHelmRelease, upgradeErr := helmAgent.UpgradeRelease(conf, request.Values, c.Config().DOConf)
+	newHelmRelease, upgradeErr := helmAgent.UpgradeRelease(conf, request.Values, c.Config().DOConf,
+		c.Config().ServerConf.DisablePullSecretsInjection)
 
 	if upgradeErr == nil && newHelmRelease != nil {
 		helmRelease = newHelmRelease

+ 1 - 1
api/server/handlers/release/upgrade_webhook.go

@@ -174,7 +174,7 @@ func (c *WebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		),
 	}
 
-	rel, err = helmAgent.UpgradeReleaseByValues(conf, c.Config().DOConf)
+	rel, err = helmAgent.UpgradeReleaseByValues(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
 
 	if err != nil {
 		notifyOpts.Status = notifier.StatusHelmFailed

+ 3 - 2
api/server/handlers/stack/helpers.go

@@ -54,7 +54,7 @@ func applyAppResource(opts *applyAppResourceOpts) (*release.Release, error) {
 		"revision": opts.stackRevision,
 	}
 
-	return opts.helmAgent.InstallChart(conf, opts.config.DOConf)
+	return opts.helmAgent.InstallChart(conf, opts.config.DOConf, opts.config.ServerConf.DisablePullSecretsInjection)
 }
 
 type rollbackAppResourceOpts struct {
@@ -106,7 +106,8 @@ func updateAppResourceTag(opts *updateAppResourceTagOpts) error {
 		StackRevision: opts.stackRevision,
 	}
 
-	_, err = opts.helmAgent.UpgradeReleaseByValues(conf, opts.config.DOConf)
+	_, err = opts.helmAgent.UpgradeReleaseByValues(conf, opts.config.DOConf,
+		opts.config.ServerConf.DisablePullSecretsInjection)
 
 	return err
 }

+ 1 - 1
api/server/handlers/v1/env_group/create.go

@@ -207,7 +207,7 @@ func rolloutApplications(
 				Values:     newConfig,
 			}
 
-			_, err = helmAgent.UpgradeReleaseByValues(conf, config.DOConf)
+			_, err = helmAgent.UpgradeReleaseByValues(conf, config.DOConf, config.ServerConf.DisablePullSecretsInjection)
 
 			if err != nil {
 				mu.Lock()

+ 1 - 1
api/server/handlers/v1/release/upgrade.go

@@ -144,7 +144,7 @@ func (c *UpgradeReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 		}
 	}
 
-	newHelmRelease, upgradeErr := helmAgent.UpgradeReleaseByValues(conf, c.Config().DOConf)
+	newHelmRelease, upgradeErr := helmAgent.UpgradeReleaseByValues(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
 
 	if upgradeErr == nil && newHelmRelease != nil {
 		helmRelease = newHelmRelease

+ 4 - 0
api/server/shared/config/env/envconfs.go

@@ -106,6 +106,10 @@ type ServerConf struct {
 
 	// Enable gitlab integration
 	EnableGitlab bool `env:"ENABLE_GITLAB,default=false"`
+
+	// DisableRegistrySecretsInjection is used to denote if Porter should not inject
+	// imagePullSecrets into a kubernetes deployment (Porter application)
+	DisablePullSecretsInjection bool `env:"DISABLE_PULL_SECRETS_INJECTION,default=false"`
 }
 
 // DBConf is the database configuration: if generated from environment variables,

+ 1 - 0
api/types/project_integration.go

@@ -80,6 +80,7 @@ type CreateAWSRequest struct {
 	AWSClusterID       string `json:"aws_cluster_id"`
 	AWSAccessKeyID     string `json:"aws_access_key_id"`
 	AWSSecretAccessKey string `json:"aws_secret_access_key"`
+	AWSAssumeRoleArn   string `json:"aws_assume_role_arn"`
 }
 
 type CreateAWSResponse struct {

+ 11 - 11
cli/cmd/config.go

@@ -26,7 +26,7 @@ var configCmd = &cobra.Command{
 	Short: "Commands that control local configuration settings",
 	Run: func(cmd *cobra.Command, args []string) {
 		if err := printConfig(); err != nil {
-			color.New(color.FgRed).Printf("An error occurred: %v\n", err)
+			color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
 			os.Exit(1)
 		}
 	},
@@ -47,14 +47,14 @@ var configSetProjectCmd = &cobra.Command{
 			projID, err := strconv.ParseUint(args[0], 10, 64)
 
 			if err != nil {
-				color.New(color.FgRed).Printf("An error occurred: %v\n", err)
+				color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
 				os.Exit(1)
 			}
 
 			err = cliConf.SetProject(uint(projID))
 
 			if err != nil {
-				color.New(color.FgRed).Printf("An error occurred: %v\n", err)
+				color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
 				os.Exit(1)
 			}
 		}
@@ -76,14 +76,14 @@ var configSetClusterCmd = &cobra.Command{
 			clusterID, err := strconv.ParseUint(args[0], 10, 64)
 
 			if err != nil {
-				color.New(color.FgRed).Printf("An error occurred: %v\n", err)
+				color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
 				os.Exit(1)
 			}
 
 			err = cliConf.SetCluster(uint(clusterID))
 
 			if err != nil {
-				color.New(color.FgRed).Printf("An error occurred: %v\n", err)
+				color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
 				os.Exit(1)
 			}
 		}
@@ -105,14 +105,14 @@ var configSetRegistryCmd = &cobra.Command{
 			registryID, err := strconv.ParseUint(args[0], 10, 64)
 
 			if err != nil {
-				color.New(color.FgRed).Printf("An error occurred: %v\n", err)
+				color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
 				os.Exit(1)
 			}
 
 			err = cliConf.SetRegistry(uint(registryID))
 
 			if err != nil {
-				color.New(color.FgRed).Printf("An error occurred: %v\n", err)
+				color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
 				os.Exit(1)
 			}
 		}
@@ -127,14 +127,14 @@ var configSetHelmRepoCmd = &cobra.Command{
 		hrID, err := strconv.ParseUint(args[0], 10, 64)
 
 		if err != nil {
-			color.New(color.FgRed).Printf("An error occurred: %v\n", err)
+			color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
 			os.Exit(1)
 		}
 
 		err = cliConf.SetHelmRepo(uint(hrID))
 
 		if err != nil {
-			color.New(color.FgRed).Printf("An error occurred: %v\n", err)
+			color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
 			os.Exit(1)
 		}
 	},
@@ -148,7 +148,7 @@ var configSetHostCmd = &cobra.Command{
 		err := cliConf.SetHost(args[0])
 
 		if err != nil {
-			color.New(color.FgRed).Printf("An error occurred: %v\n", err)
+			color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
 			os.Exit(1)
 		}
 	},
@@ -162,7 +162,7 @@ var configSetKubeconfigCmd = &cobra.Command{
 		err := cliConf.SetKubeconfig(args[0])
 
 		if err != nil {
-			color.New(color.FgRed).Printf("An error occurred: %v\n", err)
+			color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
 			os.Exit(1)
 		}
 	},

+ 3 - 3
cli/cmd/config/config.go

@@ -67,7 +67,7 @@ func initAndLoadConfig(_config *CLIConfig) {
 	if _, err := os.Stat(porterDir); os.IsNotExist(err) {
 		os.Mkdir(porterDir, 0700)
 	} else if err != nil {
-		color.New(color.FgRed).Printf("%v\n", err)
+		color.New(color.FgRed).Fprintf(os.Stderr, "%v\n", err)
 		os.Exit(1)
 	}
 
@@ -96,12 +96,12 @@ func initAndLoadConfig(_config *CLIConfig) {
 			err := ioutil.WriteFile(filepath.Join(home, ".porter", "porter.yaml"), []byte{}, 0644)
 
 			if err != nil {
-				color.New(color.FgRed).Printf("%v\n", err)
+				color.New(color.FgRed).Fprintf(os.Stderr, "%v\n", err)
 				os.Exit(1)
 			}
 		} else {
 			// Config file was found but another error was produced
-			color.New(color.FgRed).Printf("%v\n", err)
+			color.New(color.FgRed).Fprintf(os.Stderr, "%v\n", err)
 			os.Exit(1)
 		}
 	}

+ 2 - 1
cli/cmd/connect/ecr.go

@@ -3,6 +3,7 @@ package connect
 import (
 	"context"
 	"fmt"
+	"os"
 	"strings"
 	"time"
 
@@ -53,7 +54,7 @@ Would you like to proceed? %s `,
 		creds, err := agent.CreateIAMECRUser(region)
 
 		if err != nil {
-			color.New(color.FgRed).Printf("Automatic creation failed, manual input required. Error was: %v\n", err)
+			color.New(color.FgRed).Fprintf(os.Stderr, "Automatic creation failed, manual input required. Error was: %v\n", err)
 			return ecrManual(client, projectID, region)
 		}
 

+ 7 - 7
cli/cmd/connect/kubeconfig.go

@@ -351,14 +351,14 @@ Would you like to proceed? %s `,
 		agent, err := gcpLocal.NewDefaultAgent()
 
 		if err != nil {
-			color.New(color.FgRed).Printf("Automatic creation failed, manual input required. Error was: %v\n", err)
+			color.New(color.FgRed).Fprintf(os.Stderr, "Automatic creation failed, manual input required. Error was: %v\n", err)
 			return resolveGCPKeyActionManual(endpoint, clusterName, resolver)
 		}
 
 		projID, err := agent.GetProjectIDForGKECluster(endpoint)
 
 		if err != nil {
-			color.New(color.FgRed).Printf("Automatic creation failed, manual input required. Error was: %v\n", err)
+			color.New(color.FgRed).Fprintf(os.Stderr, "Automatic creation failed, manual input required. Error was: %v\n", err)
 			return resolveGCPKeyActionManual(endpoint, clusterName, resolver)
 		}
 
@@ -370,14 +370,14 @@ Would you like to proceed? %s `,
 		resp, err := agent.CreateServiceAccount(name)
 
 		if err != nil {
-			color.New(color.FgRed).Printf("Automatic creation failed, manual input required. Error was: %v\n", err)
+			color.New(color.FgRed).Fprintf(os.Stderr, "Automatic creation failed, manual input required. Error was: %v\n", err)
 			return resolveGCPKeyActionManual(endpoint, clusterName, resolver)
 		}
 
 		err = agent.SetServiceAccountIAMPolicy(resp)
 
 		if err != nil {
-			color.New(color.FgRed).Printf("Automatic creation failed, manual input required. Error was: %v\n", err)
+			color.New(color.FgRed).Fprintf(os.Stderr, "Automatic creation failed, manual input required. Error was: %v\n", err)
 			return resolveGCPKeyActionManual(endpoint, clusterName, resolver)
 		}
 
@@ -385,7 +385,7 @@ Would you like to proceed? %s `,
 		bytes, err := agent.CreateServiceAccountKey(resp)
 
 		if err != nil {
-			color.New(color.FgRed).Printf("Automatic creation failed, manual input required. Error was: %v\n", err)
+			color.New(color.FgRed).Fprintf(os.Stderr, "Automatic creation failed, manual input required. Error was: %v\n", err)
 			return resolveGCPKeyActionManual(endpoint, clusterName, resolver)
 		}
 
@@ -454,14 +454,14 @@ Would you like to proceed? %s `,
 		agent, err := awsLocal.NewDefaultKubernetesAgent(kubeconfigPath, contextName)
 
 		if err != nil {
-			color.New(color.FgRed).Printf("Automatic creation failed, manual input required. Error was: %v\n", err)
+			color.New(color.FgRed).Fprintf(os.Stderr, "Automatic creation failed, manual input required. Error was: %v\n", err)
 			return resolveAWSActionManual(endpoint, clusterName, awsClusterIDGuess, resolver)
 		}
 
 		creds, err := agent.CreateIAMKubernetesMapping(awsClusterIDGuess)
 
 		if err != nil {
-			color.New(color.FgRed).Printf("Automatic creation failed, manual input required. Error was: %v\n", err)
+			color.New(color.FgRed).Fprintf(os.Stderr, "Automatic creation failed, manual input required. Error was: %v\n", err)
 			return resolveAWSActionManual(endpoint, clusterName, awsClusterIDGuess, resolver)
 		}
 

+ 3 - 2
cli/cmd/errors.go

@@ -3,6 +3,7 @@ package cmd
 import (
 	"context"
 	"errors"
+	"os"
 	"strings"
 
 	"github.com/fatih/color"
@@ -32,7 +33,7 @@ func checkLoginAndRun(args []string, runner func(user *types.GetAuthenticatedUse
 			return ErrCannotConnect
 		}
 
-		red.Printf("Error: %v\n", err.Error())
+		red.Fprintf(os.Stderr, "Error: %v\n", err.Error())
 		return err
 	}
 
@@ -51,7 +52,7 @@ func checkLoginAndRun(args []string, runner func(user *types.GetAuthenticatedUse
 			return nil
 		}
 
-		red.Printf("Error: %v\n", err.Error())
+		red.Fprintf(os.Stderr, "Error: %v\n", err.Error())
 		return err
 	}
 

+ 1 - 1
cli/cmd/list.go

@@ -27,7 +27,7 @@ var listCmd = &cobra.Command{
 				os.Exit(1)
 			}
 		} else {
-			color.New(color.FgRed).Printf("invalid command: %s\n", args[0])
+			color.New(color.FgRed).Fprintf(os.Stderr, "invalid command: %s\n", args[0])
 		}
 	},
 }

+ 28 - 7
cli/cmd/run.go

@@ -111,12 +111,33 @@ func init() {
 }
 
 func run(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	color.New(color.FgGreen).Println("Running", strings.Join(args[1:], " "), "for release", args[0])
+	execArgs := args[1:]
+
+	color.New(color.FgGreen).Println("Running", strings.Join(execArgs, " "), "for release", args[0])
 
 	if nonInteractive {
 		color.New(color.FgBlue).Println("Using non-interactive mode. The first available pod will be used to run the command.")
 	}
 
+	if len(execArgs) > 0 {
+		release, err := client.GetRelease(
+			context.Background(), cliConf.Project, cliConf.Cluster, namespace, args[0],
+		)
+
+		if err != nil {
+			return fmt.Errorf("error fetching release %s: %w", args[0], err)
+		}
+
+		if release.BuildConfig != nil &&
+			(strings.Contains(release.BuildConfig.Builder, "heroku") ||
+				strings.Contains(release.BuildConfig.Builder, "paketo")) &&
+			execArgs[0] != "/cnb/lifecycle/launcher" &&
+			execArgs[0] != "launcher" {
+			// this is a buildpacks release using a heroku builder, prepend the launcher
+			execArgs = append([]string{"/cnb/lifecycle/launcher"}, execArgs...)
+		}
+	}
+
 	podsSimple, err := getPods(client, namespace, args[0])
 
 	if err != nil {
@@ -202,10 +223,10 @@ func run(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []strin
 	}
 
 	if existingPod {
-		return executeRun(config, namespace, selectedPod.Name, selectedContainerName, args[1:])
+		return executeRun(config, namespace, selectedPod.Name, selectedContainerName, execArgs)
 	}
 
-	return executeRunEphemeral(config, namespace, selectedPod.Name, selectedContainerName, args[1:])
+	return executeRunEphemeral(config, namespace, selectedPod.Name, selectedContainerName, execArgs)
 }
 
 func cleanup(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
@@ -815,15 +836,15 @@ func isPodExited(pod *v1.Pod) bool {
 
 func handlePodAttachError(err error, config *PorterRunSharedConfig, namespace, podName, container string) error {
 	if verbose {
-		color.New(color.FgYellow).Printf("Error: %s\n", err)
+		color.New(color.FgYellow).Fprintf(os.Stderr, "Error: %s\n", err)
 	}
-	color.New(color.FgYellow).Println("Could not open a shell to this container. Container logs:")
+	color.New(color.FgYellow).Fprintln(os.Stderr, "Could not open a shell to this container. Container logs:")
 
 	var writtenBytes int64
 	writtenBytes, _ = pipePodLogsToStdout(config, namespace, podName, container, false)
 
 	if verbose || writtenBytes == 0 {
-		color.New(color.FgYellow).Println("Could not get logs. Pod events:")
+		color.New(color.FgYellow).Fprintln(os.Stderr, "Could not get logs. Pod events:")
 		pipeEventsToStdout(config, namespace, podName, container, false)
 	}
 	return err
@@ -892,7 +913,7 @@ func deletePod(config *PorterRunSharedConfig, name, namespace string) error {
 	)
 
 	if err != nil {
-		color.New(color.FgRed).Printf("Could not delete ephemeral pod: %s\n", err.Error())
+		color.New(color.FgRed).Fprintf(os.Stderr, "Could not delete ephemeral pod: %s\n", err.Error())
 		return err
 	}
 

+ 1 - 1
cli/cmd/utils/close.go

@@ -20,7 +20,7 @@ func closeHandler(closer func() error) {
 			os.Exit(0)
 		}
 
-		color.New(color.FgRed).Printf("shutdown unsuccessful: %s\n", err.Error())
+		color.New(color.FgRed).Fprintf(os.Stderr, "shutdown unsuccessful: %s\n", err.Error())
 		os.Exit(1)
 	}()
 }

BIN
dashboard/src/assets/azure.png


+ 12 - 3
dashboard/src/components/Banner.tsx

@@ -7,9 +7,14 @@ import warning from "assets/warning.png";
 interface Props {
   type?: string;
   children: React.ReactNode;
+  noMargin?: boolean;
 }
 
-const Banner: React.FC<Props> = ({ type, children }) => {
+const Banner: React.FC<Props> = ({ 
+  type, 
+  children,
+  noMargin,
+}) => {
   const renderIcon = () => {
     if (type === "error" || type === "warning") {
       return <i className="material-icons-round">warning</i>;
@@ -20,6 +25,7 @@ const Banner: React.FC<Props> = ({ type, children }) => {
   return (
     <StyledBanner
       color={type === "error" ? "#ff385d" : type === "warning" && "#f5cb42"}
+      noMargin={noMargin}
     >
       {renderIcon()}
       {children}
@@ -29,10 +35,13 @@ const Banner: React.FC<Props> = ({ type, children }) => {
 
 export default Banner;
 
-const StyledBanner = styled.div<{ color?: string }>`
+const StyledBanner = styled.div<{ 
+  color?: string;
+  noMargin?: boolean;
+}>`
   height: 40px;
   width: 100%;
-  margin: 5px 0 10px;
+  margin: ${props => props.noMargin ? "5px 0 10px" : ""};
   font-size: 13px;
   font-family: "Work Sans", sans-serif;
   display: flex;

+ 0 - 1
dashboard/src/components/Button.tsx

@@ -37,7 +37,6 @@ const ButtonWrapper = styled.div`
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
-  box-shadow: 0 5px 8px 0px #00000010;
   cursor: ${(props: { disabled?: boolean }) =>
     props.disabled ? "not-allowed" : "pointer"};
 

+ 0 - 2
dashboard/src/components/MultiSaveButton.tsx

@@ -258,8 +258,6 @@ const Button = styled.button<ButtonProps>`
   border: 0;
   border-radius: ${(props) => (props.rounded ? "100px" : "5px 0 0 5px")};
   background: ${(props) => (!props.disabled ? props.color : "#aaaabb")};
-  box-shadow: ${(props) =>
-    !props.disabled ? "0 2px 5px 0 #00000030" : "none"};
   cursor: ${(props) => (!props.disabled ? "pointer" : "default")};
   user-select: none;
   :focus {

+ 0 - 2
dashboard/src/components/ProvisionerStatus.tsx

@@ -951,8 +951,6 @@ const Button = styled.button`
   border: 0;
   border-radius: 5px;
   background: ${(props) => (!props.disabled ? props.color : "#aaaabb")};
-  box-shadow: ${(props) =>
-    !props.disabled ? "0 2px 5px 0 #00000030" : "none"};
   cursor: ${(props) => (!props.disabled ? "pointer" : "default")};
   user-select: none;
   :focus {

+ 0 - 2
dashboard/src/components/SaveButton.tsx

@@ -195,8 +195,6 @@ const Button = styled.button<{
   border: 0;
   border-radius: ${(props) => (props.rounded ? "100px" : "5px")};
   background: ${(props) => (!props.disabled ? props.color : "#aaaabb")};
-  box-shadow: ${(props) =>
-    !props.disabled ? "0 2px 5px 0 #00000030" : "none"};
   cursor: ${(props) => (!props.disabled ? "pointer" : "default")};
   user-select: none;
   :focus {

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

@@ -689,7 +689,6 @@ const UseButton = styled.div`
   font-weight: 500;
   padding: 10px 15px;
   border-radius: 100px;
-  box-shadow: 0 2px 5px 0 #00000030;
   cursor: pointer;
   :hover {
     filter: brightness(120%);

+ 0 - 1
dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx

@@ -419,7 +419,6 @@ const Button = styled.div`
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
-  box-shadow: 0 5px 8px 0px #00000010;
   cursor: ${(props: { disabled?: boolean }) =>
     props.disabled ? "not-allowed" : "pointer"};
 

+ 1 - 2
dashboard/src/main/home/cluster-dashboard/NamespaceSelector.tsx

@@ -146,7 +146,6 @@ const Label = styled.div`
   display: flex;
   align-items: center;
   margin-right: 12px;
-
   > i {
     margin-right: 8px;
     font-size: 18px;
@@ -157,4 +156,4 @@ const StyledNamespaceSelector = styled.div`
   display: flex;
   align-items: center;
   font-size: 13px;
-`;
+`;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/SortSelector.tsx

@@ -53,7 +53,7 @@ export default class SortSelector extends Component<PropsType, StateType> {
           options={this.getSortOptions()}
           name="Sort"
           icon={sort}
-          dropdownAlignRight={true}
+          dropdownAlignRight={false}
           noMargin
         />
       </StyledSortSelector>

+ 0 - 2
dashboard/src/main/home/cluster-dashboard/dashboard/ClusterSettings.tsx

@@ -300,8 +300,6 @@ const Button = styled.button`
   border: 0;
   border-radius: 5px;
   background: ${(props) => (!props.disabled ? props.color : "#aaaabb")};
-  box-shadow: ${(props) =>
-    !props.disabled ? "0 2px 5px 0 #00000030" : "none"};
   cursor: ${(props) => (!props.disabled ? "pointer" : "default")};
   user-select: none;
   :focus {

+ 0 - 1
dashboard/src/main/home/cluster-dashboard/dashboard/NamespaceList.tsx

@@ -258,7 +258,6 @@ const Button = styled.div`
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
-  box-shadow: 0 5px 8px 0px #00000010;
   cursor: ${(props: { disabled?: boolean }) =>
     props.disabled ? "not-allowed" : "pointer"};
 

+ 0 - 1
dashboard/src/main/home/cluster-dashboard/dashboard/events/EventsTab.tsx

@@ -162,7 +162,6 @@ const InstallPorterAgentButton = styled.button`
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
-  box-shadow: 0 5px 8px 0px #00000010;
   cursor: ${(props: { disabled?: boolean }) =>
     props.disabled ? "not-allowed" : "pointer"};
   background: ${(props: { disabled?: boolean }) =>

+ 0 - 2
dashboard/src/main/home/cluster-dashboard/databases/DatabasesList.tsx

@@ -316,7 +316,6 @@ const Button = styled(Link)`
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
-  box-shadow: 0 5px 8px 0px #00000010;
   cursor: ${(props: { disabled?: boolean }) =>
     props.disabled ? "not-allowed" : "pointer"};
 
@@ -354,7 +353,6 @@ const ConnectButton = styled.button<{}>`
   border: 0;
   border-radius: 5px;
   background: #5561c0;
-  box-shadow: 0 2px 5px 0 #00000030;
   cursor: pointer;
   user-select: none;
   :focus {

+ 0 - 1
dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx

@@ -192,7 +192,6 @@ const Button = styled.div`
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
-  box-shadow: 0 5px 8px 0px #00000010;
   cursor: ${(props: { disabled?: boolean }) =>
     props.disabled ? "not-allowed" : "pointer"};
 

+ 0 - 2
dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx

@@ -871,8 +871,6 @@ const Button = styled.button`
   border: 0;
   border-radius: 5px;
   background: ${(props) => (!props.disabled ? props.color : "#aaaabb")};
-  box-shadow: ${(props) =>
-    !props.disabled ? "0 2px 5px 0 #00000030" : "none"};
   cursor: ${(props) => (!props.disabled ? "pointer" : "default")};
   user-select: none;
   :focus {

+ 0 - 2
dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx

@@ -355,8 +355,6 @@ const Button = styled.button`
   border: 0;
   border-radius: 5px;
   background: ${(props) => (!props.disabled ? props.color : "#aaaabb")};
-  box-shadow: ${(props) =>
-    !props.disabled ? "0 2px 5px 0 #00000030" : "none"};
   cursor: ${(props) => (!props.disabled ? "pointer" : "default")};
   user-select: none;
   :focus {

+ 0 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventsTab.tsx

@@ -164,7 +164,6 @@ const InstallPorterAgentButton = styled.button`
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
-  box-shadow: 0 5px 8px 0px #00000010;
   cursor: ${(props: { disabled?: boolean }) =>
     props.disabled ? "not-allowed" : "pointer"};
   background: ${(props: { disabled?: boolean }) =>

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepo.tsx

@@ -148,7 +148,7 @@ const ConnectNewRepo: React.FC = () => {
 
       <ActionContainer>
         <SaveButton
-          text="Add Repository"
+          text="Add repository"
           disabled={actionConfig.git_repo_id ? false : true}
           onClick={addRepo}
           makeFlush={true}

+ 1 - 2
dashboard/src/main/home/cluster-dashboard/preview-environments/components/ButtonEnablePREnvironments.tsx

@@ -106,7 +106,7 @@ const ButtonEnablePREnvironments = ({ setIsReady }: Props) => {
     <>
       <Container>
         <Button {...getButtonProps()}>
-          <i className="material-icons">add</i> Add Repository
+          <i className="material-icons">add</i> Add repository
         </Button>
       </Container>
     </>
@@ -134,7 +134,6 @@ const Button = styled(DynamicLink)`
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
-  box-shadow: 0 5px 8px 0px #00000010;
   cursor: ${(props: { disabled?: boolean }) =>
     props.disabled ? "not-allowed" : "pointer"};
 

+ 0 - 1
dashboard/src/main/home/cluster-dashboard/preview-environments/components/RecreateWorkflowFilesModal.tsx

@@ -62,7 +62,6 @@ const Button = styled.button`
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
-  box-shadow: 0 5px 8px 0px #00000010;
   cursor: pointer;
   border: none;
   :not(:last-child) {

+ 0 - 1
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentCard.tsx

@@ -250,7 +250,6 @@ const Button = styled.button`
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
-  box-shadow: 0 5px 8px 0px #00000010;
   cursor: pointer;
   border: none;
   :not(:last-child) {

+ 0 - 1
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentsList.tsx

@@ -179,7 +179,6 @@ const Button = styled(DynamicLink)`
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
-  box-shadow: 0 5px 8px 0px #00000010;
   cursor: ${(props: { disabled?: boolean }) =>
     props.disabled ? "not-allowed" : "pointer"};
 

+ 1 - 2
dashboard/src/main/home/cluster-dashboard/stacks/Dashboard.tsx

@@ -50,7 +50,7 @@ const Dashboard = () => {
           <RadioFilter
             selected={currentSort}
             noMargin
-            dropdownAlignRight={true}
+            dropdownAlignRight={false}
             setSelected={(sortType: any) => setCurrentSort(sortType as any)}
             options={[
               {
@@ -105,7 +105,6 @@ const Button = styled(DynamicLink)`
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
-  box-shadow: 0 5px 8px 0px #00000010;
   cursor: ${(props: { disabled?: boolean }) =>
     props.disabled ? "not-allowed" : "pointer"};
 

+ 0 - 2
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Settings.tsx

@@ -128,8 +128,6 @@ const Button = styled.button`
   border: 0;
   border-radius: 5px;
   background: ${(props) => (!props.disabled ? props.color : "#aaaabb")};
-  box-shadow: ${(props) =>
-    !props.disabled ? "0 2px 5px 0 #00000030" : "none"};
   cursor: ${(props) => (!props.disabled ? "pointer" : "default")};
   user-select: none;
   :focus {

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

@@ -157,7 +157,6 @@ export const Action = {
     overflow: hidden;
     white-space: nowrap;
     text-overflow: ellipsis;
-    box-shadow: 0 5px 8px 0px #00000010;
     cursor: ${(props: { disabled?: boolean }) =>
       props.disabled ? "not-allowed" : "pointer"};
 

+ 14 - 24
dashboard/src/main/home/dashboard/Dashboard.tsx

@@ -13,6 +13,7 @@ import TabRegion from "components/TabRegion";
 import Provisioner from "../provisioner/Provisioner";
 import FormDebugger from "components/porter-form/FormDebugger";
 import TitleSection from "components/TitleSection";
+import Banner from "components/Banner";
 
 import { pushFiltered, pushQueryParams } from "shared/routing";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
@@ -115,24 +116,20 @@ class Dashboard extends Component<PropsType, StateType> {
       );
     } else if (this.currentTab() === "create-cluster") {
       let helperText = "Create a cluster to link to this project";
-      let helperIcon = "info";
-      let helperColor = "white";
+      let helperType = "info";
       if (
-        this.context.hasBillingEnabled &&
-        this.context.usage.current.clusters !== 0 &&
-        this.context.usage.current.clusters >= this.context.usage.limit.clusters
+        true
       ) {
         helperText =
           "You need to update your billing to provision or connect a new cluster";
-        helperIcon = "warning";
-        helperColor = "#f5cb42";
+        helperType = "warning";
       }
       return (
         <>
-          <Banner color={helperColor}>
-            <i className="material-icons">{helperIcon}</i>
+          <Banner type={helperType} noMargin>
             {helperText}
           </Banner>
+          <Br />
           <ProvisionerSettings infras={this.state.infras} provisioner={true} />
         </>
       );
@@ -228,25 +225,18 @@ const Br = styled.div`
   height: 1px;
 `;
 
+const Code = styled.div`
+  font-family: monospace;
+  margin: 0 7px;
+`;
+
 const DashboardWrapper = styled.div`
   padding-bottom: 100px;
 `;
 
-const Banner = styled.div<{ color: string }>`
-  height: 40px;
-  width: 100%;
-  margin: 5px 0 30px;
-  font-size: 13px;
-  display: flex;
-  border-radius: 5px;
-  padding-left: 15px;
-  align-items: center;
-  background: #ffffff11;
-  color: ${(props) => props.color};
-  > i {
-    margin-right: 10px;
-    font-size: 18px;
-  }
+const A = styled.a`
+  margin-left: 10px;
+  color: #8590ff;
 `;
 
 const TopRow = styled.div`

+ 14 - 0
dashboard/src/main/home/infrastructure/components/credentials/AWSCredentialForm.tsx

@@ -45,6 +45,7 @@ const AWSCredentialForm: React.FunctionComponent<Props> = ({
   const { currentProject, setCurrentError } = useContext(Context);
   const [accessId, setAccessId] = useState("");
   const [secretKey, setSecretKey] = useState("");
+  const [assumeRoleArn, setAssumeRoleArn] = useState("");
   const [buttonStatus, setButtonStatus] = useState("");
   const [awsRegion, setAWSRegion] = useState("us-east-1");
   const [isLoading, setIsLoading] = useState(false);
@@ -60,6 +61,7 @@ const AWSCredentialForm: React.FunctionComponent<Props> = ({
           aws_region: awsRegion,
           aws_access_key_id: accessId,
           aws_secret_access_key: secretKey,
+          aws_assume_role_arn: assumeRoleArn,
         },
         {
           id: currentProject.id,
@@ -124,6 +126,17 @@ const AWSCredentialForm: React.FunctionComponent<Props> = ({
         }}
         label="📍 AWS Region"
       />
+      <InputRow
+        type="text"
+        value={assumeRoleArn}
+        setValue={(x: string) => {
+          setAssumeRoleArn(x);
+        }}
+        label="👤 (Optional) AWS Assume Role ARN"
+        placeholder="ex: arn:aws:iam::01234567890:role/my_assumed_role"
+        width="100%"
+        isRequired={false}
+      />
       <Flex>
         <SaveButton
           text="Continue"
@@ -145,6 +158,7 @@ const Flex = styled.div`
   display: flex;
   color: #ffffff;
   align-items: center;
+  margin-top: 30px;
   > i {
     color: #aaaabb;
     font-size: 20px;

+ 0 - 1
dashboard/src/main/home/integrations/IntegrationCategories.tsx

@@ -240,7 +240,6 @@ const Button = styled.div`
   padding-right: 12px;
   border-radius: 5px;
   cursor: pointer;
-  box-shadow: 0 5px 8px 0px #00000010;
   display: flex;
   flex-direction: row;
   align-items: center;

+ 0 - 1
dashboard/src/main/home/integrations/IntegrationList.tsx

@@ -302,7 +302,6 @@ const Button = styled.div`
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
-  box-shadow: 0 5px 8px 0px #00000010;
   cursor: ${(props: { disabled?: boolean }) =>
     props.disabled ? "not-allowed" : "pointer"};
 

+ 0 - 1
dashboard/src/main/home/launch/expanded-template/TemplateInfo.tsx

@@ -283,7 +283,6 @@ const Button = styled.div`
   border-radius: 3px;
   cursor: ${(props: { isDisabled: boolean }) =>
     !props.isDisabled ? "pointer" : "default"};
-  box-shadow: 0 5px 8px 0px #00000010;
   display: flex;
   flex-direction: row;
   align-items: center;

+ 0 - 1
dashboard/src/main/home/modals/RedirectToOnboardingModal.tsx

@@ -46,7 +46,6 @@ const ContinueButton = styled.a`
   width: 160px;
   border-radius: 5px;
   background: #616feecc;
-  box-shadow: 0 2px 5px 0 #00000030;
   cursor: pointer;
   user-select: none;
   :focus {

+ 0 - 1
dashboard/src/main/home/modals/UsageWarningModal.tsx

@@ -167,7 +167,6 @@ const Button = styled.button`
   padding: 10px 15px;
   border-radius: 3px;
   cursor: "pointer";
-  box-shadow: 0 5px 8px 0px #00000010;
   display: flex;
   flex-direction: row;
   align-items: center;

+ 0 - 1
dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_DORegistryForm.tsx

@@ -290,7 +290,6 @@ const ConnectDigitalOceanButton = styled.a`
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
-  box-shadow: 0 5px 8px 0px #00000010;
   cursor: ${(props: { disabled?: boolean }) =>
     props.disabled ? "not-allowed" : "pointer"};
 

+ 0 - 1
dashboard/src/main/home/onboarding/steps/ConnectSource.tsx

@@ -283,7 +283,6 @@ const ConnectToGithubButton = styled.a`
   margin-top: 25px;
   margin-bottom: 25px;
   text-overflow: ellipsis;
-  box-shadow: 0 5px 8px 0px #00000010;
   cursor: ${(props: { disabled?: boolean }) =>
     props.disabled ? "not-allowed" : "pointer"};
 

+ 0 - 2
dashboard/src/main/home/project-settings/APITokensSection.tsx

@@ -231,8 +231,6 @@ const InviteButton = styled.div<{ disabled: boolean }>`
   border: 0;
   border-radius: 5px;
   background: ${(props) => (!props.disabled ? "#616FEEcc" : "#aaaabb")};
-  box-shadow: ${(props) =>
-    !props.disabled ? "0 2px 5px 0 #00000030" : "none"};
   cursor: ${(props) => (!props.disabled ? "pointer" : "default")};
   user-select: none;
   :focus {

+ 0 - 2
dashboard/src/main/home/project-settings/InviteList.tsx

@@ -579,8 +579,6 @@ const InviteButton = styled.div<{ disabled: boolean }>`
   border: 0;
   border-radius: 5px;
   background: ${(props) => (!props.disabled ? "#616FEEcc" : "#aaaabb")};
-  box-shadow: ${(props) =>
-    !props.disabled ? "0 2px 5px 0 #00000030" : "none"};
   cursor: ${(props) => (!props.disabled ? "pointer" : "default")};
   user-select: none;
   :focus {

+ 0 - 1
dashboard/src/main/home/project-settings/ProjectSettings.tsx

@@ -232,7 +232,6 @@ const DeleteButton = styled.div`
   margin-left: 0;
   justify-content: center;
   border-radius: 5px;
-  box-shadow: 0 2px 5px 0 #00000030;
   cursor: pointer;
   user-select: none;
   :focus {

+ 296 - 0
dashboard/src/main/home/provisioner/AzureFormSection.tsx

@@ -0,0 +1,296 @@
+import React, { Component, useContext, useEffect, useState } from "react";
+import styled from "styled-components";
+
+import close from "assets/close.png";
+import { isAlphanumeric } from "shared/common";
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { InfraType } from "shared/types";
+
+import InputRow from "components/form-components/InputRow";
+import CheckboxRow from "components/form-components/CheckboxRow";
+import SelectRow from "components/form-components/SelectRow";
+import Helper from "components/form-components/Helper";
+import Heading from "components/form-components/Heading";
+import SaveButton from "components/SaveButton";
+import CheckboxList from "components/form-components/CheckboxList";
+
+type PropsType = {
+  setSelectedProvisioner: (x: string | null) => void;
+  handleError: () => void;
+  projectName: string;
+  highlightCosts?: boolean;
+  infras: InfraType[];
+  trackOnSave: () => void;
+};
+
+const provisionOptions = [
+  { value: "docr", label: "DigitalOcean Container Registry" },
+  { value: "doks", label: "DigitalOcean Kubernetes Service" },
+];
+
+const tierOptions = [
+  { value: "basic", label: "Basic" },
+  { value: "professional", label: "Professional" },
+];
+
+const regionOptions = [
+  { value: "ams3", label: "Amsterdam 3" },
+  { value: "blr1", label: "Bangalore 1" },
+  { value: "fra1", label: "Frankfurt 1" },
+  { value: "lon1", label: "London 1" },
+  { value: "nyc1", label: "New York 1" },
+  { value: "nyc3", label: "New York 3" },
+  { value: "sfo2", label: "San Francisco 2" },
+  { value: "sfo3", label: "San Francisco 3" },
+  { value: "sgp1", label: "Singapore 1" },
+  { value: "tor1", label: "Toronto 1" },
+];
+
+const AzureFormSectionFC: React.FC<PropsType> = (props) => {
+  const [selectedInfras, setSelectedInfras] = useState([...provisionOptions]);
+  const [applicationId, setApplicationId] = useState("");
+  const [azureServicePrincipal, setAzureServicePrincipal] = useState("");
+  const [tenantId, setTenantId] = useState("");
+  const [subscriptionId, setSubscriptionId] = useState("");
+  const [clusterName, setClusterName] = useState("");
+  const [clusterNameSet, setClusterNameSet] = useState(false);
+  const [provisionConfirmed, setProvisionConfirmed] = useState(false);
+
+  const context = useContext(Context);
+
+  // This is added only for tracking purposes
+  // With this prop we will track down if the user has had an intent of filling the formulary
+  const [isFormDirty, setIsFormDirty] = useState(false);
+
+  useEffect(() => {
+    if (!isFormDirty) {
+      return;
+    }
+
+    window.analytics?.track("provision_form-dirty", {
+      provider: "do",
+    });
+  }, [isFormDirty]);
+
+  useEffect(() => {
+    if (props.infras) {
+      // From the dashboard, only uncheck and disable if "creating" or "created"
+      let filtered = selectedInfras;
+      props.infras.forEach((infra: InfraType, i: number) => {
+        let { kind, status } = infra;
+        if (status === "creating" || status === "created") {
+          filtered = filtered.filter((item: any) => {
+            return item.value !== kind;
+          });
+        }
+      });
+      setSelectedInfras(filtered);
+    }
+  }, [props.infras]);
+
+  useEffect(() => {
+    setClusterNameIfNotSet();
+  }, [props.projectName]);
+
+  const setClusterNameIfNotSet = () => {
+    let projectName = props.projectName || context.currentProject?.name;
+
+    if (!clusterNameSet && !clusterName.includes(`${projectName}-cluster`)) {
+      setClusterName(
+        `${projectName}-cluster-${Math.random().toString(36).substring(2, 8)}`
+      );
+    }
+  };
+
+  const checkFormDisabled = () => {
+    if (!provisionConfirmed) {
+      return true;
+    }
+
+    let { projectName } = props;
+    if (projectName || projectName === "") {
+      return (
+        !isAlphanumeric(projectName) ||
+        selectedInfras.length === 0 ||
+        !clusterName
+      );
+    } else {
+      return selectedInfras.length === 0 || !clusterName;
+    }
+  };
+
+  const getButtonStatus = () => {
+    if (props.projectName) {
+      if (!isAlphanumeric(props.projectName)) {
+        return "Project name contains illegal characters";
+      }
+    }
+    if (!provisionConfirmed || props.projectName === "" || !clusterName) {
+      return "Required fields missing";
+    }
+  };
+
+  return (
+    <StyledAWSFormSection>
+      <FormSection>
+        <CloseButton onClick={() => props.setSelectedProvisioner(null)}>
+          <CloseButtonImg src={close} />
+        </CloseButton>
+        <Heading isAtTop={true}>Azure credentials</Heading>
+        <InputRow
+          type="text"
+          value={applicationId}
+          setValue={(x: string) => setApplicationId(x)}
+          label="⚙️ Azure application (client) ID"
+          placeholder="ex: 123456780-abcd-1234-abcd-12345678"
+          width="100%"
+          isRequired={true}
+        />
+        <InputRow
+          type="password"
+          value={azureServicePrincipal}
+          setValue={(x: string) => setAzureServicePrincipal(x)}
+          label="🔒 Azure service principal"
+          placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
+          width="100%"
+          isRequired={true}
+        />
+        <InputRow
+          type="text"
+          value={tenantId}
+          setValue={(x: string) => setTenantId(x)}
+          label="👤 Azure tenant ID"
+          placeholder="ex: 123456780-abcd-1234-abcd-12345678"
+          width="100%"
+          isRequired={true}
+        />
+        <InputRow
+          type="text"
+          value={subscriptionId}
+          setValue={(x: string) => setSubscriptionId(x)}
+          label="💳 Azure subscription ID"
+          placeholder="ex: 123456780-abcd-1234-abcd-12345678"
+          width="100%"
+          isRequired={true}
+        />
+      </FormSection>
+      {props.children ? props.children : <Padding />}
+      <SaveButton
+        text="Submit"
+        onClick={() => {}}
+        makeFlush={true}
+        helper="Note: Provisioning can take up to 15 minutes"
+      />
+    </StyledAWSFormSection>
+  );
+};
+
+export default AzureFormSectionFC;
+
+const Highlight = styled.a`
+  color: #8590ff;
+  cursor: pointer;
+  text-decoration: none;
+  margin-left: 5px;
+`;
+
+const Padding = styled.div`
+  height: 15px;
+`;
+
+const Br = styled.div`
+  width: 100%;
+  height: 2px;
+`;
+
+const StyledAWSFormSection = styled.div`
+  position: relative;
+  padding-bottom: 35px;
+`;
+
+const FormSection = styled.div`
+  background: #ffffff11;
+  margin-top: 25px;
+  background: #26282f;
+  border-radius: 5px;
+  margin-bottom: 25px;
+  padding: 25px;
+  padding-bottom: 16px;
+  font-size: 13px;
+  animation: fadeIn 0.3s 0s;
+  position: relative;
+`;
+
+const CloseButton = styled.div`
+  position: absolute;
+  display: block;
+  width: 40px;
+  height: 40px;
+  padding: 13px 0 12px 0;
+  z-index: 1;
+  text-align: center;
+  border-radius: 50%;
+  right: 15px;
+  top: 12px;
+  cursor: pointer;
+  :hover {
+    background-color: #ffffff11;
+  }
+`;
+
+const GuideButton = styled.a`
+  display: flex;
+  align-items: center;
+  margin-left: 20px;
+  color: #aaaabb;
+  font-size: 13px;
+  margin-bottom: -1px;
+  border: 1px solid #aaaabb;
+  padding: 5px 10px;
+  padding-left: 6px;
+  border-radius: 5px;
+  cursor: pointer;
+  :hover {
+    background: #ffffff11;
+    color: #ffffff;
+    border: 1px solid #ffffff;
+
+    > i {
+      color: #ffffff;
+    }
+  }
+
+  > i {
+    color: #aaaabb;
+    font-size: 16px;
+    margin-right: 6px;
+  }
+`;
+
+const CloseButtonImg = styled.img`
+  width: 14px;
+  margin: 0 auto;
+`;
+
+const CostHighlight = styled.span<{ highlight: boolean }>`
+  background-color: ${(props) => props.highlight && "yellow"};
+`;
+
+const StyledInfoTooltip = styled.div`
+  display: inline-block;
+  position: relative;
+  margin-right: 2px;
+  > i {
+    display: flex;
+    align-items: center;
+    position: absolute;
+    top: -10px;
+    font-size: 10px;
+    color: #858faaaa;
+    cursor: pointer;
+    :hover {
+      color: #aaaabb;
+    }
+  }
+`;

+ 36 - 1
dashboard/src/main/home/provisioner/ProvisionerSettings.tsx

@@ -15,10 +15,12 @@ import Helper from "components/form-components/Helper";
 import AWSFormSection from "./AWSFormSection";
 import GCPFormSection from "./GCPFormSection";
 import DOFormSection from "./DOFormSection";
+import AzureFormSection from "./AzureFormSection";
 import SaveButton from "components/SaveButton";
 import ExistingClusterSection from "./ExistingClusterSection";
 import { useHistory, useLocation } from "react-router";
 import { pushFiltered } from "shared/routing";
+import azure from "assets/azure.png";
 
 type Props = {
   isInNewProject?: boolean;
@@ -163,6 +165,21 @@ const ProvisionerSettings: React.FC<Props> = ({
       );
     }
 
+    if (selectedProvider === "azure") {
+      return (
+        <AzureFormSection
+          handleError={handleError}
+          projectName={projectName}
+          infras={infras}
+          highlightCosts={highlightCosts}
+          setSelectedProvisioner={(x: string | null) => {
+            handleSelectProvider(x);
+          }}
+          trackOnSave={() => trackOnSave(selectedProvider)}
+        />
+      );
+    }
+
     if (selectedProvider === "do") {
       return (
         <DOFormSection
@@ -267,10 +284,28 @@ const ProvisionerSettings: React.FC<Props> = ({
                   <InfoTooltip text={""} />
                   */}
                 </CostSection>
-                <BlockDescription>Hosted in your own cloud.</BlockDescription>
+                <BlockDescription>Hosted in your own cloud</BlockDescription>
               </Block>
             );
           })}
+          {
+            window.location.href.includes("dashboard.staging.getporter.dev") && (
+              <Block
+                key={3}
+                disabled={isUsageExceeded}
+                onClick={() => {
+                  if (!isUsageExceeded) {
+                    handleSelectProvider("azure");
+                    setHighlightCosts(false);
+                  }
+                }}
+              >
+                <Icon src={azure} />
+                <BlockTitle>Azure</BlockTitle>
+                <BlockDescription>Hosted in your own cloud</BlockDescription>
+              </Block>
+            )
+          }
         </BlockList>
       ) : (
         <>{renderSelectedProvider()}</>

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

@@ -76,6 +76,7 @@ const createAWSIntegration = baseApi<
     aws_cluster_id?: string;
     aws_access_key_id: string;
     aws_secret_access_key: string;
+    aws_assume_role_arn?: string;
   },
   { id: number }
 >("POST", (pathParams) => {

+ 2 - 0
ee/api/server/handlers/credentials/get_credentials.go

@@ -1,3 +1,4 @@
+//go:build ee
 // +build ee
 
 package credentials
@@ -120,6 +121,7 @@ func (c *CredentialsGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 			AWSClusterID:       awsInt.AWSClusterID,
 			AWSSecretAccessKey: awsInt.AWSSecretAccessKey,
 			AWSSessionToken:    awsInt.AWSSessionToken,
+			AWSAssumeRoleArn:   []byte(awsInt.AWSAssumeRoleArn),
 		}
 	}
 

+ 8 - 2
internal/helm/agent.go

@@ -180,6 +180,7 @@ func (a *Agent) UpgradeRelease(
 	conf *UpgradeReleaseConfig,
 	values string,
 	doAuth *oauth2.Config,
+	disablePullSecretsInjection bool,
 ) (*release.Release, error) {
 	valuesYaml, err := chartutil.ReadValues([]byte(values))
 
@@ -189,13 +190,14 @@ func (a *Agent) UpgradeRelease(
 
 	conf.Values = valuesYaml
 
-	return a.UpgradeReleaseByValues(conf, doAuth)
+	return a.UpgradeReleaseByValues(conf, doAuth, disablePullSecretsInjection)
 }
 
 // UpgradeReleaseByValues upgrades a release by unmarshaled yaml values
 func (a *Agent) UpgradeReleaseByValues(
 	conf *UpgradeReleaseConfig,
 	doAuth *oauth2.Config,
+	disablePullSecretsInjection bool,
 ) (*release.Release, error) {
 	// grab the latest release
 	rel, err := a.GetRelease(conf.Name, 0, true)
@@ -220,6 +222,7 @@ func (a *Agent) UpgradeReleaseByValues(
 		rel.Namespace,
 		conf.Registries,
 		doAuth,
+		disablePullSecretsInjection,
 	)
 
 	if err != nil {
@@ -383,6 +386,7 @@ func (a *Agent) InstallChartFromValuesBytes(
 	conf *InstallChartConfig,
 	values []byte,
 	doAuth *oauth2.Config,
+	disablePullSecretsInjection bool,
 ) (*release.Release, error) {
 	valuesYaml, err := chartutil.ReadValues(values)
 
@@ -392,13 +396,14 @@ func (a *Agent) InstallChartFromValuesBytes(
 
 	conf.Values = valuesYaml
 
-	return a.InstallChart(conf, doAuth)
+	return a.InstallChart(conf, doAuth, disablePullSecretsInjection)
 }
 
 // InstallChart installs a new chart
 func (a *Agent) InstallChart(
 	conf *InstallChartConfig,
 	doAuth *oauth2.Config,
+	disablePullSecretsInjection bool,
 ) (*release.Release, error) {
 	cmd := action.NewInstall(a.ActionConfig)
 
@@ -423,6 +428,7 @@ func (a *Agent) InstallChart(
 		conf.Namespace,
 		conf.Registries,
 		doAuth,
+		disablePullSecretsInjection,
 	)
 
 	if err != nil {

+ 2 - 1
internal/helm/postrenderer.go

@@ -32,11 +32,12 @@ func NewPorterPostrenderer(
 	namespace string,
 	regs []*models.Registry,
 	doAuth *oauth2.Config,
+	disablePullSecretsInjection bool,
 ) (postrender.PostRenderer, error) {
 	var dockerSecretsPostrenderer *DockerSecretsPostRenderer
 	var err error
 
-	if cluster != nil && agent != nil && regs != nil && len(regs) > 0 {
+	if !disablePullSecretsInjection && cluster != nil && agent != nil && regs != nil && len(regs) > 0 {
 		dockerSecretsPostrenderer, err = NewDockerSecretsPostRenderer(cluster, repo, agent, namespace, regs, doAuth)
 
 		if err != nil {

+ 6 - 2
internal/models/integrations/aws.go

@@ -29,6 +29,9 @@ type AWSIntegration struct {
 	// 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.
 	// ------------------------------------------------------------------
@@ -141,8 +144,9 @@ func (a *AWSIntegration) GetBearerToken(
 	}
 
 	tok, err := generator.GetWithOptions(&token.GetTokenOptions{
-		Session:   sess,
-		ClusterID: validClusterId,
+		AssumeRoleARN: a.AWSAssumeRoleArn,
+		Session:       sess,
+		ClusterID:     validClusterId,
 	})
 
 	if err != nil {

+ 3 - 0
internal/repository/credentials/credentials.go

@@ -37,6 +37,9 @@ type AWSCredential struct {
 
 	// An optional region associated with this AWS credential
 	AWSRegion []byte `json:"aws_region"`
+
+	// An optional assume role ARN
+	AWSAssumeRoleArn []byte `json:"aws_assume_role_arn"`
 }
 
 type AzureCredential struct {

+ 2 - 2
internal/templater/helm/values/writer.go

@@ -42,7 +42,7 @@ func (w *TemplateWriter) Create(
 		Values:    vals,
 	}
 
-	_, err := w.Agent.InstallChart(conf, nil)
+	_, err := w.Agent.InstallChart(conf, nil, false)
 
 	if err != nil {
 		return nil, err
@@ -64,7 +64,7 @@ func (w *TemplateWriter) Update(
 		Values: vals,
 	}
 
-	_, err := w.Agent.UpgradeReleaseByValues(conf, nil)
+	_, err := w.Agent.UpgradeReleaseByValues(conf, nil, false)
 
 	if err != nil {
 		return nil, err

+ 3 - 2
provisioner/client/client.go

@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"net/http"
 	"net/url"
+	"os"
 	"strings"
 	"time"
 
@@ -155,9 +156,9 @@ func (c *Client) postRequest(relPath string, data interface{}, response interfac
 
 		if i != int(retryCount)-1 {
 			if httpErr != nil {
-				fmt.Printf("Error: %s (status code %d), retrying request...\n", httpErr.Error, httpErr.Code)
+				fmt.Fprintf(os.Stderr, "Error: %s (status code %d), retrying request...\n", httpErr.Error, httpErr.Code)
 			} else {
-				fmt.Printf("Error: %v, retrying request...\n", err)
+				fmt.Fprintf(os.Stderr, "Error: %v, retrying request...\n", err)
 			}
 		}
 	}

+ 2 - 0
provisioner/server/handlers/credentials/get_credentials_ee.go

@@ -1,3 +1,4 @@
+//go:build ee
 // +build ee
 
 package credentials
@@ -99,6 +100,7 @@ func (c *CredentialsGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 			AWSSecretAccessKey: awsInt.AWSSecretAccessKey,
 			AWSSessionToken:    awsInt.AWSSessionToken,
 			AWSRegion:          []byte(awsInt.AWSRegion),
+			AWSAssumeRoleArn:   []byte(awsInt.AWSAssumeRoleArn),
 		}
 	} else if ceToken.AzureCredentialID != 0 {
 		azInt, err := repo.AzureIntegration().ReadAzureIntegration(ceToken.ProjectID, ceToken.AzureCredentialID)