Răsfoiți Sursa

Merge branch 'nafees/pr-env-validator' into dev

Mohammed Nafees 3 ani în urmă
părinte
comite
e8be4f26ef
42 a modificat fișierele cu 543 adăugiri și 47 ștergeri
  1. 127 0
      api/server/handlers/environment/validate_porter_yaml.go
  2. 12 0
      api/server/handlers/release/update_image_batch.go
  3. 30 1
      api/server/router/cluster.go
  4. 8 0
      api/types/environment.go
  5. BIN
      dashboard/src/assets/azure.png
  6. 0 1
      dashboard/src/components/Button.tsx
  7. 0 2
      dashboard/src/components/MultiSaveButton.tsx
  8. 0 2
      dashboard/src/components/ProvisionerStatus.tsx
  9. 0 2
      dashboard/src/components/SaveButton.tsx
  10. 0 1
      dashboard/src/components/repo-selector/ContentsList.tsx
  11. 0 1
      dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx
  12. 1 1
      dashboard/src/main/home/cluster-dashboard/SortSelector.tsx
  13. 0 2
      dashboard/src/main/home/cluster-dashboard/dashboard/ClusterSettings.tsx
  14. 0 1
      dashboard/src/main/home/cluster-dashboard/dashboard/NamespaceList.tsx
  15. 0 1
      dashboard/src/main/home/cluster-dashboard/dashboard/events/EventsTab.tsx
  16. 0 1
      dashboard/src/main/home/cluster-dashboard/dashboard/incidents/IncidentsTab.tsx
  17. 0 2
      dashboard/src/main/home/cluster-dashboard/databases/DatabasesList.tsx
  18. 0 1
      dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx
  19. 0 2
      dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx
  20. 0 2
      dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx
  21. 0 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventsTab.tsx
  22. 0 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/incidents/EventsTab.tsx
  23. 0 1
      dashboard/src/main/home/cluster-dashboard/preview-environments/components/ButtonEnablePREnvironments.tsx
  24. 0 1
      dashboard/src/main/home/cluster-dashboard/preview-environments/components/RecreateWorkflowFilesModal.tsx
  25. 0 1
      dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentCard.tsx
  26. 0 1
      dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentsList.tsx
  27. 1 2
      dashboard/src/main/home/cluster-dashboard/stacks/Dashboard.tsx
  28. 0 2
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Settings.tsx
  29. 0 1
      dashboard/src/main/home/cluster-dashboard/stacks/components/styles.ts
  30. 0 1
      dashboard/src/main/home/integrations/IntegrationCategories.tsx
  31. 0 1
      dashboard/src/main/home/integrations/IntegrationList.tsx
  32. 0 1
      dashboard/src/main/home/launch/expanded-template/TemplateInfo.tsx
  33. 0 1
      dashboard/src/main/home/modals/RedirectToOnboardingModal.tsx
  34. 0 1
      dashboard/src/main/home/modals/UsageWarningModal.tsx
  35. 0 1
      dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_DORegistryForm.tsx
  36. 0 1
      dashboard/src/main/home/onboarding/steps/ConnectSource.tsx
  37. 0 2
      dashboard/src/main/home/project-settings/APITokensSection.tsx
  38. 0 2
      dashboard/src/main/home/project-settings/InviteList.tsx
  39. 0 1
      dashboard/src/main/home/project-settings/ProjectSettings.tsx
  40. 296 0
      dashboard/src/main/home/provisioner/AzureFormSection.tsx
  41. 36 1
      dashboard/src/main/home/provisioner/ProvisionerSettings.tsx
  42. 32 0
      internal/integrations/preview/validate.go

+ 127 - 0
api/server/handlers/environment/validate_porter_yaml.go

@@ -0,0 +1,127 @@
+package environment
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/google/go-github/v41/github"
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/integrations/preview"
+	"github.com/porter-dev/porter/internal/models"
+	"gorm.io/gorm"
+)
+
+type ValidatePorterYAMLHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewValidatePorterYAMLHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *ValidatePorterYAMLHandler {
+	return &ValidatePorterYAMLHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *ValidatePorterYAMLHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+
+	envID, reqErr := requestutils.GetURLParamUint(r, "environment_id")
+
+	if reqErr != nil {
+		c.HandleAPIError(w, r, reqErr)
+		return
+	}
+
+	request := &types.ValidatePorterYAMLRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	env, err := c.Repo().Environment().ReadEnvironmentByID(project.ID, cluster.ID, envID)
+
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("no such environment with ID: %d", envID)))
+			return
+		}
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error reading environment with ID: %d. Error: %w", envID, err)))
+		return
+	}
+
+	ghClient, err := getGithubClientFromEnvironment(c.Config(), env)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res := &types.ValidatePorterYAMLResponse{}
+
+	if request.Branch == "" { // get the default branch name
+		repo, _, err := ghClient.Repositories.Get(r.Context(), env.GitRepoOwner, env.GitRepoName)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+
+		request.Branch = repo.GetDefaultBranch()
+	}
+
+	fileContents, _, ghResp, err := ghClient.Repositories.GetContents(
+		context.Background(), env.GitRepoOwner, env.GitRepoName, "porter.yaml",
+		&github.RepositoryContentGetOptions{
+			Ref: request.Branch,
+		},
+	)
+
+	if ghResp.StatusCode == 404 {
+		res.Errors = append(res.Errors, "no porter.yaml file found in the root of the repository")
+		c.WriteResult(w, r, res)
+		return
+	}
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	contents, err := fileContents.GetContent()
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if contents == "" {
+		res.Errors = append(res.Errors, "porter.yaml file is empty")
+		c.WriteResult(w, r, res)
+		return
+	}
+
+	validator := preview.NewPorterYAMLValidator()
+
+	err = validator.Validate(contents)
+
+	if err != nil {
+		res.Errors = append(res.Errors, err.Error())
+		c.WriteResult(w, r, res)
+		return
+	}
+}

+ 12 - 0
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()
@@ -105,6 +111,12 @@ func (c *UpdateImageBatchHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 				_, err = helmAgent.UpgradeReleaseByValues(conf, c.Config().DOConf)
 
 				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()

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

@@ -346,7 +346,7 @@ func getClusterRoutes(
 			Router:   r,
 		})
 
-		// PATCH /api/projects/{project_id}/clusters/{cluster_id}/environment/{environment_id}/toggle_new_comment -> environment.NewToggleNewCommentHandler
+		// PATCH /api/projects/{project_id}/clusters/{cluster_id}/environments/{environment_id}/toggle_new_comment -> environment.NewToggleNewCommentHandler
 		toggleNewCommentEndpoint := factory.NewAPIEndpoint(
 			&types.APIRequestMetadata{
 				Verb:   types.APIVerbUpdate,
@@ -375,6 +375,35 @@ func getClusterRoutes(
 			Router:   r,
 		})
 
+		// GET /api/projects/{project_id}/clusters/{cluster_id}/environments/{environment_id}/validate_porter_yaml -> environment.NewValidatePorterYAMLHandler
+		validtatePorterYAMLEndpoint := factory.NewAPIEndpoint(
+			&types.APIRequestMetadata{
+				Verb:   types.APIVerbGet,
+				Method: types.HTTPVerbGet,
+				Path: &types.Path{
+					Parent:       basePath,
+					RelativePath: relPath + "/environments/{environment_id}/validate_porter_yaml",
+				},
+				Scopes: []types.PermissionScope{
+					types.UserScope,
+					types.ProjectScope,
+					types.ClusterScope,
+				},
+			},
+		)
+
+		validatePorterYAMLHandler := environment.NewValidatePorterYAMLHandler(
+			config,
+			factory.GetDecoderValidator(),
+			factory.GetResultWriter(),
+		)
+
+		routes = append(routes, &router.Route{
+			Endpoint: validtatePorterYAMLEndpoint,
+			Handler:  validatePorterYAMLHandler,
+			Router:   r,
+		})
+
 		// GET /api/projects/{project_id}/clusters/{cluster_id}/deployments -> environment.NewListDeploymentsByClusterHandler
 		listDeploymentsEndpoint := factory.NewAPIEndpoint(
 			&types.APIRequestMetadata{

+ 8 - 0
api/types/environment.go

@@ -138,3 +138,11 @@ type UpdateEnvironmentSettingsRequest struct {
 	DisableNewComments bool     `json:"disable_new_comments"`
 	GitRepoBranches    []string `json:"git_repo_branches"`
 }
+
+type ValidatePorterYAMLRequest struct {
+	Branch string `schema:"branch"`
+}
+
+type ValidatePorterYAMLResponse struct {
+	Errors []string `json:"errors"`
+}

BIN
dashboard/src/assets/azure.png


+ 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 - 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

@@ -241,8 +241,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 - 1
dashboard/src/main/home/cluster-dashboard/dashboard/incidents/IncidentsTab.tsx

@@ -153,7 +153,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

@@ -150,7 +150,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 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/incidents/EventsTab.tsx

@@ -155,7 +155,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 - 1
dashboard/src/main/home/cluster-dashboard/preview-environments/components/ButtonEnablePREnvironments.tsx

@@ -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

@@ -243,7 +243,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

@@ -180,7 +180,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"};
 

+ 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

@@ -684,8 +684,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

@@ -236,7 +236,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()}</>

+ 32 - 0
internal/integrations/preview/validate.go

@@ -0,0 +1,32 @@
+package preview
+
+import (
+	"github.com/porter-dev/switchboard/pkg/models"
+	"github.com/porter-dev/switchboard/pkg/parser"
+)
+
+type driverBasedResourceValidator func(*models.Resource)
+
+type porterYAMLValidator struct {
+	driverValidators map[string]driverBasedResourceValidator
+}
+
+func NewPorterYAMLValidator() *porterYAMLValidator {
+	return &porterYAMLValidator{
+		driverValidators: make(map[string]driverBasedResourceValidator),
+	}
+}
+
+func (v *porterYAMLValidator) Validate(contents string) error {
+	resGroup, err := parser.ParseRawBytes([]byte(contents))
+
+	if err != nil {
+		return err
+	}
+
+	for range resGroup.Resources {
+
+	}
+
+	return nil
+}